책에서 상속의 개념이 필요한 문제를 제기, "employee problem"(이 문제를 간략하게 요약하자면 회사가 있을때 처음에는 고용형태가 permanent 밖에 없었는데 회사가 커지면서 다른 형태의 고용형태가 생겼을때(예를 들어 part time, sales permanent) 새로운 형태의 고용에 대한 클래스를 만드는 것은 당연하나 이 모두를 담기위한 department 클래스의 변형이 불가피 하다. 이럴때 상속의 개념을 이용하면 department의 클래스 변형 없이 가능하게 한다라는 내용).
student하는 클래스가 person이라는 클래스를 상속할때 student 클래스의 정의시 다음과 같이 서술한다. class student : public person {클래스 내용...}. 여기서 student를 sub 클래스 (혹은 derived 클래스)라고 하며 person을 super 클래스 (혹은 base 클래스)라고 한다.
상속하고 있는 클래스의 객체 생성 과정: 1.메모리 공간의 할당, 이때 상속되는 클래스를 감안해서 메모리 공간 할당. 2.base 클래스의 생성자 실행. 3.derived 클래스의 생성자 실행.(추가적으로 설명하자면 derived 클래스가 생성될때 derived 클래스의 생성자가 먼저 호출이 되나 그 생성자의 몸체({}부분)가 실행하기에 앞서 이 클래스가 base 클래스를 상속하고 있음을 알고 base 클래스의 생성자를 호출해서 실행하고 그 다음에 derived 클래스의 생성자의 몸체가 실행이 된다.)
AAA 클래스의 생성자가 여러개일때(함수 오버로드) derived 클래스인 BBB 클래스의 생성자가 특정 AAA 클래스의 생성자를 호출하고자 할때 BBB(int j) : AAA(j) {BBB 생성자 내용} 식으로 선언한다(이는 앞서 배운 const 맴버 변수를 초기화 할때 사용하는 member initializer 와 같은 형태이다).
base 클래스의 맴버 변수가 private으로 선언되어 있을 시에는 위와 같이 member initializer방식으로 base 클래스의 초기화 하고 public으로 선언되어 있을때는 base 클래스의 맴버 변수를 직접 초기화도 가능하다.
상속된 개체의 소멸과정은 생성과정과 반대로 derived 클래스의 소멸자가 먼저 실행되고 base 클래스의 소멸자가 그 다음으로 실행된다.
protected 멤버는 상속 관계에 놓여 있는 경우에 접근을 허용한다는 것을 제외하고 private 멤버와 동일하다.
상속에는 세가지 형태 public, private, protected가 있는데 각각의 형태에 따라 상속되는 과정에서 접근 권한이 변경된다(아래 표와 같다).
control 클래스 : 기능적 성격이 강한 클래스. 프로그램의 주요기능을 설명한다.
entity 클래스 : 데이터적인 성격이 강한 클래스. 저장되어야 하는 최소 단위 데이터로 인식
chapter8 : 상속과 다형성
상속의 조건 :: 1.IS-A관계 : "A student is a person" 의 문장에서 student 클래스와 person 클래스는 IS-A 관계에 있다.곧 student가 person 클래스를 상속하게 된다. 2.HAS-A관계 : "The police have a cudgel"문자에서 police 클래스는 cudgel클래스와 HAS-A관계에 있다. 곧 police 클래스는 cudgel 클래스를 상속하게 된다.
그러나 HAS-A관계는 상속말고도 다른 방식으로 표현이 가능하다(police 클래스 안에 cudgel 클래스 맴버 변수 선언 혹은 cudgel 클래스를 가리키는 포인터 변수 선언). 그렇기에 거의 대부분의 클래스 상속은 IS-A관계를 표현하기 위해 사용된다.
Design pattern이란? 문제 상황에 따른 좋은 클래스 디자인 모델의 집합
CCC 클래스가 BBB 클래스를 상속하고 있고(CCC->BBB) BBB클래스가 AAA 클래스를 상속하고 있다면(BBB->AAA) AAA객체 포인터에 BBB객체와 CCC 객체의 주소를 넣을수 있다(AAA* t1=new BBB; AAA* t2=new CCC 가능). 곧 base 객체 포인터를 이용해서 derived 클래스의 객체의 주소를 담을 수 있다. 그러나 이때 base 객체 포인터로 정의된 변수는 base 클래스 내에 선언된 멤버만 접근이 가능하다(객체 레퍼런스도 마찬가지이다).
이를 해결하기 위한 방법이 virtual 키워드를 이용한 함수 오버라이딩.
함수 overriding : base 클래스에 선언된 형태의 함수를 derived 클래스에서 다시 선언하는 현상
virtual 키워드 : base 클래스(AAA)가 func이라는 이름의 함수를 virtual라는 키워드로 가상으로 선언하게 되면(virtual func(func매개변수){func의 함수 내용}) base 클래스의 객체 포인터로 derived 클래스(BBB)의 객체를 가리킬때(AAA* t1 = new BBB), 그 객체 포인터로 base 클래스의 func 함수에 접근하게 되면 자동적으로 derived에 오버라이딩하는 함수를 호출하게 된다. 이러한 형식의 함수 호출을 dynamic binding이라 한다(컴파일 되는 동안에 호출될 함수가 결정되는 것이 아니라 실행하는 동안에 호출될 함수가 결정되기때문).
다형성이란? 모습은 같은데 형태가 다른것 (dynamic binding, 함수 overriding등)
그렇다면 overriding 당한 함수는 어떻게 호출할수 있나? base 함수::virtual 함수 이름(AAA::func())으로 호출 가능하다.
virtual 키워드를 이용한 가상함수는 그 특성이 상속된다. 예를 들어 base 클래스 AAA에 virtual func으로 가상화된함수가 있고 derived 클래스 BBB에 함수 overriding을 위해 func 함수가 있을때 BBB 클래스의 func 함수도 virtual이란 키워드를 붙여 주지 않았다고 하더라도 저절로 가상화가된다.
이렇듯 가상화된 함수는 상속하는 클래스의 함수 오버라이딩을 위한 것이라서 base 클래스에 virtual 함수에 내용을 지정할 필요가 거의 없다. 그렇기 때문에 이러한 가상함수는 순수 가상 함수로 선언한다(virtual int func()=0 과 같이 =0을 붙이면 pure virtual function임을 나타내는 것이다). 이러한 순수 가상 함수를 포함하고 있는 base 클래스를 추상(abstract) 클래스라고 한다. 추상 클래스는 객체화되지 못한다.
가상함수 말고도 소멸자 역시 virtual 키워드를 이용해서 가상화 되어야 한다. 예를 들어 base 클래스(AAA)와 derived 클래스(BBB)가 있고 AAA* t1 = new BBB로 선언된 객체 포인터 t1이 있을때 t1을 소멸하게 되면 AAA 클래스의 소멸자만이 호출되어 BBB안에 있을지 모르는 동적메모리가 free되지 않는다. 이를 방지 하기 위하여 소멸자 역시 가상화 하는 것이 좋다(두 클래스의 소멸자의 이름은 다르나 소멸자라는 특성때문에 이름이 다르더라도 overriding된다),
8장에서 배운 내용을 가지고 7장의 employee problem을 완벽히 해결할수 있다(기본적으로 employee라는 base 클래스를 만들고 permenant와 parttime, salesperment클래스를만들어서 employee 클래스를 상속한다. employee클래스안에 permenant등 상속될 클래스의 각각의 구현이 다른 맴버함수에 대해서 virtual키워드로 가상화 시키면 contol클래스에서 employee 객체 포인터로 클래스들을 선언하더라도 각각의 클래스의 맴버 함수에 접근할수 있게 되어진다).
chapter9 : virtual의 원리와 다중 상속
객체가 생성되면, 멤버 변수는 객체 내에 존재한다. 그러나 멤버 함수는 메모리의 한 공간에 존재하면서, 모든 객체가 공유하는 형태를 취한다.
1개의 가상 함수를 포함하는 클래스에 대해서, 컴파일러는 가상 함수 테이블(virtual table,VTable)이라는 것을 만들어 준다. 이 VTable는 key로 함수의 이름이 들어가고 value로 그 함수가 메인 메모리에 위치한 메모리 주소를 갖게 된다. 함수가 overriding 되었을때 derived 클래스의 VTable에는 overriding된 base 클래스의 virtual 함수는 포함되어 있지 않는다.
하나 이상의 가상 함수를 멤버로 지니는 클래스의 객체에는 VTable을 위한 포인터가 멤버로 추가된다.
이렇듯 가상 함수를 지니는 클래스의 객체가 생성되어지고 그 객체의 함수가 호출되어지면 VTable을 가리키는 포인터를 참조해서 VTable의 내용을 읽게 되고 VTable안에 존재 하는 key로 함수를 찾게 되어 value(함수의 메모리 상의 위치)를 가져오게 된다. 곧 base 클래스의 virtual 함수가 derived 클래스에 의해 overriding되어지면 derived 클래스의 객체의 VTable에는 base 클래스의 virtual 함수에 대한 정보가 없기 때문에 derived 클래스의 함수가 호출되는 것이다.
가상함수가 없으면 VTable이 생성되지 않고 직접 호출할 함수에 접근하기때문에 성능은 좋아진다.
다중 상속에 대한 이야기도 있지만 필자가 말하듯 굉장히 모호해질수 있는 문법인지라 이 내용은 생략한다.