[C++] virtual 소멸자

·2023년 8월 23일
0

C++

목록 보기
13/17
post-custom-banner

virtual 소멸자

상속 시 소멸자를 가상 함수로 만들어야 함

#include <iostream>

class Parent 
{
 public:
  Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
  ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};

class Child : public Parent 
{
 public:
  Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
  ~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};

int main() 
{
  std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
  { Child c; }
  
  std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
  {
    Parent *p = new Child();
    delete p;
  }
}

실행 결과
--- 평범한 Child 만들었을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
--- Parent 포인터로 Child 가리켰을 때 ---
Parent 생성자 호출
Child 생성자 호출
Parent 소멸자 호출

delete p;를 하더라도 p가 가리키는 것은 Parent 객체가 아닌 Child 객체이기 때문에
보통의 Child 객체가 소멸되는 것과 같은 순서로 생성자와 소멸자들이 호출되어야 함
-> 그러나 실제로는 Child의 소멸자가 호출되지 않음
Child 객체에서 동적으로 할당하는 메모리가 있고, 소멸자에서 해제하는데
소멸자가 호출되지 않는다면 메모리 누수(memory leak)가 발생됨

=> virtual 키워드를 통해 해결할 수 있음

#include <iostream>

class Parent 
{
 public:
  Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
  virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};

class Child : public Parent 
{
 public:
  Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
  ~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};

int main() 
{
  std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
  { 
    // 이 {} 를 빠져나가면 c 가 소멸된다.
    Child c; 
  }
  std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
  {
    Parent *p = new Child();
    delete p;
  }
}

실행 결과
--- 평범한 Child 만들었을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
--- Parent 포인터로 Child 가리켰을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출

Child 소멸자를 호출하면서 Child 소멸자가 알아서 부모의 소멸자도 호출해 주기 때문에
Parent 소멸자도 호출이 됨
반면 Parent 소멸자를 먼저 호출하면, Parent는 Child가 있는지 없는지 모르므로
Child 소멸자를 호출 해 줄 수 없음


가상 함수의 구현 원리

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

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

컴파일러는 가상 함수가 하나라도 존재하는 클래스에 대해서 가상 함수 테이블을 만들게 됨
가상 함수를 호출할 때는 해당 테이블을 거쳐서 어떤 함수를 사용할지 정하게 됨

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

컴파일러는
(1) p가 Parent를 가리키는 포인터니까, func1()의 정의를 Parent 클래스에서 찾아봐야지
(2) func1()은 가상함수네? func1()을 직접 실행하는 게 아니라 가상 함수 테이블에서 func1() 에 해당하는 함수를 실행해야지
(3) 프로그램 실행 시에 가상 함수 테이블에서 func1()에 해당하는 함수를 호출함

-> 즉 일반적인 함수 보다 약간 더 시간이 오래 걸림
C++에서는 디폴트로 모든 멤버 함수를 가상 함수가 되도록 설정하지 않고
필요 시 virtual 키워드를 붙여서 사용함


순수 가상 함수

#include <iostream>

class Animal {
 public:
  Animal() {}
  virtual ~Animal() {}
  virtual void speak() = 0;
};

class Dog : public Animal {
 public:
  Dog() : Animal() {}
  void speak() override { std::cout << "왈왈" << std::endl; }
};

class Cat : public Animal {
 public:
  Cat() : Animal() {}
  void speak() override { std::cout << "야옹야옹" << std::endl; }
};

int main() {
  Animal* dog = new Dog();
  Animal* cat = new Cat();

  dog->speak();
  cat->speak();
}

가상 함수에 =0;을 붙여서 반드시 오버라이딩 되도록 만든 함수를 순수 가상 함수라고 부름
순수 가상 함수는 본체가 없기 때문에 이 함수를 호출하는 것은 불가능함
-> 그렇기 때문에 Animal 객체를 생성하는 것 또한 불가능함

// 즉, 아래와 같은 코드는 불가능
Animal a;
a.speak();

Animal처럼 순수 가상 함수를 최소 한 개 이상 포함하고 있는 클래스는
객체를 생성할 수 없으며 인스턴스화 시키기 위해서는 이 클래스를 상속 받는
클래스를 만들어서 모든 순수 가상 함수를 오버라이딩 해 주어야만 함
-> 이렇게 순수 가상 함수를 최소 한 개 이상 포함하고 있는 클래스를 가리켜
추상 클래스라고 부름

(참고)
private 안에 순수 가상 함수를 정의하여도 문제 될 것이 없음
private 에 정의되어 있다고 해서 오버라이드 안된다는 뜻이 아니기 때문
(다만 자식 클래스에서 호출 불가)

추상 클래스는 비록 객체는 생성할 수 없지만 추상 클래스를 가리키는 포인터는
문제 없이 생성 가능함

Animal* dog = new Dog();
Animal* cat = new Cat();

dog->speak();
cat->speak();

다중 상속

한 클래스가 여러 개의 클래스를 상속 받는 것
생성자 호출 순서는 상속하는 순서에 좌우됨

#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; }

실행 결과
A 생성자 호출
B 생성자 호출
C 생성자 호출

A -> B -> C 순으로 호출됨
만약 상속 순서를 class C : public B, public A로 변경하면
B -> A -> C 순으로 호출됨


다중 상속 시 주의할 점

// 같은 멤버 변수 이름을 가진 클래스들을 상속받은 경우

class A 
{
 public:
  int a;
};

class B 
{
 public:
  int a;
};

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

int main() 
{
  C c;
  c.a = 3;
}

컴파일 시 오류 발생
c.a가 A의 a인지 B의 a인지 구분 할 수 없기 때문

// 다이아몬드 상속

class Human 
{
  // ...
};

class HandsomeHuman : public Human 
{
  // ...
};

class SmartHuman : public Human 
{
  // ...
};

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

상속이 되는 두 개의 클래스가 공통의 베이스 클래스를 포함하고 있는 형태를 가리켜서
다이아몬드 상속이라고 부름

만약 Human에 name이라는 멤버 변수가 있다면 HandsomeHuman과 SmartHuman 모두 name이라는 변수를 가짐
그런데 Me 가 이 두 개의 클래스를 상속 받으니 name 이라는 변수가 겹치게 됨

-> 해결 방법
Human을 virtual로 상속 받음

class Human 
{
 public:
  // ...
};

class HandsomeHuman : public virtual Human 
{
  // ...
};

class SmartHuman : public virtual Human 
{
  // ...
};

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

virtual 형태로 Human을 상속 받으면 Me에서 다중 상속 시에도
컴파일러가 언제나 Human을 한 번만 포함하도록 지정할 수 있게 됨

(참고)
가상 상속 시 Me의 생성자에서 HandsomeHuman, SmartHuman, Human의 생성자를 호출해주어야만 함

post-custom-banner

0개의 댓글