Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
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
Archives
Today
Total
관리 메뉴

로또

12. 객체의 모든 부분을 빠짐없이 복사하자. 본문

책/Effective C++

12. 객체의 모든 부분을 빠짐없이 복사하자.

아롱로또 2023. 11. 8. 20:14

다루는 내용

복사 생성자나 복사 대입 연산자를 직접 만든다면 객체의 모든 부분을 복사할 수 있게 구현하자.

복사 생성자와 복사 대입 연산자 구현

#include <iostream>
#include <string>

using namespace std;

void LogCall(const string& log){
    cout << log << endl;
}

class Customer{
public:
    Customer(){}
    Customer(const string& _name): name(_name){}
    Customer(const Customer& rhs): name(rhs.name){
        LogCall("Customer Copy Constructor");
    }
    Customer& operator=(const Customer& rhs){
        LogCall("Customer Copy assignment operator");
        name = rhs.name;
        return *this;
    }
    string GetName(){
        return name;
    }
private:
    string name;
};

int main(){
    Customer c1("Lotto");
    Customer c2(c1); // Copy Constructor
    Customer c3;
    c3 = c1; // Copy assignment operator

    cout << "c1 name: " << c1.GetName() << endl;
    cout << "c2 name: " << c2.GetName() << endl;
    cout << "c3 name: " << c3.GetName() << endl;

    return 0;
}

실행 결과

 

직접 만든 복사 생성자와 복사 대입 연산자가 성공적으로 호출되어 정상적으로 실행된 모습을 확인할 수 있다.

 

부분 복사 문제

다음과 같이 Customer 클래스에 데이터 멤버가 추가되는 경우를 생각해보자.

class Customer{
public:
    Customer(){}
    Customer(const string& _name, const int _number): name(_name), number(_number){}
    Customer(const Customer& rhs): name(rhs.name){
        LogCall("Customer Copy Constructor");
    }
    Customer& operator=(const Customer& rhs){
        LogCall("Customer Copy assignment operator");
        name = rhs.name;
        return *this;
    }
    string GetName(){ return name; }
    int GetNumber(){ return number; }

private:
    string name;
    int number;
};

int main(){
    Customer c1("Lotto", 12);
    Customer c2(c1); // Copy Constructor
    Customer c3;
    c3 = c1; // Copy assignment operator

    cout << "c1 number: " << c1.GetNumber() << endl;
    cout << "c2 number: " << c2.GetNumber() << endl;
    cout << "c3 number: " << c3.GetNumber() << endl;

    return 0;
}

실행 결과

 

데이터 멤버 number가 추가되었지만 별도로 복사 생성자나 복사 대입 연산자에 구현해주지 않았으므로 실제로는 객체의 일부분(name)만 복사될 것이다. 실행 결과, 위처럼 올바르지 않은 값이 저장되어 있음을 알 수 있다. 이는 컴파일러에서 아무런 메시지 없이 정상적으로 조용히 컴파일된다.

 

이러한 문제는 상속 관계에서 빈번히 나타날 수 있다.

class PriorityCustomer: public Customer{
public:
    PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority){
        LogCall("PriorityCustomer Copy Constructor");
    }
    PriorityCustomer& operator=(const PriorityCustomer& rhs){
        LogCall("PriorityCustomer Copy assignment operator");
        priority = rhs.priority;
        return *this;
    }
private:
    int priority;
}

PriorityCustomer는 복사 생성자 및 복사 대입 연산자에서 모든 데이터 멤버에 대해 올바르게 복사하고 있다. 하지만 PriorityCustomer는 Customer의 파생 클래스로 priority 변수 외에도 name, number 변수를 멤버 변수로 가지고 있다는 사실을 잊어서는 안된다.

 

위 코드는 복사 생성자에서 기본 클래스인 Customer에 대한 처리가 존재하지 않으므로 Customer는 기본 생성자에 의해 초기화될 것이다.

복사 대입 연산자의 경우에도 기본 클래스의 데이터 멤버는 별도의 변경이 이루어지지 않고 그대로 유지될 것이다.

 

문제를 피하기 위해서는 다음과 같이 복사 생성자나 복사 대입 연산자에서 기본 클래스의 복사 함수를 호출하도록 변경해주자.

class PriorityCustomer: public Customer{
public:
    PriorityCustomer(const PriorityCustomer& rhs)
    :Customer(rhs), priority(rhs.priority){ // 기본 클래스의 복사 생성자 호출
        LogCall("PriorityCustomer Copy Constructor");
    }
    PriorityCustomer& operator=(const PriorityCustomer& rhs){
        LogCall("PriorityCustomer Copy assignment operator");
        Customer::operator=(rhs); // 기본 클래스의 복사 대입 연산자 호출
        priority = rhs.priority;
        return *this;
    }
private:
    int priority;
}

 

코드 중복에 대해서

 

복사 생성자나 복사 대입 연산자 모두 인자로 받은 객체의 데이터를 복사하여 저장한다는 점에서 동일한 기능을 가진다. 때문에 이러한 중복을 피하기 위해 이전 게시글 "3. 낌새만 보이면 const를 들이대 보자!" 에서 등장했던 코드 중복 내용을 생각할 수 있다. 

 

복사 대입 연산자에서 복사 생성자를 호출하는 것은, 이미 생성되어 버젓이 존재하는 객체를 다시 생성한다는 문제가 있다.

 

반대로 복사 생성자에서 복사 대입 연산자를 호출하는 것 또한 문제가 있다. 생성자의 역할은 새로 만들어진 객체를 초기화하는 것이고, 대입 연산자의 역할은 이미 초기화가 끝난 객체에 대해 값을 주는 것이다. 생성 중인 객체에 대입 연산은 말이 되지 않는 행동이므로 하지 말자.

 

대신, 복사 생성자와 복사 대입 연산자에서 겹치는 부분을 별도의 멤버 함수로 분리하고 필요할 때 해당 함수를 호출하는 방법을 사용할 수 있다.

 

이것만은 잊지 말자!

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