[C++] 상속과 다형성, 가상함수
LastMod:
👨💻 개인 공부 기록용 블로그 입니다.
💡 틀린 내용이나 오타는 댓글, 메일로 제보해주시면 감사하겠습니다!! (__)
42 Cursus - Cpp Module을 진행하기 위해 정리했었던 C++98 기본 개념들로, C++11, C++14와 다른 내용이 있을 수 있습니다.
각 개념은 정리한 시간 순으로 배치되어 있으며, 한 포스트에 배치된 개념이 크게 관련이 없을 수 있습니다.
상속 (Inheritance)
- 한 클래스가 다른 클래스의 속성을 물려받는 것.
-
기존에 작성된 부분을 재활용할 수 있고, 공통적인 부분을 기초 클래스에 작성하여 파생 클래스에서 중복되는 부분을 제거할 수 있다는 장점을 가진다.
class Parent { private: secret; public: opened; } class Child : public Parent { // Child는 Parent를 public 하게 상속 받는다. // public 자리에는 다른 접근지정자가 올 수 있다. // 만약 private가 온다면, private 보다 접근 범위가 넓은 멤버는 전부 private로 상속받는다. // 그렇게 되면 Child::opened는 private가 되어 밖에서 접근할 수 없게 된다. }
- 내부적으로 Child는 Parent와 완전히 다른 객체가 아니다. Parent위에 Child가 얹어지는 방식이라고 보는게 편하다.
- 즉, Parent 만 가지고 있는 부분은 Parent 객체와 다를 바 없고, Child 객체에서 추가되는 멤버들은 상속하는 객체위에 따로 올라가는 것이다.
- 따라서 파생클래스의 생성자는 원본 클래스의 기본 생성자 → 파생 클래스의 해당하는 생성자, 파생클래스의 소멸자는 파생 클래스의 소멸자 → 원본 클래스의 소멸자 순으로 호출된다.
다형성 (Polymorphism)
- 모습은 같은데 형태는 다른 것.
- 같은 이름을 가지는 각 요소들이 다양한 자료형에 속하는 것이 허가되는 성질. 같은 이름의 함수여도, 다양한 인자와 다양한 리턴 값을 가질 수 있게 해준다.
virtual
을 이용한 가상함수의 동적 바인딩과, 템플릿, 오버로딩 등이 다형성의 예이다.- C++에는 대표적으로 4가지 다형성이 있다.
- 임시 다형성 (Ad-hoc Polymorphism) : 함수 및 연산자 오버로딩
- 서브타입 다형성 (Subtype Polymorphism) : 런타임 다형성 (Runtime Polymorphism) 이라고도 한다. ‘다형성’ 이라고 했을 때 뜻하는 일반적인 것이며, 다형성 함수는 vtable을 통하여 실행 중 어떤 함수를 호출해야 할 지 고르게 된다.
- 강제 다형성 : 캐스팅
- 매개변수 다형성 : 컴파일타임 다형성 (Complie-time Polymorphism) 이라고도 한다. 주로 템플릿을 뜻한다.
함수 오버라이딩 (Function Overriding)
- 사실 C++에서는 오버라이딩이라는 단어는 쓰지 않고, 함수 재정의 (Function Redefinition) 이라는 단어를 쓴다.
- 일반적으로 함수의 재정의는 불가능하지만, 파생 클래스에서는 부모 클래스의 멤버 함수를 재정의할 수 있다. 이를 다른 언어에서는 오버라이딩 (Overriding)이라고 한다.
class A
{
public:
void hello()
{
std::cout<<"this is a\n";
}
};
class B : public A
{
void hello()
{
std::cout<<"this is b\n";
}
};
// B::hello(); 를 호출하면 this is b가 출력된다.
가상 함수 (Virtual Functions)
-
위의 함수 오버라이딩의 예제는 정상적으로 동작하지만, 다음과 같은 어지러운 상황이 생길 수 있다.
class A { public: void hello() { std::cout<<"this is a\n"; } }; class B : public A { void hello() { std::cout<<"this is b\n"; } }; A a; B b; A* pointer = &b; // 놀랍게도 가능하다, B는 A를 상속받는 클래스이기 때문이다. pointer->hello() // ??????
- 위와 같은 상황에서, 마지막 코드는 this is a를 출력하게 된다.
Virtual 키워드가 갖는 의미와 동적 바인딩 (Dynamic Binding)
virtual
키워드를 통해 런타임에 어떤 함수를 호출해야할 지 가리키는 것을 동적 바인딩 (Dynamic Binding)이라고 한다.- 즉,
virtual
키워드는 컴파일러에게, “이 함수는 상속되어 재구현 됐을 수 있어” 라고 알려주는 것과 같다. -
virtual
키워드가 하나라도 붙는다면, 내부적으로vtable
이라는 가상함수 테이블을 생성하게 된다.위와 같이, vtable을 통해 런타임에 어떤 함수를 호출할 지 고르게 된다.
- 즉, 가상함수는 항상
vtable
을 거치기 때문에, 다른 함수보다 오버헤드가 크다.
가상 소멸자 (Virtual Destructor)
- 단순하게 소멸자에
virtual
키워드가 붙은 것이다. - 이게 왜 필요해?? 라고 생각할 수 있지만, 부모 클래스 포인터가 자식 객체를 가리키고 있다고 해보자.
- 부모 클래스 포인터는 자식 클래스의 내용을 담을 수 없기 때문에, 위 자식 객체를 소멸시키게 되면 자식 클래스의 소멸자가 아닌 부모 클래스의 소멸자가 호출된다.
- 그렇게 되면, 자식 클래스만 가지고 있는 유니크한 부분은 모두 메모리 누수이다.
- 따라서 상속될 여지가 있다면, 그 클래스의 소멸자는 항상 가상함수로 만들어 주는게 바람직 하다.
순수 가상 함수 (Pure Virtual Functions)와 추상 클래스 (Abstract Class)
- 가상 함수 뒤에
=0
이 붙는 것을 순수 가상 함수(Pure Virtual Function) 라고 한다. - 이는 상속받은 객체에서 해당 함수를 반드시 재구현 해야 한다는 의미이다.
- 순수 가상 함수를 포함하는 클래스를 추상 클래스 (Abstract Class) 라고 하며, 추상 클래스를 객체화 할 수 없다.
- 이는 조금만 생각해보면 당연하다. 멤버 함수 중 껍데기만 있는 게 존재하는데, 객체화 해서 이를 호출한다면?
다이아몬드 상속 문제 (Diamond “Multiple” Inheritance Problem)
class A
{};
class B1 : public A
{};
class B2 : public A
{};
class C : public B1, public B2
{};
- C++은 다중 상속이 가능하다 보니, 1번과 같은 미친(?) 형태가 나올 수 있다. (실제로는 메모리에 2번과 같이 적재된다.)
- 이게 뭐가 문제야? 라고 생각할 수 있지만, C 클래스를 보게 되면 A클래스가 겹치게 되는 문제가 생긴다.
- C에서 A에 있는 부분을 호출하려면, 컴파일러는
C::B1::A
를 호출해야 할 지,C::B2::A
를 호출해야할 지 고민에 빠지고, 오류를 뱉게 된다. - 이를 해결하기 위해서, B1과 B2가 A를 상속 받을 때, 가상으로 상속 받으면 된다.
class B1 : virtual public A {};
- 가상 상속은 상속 시 부모 타입의 메모리가 중복되지 않도록 방지해준다.
- 하지만, C가 A의 생성자를 명시적으로 호출해주어야 한다.
- 이는 항상 메모리를 절약하는 방법은 아니다. 오버헤드가 커질 수 있다. 그래서 다중 상속 자체를 지원하지 않는 언어도 많다.
Reference
CPP 레퍼런스 (파생 클래스와 virtual 클래스 상속)
C++ 기초 개념 6-3 : virtual 소멸자, 가상 함수 테이블(virtual function table), 다중 상속, 가상 상속
Leave a comment