본문으로 바로가기

Inheritance

C++에서 상속(Inheritance)을 사용하는 4가지 이유

  • Class relationship: 클래스간의 관계를 설정하기 위해서
  • Code reuse: 클래스간의 관계를 설정하면 코드를 재사용 가능
  • Class interface consistency: 일관적인 인터페이스를 구현하기 위함 (interface, abstract, pure virtual function)
  • Dynamic function binding: 런타임에 동적으로 함수를 지정하기 위함 (virtual function, virtual table)

상속에서 접근지시자

부모 클래스를 상속받을 때 public, protected, private 세 가지 접근 지시자를 활용할 수 있다. 각각 사용된 접근 지시자에 따라 접근 가능 여부는 다음과 같다.

class A 
{
    public:
       int x;
    protected:
       int y;
    private:
       int z;
};

class B : public A
{
    // x is public
    // y is protected
    // z is not accessible from B
};

class C : protected A
{
    // x is protected
    // y is protected
    // z is not accessible from C
};

class D : private A    // 'private' is default for classes
{
    // x is private
    // y is private
    // z is not accessible from D
};

상속관계에서 생성자 소멸자 호출 순서

상속관계에서는 반드시 다음과 같은 순서로 생성자와 소멸자가 호출된다.

부모 클래스 생성자 → 자식 클래스 생성자 → 자식 클래스 소멸자 → 부모 클래스 소멸자

아래의 예시에서 생성자와 소멸자가 호출되는 순서를 살펴보자.

stack에 객체를 생성하는 경우

#include <iostream>
class Animal {
public:
    Animal()
    {
        std::cout << "Animal()" << std::endl;
    }
    ~Animal()
    {
        std::cout << "~Animal()" << std::endl;
    }
};
class Cat : public Animal {
public:
    Cat()
    {
        std::cout << "Cat()" << std::endl;
    }
    ~Cat()
    {
        std::cout << "~Cat()" << std::endl;
    }
};

int main()
{
    Cat cat;
    return 0;
}

출력

Animal()
Cat()
~Cat()
~Animal()

Heap에 객체 생성 시

new를 활용하여 Heap 공간에 Cat의 객체를 생성할 때에도 같은 결과를 얻을 수 있다.

int main()
{
    Cat * cat = new Cat();
    delete cat;
    return 0;
}

출력

Animal()
Cat()
~Cat()
~Animal()

소멸자 가상화

상속관계에서 다형성을 구현하기 위해 Animal 포인터 변수에 Cat 객체를 저장할 경우 Cat의 소멸자는 호출되지 않는 문제가 발생한다.

int main()
{
    Animal * polyCat = new Cat();
    delete polyCat;
    return 0;
}

출력

Animal()
Cat()
~Animal()

문제를 해결하기 위해서는 부모 클래스의 소멸자를 반드시 가상화해야 한다.

class Animal {
public:
    Animal()
    {
        std::cout << "Animal()" << std::endl;
    }
    virtual ~Animal()
    {
        std::cout << "~Animal()" << std::endl;
    }
};
// class Cat 생략
int main()
{
    Animal * polyCat = new Cat();
    delete polyCat;
    return 0;
}

출력

Animal()
Cat()
~Cat()
~Animal()

동적 다형성

상속과 virtual을 활용하면 런타임 시간에 생성될 객체와 호출될 함수를 지정할 수 있다. 런타임에 결정되기 때문에 Dynamic Porymorphism 이라고도 부른다. 아래의 예시의 main 함수를 살펴보자. 런타임 시간에 받는 입력을 통해서 Cat 생성자를 호출할지, Dog 생성자를 호출할지 결정하게 된다. 그리고 animPtr에서 Speak함수를 호출할 때 생성된 객체가 Cat이라면 Cat의 Speak함수가 호출되고, Dog라면 Dog의 Speak함수가 호출됨을 확인할 수 있다.

class Animal {
public:
    virtual ~Animal() = default;
    virtual void Speak()
    {
        std::cout << "Animal" << std::endl;
    }
};
class Cat : public Animal {
public:
    void Speak() override
    {
        std::cout << "mewo~" << std::endl;
    }
};
class Dog : public Animal {
public:
    void Speak() override
    {
        std::cout << "bark!" << std::endl;
    }
};

int main()
{
    std::vector<Animal*> animals;

    // dynamic, runtime polymorphism
    for (int i = 0; i < 5; ++i)
    {
        int type;
        std::cin >> type;
        if (type == 1)
            animals.push_back(new Cat());
        else
            animals.push_back(new Dog());
    }

    for (auto animPtr : animals)
    {
        animPtr->Speak();
        delete animPtr;
    }
    return 0;
}

입력

1 2 1 1 2

출력

mewo~
bark!
mewo~
mewo~
bark!

다음 글에서는 함수를 가상화했을 때 어떻게 다형성이 동작하는지에 대해서 메모리 수준에서 살펴보고 가상 함수 테이블에 대한 개념을 다뤄본다.

 


출처: https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg