Effective C++/Chapter 2: 생성자, 소멸자 및 대입 연산자

[Effective C++] 항목 12: 객체의 모든 부분을 빠짐없이 복사하자

yeoul0714 2025. 4. 25. 01:34

1. 개요


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

 

1. 복사 생성자

2. 복사 대입 연산자

 

우리는 이 둘을 통틀어서 복사 함수라고 부릅니다.

 

우리는 컴파일러가 자동으로 생성해주는 복사 함수 이외에 개발자가 따로 선언하는 것도 가능합니다.

 

그러나 컴파일러는 우리가 복사 함수를 잘못 선언해도 알려주지 않습니다.

 

그 예시를 한번 보도록 하죠


 

2. 복사 함수에 문제가 생길 때


void logCall(const std::string& funcName); // make a log entry
class Customer {
public:

    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);

private:
    std::string name;
};

Customer::Customer(const Customer& rhs): name(rhs.name) // copy rhs’s data
{
    logCall("Customer copy constructor");
}
Customer& Customer::operator=(const Customer& rhs)
{
    logCall("Customer copy assignment operator");
    name = rhs.name; // copy rhs’s data
    return *this; // see Item 10
}

 

위의 복사함수들을 보면 아무런 문제가 없습니다.

 

아주 잘 작동하는 코드 입니다.

 

그러나

 

Customer Class에 멤버를 하나 추가하면 추가하면 문제가 생기기 시작합니다.

class Date {  }; // for dates in time
class Customer 
{
public:
                 // as before
private:
    std::string name;
    Date lastTransaction;
};

 

Date타입의 멤버변수가 추가 되었습니다.

 

이렇게 되면 복사 함수는 더이상 완벽하지 않습니다.

 

name은 잘 복사하고 있지만 lastTransaction은 복사하지 않아서 결과적으로 부분 복사(partial copy)가 발생합니다.

 

그러나 컴파일러는 우리에게 어떠한 에러도 돌려주지 않습니다.

 

그래서 새로운 멤버 변수가 생기면 우리는 복사 함수와 생성자에 하나하나 새롭게 추가해주어야 합니다.


 

3. 상속 관계에서의 문제


class PriorityCustomer: public Customer { // a derived class
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);

private:
    int priority;
};

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer copy assignment operator");
    priority = rhs.priority;
    return *this;
}

 

PriorityCustomer 클래스는 현재 Customer 클래스를 상속받고 있는 상태입니다.

 

PriorityCustomer의 복사함수를 보면 자신의 멤버를 전부 복사하고 있고 문제가 없는 듯 보일 수 있습니다.

 

그러나

 

간과한 것이 있습니다. 

 

그것은 바로 Customer를 상속받고 있다는 사실이지요.

 

PriorityCustomer 클래스는 Customer 클래스의 데이터들도 가지고 있을텐데 이 부분은 전혀 복사가 되지 않고 있습니다.

 

그러므로 파생 클래스의 복사 함수를 직접 만들려고 한다면 기본 클래스 부분을 복사에서 빼놓지 않도록 주의해야합니다.

 

그 방법은 바로 파생 클래스의 복사 함수에서 기본 클래스의 복사 함수를 호출하면 됩니다.


 

4. 상속 관계에서의 문제 해결법


PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: Customer(rhs), // invoke base class copy ctor
priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs); // assign base class parts
    priority = rhs.priority;
    return *this;
}

 

이렇게 하면 기본 클래스의 데이터까지 아주 잘 복사됩니다.

 

우리는 복사 함수를 직접 만들때 2가지를 꼭 기억해야 합니다.

 

1. 해당 클래스의 데이터를 모두 복사

2. 기본 클래스의 복사 함수도 꼭 호출 (기본 클래스의 데이터 복사를 위해)


 

5. 이상한 해결법 (해결법 아님)


여기까지 글을 읽었다면 한가지 의문을 가질 수 있습니다.

 

복사 함수 2가지는 본문이 비슷하게 나오는 경우가 자주 있습니다. (복사 해야할 대상이 대부분 비슷할겁니다.)

 

그래서 복사 생성자에서 복사 대입 연산자를 호출하거나 그 반대를 해서 코드 중복을 피하려고 할지도 모릅니다.

 

그러나 이렇게 하면 절대로 안됩니다.

 


5-1. 복사 대입 연산자에서 복사 생성자 호출

아얘 말이되지 않는 부분입니다.

 

  • 복사 생성자: 새로운 객체를 생성할 때 호출
  • 복사 대입 연산자: 이미 존재하는 객체에 값을 대입할 때 호출

 

애초에 역할이 다른 두 함수입니다.

 

이미 대입을 했다는 것은 객체가 만들어졌다는 것인데 또 다시 복사 생성자를 호출한다는 개념은

 

말 자체가 성립이 안됩니다.

 

어떻게든 하는건 되지만 책에서는 절대로 하지말라고 코드 조차 공개하지 않고 있습니다.


5-2. 복사 생성자에서 복사 대입 연사자 호출

이것도 역시 말도 안됩니다.

 

생성자는 새로 만들어진 객체를 초기화 하는 것이고, 복사 대입 연산자는 이미 만들어진 객체에 값을 주는 것입니다.

 

생성자에서 생성중인 객체에서 복사 대입을 한다니 말이 안됩니다.

 

깊게 생각할 필요도 없이 해서는 안되는 행동입니다.

 

이해하려고 노력하지 마십시오.


5-3. 중복을 어떻게 해서든 피하고 싶다면

복사 생성자와 복사 대입 연산자의 코드가 비슷하다고 느껴진다면 겹치는 부분을 멤버 함수로 빼놓고

 

그 함수를 호출하는 방법을 쓰도록 합시다.

 

 

이것만은 잊지 말자!

  • 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 합니다.
  • 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 마세요. 
    대신, 공통된 동작을 제3의 함수에 분리하고 양쪽에서 이것을 호출해서 사용하게 합시다.

 

https://yeoul0714.tistory.com/45

 

[Effective C++] 항목 13: 자원 관리에는 객체가 그만!

1. 개요이번 장은 자원을 관리하는 법에 대해서 기술하는 챕터이다. 특히 항목 13은 포인터를 사용한뒤 해제하는 방법들을 소개한다. void f(){ Investment *pInv = createInvestment(); // 무언가 동작 delete pInv

yeoul0714.tistory.com