[전문가를 위한 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;
        }
        
    • 메서드 오버라이딩 특수 케이스
      • 베이스 클래스가 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;
          }
          
    • 베이스 클래스 메서드가 오버로드 된 경우
      • 베이스 클래스에서 오버로딩 한 메서드 중 하나만 오버라이딩 한 경우, 다른 오버라이딩 되지 않은 베이스 클래스의 메서드들은 가려진다.
    • 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;
        }