Programming/C++

[Effective C++] Chapter 2. 생성자, 소멸자 및 대입 연산자(2)

며용 2022. 6. 12. 22:46

항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자

 

기본 클래스에 virtual 소멸자가 없다면, 파생 클래스의 소멸자 실행이 건너뛰어져 발생하는 메모리릭을 방지하도록 해야함

  • 가상 함수를 하나라도 가진 클래스 (=기본 클래스로 사용하겠다) --> 클래스의 소멸자도 가상 소멸자여야 함
    • 자연스럽게 소멸자가 가상 소멸자로 되어 있으면 기본 클래스구나 하고 인식할 수 있음
  • 추상 클래스(=기본 클래스로 쓰일 목적)로 만들고 싶은 클래스 --> 순수 가상 소멸자를 선언하자(=순수 가상함수 있으면 추상 클래스)

 

class Base
{
public:
    Base();
    virtual ~Base();
}

clase Object : public Base
{
public:
    Object();
    ~Object();
}

int main()
{
    Base *pBase = new Object;
    delete pBase; 
}

 

기본 클래스에서 소멸자를 가상 소멸자로 선언하지 않을 경우?

delete pBase를 할 경우 Base의 소멸자만 호출함 --> Object의 소멸자가 호출되어야함.

 

==> virtual ~Base(); 이를 막기 위해서 Base 클래스에는 virtual 소멸자를 사용하자 (파생클래스 소멸자는 생략 가능)

 

  • 파생클래스 소멸자 --> 기본 클래스 소멸자 순으로 작동함
    • 부모 클래스 변수에 자식 클래스를 동적할당 해야한다면 virtual을 붙여서 실제 인스턴스된 객체의 함수가 불릴 수 있도록 하자
  • virtual을 붙이면 virtual table(가상 함수를 가리키는 function pointer table)을 갖게 되면서 class 사이즈가 커지게 되므로 --> 기본클래스/다형성을 갖도록 설계된 클래스에만 가상 소멸자를 선언하자)
    • virtual 키워드를 쓰는 순간 32bit 체제에서는 32bit / 64bit 에서는 64bit 만큼 객체의 크기가 커지게 됨
      (가상함수테이블(어떤 가상 함수를 호출해야 하는지 결정하는 정보)을 가리키는 포인터의 크기)
    • 생각없는 가상 소멸자 선언은 객체의 크기만 키움

 

더보기
#include <iostream>
using namespace std;

class A
{
public:
    ~A() {}
};

class B : public A
{
public:
    ~B() {}
};

int main()
{
    //문제가 되는 경우
    A* a = new b;
    delete a;
    
    //문제가 되지 않는 경우
    B* b = new B;
    delete b;
    //std::string / STL 컨테이너 같은 타입 클래스들은 가상 소멸자가 없어 기본 클래스로 두게 되면 문제가 생김
    //B를 생성하고 B를 지우기 때문에 문제가 되지 않음. 
    //but B를 만들고 A(ex) standard vector)를 지우면 메모리 누수가 될 수 있음
    
    return 0;
}

 

 

 


항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자

 

소멸자가 호출되는 경우

  • 정상적으로 객체가 종료되었을 때
  • 예외처리 매커니즘에 의해 객체가 소멸될 때

 

어떤 동작이 예외를 일으키며 fail할 가능성이 있고 그 예외를 처리해야할 필요가 있다면

--> 소멸자가 아닌 다른 함수에서 먼저 처리하도록 하자

--> 일반 함수에서 예외 발생 시 소멸자에서 예외를 삼키던지/프로그램을 끝내던지 해야함

 

public:
    ~Object()
    {
        base.sth();
    }
private:
    Base base;
  • sth()에서 예외가 발생하게 되면, 잘 될 수도 있지만 정의되지 않은 행동을 할 수도 있다.
    • ==> try-catch 사용하여 예외가 다른 곳으로 전파되는 것을 막자
      • 프로그램을 바로 끝내거나(abort()) 실패한 로그를 출력하거나 assert 걸거나
    • ==> 또는 예외 발생이 소멸자가 아닌 다른 함수에서 비롯되어야 한다.
      • void sth()
        {
            base.sth();
            check = true;
        }
    • 소멸자에서는 예외가 빠져나가면 안된다. 소멸자에서 끝내야 함

 

class DBConnector
{
public:
    void close()
    {
        //일반 함수에서 실패 가능성 있는 동작을 실행
        //정상 동작 시 true를 반환하는 것으로 가정
        closed = db.close();
    }
    ~DBConnector()
    {
        //소멸자에서 한 번 더 동작
        if(!closed) 
        try
        {
            db.close();
        }
        catch()
        {
            //그래도 실패하면 로그 등 예외 처리
            LOG("Failed");
        }
    }
private:
    DBConnection db;
    bool closed; //예외 발생 여부 먼저 체크
};

사용자 정의가 아닌 소멸자에서만 close()를 실행하게 되면 예외가 소멸자를 떠나게 됨.

PC버전은 assert를 걸고 임베디드는 Log를 남기자

 

 

try-catch?

==> cpp에선 사용하지 않음

refer: https://google.github.io/styleguide/cppguide.html#Exceptions

 

Google C++ Style Guide

Google C++ Style Guide Background C++ is one of the main development languages used by many of Google's open-source projects. As every C++ programmer knows, the language has many powerful features, but this power brings with it complexity, which in turn ca

google.github.io

(c와 cpp를 같이 사용하는 케이스가 많은데 c에선 exception을 제공하지 않음)

(예외 처리를 위한 점프 코드를 넣어야 할 수도 있고)

 

 

 


항목 9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자

 

초기화되지 않은 데이터 멤버는 정의되지 않은 상태에 있다!

 

생성자/소멸자 안에서 가상 함수를 호출하면 안됨

--> 가상함수여도 생성 중이거나 소멸 중일 떄는 파생 클래스와 관련이 없게 됨

 

Base 클래스를 상속받는 Derived 클래스 객체가 생성될 때, Base 생성자 내부 에서는 객체 타입이 Base임

--> 이때 가상 함수 호출 시 Base 함수가 호출됨

--> virtual table이나 Derived 클래스가 아직 생성/초기화 되지 않은 단계에서 함수를 잘못 부르면 어떤 동작을 할 지 알 수 없음

 

==> 객체 생성/소멸 시점에서 파생 클래스의 가상함수, 멤버는 초기화 되지 못했기 때문에 접근하지 못하게 막음 (C++)

==> =파생 클래스 생성/소멸을 위해 기본 클래스 생성/소멸자가 호출되는 시점에는 객체의 타입 = 기본 클래스 (파생 클래스의 미초기화 멤버에 접근을 막기 위해 내려가지 않는다)

 

🔹 해결책: 기본 클래스의 비가상 멤버 함수로 두고 파라미터로 필요한 정보를 기본 클래스에 넘기는 규칙을 두자

==> 파생 클래스에서 기본 클래스에 넘겨줄 파라미터를 return static 변수     

        static std::string createLogString( parameters );

 

 

class Transaction
{
public:
    Transaction()
    {
        cout << "부모 생성자" << endl;
        logTransaction();
    }
    
    //기본 클래스에서 가상 함수 호출 시 정의되어 있지 않으면 링크 에러남. (정의되어 있으면 더 골치 아픔, 기본 클래스에 정의된 가상 함수가 동작함)
    //virtual void logTransaction() const; //LINK ERROR
    virtual void logTransaction() { cout << "부모 logTransaction" << endl; };
};

class BuyTransaction : public Transaction
{
public:
    BuyTransaction()
    {
        cout << "자식 생성자" << endl;
    }
    virtual void logTransaction() { cout << "자식 logTransaction" << endl; };
};

int main()
{
    BuyTransaction buyTransaction;
    return 0;
}
  • 파생클래스 객체 생성 호출 순서
    • 기본 클래스 생성자 호출 --> 기본 멤버 초기화 --> 파생 클래스 생성자 호출 --> 파생 멤버 초기화
    • 기본 클래스 생성자가 돌아가는 시점에서 파생 클래스 데이터 멤버는 아직 초기화 되기 전이라 접근 불가
  • 파생클래스 객체 소멸 호출 순서
    • 파생 클래스 소멸 --> 파생 멤버 비정의(소멸) --> 기본 클래스 소멸 --> 기본 멤버 비정의(소멸)