[전문가를 위한 C++] 10장. 상속 활용하기
Chapter 10. 상속 활용하기
10.1 상속을 이용한 클래스 구현
-
포인터나 레퍼런스는 해당 클래스 뿐 아니라 파생 클래스도 가리킬 수 있다. virtual로 선언됐다면 가장 적합한 메서드를 가리킨다.
class Base { public: virtual void someMethod(); }; class Derived : public Base { public: void someMethod() overrid; }; int main() { Derived d; Base &ref = d; ref.someMethod(); // Derived(실 형식=실제 생성되는 객체)의 someMethod()가 호출된다. }
-
베이스 클래스에 virtual이 선언되지 않았다면 접근자의 메서드가 호출된다.
class Base { public: void someMethod(); }; class Derived : public Base { public: void someMethod(); }; int main() { Derived d; Base &ref = d; ref.someMethod(); // Base(접근자)의 someMethod()가 호출된다. }
-
virtual 소멸자의 필요성
- 소멸자를 virtual로 선언하지 않으면 객체가 소멸할 때 메모리가 해제되지 않을 수 있다.
- 예를 들어, 파생 클래스의 생성자에서 동적으로 할당한 메모리를 사용하다가 소멸자에서 삭제하도록 작성했을 때 이 소멸자가 호출되지 않으면 메모리가 해제되지 않는다. (심지어 스마트 포인터도 그렇다!)
-
소멸자에서 따로 처리할 일은 없고, virtual로만 지정하고 싶다면 default로 지정하자.
virtual ~Base() = default;
10.2 코드 재사용을 위한 상속
10.3 부모를 공경하라
- 객체의 생성 과정
- 베이스 클래스라면 디폴트 생성자를 실행한다. 단, 생성자 이니셜라이저가 있다면 디폴트 생성자 대신 생성자 이니셜라이저를 호출한다.
- static으로 선언하지 않은 데이터 멤버를 코드에 나타난 순서대로 생성한다.
- 클래스 생성자 본문을 실행한다.
- 객체의 소멸 과정
- 현재 클래스의 소멸자를 호출한다.
- 현재 클래스의 데이터 멤버를 생성할 때와 반대로 호출한다.
- 부모 클래스가 있다면 부모의 소멸자를 호출한다.
- 업캐스팅과 다운 캐스팅
-
업 캐스팅 : 베이스 클래스 타입에서 파생 클래스를 참조하는 것.
Base myBase = myDerived; // 슬라이싱! 자식 클래스 특성이 사라진다. Base& myBase = myDerived; // 슬라이싱이 발생하지 않는다.
-
다운 캐스팅은 꼭 필요할 때만 사용하고, 반드시 dynamic_cast()를 활용한다.
Derived* d = dynamic_cast<Derived*>(base); if (d != nullptr) { // d로 Derived의 메서드에 접근하는 코드를 작성한다. }
10.4 다형성을 위한 상속
-
- 순수 가상 메서드가 하나라도 정의된 클래스를 추상 클래스라고 한다.
- 추상 클래스에 대해서는 메서드를 만들 수 없다. 객체를 파생클래스로 생성하면 가능하다.
-
클래스를 파생 클래스가 아닌 다른 코드에서 직접 인스턴스를 생성하지 못하게 하려면 추상 클래스로 만든다.
class SpreadSheetCell { public: virtual ~SpreadSheetCell() = default; // 소멸자는 명시적으로 디폴트로 선언한다. virtual void getString() = 0; } class StringSpreadSheetCell : public SpreadSheetCell { public: void getString() override; }; class DoubleSpreadSheetCell : public SpreadSheetCell { public: void getString() override; }; int main() { std::unique_ptr<SpreadSheetCell> cell(new StringSpreadSheetCell()); return 0; }
-
다형성 최대로 활용하기
vector<unique_ptr<SpreadSheetCell>> cellArray; cellArray.push_back(make_unique<StringSpreadSheetCell>()); cellArray.push_back(make_unique<DoubleSpreadSheetCell>()); // 런타임 시간에는 가리키는 객체에 따라 메서드가 호출된다. cellArray[0].getString(); // StringSpreadSheetCell의 getString() 메서드 호출 cellArray[1].getString(); // DoubleSpreadSheetCell의 getString() 메서드 호출
- 나중에 대비하기
- 변환 생성자
- DoubleSpreadSheetCell을 StringSpreadSheetCell 타입으로 변환시키는 기능은 변환 생성자를 추가하는 방식으로 구현할 수 있다.
- 복제 생성자와 비슷하지만 동일한 클래스가 아니라 형제 클래스 객체에 대한 레퍼런스를 인수로 받는다. 디폴트 생성자를 반드시 선언 해야 한다. 생성자를 사용자가 직접 작성하면 컴파일러가 더 이상 자동으로 만들어주지 않기 때문이다.
class StringSpreadSheetCell : public SpreadSheetCell { public: StringSpreadSheetCell() = default; StringSpreadSheetCell(const DoubleSpreadSheetCell& inDoubleCell); };
- 변환 생성자
10.6 상속에 관련된 미묘하면서 흥미로운 문제들
- 오버라이드한 메서드 속성 변경하기
- 리턴 타입 변경하기
- pick.cpp 예제를 참고하자.
- 메서드 매개변수 변경하기
- 파생 클래스를 정의하는 코드에서 부모 클래스에 있는 것과 이름은 똑같고 매개변수만 다르게 지정하면 부모 클래스의 메서드가 오버라이드 되지 않고 새롭게 정의되며, 부모 클래스의 메서드는 가려진다.
class Base { public: virtual void someMethod(); }; class Derived { public: virtual void someMethod(int i); }; int main() { Derived d; d.someMethod(); // 부모의 메서드는 가려졌다. compile error! return 0; }
- 파생 클래스를 정의하는 코드에서 부모 클래스에 있는 것과 이름은 똑같고 매개변수만 다르게 지정하면 부모 클래스의 메서드가 오버라이드 되지 않고 새롭게 정의되며, 부모 클래스의 메서드는 가려진다.
- 생성자 상속
- using 키워드를 작성하면 부모 클래스의 디폴트 생성자를 제외한 모든 생성자를 상속한다.
class Base { public: virtual ~Base() = default; Base() = default; Base(std::string_view str); Base(float f); }; class Derived { public: using Base::Base; Derived(int i); Derived(float f); // float 버전의 Base 생성자를 오버라이드 한다. }; int main() { Derived d1(1); // Derived 생성자 호출 Derived d2("hello"); // Base 생성자 호출. using 키워드를 작성하지 않는다면 Derived는 이 생성자를 상속하지 않으므로 Compile Error가 발생한다. return 0; }
- 상속한 생성자를 활용할 때는 모든 변수가 제대로 초기화 됐는지 확인해야 한다.
class Base { public: virtual ~Base() = default; Base(std::string_view str) : mStr(str) {} private: std::string mStr; }; class Derived { public: using Base::Base; Derived(int i) : Base(""), mInt(i) {} private: int mInt; // int mInt = 0; // 이렇게 초기화하자. }; int main() { Derived d1(10); // Derived(int i) 생성자 호출 Derived d2("Hello"); // Base(string_view str) 생성자 호출. mInt는 초기화 되지 않는다. return 0; }
- using 키워드를 작성하면 부모 클래스의 디폴트 생성자를 제외한 모든 생성자를 상속한다.
- 메서드 오버라이딩 특수 케이스
- 베이스 클래스가 static인 경우
- C++에서 static 메서드를 오버라이드 할 수 없다.
-
static 메서드를 호출할 때는 실제로 속한 객체를 찾지 않고, 컴파일 시간에 지정된 타입만 보고 호출할 메서드를 결정한다.
#include<iostream> class BaseStatic { public: static void beStatic() { std::cout << "Base beStatic()" << std::endl; } }; class DerivedStatic : public BaseStatic { public: static void beStatic() { std::cout << "Derived beStatic()" << std::endl; } }; int main() { DerivedStatic d; BaseStatic& ref = d; d.beStatic(); // Derived beStatic() ref.beStatic(); // Base beSataic() return 0; }
- 베이스 클래스가 static인 경우
- 베이스 클래스 메서드가 오버로드 된 경우
- 베이스 클래스에서 오버로딩 한 메서드 중 하나만 오버라이딩 한 경우, 다른 오버라이딩 되지 않은 베이스 클래스의 메서드들은 가려진다.
- private, protected로 선언된 베이스 클래스 메서드
- private, protected 메서드도 오버라이딩 할 수 있다. 기존 클래스와 전반적인 골격은 그대로 유지한 채 특정한 기능만 변경할 때는 private, protected 메서드를 오버라이드 하는 것이 좋다.
- 베이스 클래스 메서드에 디폴트 인수가 지정된 경우
-
베이스 클래스와 파생 클래스에서 지정한 디폴트 인수가 서로 다를 수 있다. 이 때, 실제 내부에 있는 객체가 아닌 변수에 선언된 타입에 따라 결정 된다.
class Base { public: virtual ~Base() = default; virtual void go(int i = 2) { std::cout << "i = " << i << std::endl; } }; class Derived { public: virtual void go(int i = 7) { std::cout << "i = " << i << std::endl; } }; int main() { Base b; Derived d; Base& ref = d; b.go(); // i = 2 d.go(); // i = 7 ref.go(); // i = 2 return 0; }
-
- 리턴 타입 변경하기