반응형
반응형

0. 요약

  1. new와 delete 형태를 맞추자
    • new 에 []를 썼으면 delete에 []를 쓰자
    • new 에 []를 안썼으면 delete에 []를 쓰지 말자

1. new, delete의 동작 순서

  1. new

    • 메모리가 할당됨
    • 할당된 메모리에 한 개 이상의 생성자가 호출됨
  2. delete

    • 할당된 메모리에 한 개 이상의 소멸자가 호출됨
    • 메모리가 해제됨

2. 객체 1개 vs 배열로 할당/해제

  1. 한 개 객체 할당 시 메모리 구조
    • object
  2. 객체 배열 할당 시 메모리 구조
    • 객체 수(n) - object - obejct - ...
    • 위의 구조로 delete 연산자가 몇 번 호출 될지 쉽게 알 수 있음
  3. 주의 사항
    • delete 뒤에 [] 를 붙여줘야만 포인터가 배열을 가리키고 있다고 이해함
    • 만약 [] 가 없으면 단일 객체로 간주하고 1개의 객체만 delete 처리함

3. 결론

  1. new 와 delete 형태를 맞추자
    • new 에 []를 썼으면 delete에 []를 쓰자
    • new 에 []를 안썼으면 delete에 []를 쓰지 말자
  2. 배열 타입을 typedef으로 만들지 않도록 하자
    • string 또는 vector type을 활용하자!

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. RAII 클래스가 관리하는 실제 자원을 반환하는 방법을 고민하자!
  2. 명시적으로 자원을 반환하자
    • 별도 함수 또는 연산자 오버로딩을 통해 자원을 반환
  3. 암시적으로 자원을 반환하자
    • 형변환 연산자 오버로딩을 통해 자원을 반환

1. RAII클래스가 관리하는 실제 자원을 반환하는 방법 필요

  1. 아래 예제에서 pInv는 shared_ptr 자료형이기 때문에 dayHeld에 인자로 사용 못함
  2. pInv에서 관리하는 실제 자원을 반환해야 사용가능하다!
int dayHeld(const Investment *pi); // 투자 이후 경과일 수
std::shared_ptr<Invesment> pInv(createInvestment()); // 자원 획득

int days = dayHeld(pInv) // => 실패!!

2. 자원을 반환하는 방법

  1. 명시적 변환(explicit conversion) 방법
    • 별도 함수(get)을 통해 자원을 반환 하자!
    • operator 오버로딩(->, * 등)을 통해 자원을 반환하자
// 1. 함수를 통해 자원 반환
int days = dayHeld(pInv.get());

// 2. 연산자 오버로딩을 통해 자원 반환
class Invesment{
public:
  bool isTaxFree() const;
}

// 2.1 -> 연산자 오버로딩
Investment* createInvestment();
std::shared_ptr<Investment> pi1(createInvestment());
bool taxable1 = !(pi1->isTaxFree());

// 2.2 * 연산자 오버로딩
std::shared_ptr<Invetsment> pi2(createInvestment();
bool taxable2 = !((*pi2).isTaxFree());
  1. 암시적 변환(implicit conversion) 방법
    • 암시적 형변환 oeprator를 통해 자원을 반환!
// C-API
FontHandle getFont();
void releaseFont(FontHandle fh);

// RAII  클래스
class Font{
public:
  explicit Font(FontHandle fh) : f(fh) {}
  ~Font() { releaseFont(f)}; }

  // 암시적 변환 함수(형 변환)
  operator FontHandle() const { return f; }
private:
  FontHandle f;
};

// 함수
void changeFontSize(FontHandle f , int newSize);

// 암시적 변환 사용
Font f(getFont());
int newFontSize;
changeFontSize(f, newFontSize); // Font -> FontHandle로 암시적 변환 수행

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 자원 관리 클래스(RAII 클래스) 만들 때 복사에 대해 고민하자.
  2. 복사에 대한 선택지는 아래와 같다.
    • 복사를 금한다(Mutex)
    • 관리하는 자원의 참조 카운팅 수행(shared_ptr)
    • 관리하는 자원을 복사(string)
    • 관리하는 자원의 소유권을 이전(unique_ptr)

1.뮤텍스 관리 객체 예시

  1. 뮤텍스 잠금을 관리하는 객체 구현
class Lock{
public:
   explicit Lock(Mutex *pm) : mutexPtr(pm){
      lock(mutexPtr);
   }
   ~Lock(){
      unlock(mutexPtr);
   }
private:
   Mutex *mutexPtr;
}
  1. 사용은 RAII 방식으로
Mutex m;
{
   Lock m1(&m); // lock
} // 블록이 끝나면 unlock
  1. 만약 Lock객체가 복사 된다면?
    • 사본이 의미가 없기 때문에 복사가 되면 안된다.

2. RAII 객체 복사 구현시 선택지

  1. 복사를 금지(3.참고)
  2. 관리하는 자원의 참조 카운팅 수행(4.참고)
  3. 관리하는 자원을 복사함(5.참고)
  4. 관리하는 자원의 소유권을 이전함(6.참고)

3. 복사를 금지

  1. RAII 객체가 복사 되면 안되는 경우 금지
    • 위의 Mutex의 경우에서 사용
  2. 복사 생성자를 금지

4. 관리하는 자원의 참조 카운팅 수행

  1. 자원을 사용중인 마지막 객체가 소멸될 때 까지 유지해야할 경우(shared_ptr의 경우) 사용
  2. 방법
    • 복사 시에 자원을 참조하는 객체의 개수 카운트를 증가
    • 참조 객체 수가 0이되면 자원을 해제하도록 구현
  3. shared_ptr을 이용하는 방법
    • 참조 카운팅 구현하려면 멤버 변수를 shared_ptr로 구현하면 됨
  4. shared_ptr의 삭제자
    • 삭제자(deleter) : 참조 카운트가 0이 되었을 때 호출되는 함수 또는 함수 객체
    • shared_ptr은 삭제자 지정(변경)을 허용함
  5. Mutex 예제에서 shared_ptr을 이용하도록 변경!
    • shard_ptr은 참조 개수가 0이 되면 가리키고 있던 대상을 삭제 하기 때문에 Mutex의 예 맞지 않음(Mutex는 다 썻을 때 잠금 해제만 하면 됨)
    • 이럴 때 shared_ptr의 삭제자를 사용하면 참조 객체가 0이 될 때 Mutex를 unlock하도록 구현할 수 있음
class Lock{
public:
   explicit Lock(Mutex *pm)
      : mutexPtr(pm, unlock) // 삭제자로 unlock을 지정
   {
      lock(mutexPtr.get());
   }
private:
   shared_ptr<Mutex> mutexPtr;
}

Mutex m;
{
   Lock m1(&m); // lock
} // 블록이 끝나면 삭제자(unlock)가 호출

5. 관리하는 자원을 복사함

  1. 복사가 필요할 경우 복사를 지원(string의 경우)
  2. 이 때 반드시 깊은 복사를 수행해야함

6. 관리하는 자원의 소유권을 이전

  1. 자원을 참조하는 RAII 객체를 딱 하나만 만들고 싶을 때 사용(unique_ptr의 경우)

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 자원 관리 객체에서 자원을 관리하자.(RAII 사용)
  2. 자원 관리 객체는 소멸자에서 해당 자원을 해제 하자!

1. 메모리가 누수될 수 있는 상황 가정

  1. 구현한 내용
    • 투자를 모델링한 기본 클래스 Investment
    • Investment 계열의 클래스를 반환하는 팩토리 함수 createInvestment
  2. 사용부
    • createInvestment를 이용해 Investment 계열 클래스 생성
    • 사용 후 객체 반환(delete)
  3. 문제 사항
    • 삭제 전에 return 문이 있을 경우
    • 삭제 전에 goto문에 의해 루프를 빠져 나갈 경우
    • 삭제 전에 예외가 던져질 경우
// 투자를 모델링한 기본 클래스
class Investment{...};

// Invesment를 상속한 클래스를 return하는 팩토리 함수
Invesment* createInvestment();

void f(){
   Invesment *pInv = createInvestment();
   ...
   delete pInv;
}

2. 해결 방안

  1. 자원을 획득한 후에 자원 관리 객체에게 넘기자!
    • createInvestment를 통해 만든 자원을 auto_ptr에게 넘겨 초기화하는데 사용
    • RAII라고 불림
    • 자원 획득과 자원 관리객체의 초기화가 한문장에서 이루어지는 것이 일상적임
  2. 자원 관리 객체는 자신의 소멸자를 사용해 자원이 확실히 해제되도록 하자!
    • 소멸자는 객체가 소멸될 때 자동으로 호출됨
    • 소멸자가 호출될 때 자원을 해제하면 자동으로 확실히 해제됨
    • 단, 예외가 발생하면 꼬이지만 항목8에서 해결할 예정
  3. 예제(스마트 포인터)
void f(){
   std::unique_ptr<Investment> pInv(createInvesment());
   //책에서는 auto_ptr로 예제를 들었으나, C++11에서 unique_ptr로 대체됨
}

3. 스마트 포인터

  1. unique_ptr

    • 자원에 대해 유일한 소유권을 가짐
    • unique_ptr을 복사 또는 대입하면, 기존 unique_ptr은 null로 변경됨
std::unique_ptr<Invesment> pInv1(createInvesment());
std::unique_ptr<Invesment> pInv2(pInv1);  // pInv1 = null로 할당됨
pInv1 = pInv2;  // pInv2 는 null로 할당됨
  1. shared_ptr
    • 참조 카운팅 방식 스마트 포인터(RCSP:Reference-Counting Smart Pointer)
    • 자원을 가리키는 외부 객체 개수를 확인하다가 0이 되면 자원을 자동으로 삭제함
std::shared_ptr<Investment> pInv1(createInvestment());
std::shared_ptr<Investment> pInv2(pInv1); // pInv1과 pInv2는 같은 객체를 가리킴
pInv1 = pInv2; // 위와 동일ㅈ

참고

  1. Effective C++

별첨

  1. RAII(Resource Acquisition Is Initialization)
    • 생성자에서 리소스를 획득하고 해당 소멸자에서 해제하는 것!
    • 주로 포인터, 뮤텍스 등의 자원의 소멸이 누락되어 발생되는 문제를 방지하기 위해 별도의 자원 관리 객체를 두고, 객체 생성시 자원 획득 및 소멸시 자원 해제하는 것을 얘기함
  2. RAII 클래스 예시
    • string, vector, thread, unique_ptr, shared_ptr, lock_guard, unique_lock, shared_lock
반응형
반응형

0. 요약

  1. 객체 복사 시 모든 데이터 멤버가 빠지지 않도록 하자

  2. 객체 복사 시 모든 기본 클래스 부분이 빠지지 않도록 하자

  3. 복사 생성자와 대입 연산자 구현 시 주의

    • 한쪽을 이용해 다른 한쪽을 구현하려고 하지 말자
    • 필요시 공통된 동작을 제 3의 함수에 분리하자
    • 양쪽에서 이 함수를 호출하도록 구현하자

1. 객체 복사 함수

  1. 객체 복사 함수의 종류
    • 복사 생성자
    • 복사 대입 연산자
  2. 객체 복사 함수 특징
    • 사용자가 만들지 않으면 자동으로 생성된다.
    • 사용자가 만들면 자동으로 생성하지 않는다.
  3. 객체 복사 함수를 만드는 경우 및 주의점
    • 컴파일러가 자동으로 생성해준 복사 함수로는 부족할 때
    • 사용자가 일부 데이터 복사를 누락해도 컴파일러는 알려주지 않는다!

2. 객체 복사 함수 생성 시 주의점

  1. 누락된 멤버 변수가 없도록 해라
    • 누락되어도 컴파일러는 알려주지 않아 부분복사가 될 수 있다.
  2. 상속 받은 클래스의 복사도 누락 없도록 해라
// 복사 생성자
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
   : Customer(rhs),  // 부모 클래스의 복사 생성자 호출
   priority(rhs.priority){}   // 데이터 복사

// 대입 연산자
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs){
   Custormer::operator=(rhs); // 부모 클래스의 대입 연산자 호출
   priority=rhs.priority; // 데이터 대입
   return *rhis;
}

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. operator= 에서 자기대입에 대한 처리를 반드시 하자
    • 일치성 검사를 통해 자기대입일 경우 처리하지 않도록 하는 방법
    • 본래의 사본을 만들어 할당받는 방법
    • 사본을 swap하는 방법

1. 자기 대입의 예시

  1. 자기 대입이란?
    • 어떤 객체가 자기 자신에 대해 대입연산자를 적용하는 것
class widget{...};
Widget w;
w = w;
  1. 배열 또는 컨테이너 순환 중 자기 대입

    • i와 j가 같다면 자기 대입
a[i] = a[j];
  1. 중복 참조로 인해 자기 대입 발생
    • 중복 참조 : 여러 곳에서 하나의 객체를 참조하는 상태
    • px와 py가 가리키는 대상이 같으면 자기 대입
*px = *py;

2. 자기 대입 문제

  1. 정상적인 경우
    • 대입 연산 시 기존의 pb 포인터 삭제
    • rhs의 pb포인터 값을 재할당 및 복사함
  2. 자기 대입일 경우
    • 대입 연산시 기존의 pb포인터 삭제
    • rhs의 pb포인터도 삭제되어 버림
    • 대입 후에 비어 있는 Bitmap이 할당됨
class Bitmap{...};
class Widget{
public:
   Widget& operator=(const Widget& rhs);
private:
   Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs){
   delete pb;
   pb = new Bitmap(*rhs.pb);
   return *this;
}

3. 해결 방법

  1. 대입 연산자에서 일치성 검사 수행
    • 자기 대입이 일어나는 경우는 극히 적음
    • 하지만 대입할 때 마다 일치성 검사를 하므로 효율 떨어짐
    • 또한 new 단계에서 예외 발생 시 삭제된 pb만 남음
Widget& Widget::operator=(const Widget& rhs){
   if (this == &rhs) return *this; // 일치성 검사 수행
   // 이후 과정 동일
   delete pb;
   pb = new Bitmap(*rhs.pb);
   return *this;
}
  1. 삭제 전에 본래의 pb를 복제
    • new 단계에서 예외 발생해도 pb 유지 가능
    • 복제를 통해 자기 대입에 대한 방어도 가능함
Widget& Widget::operator=(const Widget& rhs){
   Bitmap *pOrig = pb; // 원래 pb를 복제
   pb = new Bitmap(*rhs.pb);  // rhs의 pb를 대입
   delete pOrig;  // 원래 pb를 삭제
   return *this;
}
  1. 복사 후 바꾸기 방법(copy and swap)
    • 예외 안정성과 자기대입 안정성을 동시에 가진 operator= 구현 방법
    • 29 항목에서 확인
    • 방법 1과 같이 rhs 사본을 만들어 swap하는 방법
    • 방법 2와 같이 값에 의한 전달 시 사본이 만들어지는 특징을 살려 바로 swap하는 방법
class Widget{
   void swap(Widget& rhs);
};

// 방법 1.
Widget& Widget::operator=(const Widget& rhs){
   Widget temp(rhs); // rhs 사본 만듬
   swap(temp); // *this를 사본과 맞바꿈
   return *this;
}

// 방법 2. 항목 20 참고
Widget& Widget::operator=(Widget rhs){
   swap(rhs);
   return *this;
}

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 대입 연산자는 *this 참조자를 반환!

1. C++ 기본 자료형의 대입 예시

  1. x = y = z = 15
    • (z = 15) => z에 15가 대입
    • y = z => y에 z(15)가 대입
    • x = y => x에 y(15)가 대입
    • 결론적으로 x,y,z에 모두 15가 대입된다.

2. 사용자 정의 타입도 위의 예시가 가능하게 하자!

  1. 대입 관련 연산자(=, +=, -=, *=)
    • *this를 return
    • 객체의 참조자를 반환
class Widget{
public:
   Widget& operator=(const Widget& rhs){
      ...
      return *this;
   }
}

Widget a,b,c;
a = b = c; // (O) 가능함

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 생성 소멸 과정 중에는 가상 함수를 호출 하지 말자!

1. 생성/소멸 중 가상 함수를 호출하면 안되는 이유

  1. BuyTransaction 객체 생성 시 생성자 호출 순서
    • 기본 클래스 생성자 호출 => 파생 클래스 생성자 호출
  2. 기본 클래스 생성자 호출 시점에 객체 타입은 기본 클래스!
    • 호출 되는 가상 함수는 모두 기본 클래스 타입으로 결정됨
    • 런타임 타입 정보 사용 요소(dynamic_cast, typeid 등) 사용 시에도 기본 클래스 타입으로 취급
  3. 마찬가지로 소멸자에 대해
    • 파생 클래스 소멸자 호출 => 기본 클래스 소멸자 호출
    • 기본 클래스 소멸자 호출 시점에 객체 타입은 기본 클래스
  4. 기본 클래스 타입인 이유
    • 기본 클래스가 생성되는 시점에 파생 클래스 데이터는 미초기화 된 상태
    • 만약 기본 클래스에서 파생 클래스 함수를 호출 할 수 있다면, 미초기화된 데이터 사용 가능함
    • 따라서 C++에서 이런 실수를 하지 못하도록 막음
  5. 결론적으로
    • Transaction 생성자에서 호출되는 logTransaction 함수는 BuyTransaction 것이 아니라, Transaction의 것이 호출됨
// 주식 거래 기본 클래스
class Transaction{
public:
   Transaction();
   // 로그 타입에 따라 달라지는 로그 기록을 만들기 위해 virtual 함수 정의
   virtual void logTransaction() const = 0;
}

Transaction::Transaction(){
   logTransaction(); // 로깅하기 시작함
}

// 주식 거래 매수 클래스
class BuyTransaction : public Transaction{
public:
   virtual void logTransaction() const;
}

// 주식 거래 매도 클래스
class SellTransaction : public Transaction{
public:
   virtual void logTransaction() const;
}

// 사용
BuyTransaction b;
  1. 위의 예시는 상대적으로 발견 쉬움

    • 소멸자에 가상함수가 있는 경우 경고 메시지를 주는 컴파일러도 있음
    • Transaction의 logtTransaction함수가 순수가상 함수이기 때문에 링킹 에러 발생
  2. 아래와 같은 형태면 발견 어려움

    • 만약 여러 생성자로 인해 공통 작업을 별도 함수로 정의
    • 해당 함수에서 가상함수 호출
    • 컴파일러에서 오류를 알 수 없음
class Transaction {
public:
   Transaction(){
      init();
   }
   virtual void logTransaction() const = 0;
private:
   void init(){
      logTransaction();
   }
}

2. 해결 방법

  1. 가상 함수를 비가상함수로 변환
    • 가상 함수이면, 각 객체에 맞게 정보를 알 수 있다.
    • 하지만 비가상함수로 변경되면서 필요한 정보를 전달 받아야함
  2. 파생 클래스 생성자로부터 필요한 정보를 기본 클래스 생성자가 전달받아야 함
    • logInfo 정보를 전달 받아 각 타입에 맞게 logTransaction 함수를 수행함
  3. logInfo를 만들어 주는 createLogString static 함수 생성
    • 기본 클래스 생성자 쪽으로 넘길 값을 생성하는 용도
    • 멤버 초기화 리스트가 많은 경우 편리함
    • static 멤버이기 때문에 미초기화된 BuyTransaction 데이터를 건드릴 위험이 없다.
class Transaction {
public:
   explicit Transaction(const std::string& logInfo);
   void logTransaction(const std::string& logInfo) const;
}

Transaction::Transaction(const std::string& logInfo){
   logTransaction(logInfo);
};

class BuyTransaction: public Transaction{
public:
   BuyTransaction( params )
      : Transaction(CreateLogString(params)) {}
private:
   static std::string createLogString(parmas);
};

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 소멸자에서 예외가 발생하지 않도록 하자!
    • 예외는 소멸자가 아닌 함수에서 처리하도록 하자!
  2. 소멸자에서 호출되는 함수가 예외 가능성이 있다면..
    • 소멸자에서 삼켜버리던지, 프로그램을 종료하던지 처리 필요

1. 소멸자에 예외가 있으면 안되는 이유

  1. Widget 객체를 담는 10 크기를 갖는 벡터 v 예시
    • 함수 사용 후 10개 만큼 메모리가 해제됨
    • 만약 1번째 Widget에서 예외가 발생했다면??
    • 2~10번째 Widget의 메모리가 누수됨!
void doSomething(){
   std::vector<Widget> v;
} // 여기서 v는 메모리가 삭제됨
  1. DBConnection 객체 예시
    • DBConnection은 create 함수와 close 함수를 제공
    • DBConn은 소멸자에서 DBConnection의 close 함수 호출
    • 만약 close하면서 예외가 발생했다면???
    • 예외가 전판되어 소멸자에서 예외가 나가도록 내버려 두게됨!
// DB 연결을 담당하는 클래스
class DBConnection{
public:
   static DBConnection create();
   void close();
}

// DBConnection 객체를 관리하는 클래스
class DBConn{
public:
   ~DBConn(){
      db.close(); // DBConnection 객체 db의 close함수 호출
   }
private:
   DBConnection db;
}

void doSomeThing{
   vector<DBConn> vecDbc;
   vecDbc.push(DBConnection::create());
   vecDbc.push(DBConnection::create());
}// vecDbc 메모리가 해제됨

2. 해결 방법

  1. close에서 예외가 발생하면 프로그램을 바로 끝내라
    • 에러 발생 후에 프로그램 지속이 어려운 경우 괜찮은 선택
DBConn::~DBConn(){
   try{
      db.close();
   } catch(...) {
      //close 호출 실패 로그 작성
      std::abort();
   }
}
  1. close를 호출한 곳에서 일어난 예외를 삼켜라!(무시하라)
    • 예외를 무시한 뒤라도 프로그램이 신뢰성 있게 실행 될 수 있어야 함
DBConn::~DBConn(){
   try{
      db.close();
   } catch(...){
      //close 호출 실패 로그만 작성
   }
}
  1. close 호출 책임을 소멸자에서 사용자로 넘겨라!
    • DBConn에서 close 함수를 제공하여 예외 발생 시 사용자가 예외에 대해 대응!
    • 만약 닫히지 않았으면 소멸자에서 한번 더 닫을 수 있음
    • 소멸자에서 호출하는 close 마저 실패하면 '끝내거나, 삼키거나'를 선택해야함
class DBConn {
public:
   void close(){
      db.close();
      closed = true;
   }
   ~DBConn(){
      if(!closed){
         try{
            db.close();
         } catch(...){
            //close 호출 실패 로그 작성
         }
      }
   }
private:
   DBConnection db;
   bool closed;
}

3. 해결 포인트

  1. 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다!
    • 소멸자에 있다면 사용자는 예외에 대처할 기회가 없다.

참고

  1. Effective C++
반응형
반응형

0. 요약

  1. 가상 함수를 하나라도 가진 클래스는 가상 소멸자를 가져야한다.!

  2. STL 컨테이너, string 타입은 다형성을 갖도록 설계 되어 있지 않다.

    • 상속해서 사용시 주의가 필요하다!
  3. 추상 클래스를 만들 때 마땅한 순수 가상함수가 없으면, 소멸자를 순수 가상함수로 만들자!

  4. 기본 클래스로 설계되지 않거나, 다형성을 갖지 않도록 하려면 가상 소멸자를 만들지 말라!

1. 가상 소멸자의 필요성

  1. 예시 상황
    • TimerKeeper라는 인터페이스(기본 클래스)를 만듦
    • 각 시계를 TimerKeeper를 상속받아 구현
    • 팩토리 함수(getTimeKeeper)를 만들어 다양한 시계를 기본 클래스로 반환(TimeKeeper)
    • 모두 사용 후 메모리 반환(delete)
//시계 인터페이스
class TimeKeeper {
public:
   TimeKeeper();
   ~TimeKeeper();
}

// 시계 구현
class AtomicClock: public TimeKeeper {...};
class WaterClock: public TimeKeeper {...};
class WristClock: public TimeKeeper {...};

// 팩토리 함수
TimeKeeper* getTimeKeeper();


//사용 부
TimeKeeper *ptk = getTimeKeeper();
...(사용)
delete ptk; // 메모리 삭제
  1. 문제 사항(부분 소멸 문제 발생)
    • 메모리 해제 시 각 시계(Atomic, Water, Wrist 등)의 소멸자 호출되지 않음
    • 메모리 해제시 기본 클래스 부분(TimeKeeper) 만 삭제됨
  2. 해결 방법
    • 기본 클래스에 가상 소멸자를 붙여라!
class TimeKeeper {
public:
   TimeKeeper();
   virtual ~TimeKeeper();
};
  1. 원리

    • 아래 가상 함수 테이블 참고
    • 소멸자를 virtual 선언하면, 가상함수 테이블 생성됨
    • 메모리가 해제 될 때 가상함수 테이블을 참고하여 알맞은 크기 만큼 메모리가 해제됨
  2. 간단한 적용 방법

    • 가상 함수를 하나라도 가진 클래스의 소멸자는 가상 소멸자로 정의!
    • 가상 소멸자가 없는 클래스는 기본 클래스 의지 없음!

2. 가상 함수 테이블

  1. 가상 함수 테이블(vtbl, virtual table)
    • 가상 함수를 가리키는 함수 포인터를 저장하는 공간
    • 각 클래스가 가지고 있음
  2. 가상 함수 테이블 포인터(vptr, virtual table pointer)
    • 객체가 생성 될 때 가상 함수 테이블을 가리킴
  3. 가상 함수를 가지고 있는 클래스는 가상 함수 테이블을 가짐
    • 아키텍처(32,64bit)에 따라 클래스 타입의 크기가 커진다.
    • 아래 예에서 Point의 크기는 원래 8byte(int가 4byte라면)
    • 하지만 가상 함수를 가졌다면 16byte가 된다(64bit 운영체제에서 포인터는 8bytef)
class Base
{
public:
    FunctionPointer *__vptr;
    virtual void function1() {};
    virtual void function2() {};
};

class D1: public Base
{
public:
    virtual void function1() {};
};

class D2: public Base
{
public:
    virtual void function2() {};
};
  1. 예시
    • Base는 __vptr 포인터를 가지고, D1,D2는 상속받는다.
    • 각 클래스는 아래 그림과 같이 각각 vTable을 가진다.
    • 객체가 생성 될 때 각자에 맞는 vTable을 가리킨다.

3. 가상 소멸자가 없는 클래스를 조심하자

  1. STL 컨테이너 타입, string 타입은 가상 소멸자가 없다.
    • 아래와 같이 구현 시 메모리 누수 발생
    • string쪽은 메모리 해제 되지만, SpecialString 자원 누수됨
    • SpecialString의 소멸자가 호출되지 않음!
class SpecialString: public std::string{...};

SpecialString *pss = new SpecialString("Impending Doom");
std::string *ps;
ps = pss;
delete ps;
  1. STL 컨테이너 타입, string 타입은 다형성을 갖도록 설계되지 않았다!
  2. 나만의 컨테이너를 만들 때 자제하자!

4. 순수 가상 소멸자의 용도

  1. 추상 클래스 만드는데 마땅한 순수 가상 함수가 없을 때
    • 어차피 추상 클래스는 기본 클래스로 사용하기 위한 목적
    • 기본 클래스는 가상 소멸자를 가져야함
    • 그러므로 가상 소멸자를 순수 가상 함수로 만들자!
class AWOV{
public:
   virtual ~AWOV() = 0;
}

// 순수 가상 소멸자 정의 반드시 필요!
AWOV::~AWOV() {}
  1. 주의 할 점

    • 순수 가상 소멸자의 정의가 필요함
    • 없으면 링킹 에러 발생!
    • 이유는 아래 소멸자 호출 순서 참고!
  2. 소멸자 호출 순서

    • 파생 클래스 소멸자 호출
    • 기본 클래스쪽으로 올라오면서 각 소멸자가 호출됨
    • 컴파일러는 ~AWOV 호출 코드를 만들기 위해 파생 클래스의 소멸자를 사용

참고

  1. Effective C++
  2. https://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
반응형

+ Recent posts

반응형