Skip to main content

[C++ Primer Plus] Chapter 13. 클래스의 상속

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




서론 #

  • C의 전통적인 함수 라이브러리는

    • 그 라이브러리의 소스 코드를 제공하지 않는다면, 사용자의 특정 요구에 맞게 그 함수를 확장하거나 수정할 수 없다.
  • C++의 클래스 라이브러리는

    • 클래스 상속을 사용한다. 이것은 기초 클래스(base class)로 부터 멤버 변수, 함수들을 상속받아서 새로운 파생 클래스(derived class)를 만드는 것이다.
    • 기초 클래스의 소스 코드에 접근할 필요 없이, 상속으로 새로운 기능이나 데이터를 추가하거나, 함수의 동작 방식을 변경할 수 있다.



기초 클래스를 상속 받는 파생 클래스 #

  • 파생 클래스는 기초 클래스의 구현들을 상속받는다. (데이터 멤버)

  • 파생 클래스는 기초 클래스의 인터페이스를 상속받는다. (메서드)

  • 파생 클래스는 자기 자신의 생성자를 필요로 한다.

  • 파생 클래스는 부가적인 데이터 멤버들과 멤버 함수들을 필요한 만큼 추가할 수 있다.

class BaseClass
{
private:
    int baseClassNumber;

public:
    BaseClass() 
    {
        baseClassNumber = 0;
    };

    BaseClass(int n)
    {
        baseClassNumber = n;
    }

    int GetBaseClassNumber() const
    {
        return baseClassNumber;
    }
};

class DerivedClass : public BaseClass
{
private:
    int derivedClassNumber;

public:
    // 기초 클래스의 생성자를 멤버 초기자 리스트 문법을 사용해 먼저 호출한다. 
    DerivedClass(int base, int derived) : BaseClass(base)
    {
        derivedClassNumber = derived;
    }

    int Sum()
    {
        // 기초 클래스의 private 멤버는 public 멤버로 접근할 수 있다. 
        return derivedClassNumber + GetBaseClassNumber();
    }
};

  • 생성자
    • 파생 클래스의 생성자는 기초 클래스의 생성자를 사용해야 한다.
    • 프로그램은 기초 클래스의 객체를 생성 한 후, 파생 클래스의 객체를 생성한다.
    • 파생 클래스 생성자의 몸체 안으로 들어가기 전에 기초 클래스 객체를 생성하기 위해 멤버 초기자 리스트 문법을 사용할 수 있다.
DerivedClass::DerivedClass(int base, int derived) : BaseClass(base)
{
    derivedClassNumber = derived;
}

  • 멤버 초기자 리스트를 생략하면?
    • 디폴트 기초 클래스 생성자를 사용한다.
DerivedClass::DerivedClass(int base, int derived) // 이것은 : BaseClass()와 같다. 
{
    derivedClassNumber = derived;
}

  • 원한다면, 파생 클래스 멤버들에도 멤버 초기자 리스트 문법을 사용할 수 있다.
DerivedClass::DerivedClass(int base, int derived) 
    : BaseClass(base), derivedClassNumber(derived);
{

}

  • 파괴자는 반대 순서로 일어난다.
    • 파생 클래스 파괴자의 먼저 호출되고, 기초 클래스의 파괴자라 호출된다.

  • 파생 클래스는 private이 아니면 기초 클래스의 메서드들을 사용할 수 있다.
class DerivedClass : public BaseClass
{
private:
    int derivedClassNumber;

public:
    // ...

    int Sum()
    {
        // 기초 클래스의 private 멤버는 public 멤버로 접근할 수 있다. 
        return derivedClassNumber + GetBaseClassNumber();
    }
};

  • 기초 클래스 포인터, 참조는 명시적 데이터형 변환 없이도 파생 클래스의 객체를 지시, 참조할 수 있다.
  • 반대는 안 된다.
DerivedClass derivedClass(5, 6);

// 기초 클래스로 파생 클래스를 지시, 참조할 수 있다. 
BaseClass * basePtr = &derivedClass;
BaseClass & baseRef = derivedClass;

// 기초 클래스의 메서드를 사용할 수 있다. 
basePtr->GetBaseClassNumber();
baseRef.GetBaseClassNumber();

// 파생 클래스의 메서드는 사용할 수 없다. 
basePtr->Sum(); // (X)
baseRef.Sum();  // (X)
BaseClass baseClass(5);

// 반대로
// 파생 클래스로 기초 클래스를 지시, 참조할 수 없다. 
DerivedClass * derivedPtr = &baseClass; // (X)
DerivedClass & derivedRef = baseClass;  // (X)

  • 따라서 기초 클래스 포인터, 참조를 매개변수를 사용하는 함수는 파생 클래스 객체에도 사용할 수 있다.
void Show(BaseClass & baseRef)
{
    cout << baseRef.GetBaseClassNumber() << endl;
}

int main()
{
    BaseClass baseClass(5);
    DerivedClass derivedClass(5, 6);

    Show(baseClass);
    Show(derivedClass);  // (O)
}

  • 파생 클래스의 객체로 기초 클래스 객체를 초기화하는 것도 간접적으로나마 허용한다.
  • 초기화하는 생성자의 원형은 BaseClass(const DerivedClass &);일 것이다.
  • 이러한 생성자를 만들지 않았으므로 암시적인 복사 생성자가 그 역할을 대신한다. BaseClass(const BaseClass &);
  • 기초 클래스의 참조는 파생 클래스를 참조할 수 있으므로, 파생 클래스의 멤버들을 복사한다.
DerivedClass derivedClass(5, 6);
BaseClass baseClass(derivedClass); // (O)

// 이러한 복사 생성자가 암시적으로 호출되었을 것이다. 
BaseClass::BaseClass(const BaseClass& b)
{
    baseClassNumber = b.baseClassNumber;
}

  • 대입도 마찬가지이다. BaseClass & operator=(const BaseClass &)과 같은 암시적인 오버로딩 대입 연산자를 대입에 사용한다.
DerivedClass derivedClass(5, 6);
BaseClass baseClass;
baseClass = derivedClass; // (O)



is-a 관계 #

  • is-a관계는 파생 클래스와 기초 클래스의 특별한 관계를 나타낸다.

  • 바나나와 과일
    • 바나나는 과일이다. 바나나 is a 과일.
    • is-a 관계는 파생 클래스가 기초 클래스이기도 하다는 것을 뜻한다.
    • 바나나(파생 클래스)는 과일(기초 클래스)이다.

  • 점심과 과일
    • 점심은 과일를 가진다. 점심 has a 과일. 점심이 과일인 것은 아니다.
    • 이것은 has-a 관계이다.
    • 점심 클래스는 데이터 멤버로 과일 객체를 가진다.



public 다형 상속 #

  • 호출하는 객체가 어떤 것인가에 따라서 메서드의 행동이 달라질 수 있다.

  • 메서드가 여러 가지 다른 행동을 할 수 있기 때문에 그러한 행동을 다형이라고 부른다.

  • 방법

    • 기초 클래스 메서드를 파생 클래스에서 다시 정의한다.
    • 그리고 virtual 키워드를 사용해서 가상 메서드로 만든다.
  • virtual 키워드

    • virtual 키워드를 사용한 메서드를 가상 메서드라고 한다.
    • virtual 키워드를 사용하면 각각의 클래스에서 서로 다른 행동을 하는 메서드을 만들 수 있다.
      • 만약 같은 행동을 한다면 기초 클래스에 단 한 번만 선언된다.
    • virtual 키워드를 사용하지 않을 경우, 프로그램은 참조형이나 포인터형에 기초하여 메서드를 선택한다.
    • virtual 키워드를 사용할 경우, 프로그램은 참조형이나 포인터형이 지시하는 객체가 무엇인가에 기초하여 메서드를 선택한다.
class Animal
{
public:    
    virtual void WhatIsThis()
    {
        cout << "이것은 Animal 객체입니다." << endl;
    }
};

class Dog : public Animal
{
public:
    virtual void WhatIsThis()
    {
        cout << "이것은 Dog 객체입니다." << endl;
    }
};

int main()
{
    Animal animal;
    Dog dog;

    Animal * aniPtr = &animal;
    Animal * dogPtr = &dog;

    aniPtr->WhatIsThis();  // Animal의 WhatIsThis() 호출
    dogPtr->WhatIsThis();  // Dog의 WhatIsThis() 호출
}
  • 출력 결과
    • 포인터가 실제 어떤 객체를 가리지는 지 판단해서 메서드를 선택한다.
이것은 Animal 객체입니다.
이것은 Dog 객체입니다.

  • 만약 virtual 키워드를 사용하지 않으면 출력 결과는 다음과 같다.
    • 포인터가 실제 어떤 객체를 가리키는 지에 상관없이 포인터형으로만 메서드를 선택한다.
이것은 Animal 객체입니다.
이것은 Animal 객체입니다.

  • virtual 파생 클래스 메서드에서 기초 클래스의 메서드를 부르는 방법
    • 사용 범위 결정 연산자를 사용한다.
class Dog : public Animal
{
public:
    virtual void WhatIsThis()
    {
        Animal::WhatIsThis();  // Animal 기초 클래스의 메서드를 호출한다. 
        WhatIsThis();           // 이것은 자기 자신을 부르는 재귀호출이다. 
        
        cout << "이것은 Dog 객체입니다." << endl;
    }
};

  • 가상 파괴자의 필요성
    • 파괴자가 가상이 아니면, 포인터형에 대한 파괴자만 호출될 것이다.
class Animal
{
public:
    ~Animal()
    {
        cout << "Animal 파괴자 호출됨" << endl;
    };
};

class Dog : public Animal
{
public:
    ~Dog()
    {
        cout << "Dog 파괴자 호출됨" << endl;
    }
};
// 예시 1

Animal * ptr = new Animal;
delete ptr;  // Animal 파괴자 호출
// 예시 2

Animal * ptr = new Dog;
delete ptr;  // Animal 파괴자 호출
// 예시 3

Dog * ptr = new Dog;
delete ptr;  // Dog 파괴자 호출 후, Animal 파괴자 호출

  • Dog객체를 지시하는 경우에도 Animal 객체의 파괴자만 호출한다. (예시 2번)
  • 하지만, 파괴자를 가상으로 만들면 실제로 지시하는 Dog 객체의 파괴자를 호출한 후 Animal객체의 파괴자를 호출한다.
class Animal
{
public:
    virtual ~Animal() // 가상 소멸자
    {
        cout << "Animal 파괴자 호출됨" << endl;
    };
};

class Dog : public Animal
{
public:
    ~Dog()
    {
        cout << "Dog 파괴자 호출됨" << endl;
    }
};
// 예시 2

Animal * ptr = new Dog;
delete ptr;  // Dog 파괴자 호출 후, Animal 파괴자 호출

일반적으로, 파괴자가 필요 없는 기초 클래스라 하더라도 가상 파괴자를 제공해야 한다.



정적 바인딩과 동적 바인딩 #

  • 바인딩(binding; 결합)

    • 함수가 호출되었을 때 어떤 블록을 실행할지 결합하는 것을 바인딩이라고 한다.
  • 정적 바인딩(static binding) = 초기 바인딩(early binding)

    • 컴파일 동안 일어나는 바인딩.
  • 동적 바인딩(dynamic binding) = 말기 바인딩(lately binding)

    • 프로그램 실행 시에 올바른 가상 메서드가 선택되도록 하는 바인딩.

  • C++은 일반적으로 한 데이터형의 주소를 다른 데이터형 포인터에 대입하는 것을 허용하지 않는다.
  • 참조하는 것도 허용하지 않는다.
double d = 1.5;

int * iPtr = &d; // (X)
int & iRef = d;  // (X)

  • 그러나 앞서 보았듯이, 상속하는 클래스는 허용된다.
DerivedClass d;

BaseClass * bPtr = &d; // (O) 업캐스팅
BaseClass & bRef = d;  // (O)

  • 업캐스팅(upcasting)
    • 파생 클래스의 참조/포인터를 기초 클래스의 참조/포인터로 변환하는 것을 업캐스팅이라고 한다.
    • public 상속에서는 명시적인 데이터형 변환이 없어도 업캐스팅이 허용된다.
    • 업캐스팅은 전이된다.
      • 예를 들어, DerivedClass를 상속받는 DerivedDerivedClass가 있다면, 이 클래스 또한 BaseClass에 의해 참조될 수 있다.

  • 다운캐스팅(downcasting)
    • 반대로 기초 클래스의 참조/포인터를 파생 클래스의 참조/포인터로 변환하는 것을 다운캐스팅이라고 한다.
    • 다운캐스팅은 명시적인 데이터형 변환 없이는 허용되지 않는다.
      • 왜냐하면 is-a관계는 일반적으로 대칭적이지 않기 때문이다. 예를 들어, 사과 is a 과일이지만, 과일 is a 사과는 아니다.

  • 암시적 업캐스팅 때문에 동적 바인딩이 필요한 것이다.
  • virtual 멤버 함수는 이러한 필요성 때문에 만들어 졌다.
    • 컴파일러는 virtual이 아닌 멤버 함수는 정적 바인딩을 사용한다.
    • 반면 virtual인 멤버 함수는 프로그램이 실행되는 동안에 결정되는 객체형에 따라서 맞는 함수를 바인딩한다. 즉, 동적으로 바인딩한다.

  • 왜 두 종류의 바인딩이 필요한가?
    • 효율성
      • 프로그램이 무언가를 실행 시간에 결정하려면, 기초 클래스 참조/포인터가 지시하는 객체가 무엇인지 추적하는 방법이 필요하다.
      • 이것은 가외의 처리 부담(다음 차례에 설명되어 있다)이 생긴다.
      • 따라서 동적 바인딩이 필요 없는 경우, 정적 바인딩이 좀 더 효율적이며,
      • C++에서는 정적 바인딩이 디폴트로 되어있다.
    • 개념 모델
      • 파생 클래스에서 다시 정의되는 것을 원하지 않는 멤버 함수는 가상이 아닌 함수로 만듦으로써,
      • 다시 정의되면 안 된다는 의도를 드러낸다.

  • 가상 함수는 어떻게 동작하는가?
    • 일반적으로 컴파일러는 각각의 객체에 숨겨진 멤버를 하나씩 추가한다.
      • 그 숨겨진 멤버는 어떤 배열을 지시하는 포인터이다.
      • 배열에는 해당 클래스의 객체들을 위해 선언된 가상 함수들의 주소가 저장되어 있다.
      • 이 배열을 가상 함수 테이블(virtual function table; vtbl) 이라고 한다.
    • 파생 클래스가 가상 함수를 다시 정의하지 않으면, vtbl은 그 함수의 오리지널 버전의 주소를 저장한다.
      • 반면, 파생 클래스가 새로운 함수를 정의하면, 그 주소가 vtbl에 저장된다.
    • 따라서 가상 함수를 호출하면, 프로그램은 객체에 있는 vtbl에 접근한다.
      • 사용하는 함수가 첫 번째 가상 함수라면, 프로그램은 vtbl의 첫 번째 주소를 사용한다.
    • 이렇듯 가상 함수를 사용하면 메모리, 실행 속도 면에서 약간의 부담이 따른다.
      • 각 객체의 크기가 커진다. (vtbl을 가리키는 주소를 저장하므로)
      • 컴파일러는 테이블을 만들어야 한다.
      • 함수 호출 시 테이블에 접근하는 가외의 단계가 더 필요하다.

  • 생성자는 가상으로 선언할 수 없다.
    • 파생 클래스의 객체 생성
      • (1) 파생 클래스의 생성자를 호출한다.
      • (2) 파생 클래스의 생성자가 기초 클래스의 생성자를 호출한다.
    • 이 시퀀스는 상속 매커니즘과 다르다. 그래서, 파생 클래스는 기초 클래스 생성자를 상속하지 않는다.
    • 따라서 가상으로 만들 이유가 전혀 없다.

  • 파생 클래스가 가상 함수를 다시 정의 하지 않으면?
    • 기초 클래스의 버전을 사용한다.
    • 만약, 파생 클래스가 길게 이어진 파생 사슬의 일부라면?
      • 가장 최근에 정의된 버전을 사용한다.
      • 다만, 기초 클래스의 버전이 은닉되어 있는 경우는 예외이다.

  • 가상 함수를 다시 정의하면, 메서드가 은닉된다.
class BaseClass
{
public:
    virtual void Show(int n) const; // 매개변수 존재
};

class DerivedClass : public BaseClass
{
public:
    virtual void Show() const; // 매개변수 없음.  
};

int main()
{
    DerivedClass derived;
    derived.Show();  // (O)
    derived.Show(1); // (X) 매개변수 없는 버전에 의해 가려졌다. 
}
  • 매개변수가 없는 새로운 정의는 매개변수가 있는 기초 클래스 버전을 가린다.

    • 다시 말하면, 상속된 메서드를 다시 정의하는 것은 오버로딩과는 다르다.
    • 매개변수 시그내처와는 상관 없이 같은 이름을 가진 모든 기초 클래스 메서드들을 가린다.
  • 따라서 이것 때문에 두 가지 규칙이 성립된다.

    • (1) 재정의 시 오리지널 원형과 정확히 일치시켜야 한다.
      • 예외: 리턴형의 공변(covariance)
        • 리턴형이 기초 클래스에 대한 참조/포인터인 경우 파생 클래스에 대한 참조/포인터로 대체 될 수 있다.
    • (2) 기초 클래스 선언이 오버로딩되어 있다면, 파생 클래스에서 모두 재정의해야 한다.
      • 만약 한 가지 버전만 제공하면, 그 한 가지가 나머지를 모두 가린다.
      • 변경이 필요 없다면 다음과 같이 재정의 할 수 있겠다.
class BaseClass
{
public:
    virtual BaseClass & ReturnTest();

    virtual void Show() const;
    virtual void Show(int n) const;
};

class DerivedClass : public BaseClass
{
public:
    virtual DerivedClass & ReturnTest(); // (1번 설명 예시) 동일한 함수 시그내처이다. 
    
    virtual void Show() const; 
    virtual void Show(int n) const
    {
        BaseClass::Show(n); // (2번 설명 예시) 필요 없다면, 이렇게 하면 된다. 
    }
};



접근 제어: protected #

  • 클래스 접근 제어를 위한 키워드 세 가지

    1. private
    2. protected
    3. public
  • protected

    • 바깥 세계에서 바라보면 private처럼 행동하지만,
    • 파생 클래스 입장에서는 public 멤버와 같이 행동한다.

가능하다면 protected 접근 제어보다 private 접근 제어를 사용해서 메서드를 통해 안전하게 기초 클래스의 데이터에 접근하도록 해야한다.



추상화 기초 클래스 #

  • 원과 타원 클래스를 만들고자 한다.
    • 원을 타원의 한 종류로 보아 타원을 상속받아서 원 클래스를 만들 수 있겠다.
    • 하지만 타원에 속한 것들 중 대다수는(반장경 변수, 반당경 변수, 회전시키기 함수 등)은 원에는 필요 없는 것들이다.
    • 그냥 따로 각각 클래스를 만드는 게 낫겠다.
    • 아니면 공통적인 것들만 뽑아서 기초 클래스를 만들어보던지?

  • 추상화 기초 클래스(abstract basic class; ABC)
    • 두 클래스의 공통적인 부분만 따로 뽑아서 만든 기초 클래스이다.
class BaseEllipse
{
private:
    double x, y;

public:
    BaseEllipse(double x0 = 0, double y0 = 0) : x(x0), y(y0) {}
    virtual ~BaseEllipse() {}

    void Move(int nx, int ny)
    {
        x = nx;
        y = ny;
    }

    virtual double Area() const = 0; // 순수 가상 함수
};

  • Area()는 원과 타원 각각 다르게 구현해야 하므로 순수 가상 함수(pure virual function) 로 만들었다.
    • 순수 가상 함수는 함수 선언 뒤에 =0을 가진다.
    • 순수 가상 함수는 반드시 정의를 할 필요는 없다. 해도 되고, 안 해도 된다.
    • 순수 가상 함수가 들어 있는 클래스는 자신의 객체를 생성할 수 없다.
      • 왜냐하면 순수 가상 함수는 기초 클래스의 역할을 하기 위해서만 존재하기 때문이다.
    • 어떤 클래스가 진짜 ABC가 되려면 순수 가상 함수가 적어도 하나 이상 있어야 한다.



상속과 동적 메모리 대입 #

1. 파생 클래스가 new를 사용하지 않는 경우.

  • 기초 클래스는 동적 메모리 대입을 사용한다. 그래서 파괴자, 복사 생성자, 대입 연산자를 가지고 있다.

  • 하지만 파생 클래스는 동적 메모리 대입을 사용하지 않는다.

  • 이 때, 파생 클래스에도 명시적 파괴자, 복사 생성자, 대입 연산자를 정의해야 할까?

    • 정답은 아니오이다.
  • 파괴자

    • 파생 클래스의 디폴트 파괴자는 기초 클래스의 파괴자를 항상 호출한다.
    • 따라서 디폴트 파괴자로 충분하다.
  • 복사 생성자

    • 파생 클래스의 디폴트 복사 생성자는 가지고 있는 기초 클래스의 성분을 복사하기 위해, 명시적인 기초 클래스의 복사 생성자를 사용한다.
    • 따라서 파생 클래스의 디폴트 복사 생성자로도 괜찮다.
  • 대입 연산자

    • 마찬가지로, 기초 클래스의 대입 연산자를 사용하므로 파생 클래스는 명시적으로 대입 연산자를 정의하지 않아도 된다.

2. 파생 클래스가 new를 사용하는 경우

  • 당연히 명시적 파괴자, 복사 생성자, 대입 연산자를 정의해야 한다.

  • 파괴자

    • 파생 클래스에서 사용한 메모리를 해제하는 명시적 파괴자를 만든다.
  • 복사 생성자

    • 파생 클래스의 복사 생성자는 자신의 데이터에만 접근할 수 있으므로 기초 클래스의 복사 생성자를 호출한다.
DerivedClass::DerivedClass(const DerivedClass & d) 
     : BaseClass(d)  // 기초 클래스의 복사 생성자를 호출한다. 
{
    // new로 메모리 할당 후 복사
}
  • 대입 연산자
    • 마찬가지로, 기초 클래스의 대입 연산자를 호출해준다.
DerivedClass & DerivedClass::operator=(const DerivedClass & d)
{
    // ...
    
    // 기초 클래스의 대입 연산자를 호출한다. 
    // *this = d; 와 같은 효과이다. 
    BaseClass::operator=(d); 
    
    //...
}

  • 기초 클래스의 friend에 접근하는 법
    • 프렌드는 멤버 함수가 아니기 때문에 사용 범위 결정 연산자로 접근할 수 없다.
    • 해결 방법
      1. 강제 데이터형 변환을 사용해서 접근한다.
      2. 나중에 소개할 dynamic_cast<> 연산자를 사용할 수도 있다.
class BaseClass
{
public:
    friend ostream & operator<<(ostream & os, const BaseClass & b)
    {
        os << "기초 클래스";
    }
};

class DerivedClass : public BaseClass
{
public:
    friend ostream & operator<<(ostream & os, const DerivedClass & d)
    {
        // 방법 1
        os << (const BaseClass &)d << " 그리고, 파생 클래스";
        
        // 방법 2
        os << dynamic_cast<const BaseClass &>(d) << " 그리고, 파생 클래스";
    }
};



그 외의 사항들 #

  • 상속되지 않는 것
    • 생성자
    • 파괴자
    • 대입 연산자

  • 파생 클래스 객체를 기초 클래스 객체에 대입하면 어떻게 될까?
    • 왼쪽에 있는 객체에 의해 호출되므로, 기초 클래스의 대입 연산자가 호출된다.
    • 따라서 파생 클래스가 가지는 값들은 모두 무시된다.
BaseClass base;
DerivedClass derived;

base = derived;  // BaseClass의 대입 연산자가 호출된다. 

  • 반대로 기초 클래스 객체를 파생 클래스에 대입하면 어떻게 될까?
    • 파생 클래스의 대입 연산자가 호출된다.
    • 파생 클래스의 대입 연산자의 매개변수는 파생 클래스이다.
    • 하지만 파생 클래스는 자동으로 기초 클래스를 참조할 수 없다.
    • 대안
      1. 변환 생성자를 만든다.
      2. 기초 클래스를 대입하기 위한 또다른 대입 연산자를 만든다.
class DerivedClass
{
public:
    // 변환 생성자 (방법 1)
    DerivedClass(const BaseClass & b)
    {
        //...
    }
    
    // 대입 연산자 
    DerivedClass & operator=(const DerivedClass & d)
    {
        //...
    }
    
    // 기초 클래스를 대입하기 위한 또다른 대입 연산자 (방법 2)
    DerivedClass & operator=(const BaseClass & b)
    {
        //...
    }
};

int main()
{
    BaseClass base;
    DerivedClass derived;

    derived = base;  // DerivedClass의 대입 연산자가 호출된다. 
}

  • 부적절한 코드는 동적 결합을 불가능하게 만들 수 있으므로 주의하자.
class BaseClass
{
public:
    virtual void View() const;
};

class DerivedClass : public BaseClass
{
public:
    virtual void View() const;
};

void CallByRefernce(const BaseClass & b)
{
    b.View(); 
}

void CallByValue(BaseClass b)
{
    b.View();
}

int main()
{
    DerivedClass derived;
    
    // 실제 참조하는 객체인 DerivedClass의 View()를 호출된다. 
    CallByRefernce(derived); 
    
    // BaseClass(const BaseClass &)생성자로 만든 BaseClass의 View()를 호출한다. 
    CallByValue(derived);
}

  • 멤버 함수의 특성
함수 상속 멤버 또는 프렌드 디폴트로 생성 가상으로 선언 리턴형
생성자 X 멤버 O X X
파괴자 X 멤버 O O X
= X 멤버 O O O
& O 둘 중 하나 O O O
변환 O 멤버 X O X
() O 멤버 X O O
[] O 멤버 X O O
-> O 멤버 X O O
op= O 둘 중 하나 X O O
new O static 멤버 X X void *
delete O static 멤버 X X void
기타 연산자 O 둘 중 하나 X O O
기타 멤버 O 멤버 X O O
프렌드 X 프렌드 X X O