설계가 잘 된 클래스들을 보면 객체를 복사하는 함수가 딱 둘만 있습니다.

 

복사생성자와 복사 대입 연산자 입니다. 이둘은 우리가 선언하지 않으면 컴파일러가 자동으로 만들어주는데, 깊은 복사가 되지 않습니다.

 

우리가 복사생성자와 복사 대입 연산자를 선언하여 깊은 복사를 해줘야 합니다.

 

이문제가 가장 크게 나타는 경우가 있는데, 예제를 보겠습니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class B : public A{
    
    public:
      ...
      B(const B& rhs) : num(rhs.num) { //복사생성자
        LOG("COPY constructor");
      }    
      B& operator=( const B& rhs){ //복사 대입 연산자
        LOG("COPY operator");
        
        num = rhs.num;
        return *this;
      }
    
    private:
      int num;
};
 

 

위의 코드에 문제점은?

상속 관계에서 B의 복사생성자에는 기본클래스 A의 생성자에 넘길 인자들도 명시되어 있지 않아서 B객체의 A 부분은 인자 없이 실행되는 A 생성자, 즉 기본 생성자에 의해 초기화 됩니다. 

 

위와 같은 코드는 아래와 같이 바꾸셔야 합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class B : public A{
    
    public:
      ...
      B(const B& rhs) : A(rhs), num(rhs.num) { //복사생성자
        LOG("COPY constructor");
      }    
      B& operator=( const B& rhs){ //복사 대입 연산자
        LOG("COPY operator");
        
        A::operator=(rhs);
        num = rhs.num;
        return *this;
      }
    
    private:
      int num;
};
 

 

우리는 위의 코드처럼 두가지를 꼭 확인해야합니다.

 

1. 해당클래스의 데이터 멤버를 모두 복사합니다.

2. 이 클래스가 상속한 기본클래스의 복사 함수도 꼬박꼬박 호출해 주도록 합시다.

 

그리고

복사생성자와 복사 대입 연산자의 코드 중복은 양쪽에서 겹치는 부분을 별도의 멤버 함수에 분리해 놓은 후에 이함수를 호출하게 만드는게 좋습니다. ( Private 멤버로 init......() )

 

절대 복사 대입 연산자에서나, 복사 생성자에서 서로를 호출해서는 안됩니다.

 

이것 만은 잊지 말자!

◆ 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본클래스 부분을 빠뜨리지 말고 복사해야 합니다.

◆ 클래스의 복사 함수 두개를 구현할 때,  한쪽을 이용해서 다른쪽을 구현하려는 시도는 절대로 하지 마세요, 그대신, 공통된 동작을 제3의 함수에다가 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결 합시다.


자기 대입(self assignment)이란?

어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말합니다.

 

1
2
3
4
5
6
class Widget { ... };
 
Widget w;
...
 
w = w; // 자기에 대한 대입

 

위와 같은 구현을 하는 사람은 극히 드물겠지만, 예를 들어

 

a[i] = a[j]; 와 같이 i 및 j 가 같은 값을 갖게 되면 자기 대입문이 됩니다.

*px = *py; 와 같이 px 및 py가 가리키는 대상이 같으면 자기 대입문이 되고 맙니다.

 

위와 같이 이러한 자기대입이 생기는 이유는?

여러 곳에서 하나의 객체를 참조하는 상태, 다시말해 중복참조(aliasing)라고 불리는 것 때문입니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Bitmap { ... };
 
class Widget {
    ...
    public:
     Widget& operator=(const Widget& rhs) {
        delete pb;                // 현재 bitmap 삭제
        pb = new Bitmap(*rhs.pb); 
        return *this;             
     }
    private:
     Bitmap* pb;
};
 
void main()
{
    Bitmap b;
    Widget w1, w2;        
    w1.pb = w2.pb = &b;
    // w1, w2 같은 비트맵을 가르키고 있다.
    w1 = w2;    
}

 

위의 구현의 문제점은?

같은 비트맵을 가르키게된 상태에서 대입 연산자가 호출되고, delete pb로 인해 두개의 w1, w2객체의 bitmap은 사라지게 됩니다. 그런데 이미 지워진 비트맵으로 pb를 생성하려 하니 예외가 발생하게 되는 것 입니다.

 

중요한것은

코드에서도 보면 아시겠지만, 하나의 객체를 여러곳에서 참조 하고 있는 상태에서 자기대입의 문제점을 말하고 있습니다.

 

위의 문제점은 객체가 같은지 즉 자기대입인지 검사문장(일치성테스트)으로 해결은 됩니다.

if( this == & rhs ) return *this;

 

말그대로, 자기대입이면, 아무것도 안하고 리턴하게끔 하는 것입니다.

하지만 위의 코드로는 자기대입에 대한 처리가 있을때의 예외처리만이 가능할뿐이지

if 문을 통과 하여 new 연산시 동적 할당에 필요한 메모리가 부족하다든지, 다양한 예외가 나오게 되면,  Widget객체는 결국 삭제된 Bitmap을 가리키는 포인터를 가지고 남습니다.

 

위와 같은 문제점을 해결하기 위해서는?

1
2
3
4
5
6
7
8
9
Widget& Widget::operator=(const Widget& rhs){
    
//  delete pb;  pb를 바로 삭제하지 않고, 복사본을 만든다.
    Bitmap * pOri = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOri;
    
    return *this;
}

 

위의 구현은 예외 안정성과, 자기대입 안정성을 동시에 가졌지만, 가장 효율적인 방법이라고는 할 수 없습니다.

 

사실상,

operator= 작성에 아주 자주 쓰이는 '복사 후 맞바꾸기(copy and swap) 기법을 들수 있습니다.

 

1
2
3
4
5
6
7
Widget& Widget::operator=(const Widget& rhs){
    
    Widget temp(rhs); // rhs의 데이터에 대해 사본을 하나 만듭니다.
    swap(temp);       // *this의 데이터를 그 사본의 것과 맞바꿉니다.
   
    return *this;
}

 

 

위의 코드처럼 구현하게되면,

대입되는 객체가 자기대입인지는 상관없고, 무조건 사본을 만들어 사본과 자신을 바꾸기때문에 자기 자신이면 그냥 Swap을 해도 자기자신이 되는거고, 아니라면 값이 바뀌는 것입니다.

 

또한,

rhs의 복사본 temp를 지역으로 선언 되어있다. 그리고 swap함수를 통해 this와 temp를 데이터를 바꾸게 되는데, 바꾼후 temp에는 this의 Bitmap포인터가 있을것이고, this는 rhs의 포인터로 바껴있을것이다. 이렇게 되면 자동적으로 블럭이 끝나는 순간 temp는 소멸되면서 말그대로 this의 Bitmap포인터가 소멸되는 것이다. 바꿔주면서 삭제가 자동이루어진 코드인 것이다.

 

예제중에서는 예외의 안정성과, 자기대입 안정성을 두루 갖춘 구현이라고 할 수 있겠습니다.

 

 

이것 만은 잊지 말자!

◆ operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만듭시다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정 할 수도 있으며, 복사후 맞바꾸기 기법을 써도 됩니다.

◆ 두개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게ㅔ 동작하는지 확인해 보세요.



C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 재미있는 성질을 갖고 있습니다.

 

int x, y, z;

x = y = z = 15;
 
위의 구현은
일종의 관례이고, 클래스에 대입 연산자가 혹시 들어간다면 이 관례를 지키는 것이 좋습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
class Widget
{
    public:
     /**
      * (= 뿐만아니라 +=, -=, *=, /= 등 *this를 반환하도록 만든다.)
      */
    
     Widget& operator=(const Widget& rhs)
     {
        ...
        return *this;
     }
};
 
 
이관례는 모든 기본제공 타입들이 따르고 있을뿐만 아니라, 표준 라이브러리에 속한 모든  타입 ( string, vector, complex, tr1::shared_ptr 등) 에서도 따르고 있다는 점을 무시 못할 것입니다.
 
 
 이것 만은 잊지 말자!

◆ 대입 연산자는 *this의 참조자를 반환하도록 만드세요.


 

객체 생성 및 소멸 과정 중에는 가상함수를 호출하면 절대로 안 됩니다!!

우선 코드부터 봅시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
 
class Transaction
{
public:
Transaction();
virtual void logTransaction() const = 0;
};
Transaction :: Transaction()
{
      /**
* 생성자에서 순수가상함수를 호출했음
*/
logTransaction();
}
class BuyTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void BuyTransaction::logTransaction() const
{
}
class SellTransaction : public Transaction
{
public:
virtual void logTransaction() const;
};
void SellTransaction::logTransaction() const
{
}
int main()
{
BuyTransaction b;
return 0;
}






위의 코드는 다행스럽게도(?) LINK 에러를 내준다.

언뜻 봐서는 부모클래스의 순수가상함수를 자식들이 재정의 했고, BuyTransaction 선언으로

부모생성자가 호출되면서, 자식클래스의 정의함수로 넘어가게끔 되어 문제가 없어 보인다.

 

하지만,

이 코드가 LINK 에러를 내뱉는 이유는?

 

BuyTransaction 클래스는 Transaction 클래스를 상속받았기 때문에

상속받은 클래스의 객체가 생성될 때에는 부모 클래스의 생성자가 먼저 호출된다.

부모의 생성자가 먼저 호출된 상태에서 가상 함수를 호출하면, 현재 정의가 되지 않았기 때문에 오류가 생기게 된다.

 

즉!
부모의 생성자가 먼저 호출된다는 것은 그 동안에는 부모의 클래스 타입, 즉 Transaction 클
래스 타입으로 인식을 하게 된다는 것입니다.
그러므로, Transaction()함수의 정의는 찾을 수 없으니까 LINK에러가 나는 것입니다.

 

소멸자의 경우도 비슷하게 생각하면 된다. 

파생 클래스에서 정의된 소멸자가 수행되고, 부모 클래스의 소멸자가 수행이된다. 여기서 부모 클래스 소멸자가 수행될 때는 그 객체가 부모 클래스 타입이라고 인식하게 된다는 것이다.

 
위의코드가 에러가 나지 않는다면, 끔찍한 일(?)이 발생 합니다.
 
가장중요하게 우리가 알아야 할 부분은?
 
1. 생성자 및 소멸자에서는 가상함수를 호출 하면 안된다.
2. 파생 클래스의 생성자 및 소멸자 수행시 부모 클래스의 생성자/소멸자를 수행할 때, 컴파일러는 부모 클래스의 타입으로 인식하게 된다
 
 
그러면, 위의코드처럼 우리가 원하는건 파생클래스 객체 선언과 동시에 로그정보를 호출되기를 원한다고 가정 해봅시다. 방법은 많이 있겠지만, 책의 나와 있는 방법으로 간단하게 해결하도록 하겠습니다.
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include<iostream>
 
class Transaction 
  public
    explicit Transaction(const std::string& logInfo);
    void logTransaction(const std::string& logInfo) const;   
};
Transaction::Transaction(const std::string& logInfo)
    logTransaction(logInfo); 
}
void Transaction::logTransaction(const std::string& logInfo) const
{
    std::cout<<logInfo.c_str()<<std::endl;
}
 
class BuyTransaction : public Transaction 
public:
    BuyTransaction(const char * log) : Transaction(createLogString(log)){ }
private:
    static std::string createLogString(const char * log) { return log; }
}; 
 
int main() 
    BuyTransaction b("TEST");
    return 0;
}
 
 
위의 코드는 순수가상함수였던 (logTransaction) 함수를 비가상함수로 바꾸고, 자식클래스 생성자들로 하여금 필요한 로그정보를 부모생성자로 넘기고 있습니다.  
 
 

이것 만은 잊지 말자!

◆ 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행 중인 생성자나 소멸자에 해당된느 클래스의 파생클래스 쪽으로는 내려가지 않으니까요

 


 

class Widget {}

 

vector<Widget> v;

위의 클래스가 선언되어 있고, vector<Widget> v의 사이즈가 10이라고 가정합니다.

 

v가 사이즈가 10인데, 첫번째 것을 소멸시키는 도중에 예외가 발생되었다고 가정하면, 나머지 아홉 개는 소멸자가 호출되지 않아 메모리 누수가 생깁니다.

 

위의 문제를 방지하기 위해서는 소멸자에게 예외가 나오는 것을 방지해야 합니다.

예를 들어봅시다.

 

ex ) DB를 접속하는 클래스

class DBConnection {

  public:

  /**

   * 생성자 소멸자가 아닌 따로 함수를 만들어서 생성 소멸을 할 수 있게 작업

   */

  static DBConnection create();

  void close();

};

 

ex) DBConnetion에 대한 자원 관리 클래스

class DBConn {

public :

 ~DBConn()

  {

      db.close();

   }

private:
  DBConnection db;
};
 
int main()
{
  /**
  * DBConnection 객체를 생성하고 이것을 DBConn 객체로 넘겨서 관리를 맡깁니다.
  * DBConn 인터페이스를 통해 DBConnection 객체를 사용합니다.
  * main함수 종료시 DBConn 객체가 소멸됩니다. 따라서 DBConnection 객체에 대한 close
  * 함수의 호출이 자동으로 이루어 집니다.
  */
  DBConn dbc ( DBConnection::create() );
  return 0;
}
 
위의 코드처럼
사용자의 망각을 사전에 차단하는 DBConnection에 대한 자원 관리 클래스를 만들어서 그 클래스의 소멸자에서 close를 호출하게 만드는 것입니다.
 
위와 같은 방법은 close() 함수가 제대로 호출만 된다면 문제 없다.
하지만 close() 함수를 호출했는데, 여기서 예외가 발생한다면, 또 문제가 생기게 된다.
 
이것을 피하기 위한 방법은 두가지가 있다.
 
첫째. close에서 예외가 발생하면 프로그램을 바로 끝냅니다. 대개 abort를 호출합니다.
 
DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
   {
       // close 호출이 실패했다는 로그를 작성합니다.
       std::abort();
   }
 
try ~ catch 문을 이용하여 일단 close() 함수를 실행한 뒤, 예외가 발생시 abort() 함수를 이용하여
프로그램을 끝내버린다.
 

객체 소멸이 진행되다가 에러가 발생한 후 더이상 프로그램을 실행할 수 없는 상황일 경우에 사용한다.

둘째. close를 홀출한 곳에서 일어난 예외를 삼켜버립니다. 

DBConn::~DBConn()
{
    try { db.close(); }
    catch(...)
    {
       // close 호출이 실패했다는 로그를 작성합니다.     
     }
}
 
예외 발생시 catch 블럭으로 들어가긴 하지만 아무것도 수행하지 않는다. 즉, 예외를 삼킨 것이다.
 

예외 삼키기를 선택한 것이 제대로 빛을 보려면, 발생한 예외를 그냥 무시한 뒤라도 프로그램이 신뢰성 있게 실행을 지속할 수 있어야 합니다.

 

이 두가지 방법 또한 문제점들을 가지고 있습니다.

 

이유는?

이 두 방법은 예외가 발생한 후의 처리를 하는 것인데, 실제로 close()가 예외를 던지게 된 요인에 대

한 조치를 취하는 대책이 전무한 상태입니다.

 

더 나은 방법은 아래와 같습니다. 

DBConn 클래스에서 close()를 직접 제공하는 방법이 있습니다.

 

이렇게 만들면, close() 함수가 실행 중에 발생하는 예외를 사용자가 처리할 수 있습니다.

 

class DBConn {

public:

   ...

   void close()              // 사용자에게 호출을 위해 정의한 close() 함수

  {

db.close();

closed = true; 

   }

   ~DBConn()

  {

if( !closed )

{

    try{

      db.close();

    }

    catch( ... )

   {

       // close 호출 실패 로그 출력

       // ...

   }

}

   }

 

private:

DBConnection db;

bool closed;

};

 

close() 호출 책임을 소멸자에서 사용자에게로 떠넘긴다.

예외를 처리할 수 있는 기회를 사용자에게 주는 것입니다. 이것 마저 없다면 사용자는 예외에 대처할 기회를 못잡게 됩니다. ( 2차 검증으로 DBConn 소멸자에서 마무리 )

 

 

즉!

예외가 생길 수 있는 코드는 소멸자보다는 다른 함수에서 비롯되어야 한다.

 

이것 만은 잊지 말자!

◆ 소멸자에서는 예외가 빠져나가면 안됩니다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야합니다.

◆ 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 합니다.


다형성이란?

 

사전적 의미로는 여러 형태를 갖는 것 으로 정의된다. C#, C++, Java 와 같은 객체지향 언어에서는 여러 개의 클래스가 같은 메시지에 대해서 각자 다른 방법으 작동할 수 있는 능력 이라 할 수 있겠 

 

쉽게 말해서,

부모의 추상함수를 자식 클래스들이 오버라이딩 하여, 부모의 인터페이스로 각각의 자식 클래스에 정의된 함수를 호출 할 수 있는 능력이라고 말할 수 있겠습니다.  

 

ex ) 다형성을 통한 ptk 제어 /관리 가능. ( 소스에서 일단 오버라이딩 제외 )

class TimeKeeper  

{     
   public
       TimeKeeper();

      ~TimeKeeper(); // 현재 가상소멸자가 아니다.

  };
  class AtomicClock : public TimeKeeper  {  };   

  class WaterClock : public TimeKeeper  {  }; 

  class WristWatch : public TimeKeeper  {  }; 

 

class CTimeFactory
{

  public:

       static TimeKeeper  * CreateAtomic(){  // 팩토리 함수

return new AtomicClock; 

       }

       static TimeKeeper  * CreateWater(){    // 팩토리 함수

          return new WaterClock;

       }

       static TimeKeeper  * CreateWrist(){     // 팩토리 함수

return new WristWatch;

       }

}
  
void main() 
{      
      TimeKeeper* ptk = CTimeFactory::CreateAtomic();

      delete ptk;  // 메모리 누수 방지 

}

 

팩토리 함수란?

새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 함수를 의미한다.

 

위의 코드는 다음과 같은 의미를 갖는다.

1) CTimeFactory::CreateAtomic() 가 반환하는 포인터가 파생 클래스에 대한 포인터라는 점 ( AtomicClock* )

2) 이 포인터가 가리키는 객체가 삭제될 때는 기본 클래스 포인터(TimeKeeper* 포인터)를 통해 삭제되다는 점

3) 기본 클래스에 들어있는 소멸자가 비가상 소멸자라는 점

 

 

C++ 규칙에서, 기본 클래스 포인터를 통해 파생 클래스 객체가 삭제될 때, 그 기본 클래스에 비가상 소멸자가 들어있으면 프로그램 동작은 어떻게 될지 모릅니다.

 

보통 그 객체의 파생 클래스 부분이 삭제가 되지 않습니다.

즉, 완전히 제거가 되지 않는다는 것이다.

 

그럼 이에 대한 해결책,

바로 기본 클래스에 가상 소멸자를 선언해주면 된다.   virtual ~TimeKeeper();

 

보통

그 클래스에 가상함수를 하나라도 가지고 있다면, 가상 소멸자를 가지는 것이 맞다고 봅니다.

 

가상 소멸자를 가지고 있지 않다면,

이는 어떤 클래스의 기본 클래스로 쓰이는 경우가 없다고 이해하면된다.

 

그렇다고, 가상소멸자, 가상함수를 난무 해서는 안됩니다.

가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조가 하나 들어 갑니다.

이 자료구조는 프로그램 실행 중에 주어진 객체에 대해 어떤 가상 함수를 호출해야 하는지를 결정하는데 쓰이는 정보입니다.

 

즉! 가상함수를 가지는 클래스에는 별도로 vptr(가상 함수 테이블 포인터)가 추가됩니다. 이는 가상 함수 테이블을 가리키는 포인터인데, 가상함수 테이블은 vtbl이라고 하며 이는 가상함수 포인터들을 담고 있는 배열입니다.

 

가상함수가 추가되면,

객체의 크기가 커질뿐만아니라, 다른언어로의 이식성은 기대를 접는것이 좋습니다.

 

1. 객체의 크기는 vptr( 가상 함수 테이블 포인터) 크기가 더해지면서, 커지고,

2. 이식성은 vptr을 구현환경에 따라 세부사항이 달라지는 문제가 있으므로 힘듭니다.

 

따라서

가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어있는 경우에만 한정

 

ex ) 가상함수가 전혀 없는데도 비가상 소멸자 때문에 오류인 예 STL string을 상속받은 예 

class SpecialString : public std::string  

 

    ...


   
void main() 
 { 
      SpecialString *pss = new SpecialString("ABC");
      string* ps;   
      ps = pss;   
      delete ps;
  } 

 

위의 코드는 string 객체 ps(부모클래스 객체)를 소거 함으로써,

SpecialString 클래스의 소멸자가 호출되지 않는다. 이코드는 정상 수행을 할 수가 없다.

 

가상소멸자가 없는 클래스는 STL 컨테이너 타입 전부가 바로 여기에 속한다.

표준 컨테이너를 상속받아서 클래스를 만들지 않아야 한다.

 

 

경우에 따라서는 순수 가상 소멸자를 두면 편리하게 쓸 수도 있습니다.

 

추상클래스는

본래 기본 클래스로 쓰일 목적으로 만들어진 것이고, 기본 클래스로 쓰이려는 클래스는 가상 소멸자를 가져야 합니다. 그리고 순수가상함수가 있으면 바로 추상 클래스가 됩니다.

 

 class AWOV 

 {

    public

       virtual ~AWOV() = 0// 순수 가상 소멸자 

 };     
 

일단 위의 클래스는 순수가상함수를 가지므로, 추상 클래스 입니다.

동시에 순수 가상함수가 가상 소멸자이므로, 소멸자 호출 문제로 고민 할 필요도 없습니다.

 

여기서 중요한게,

이 순수 가상 소멸자의 정의를 두지 않으면 안된다는 것입니다.

 

 AWOV :: ~AWOV() {} // 정의 

 

컴파일러는 ~AWOV의 호출코드를 만들기 위해 파생 클래스 소멸자를 사용할 것이므로, 잊지 말고 꼭 정의 해야한다. 

 

 

정리하자면,

 

기본 클래스에 가상 소멸자를 선언하는 것은 다형성(polymorphic)을 가진 기본클래스, 그러니까 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에만 적용됩니다.( 참고. 팩토리 패턴 )

 

 

 

이것 만은 잊지 말자! 

◆ 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 합니다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 합니다.

◆ 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 합니다.


프로그램을 하다보면 어떤 클래스는 절대로 복사되지 말아야하는 경우가 생깁니다. 

 

방법은?

 

우선 복사 생성자와 복사 대입 연산자 자체를 선언하지 않는 방법을 제일 먼저 떠올릴 것이다. 

하지만 컴파일러는 우리가 복사생성자나 복사대입연산자를 선언하지 않으면 자동으로 선언해준다. 

그렇다고 직접 선언하면 복사하는 것은 마찬가지 입니다.

 

해결의 열쇠는 다음과 같습니다.

 

복사 생성자와 복사 대입 연산자를 private 멤버로 선언해버리면, 외부에서 호출할 수가 없으므로 어느정도 효과를 볼 수 있습니다.

그러나! 그 클래스의 멤버 함수, 혹은 friend 함수가 호출할 수 있으므로 이들까지 막아야 합니다.

그럼 아예 정의 자체를 하지 않는 방법이 있습니다. 실제로 이러한 방법은 iostream 라이브러리의 몇몇 클래스에서도 사용되고 있다고 합니다. 

 

ex ) 복사생성자와 복사 대입 연산자를 private에 선언 및 정의 하지 않음

class HomeForSale 

   private
     HomeForSale( const HomeForSale& );  // 매개변수 이름은 필요없다. 사용 안하므로
     HomeForSale& operator=(const HomeForSale& ); 

  public:
     HomeForSale(){}
};

 

HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1);  // 컴파일 Error, 복사생성자가 public이면 LINK에러 (정의가 없으므로)

h1 = h2;                    // 컴파일 Error  

 

위와같이 private으로 오직 선언만 해주게 되면, 멤버함수나 friend 함수가 접근하다가 LINK 에러를 볼겁니다. 따라서 호출 자체를 막아 버릴 수 있게 되는 것입니다. 

  

또하나, 

위에서 friend함수나 멤버함수가 접근하면 링크에러를 발생하는데, 이 링크에러를 컴파일 에러로 옮기는 방법도 있습니다.

 

방법은?

복사생성자와 복사 대입 연산자를 private 멤버로 선언을 하되, 자체 클래스가 아닌 상위 클래스에서 상속받는 방법을 쓰는 것이다.

 

ex ) 복사를 막는 상위 클래스 제작 

class Uncopyable 

   private
       Uncopyable( const Uncopyable& );
       Uncopyable& operator=( const Uncopyable& );    
   protected
       Uncopyable() {} 
       ~Uncopyable() {} 
 };   
class HomeForSale : private Uncopyable // private 상속 
{
}; 

Uncopyable이라는 클래스를 만들어 private멤버로 복사 생성자와 복사 대입 연산자의 선언만 해준다.

그리고 복사되면 안되는 클래스는 Uncopyable 클래스를 private 상속을 받아 사용하면 된다.

private 상속은 부모 클래스의 protected, public 멤버도 private처럼 바꿔서 상속하기 때문에 자식 클래스에서 부모 클래스의 어떠한 것도 접근할 수 없게 되어버립니다.

 

참고 합시다

Boost 라이브러리에 noncopyable 복사 방지 클래스

 

 

이것 만은 잊지 말자!

◆ 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않은 채로 두십시오. Uncopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법입니다.


C++에서 기본적으로 비어있는 클래스를 만들었을경우, 컴파일러가 자동으로 선언해주는 함수가 있습니다.

이함수들은 모두 public이고 inline함수 입니다.

 

즉!

class Empty()

{

 // 이렇게 빈 껍데기 클래스를 만들게 되면

};

 

class Empty()

{

    public:

      Empty() { }                                              // 기본 생성자

      Empty( const Empty& rhs ){ }                     // 복사 생성자

      ~Empty() { }                                            // 기본 소멸자

      Empty& operator=( const Empty& rhs) { }     // 복사 대입 연산자

};

위의 클래스와 같이 자동으로 생성이 됩니다.

 

Empty e1;       // 기본 생성자, 기본소멸자

Empty e2(e1); // 복사생성자

e2 = e1;         // 복사 대입 연산자

 

 

만약에 클래스 멤버 변수가 참조자이거나 상수일 경우에는 기본 복사 대입 연산자가 생성되지 않고,

호출할경우 에러를 내버린다.

 

이유는?

기본 생성 된 복사 대입 연산자는 호출될 때 넘겨받은 인자의 내용을 모든 멤버 변수에 복사하게되는데,

멤버변수가 참조자(&)라면 대입 이전에 처음생성되는 시점에서 이미 다른위치를 참조하게 되고, 그이후에는 이 참조되는 위치를 바꿀 수 없기 때문이다.즉! 참조자는 처음에 참조된 객체 외에 다른 객체를 다시 참조할 수 없기 때문 입니다. 

 

또한, 상수일 경우에도 마찬가지이다. 생성된 이후에 값의 변경은 불가능 하기 때문에 사용자가 직접 복사 대입 연산자를 정의해 주어야 한다.

 

그리고

복사 대입 연산자를 private로 선언한 기본 클래스로부터 파생된 클래스일 경우, 기본 복사 대입 연산자는 생성되지 않는다.

 

이유는?

파생된 클래스의 객체에서 복사 대입 연산자가 호출될때, 기본클래스의 복사 생성자를 호출해야하는데 private 영역에 있어서 호출 할 수가 없기 때문이다.

 


이것 만은 잊지 말자!


◆ 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다.


 


+ Recent posts