1.상속 관계에서의 동적할당 주의점
a.보통 동적할당 문법을 쓸 때 생성하고자하는 변수의 자료형과 포인터 변수의 자료형과 같아야한다.
( A b = new A (o) / A b = new B (x)? )
b.꼭 다시 반납을 해야된다.
a에 대하여
결국 이 두 자료형이 다르면 동적할당시 오류가 나는데 이때 c++에선 예외적으로 가능한 경우가 있는데 바로 "부모 클래스의 포인터가 자식 객체를 가리키는 경우에는 서로의 자료형이 달라도된다."
포인터 자체가 가리키는 애의 자료형이 자신의 자료형과 동일해야된다.근데 예외적으로 상속관계일때 부모 클래스의 포인터변수가 자식의 객체를 가리킬때는 가능하단것이다.
포인터변수는 자료형이 뭐든 다 4바이트다.이건 포인터변수가 하는일이 오로지 주소값을 저장하는 역할 밖에 없기때문이다.부모 포인터 변수도 역시 4바이트 하지만 이렇게 자료형을 서로 맞춰줬던 이유는 자료형을 혼용하고싶지않아서다.(혼동x)
근데 상속(부모의 필드, 메서드를 재사용하기 위함)이란 개념이 등장하면서 이 경우에 예외사항이 생긴것! 우리가 클래스를 관리하기위해 등장한 개념인 상속의 목적 즉 재사용하는 이유를 생각해보면 자식객체들이 부모의 것들을 공통적으로 가지고있으면해서인데 이러다보니 부모는 껍데기처럼 동작하는 느낌이다.(=부모한테서는 가져오기만 하고 껍데기로 놔두고 실제로 쓰는건 자식객체들)
ex)동물게임에서 동물(부모클래스)은 직접적으로 등장X, 동물의 구체적 이름들(자식클래스)이 등장o
일반적인 상속 코드문이 있을때 메인함수에서 동적할당을 A * b = new B로 받고
b -> 함수이름1();
b -> 함수이름2();
이런식으로 호출한다고 가정해보자. (근데 이때 함수이름1이란게 부모, 자식 둘다 있음)
그러면 이때 이 함수이름 1이나2는 부모가 아닌 자식객체의 함수를 의미한다.포인터변수는 껍데기일뿐이고 생성한 실객체의 함수를 따라간다.이제 자식클래스가 여러개일때 어느 자식인지 구분이 안될탠데 그럴때 동적할당한 실객체에 의해서 결정이 나는것이다.
근데 이건 우리가 위에 있는 동적할당식을 봐서 아는거고 만약 호출문만 본다면 이 함수가 어느 자식 클래스의 함수인지 모른다.그래서 다형성(같은 형태지만 결과는 다르다)이라는게 필요한것이다.함수를 호출하는데 그 함수가 있는 클래스들이 A와 B였다면 A * b = new B에서는 B라는 자식클래스 속 함수를 호출했을것이고 new A였다면 A의 함수를 사용했을것이다.근데 이건 지금 동적할당식이 하나여서 우리가 구별한것이다.
3.만약 동적할당식이 여러개고 각 객체의 동일 함수 호출식을 써놓는다면 그건 어느 클래스의 함수를 말하는걸까?
위에서 말한 다형성의 특성 덕분에 자식객체들을 효율성있게 관리할 수 있다.
부모 포인터 변수로 여러 종류의 자식 객체들을 일괄적으로 컨트롤할 수 있다는것인데
한번에 관리할 수 있다는 것이 무슨뜻이냐면 예로 들어 동물 프로그램에서 부모 속 무브라는 움직이는 함수가 있지만 종류가 다른 자식 객체들에서 무브는 각자의 맞는식으로 재정의해서 쓰이는 함수가 될 것이다.객체를 여러개 생성한다했을때 여러 종류의 객체를 가리키는 동적할당식을 여러번 쓴다고하면(포인터 하나당 객체 하나) 내가 무브라는 함수를 호출했다고했을때, 그 무브가 어떤 자식의 함수인지 몰라 컨트롤 못하지않냐인데 우리는 그냥 부모 포인터(b)한테 무브라는 함수 호출(b->move)만 해주면 알아서 객체의 타입에 따라서 그 속의 무브가 자동으로 실행된다.
4.근데 다형성에 동적할당은 세트로 가는 형태인가?
별개다. 내가 동적할당을 쓰는 이유는 지역변수나 전역변수의 메모리의 한계가 있을때 혹은 유동적으로 메모리 크기를 조절할 수 있다는 메리트 때문에 쓰는거여서 직접적으로 다형성 때문에 쓰는건아니다.근데 다형성을 구현할려면 동적할당을 써야되는건 맞다.
ㄱ.첫번째 이유
이 동적할당이 포인터를 이용하기 때문이다.
다형성은 포인터를 이용하기로한 개념이기 때문에 동적할당이 구문에 들어간 것이다.
동적할당 식을 보면 우리는 이름없는 메모리를 포인터로 받아온다.
근데 보통은 포인터변수와 가리키는 객체의 자료형이 일치하는데 예외적으로 다형성에선 자료형이 다른 경우가 앞의 내용인 것.
ㄴ.두번째 이유(자식 객체를 가리키는 건 왜 사용하냐?)
부모 포인터로 자식객체를 가리키는 이유는
부모는 자식이 내것을 가져다썼으면 좋겠고, 형태는 비슷해도 다르게 썼으면 좋겠다는게 상속이였다.그러면 이 자식이 무조건 부모의것을 가지는 성질을 이용해보자라는것이다.
함수를 호출하는 구조는 일반적이든 무슨 구문이든 쓰는 방식은 다 똑같다.
(메인함수내에서 함수이름();이라고 입력)
그리고 이걸 포인터와 연계해서 호출하곤 한다.포인터는 가리키는애의 값을 읽거나 변경할 수 있고 안에 있는 함수를 호출할 수 있다.( temp->b();이런식으로 )
ㄷ.그렇다면 이걸 쓰는 이유를 다시 정리해보면 다음과 같다.
예시)
동물(=부모클래스)이 있고 그 밑에 각 동물의 종류 100마리(=자식클래스)가 있는데 그중에 물고기3마리, 인간 50명, 새 21마리 나머진 기타 등등이 있는데 만약 부모클래스에 move라는 함수가 있다고 가정하면 이걸 한번에 호출해서 컨트롤하는게 효율적이지 각각 무브하라고 호출하는건 비효율적이다.(지금 대량의 객체를 컨트롤하는게 기준이며, 하나만 움직이고싶다 이런 생각은 배제하기) 한번에 호출하면 컴퓨터가 자동으로 각 함수의 맞는 자식클래스와 매칭시켜줘서 작동시킨다.
다시말해 효율성을 위해서 자식클래스들이 어차피 move란 함수를 똑같이 다 가지고있단 성질을 알고, 내가 부모 포인터로 자식 객체를 가리킬수 있는 성질을 알고있다면 이 두 성질을 이용해 객체들을 효율적으로 컨트롤 해보자는것이다.(move 호출시 그게 형태가 무엇이든 각자 재정의했으면 재정의한 move를 쓰고 안했으면 부모가 가지고있는 move를 작동한다.)
우리가 프로그램 실행 전엔 객체의 형태(갯수나,함수들)를 모르기때문에 객체들을 직접 갯수를 파악해서 따로따로 저장해서 관리하는것보다는 자식 객체들을 부모 포인터로 다 받아와서 부모 클래스의 함수를 작동시켜주면 간단한 문제인것이다.
여기서 각자 따로 관리해준다는건 자식 포인터로 자식 객체를 가리킨다는것인데 이럴 경우는 공통된 함수를 가지고있는게 아녀서 따로따로 컨트롤해줘야된다.
우리가 뭔가 총괄적으로 관리할때는 배열로 묶어줘야되고, 배열로 묶을려면 타입이 하나여야된다.그래서 부모 포인터 배열을 만들어줘서 자식객체들을 다 넣어준것이다.이렇게 만든 배열을 가지고 for문을 써줘서 반복돌면서 원하는 함수를 일괄호출 시켜주는것이다.그리고 바로 그게 다형성이다.
5.객체지향(객체를 컨트롤해서 뭔가를 구현하는것)이란게 여기서 실현되는건데 이 다형성 덕분에 컨트롤을 하는게 굉장히 편해졌다는것에 큰 의의가 있다.
(다형성이 c++에서 가장 중요하다)
6.근데 하나만 상속된 자식을 가리켜 동적할당을 받아와서 다형성을 구현하는 코드를 짤 경우, 메인함수에서 호출하는데 오버라이드된 자식클래스 안 재정의된 함수를 호출한게 아니라 자식클래스에 재정의되지않은 함수가 호출될때 부모의 해당 함수를 복사해서 가져와서 호출하는것이냐? 그렇다면 그 함수는 엄연히 다른 자식 객체만의 함수가 호출된걸로 봐야되냐?
---> 정확히 말하면 복사한게 아닌 똑같은 함수다.함수도 주소값이 존재하는데(코드들이 저장되는 영역이 코드영역인데 이 말은 곧 이것들도 다 메모리란 소리) 함수도 주소값을 가지고있는 이유는 함수의 메모리에 접근할려면 이름과 주소값을 가지고 접근하는데 우리가 호출문에서 함수이름을 써주고 호출하면 운영체제가 함수의 주소값을 돌려준다.(절차지향언어에서 위에서 아래로 코드가 실행되는데 가다가 호출문이 있으면 다시 코드가 함수정의문으로 가는 이유)그렇기 때문에 함수의 메모리는 다른게 아니라 동일해서 같은 함수다.
7.동적할당할때 자식 객체를 생성하는데 이것도 객체니깐 생성자 함수가 불린다.또한 소멸될때 소멸자 함수도 불린다.근데 이런 상속관계에서 부모 객체와 자식 객체가 생성된 경우에는 자식 객체의 소멸자가 안불리는 상황이 발생하는 문제가 발생할 수 있는데,그 이유는 컴퓨터는 우리가 생각한 것 만큼 똑똑하지않는다. 즉 프로그래머를 전적으로 신뢰한다.
그래서 프로그래머가 애매하게 정보를 알려주면 그 정보를 처리하지않는다.
예로 들면
A * b = new B
delete b
가 있다고 가정하면 B객체를 생성한건 명확하니 생성자A와 B가 불리는건 맞다.근데 delete b 같은 경우는 b의 타입은 부모의 포인터 타입이다.그럼 컴파일러가 이걸 어떻게 받아들이냐면 b를 삭제시키는데 b가 가르키는애를 삭제시키면서 객체 삭제시켰으니깐 소멸자 불러야지하고 불러야되는데 어떤 소멸자를 부를지몰라서 자기의 타입이 부모의 포인터 타입이니깐 부모 소멸자만 부르면되겠구나 하고 부르고 객체 삭제하고 끝내버린다.(어떤 객체인지는 우리는 코드를 보면 알지만 컴퓨터 입장에선 모름)
여튼 보통은 부모 생성자 -> 자식 생성자 -> 자식 소멸자 -> 부모 소멸자 이렇게 소멸이 되어야되는데 위와 같은 자식 객체 소멸자가 안불리는 이슈가 생긴다.
이럴 경우 우리가 직접 컴퓨터에게 알려줘야한다. 바로 소멸자를 가상화해주는 방법이다.
= 소멸자 앞에 virtual을 붙여줘서 가상함수로 만들어버린다.
ex) virtual ~A( );
vitual 즉 가상함수는 함수를 오버라이드를 사용할 가능성이 있다고 하겠다고 컴파일러한테 알려주는것이 목적인 것이다.여기서 무조건 가상함수화된다고 오버라이드를 반드시 해야된다는것은 아니며 이걸 해놓고 오버라이드를 사용하는 여부는 자유다.대신 오버라이드를 할려면 무조건 virtual을 붙여야된다.
즉 부모의 소멸자에다가 가상화를 시켜주면 컴파일러에게 소멸자가 오버라이드 되어있다는걸 확인해보라는 계기를 제공하는것 그렇게 되면 컴파일러가 오버라이드 된 자식 소멸자가 있는지 확인해본다.여기서 주의할 점은 virtual을 붙여준게 동적할당 구문의 자식 객체의 소멸자가 아니라 부모 객체란 것이다.그래야 밑에서 딜리트할때 부모만 소멸해줘야겠다하고 부모 소멸자 코드로 올라가서 보는데 virtual을 그제서야보고 자식 객체 소멸을 확인한다.자식 객체를 찾으면 그제서야 부모 객체 소멸자 전에 자식 객체의 소멸자를 먼저 부른다.
8-1.그렇다면 여기서 컴파일러가 자식 객체가 존재하는지의 유무를 어떻게 찾냐?
멍청한데 단순히 virtual만 붙혀서 어떻게 찾는지 궁금할 수도 있는데 이 키워드를 썼을때 컴파일러가 어떻게 작동하냐면 가상함수 테이블을 이용한다.
클래스 내에 한 개 이상의 가상함수가 존재한다면 즉 컴파일러는 virtual란 키워드를 쓴 클래스를 발견했을때 가상함수 테이블란것에 함수이름과 함수주소를 적어놓는다.
이 가상함수 테이블은 쉽게말해 우리가 자식 객체가 있는지 눈여겨 보라고 붙여준것이니깐 컴퓨터가 그걸 기억하고 관리하기위해 가지고 현황시켜놓는 액셀파일이라고 생각하면된다.
8-2.가상함수 테이블
Key와 Value로 이뤄져있는데 Key당 하나의 Value로 짝지어져있다.
Key값
Value값
만약 클래스 A내에 가상함수가 있다면 그 클래스 안 함수들을 가상함수 테이블에 적는다.
(가상함수뿐만 아니라 다른 함수도)
8-3.만약 A 객체를 생성하면 객체에 A 클래스의 가상 함수 테이블의 주소 값이 저장된다.
그러면 A 객체의 임의의 함수를 호출하면 다음과 같은 참조 과정을 가진다.
ㄱ. A 객체의 원하는 함수를 호출
ㄴ. A 클래스의 가상 함수 테이블의 주소 참조
ㄷ. 해당 주소 참조
= 호출 시 클래스 A에 무조건 한번 들려서 테이블보고 이 번지에 이 함수가 있구나하고 보는것
8-4.그러면 자식 클래스(B)에도 virtual가 붙혀서 있는 경우는 가상함수테이블이 어떻게 생성될까?
B 클래스는 내부에 오버라이드 된 함수들이 있다면 virtual이 없어도 자동으로 가상함수테이블이 잡힌다.
왜냐하면 바로 부모클래스에 virtual가 있기때문에
-> 부모에 virtual이 있으면 상속 받아서 자식도 virtual이 있다.
그래서 자식 클래스에 오버라이드 된 함수에 virtual을 명시하지않아도 상관이 없는것이다.자식 클래스의 객체들 안에 오버라이드가 명시된 함수 옆에는 다 virtual이 생략되어있다.그니깐 결국은 얘도 가상함수 테이블이 생성되는 것!(물론 자식 클래스에서 객체를 만들면 그 안에도 해당 함수들은 virtual이 있어서 테이블에 기록되어진다.
이때 부모와 자식 클래스 안에 들어있는 오버라이드된 함수는 가상함수 테이블에 두번적히냐? -> 아니다.부모 클래스의 함수는 이미 기록되어있으니 기록할 필요가 없다.테이블에는 두번적으면 햇갈리니 하나만 적힌다. -> 그래서 자식 객체의 함수 하나만 불린다.(=부모의 함수와 주소값이 다름)
== 오버 라이딩된 가상 함수의 주소정보는 유도 클래스(=자식 클래스)의 가상함수 테이블에 포함되지 않는다.
함수란건 코드영역에 존재하기때문에 코드는 한번 빌드되면 변할일이 없기에 부모 객체의 오버라이드 되지않는 함수는 변하지않아 자식객체의 가상테이블에 주소값이 그대로 등록된다.
오버라이딩 된 함수는 부모의 함수와 다른 함수다.오버라이드란건 내가 상속된 부모의 함수를 그대로 쓰는것이 아니라 자식 쪽에서 재정의한것이 오버라이드다.(이름하고 형태정도만 같은거지 엄연히 다른 함수다.)
9.대량의 동적할당 -> 포인터 배열을 이용한다!
int main()
{
Animal * arr[100];
for(int i=0;i<100;++i)
{
arr[i]=new Bird()
}
}
예로 들어 위와 같이 포인터 배열과 for문을 이용한다했을때는 bird라는 객체만 계속 만들어질것이다.근데 여기에서 여러가지를 다양하게 만들고싶을때는 다음과 같이 코드를 짠다.
int main()
{
srand(time(null));
Animal * arr[100];
for(int i=0;i<100;++i)
{
int choice=rand()%3+1;
switch(choice)
{
case1;
{
arr[i]=new Bird();
}
break;
case2;
{
arr[i]=new Fish();
}
break;
case3;
{
arr[i]=new Person();
}
break;
}
기본에 썼던 동적할당 구문에서 포인터에 배열을 추가하고 랜덤 함수와 switch문을 이용하여 for문을 돌리면 랜덤값에 의해 다양한 자식객체가 생성된다.(rand는 시드 단위로 변경돼서 srand(time(null));코드를 추가하여 수시로 바뀌게끔 코드짜기)
우리가 만약에 생성하고자하는 객체가 몇개씩 정해놓은 상황이라면 아래와 같이 관리해야되는데 이럴경우는 for문을 이용한 호출문을 호출수 만큼 따로따로 해줘야된다.
Bird arr1[33]
Bird arr2[33]
Bird * arr3[33]
근데 이것보다 전자의 방법대로하면 호출도 그냥 for문으로 호출해버리면 한번에 편하게 할 수 있다.그리고 새로운 객체를 추가하고싶으면 더 써줘야되는게 많은데 전자에선 case만 하나 더써주면된다.
"c++은 왜 객체지향언어인가?"이걸 정리하기
10.RTTI(Run Time Type Information)
== 프로그램이 실행되고 있는 동안의 자료형의 정보
Run Time : 프로그램이 실행되고 있는 시간
Type Information : 자료형의 정보
RTTI란 프로그램 실행 중에 개체의 형식이 결정될 수 있도록 하는 메커니즘이다.다시 말하면 실행중 포인터가 가르키는 객체의 타입을 알 수 있게 해주는 하나의 방법이라고 보면된다.
기본적으로 RTTI가 필요한 이유는 A 타입에서 B 타입으로 변경할 때 정보가 필요하기 때문이다.A 클래스의 정보 B 클래스의 정보를 가지고있는 가상함수 테이블이 프로그램의 실행 단계에서 정해진다.부모의 포인터로 가르키는 자식객체가 정확히 뭔지는 프로그램이 실행될때 안다.(렌덤으로 스위치문 작동될때처럼)그래서 자료형이 런타임때 정해진다는것이다.
일시간에 타입 변환이 이루어진다면 굳이 RTTI가 필요없다. 컴파일 단계에서 충분히 알 수 있고 특정 타입으로 확정할 수 있기 때문이다.
virtual 클래스로부터 상속이 하나라도 존재하면 RTTI를 사용하고 있다고 보면 된다.
== 가상함수테이블이란 개념이 RTTI라는 이론을 접목하고있다.
11.RAII
저번 개념 복습 생성자, 소멸자에서 RAII란 개념이 나오는데, 생성자랑 소멸자 사이에서 메모리 누수 방지를 위해 메모리관리를 해주는 개념이다.
== 생성했으면 꼭 소멸해준다.
RTII와 RAII의 차이를 설명하시오 대비하기
RTII 같은 경우는 상속,다형성 관련이라서 객체지향언어의 특징 3가지를 설명하라고할때 이 2개념을 접목시켜서 말하면 점수 딸 수 있음.이 두 개념은 고급이론이기때문에
12.STL(Standard Templete Library)
표준 탬플릿 라이브러리로 가져다 활용하는 탬플릿인데 여기서 탬플릿이라하면
다음과 같다.
c++의 탬플릿은 자료형에 구애받지않게 해준다.( 신경x, 관계없는x )
함수나 클래스 이런곳에선 자료형에 엄청 구애받는데 이런 자료형에 얽매이지않도록하기위해쓰는게 탬플릿이다.
보통은 함수의 반환형과 입력값엔 자료형이 들어가는데 우리가 어떤 함수를 쓸 때, 만약 만드는 과정에서 객체들이 추가될때마다 그 객체들의 자료형은 단순 자료형부터 클래스까지 다 다를것이고 그 자료형만의 함수를 또 써줘야된다.
컴퓨터는 이런 비효율적인 반복을 싫어한다.그래서 다형성 같은것도 만든건데,이것같은경우도 효율적으로 함수 하나로 여러 자료형에 구애받지않을 방법이 없을까라는 생각에서 시작돼서 만들어진 것이다.
기존처럼 자료형에 제약받지말고 모든 자료형을 다 받아와서 커버하는 만능 자료형같은 것을 만들자해서 만든게 탬플릿이다.