KoreanFoodie's Study

C++ 기초 개념 6-3 : virtual 소멸자, 가상 함수 테이블(virtual function table), 다중 상속, 가상 상속 본문

Tutorials/C++ : Beginner

C++ 기초 개념 6-3 : virtual 소멸자, 가상 함수 테이블(virtual function table), 다중 상속, 가상 상속

GoldGiver 2022. 1. 6. 17:53

모두의 코드를 참고하여 핵심 내용을 간추리고 있습니다. 자세한 내용은 모두의 코드의 씹어먹는 C++ 강좌를 참고해 주세요!

vitrual 소멸자와 메모리 누수

사실 virtual 소멸자를 사용하는 이유는, 메모리 누수(memory leak)을 막기 위해서이다. 다음 예제 코드를 보자.

class parent {

public:
	parent() { std::cout << "부모 생성자" << std::endl; }
	virtual ~parent() { std::cout << "부모 소멸자" << std::endl; }
};

class child : public parent {

public:
	child() { std::cout << "자식 생성자" << std::endl; }
	~child() { std::cout << "자식 소멸자" << std::endl; }
};

int main() {
	parent* p_c = new child();
	std::cout << "=== p_c 소멸자 호출 ===" << std::endl;
	delete p_c;
}

다음과 같이 parent 클래스에서 소멸자를 virtual로 만들어 주면, parent 타입으로 받은 변수도 실제 child 클래스의 소멸자를 제대로 호출하여 child 객체의 메모리가 해제되지 않는 상황이 방지된다.

 

출력은 다음과 같다.

부모 생성자
자식 생성자
=== p_c 소멸자 호출 ===
자식 소멸자
부모 소멸자

이때 마지막에 부모 소멸자는 '알아서' 호출된다. child가 parent를 상속받고 있기 때문이다.

virtual 함수의 경우, 객체를 포인터가 아닌 레퍼런스 타입으로 받아도 알아서 실제 객체에 맞는 구현된 함수를 호출한다.

 

 

가상 함수의 구현 원리

모든 함수를 가상함수로 만들면 편할 것 같지만, 사실 가상함수는 오버헤드가 크다. 왜냐하면 virtual 함수는 런타임에 어떤 함수가 실행할지를 결정하는 동적 바인딩 방식이기 때문이다.

 

C++ 컴파일러는 가상 함수가 하나라도 존재하는 클래스에 대해 가상 함수 테이블(virtual function table; vtable)을 생성한다. 다음과 같은 클래스가 있다고 해 보자.

class Parent {
 public:
  virtual void func1();
  virtual void func2();
};
class Child : public Parent {
 public:
  virtual void func1();
  void func3();
};

 

두 개의 클래스는 모두 가상 함수 테이블을 생성한다.

 

만약 다음과 같은 코드가 있다고 가정해 보자.

Parent* p = Parent();
p->func1();

Parent* c = Child();
c->func1();

p의 경우, func1이 가상함수이므로, 가상함수 테이블을 한 번 더 참조하여 Parent::func1()을 호출하게 되고, c의 경우에도 마찬가지로 가상함수 테이블을 참조하여 Child::func1()을 호출하게 된다. 이는 오버헤드가 추가되는 것을 의미하므로, C++의 멤버 함수는 디폴트로 가상함수가 되도록 만들어지지 않은 것이다!

 

 

순수 가상 함수(pure virtual function)과 추상 클래스(abstract class)

먼저 코드 예제를 보자.

class fruit {
public:
	fruit() {}
	virtual ~fruit() {}
	virtual void name() = 0;
};

class apple : public fruit {
public:
	~apple() {}
	void name() override { std::cout << "apple" << std::endl; }
};

class banana : public fruit {
public:
	~banana() {}
	void name() override { std::cout << "banana" << std::endl; }
};

int main() {

	//fruit* f = new fruit(); // abstract class는 인스턴스화가 불가능하다.
	fruit* a = new apple();
	fruit* b = new banana();

	a->name();
	b->name();
}

이때, fruit의 name 함수는 순수 가상 함수(pure virtual function)으로, 상속받은 클래스에서 반드시 재정의해야 하는 함수를 의미한다. 순수 가상 함수를 가지는 클래스를 추상 클래스(abstract class)라고 하며, 추상클래스는 인스턴스화가 불가능하다.

 

 

다중 상속 (multiple inheritance)

C++에서는 자바와 다르게 다중 상속이 가능하다. 즉, 한 클래스가 다른 여러 개의 클래스들을 상속받는 것이 허용된다.

#include <iostream>

class A {
 public:
  int a;

  A() { std::cout << "A 생성자 호출" << std::endl; }
};

class B {
 public:
  int b;

  B() { std::cout << "B 생성자 호출" << std::endl; }
};

class C : public A, public B {
 public:
  int c;

  C() : A(), B() { std::cout << "C 생성자 호출" << std::endl; }
};
int main() { C c; }

c에서 생성자 호출 출력 결과는 A->B->C 순이 되는데, 만약 상속을 받을 때 class C : public B, public A로 정의를 하면 순서가 B->A->C로 바뀐다.

 

다중 상속 시 주의점1 : 이름이 같은 멤버 변수

class A {
 public:
  int a;
};

class B {
 public:
  int a;
};

class C : public B, public A {
 public:
  int c;
};

위의 경우, 변수 이름이 똑같이 a인 녀석을 상속하고 있으므로, 클래스 C 타입의 변수 a에 접근하려고 하면 에러가 발생한다.

 

다중 상속 시 주의점2 : 다이아몬드 상속(diamond inheritance)

다이아몬드 상속(diamond inheritance)은 다음과 같은 형태의 상속 관계를 의미한다.

class Human {
  // ...
};
class HandsomeHuman : public Human {
  // ...
};
class SmartHuman : public Human {
  // ...
};
class Me : public HandsomeHuman, public SmartHuman {
  // ...
};

 

즉, Me 객체가 Me::HansomeHuman::Human 객체와 Me::SmartHuman::Human 객체를 전부 가지게 되는 것이 문제인 것이다. 따라서, 상속을 받을때 virtual하게 받도록 만들면 이 문제를 해결할 수 있다.

class Human {
protected: 
	std::string name;
public: 
 	Human(const std::string& name_) : name(name_) {
 		std::cout << "Human constructor" << std::endl;
 	}
 	void print_name() { std::cout << "name : " << name << std::endl; }
};

class HandsomeHuman : public virtual Human {
	public: 
		HandsomeHuman(const std::string& name_) : Human(name_) {
			std::cout << "HandsomeHuman constructor" << std::endl;
		}
};

class SmartHuman : public virtual Human {
	public: 
		SmartHuman(const std::string& name_) : Human(name_) {
			std::cout << "SmartHuman constructor" << std::endl;
		}
};

class Me : public HandsomeHuman, public SmartHuman {
	public: 
		Me(const std::string& name_) : HandsomeHuman(name_), SmartHuman(name_), Human(name_) {}
};

int main() {
	Me* me = new Me("tony");
	me->print_name();
}

출력은 다음과 같다.

Human constructor
HandsomeHuman constructor
SmartHuman constructor
name : tony

따라서 실제 Human 객체는 단 하나만 존재하게 되는 것이다.

 

다중 상속을 언제 사용할지에 대한 글은 여기롤 참고해서 더 읽어보도록 하자.

Comments