캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있다.

복사 생성자와 복사 대입 연산자인데 이 둘을 통틀어 복사 함수라고 한다.

 

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
void logCall(const std::string& funcName);
 
class Customer
{
public:
    ...
    Customer(const Customer& rhs);              //복사 생성자.
    Customer& operator=(const Customer& rhs); //복사 대입 연산자.
    ...
 
private:
    std::string name;
};
 
Customer::Customer(const Customer& rhs) : name(rhs.name)
{
    logCall("Customer copy construction");
}
 
Customer& Customer::operator=(const Customer& rhs)
{
    logCall("Customer copy assignment operator");
 
    name = rhs.name;
 
    return *this;
}
cs

 

문제될 것이 없는 코드이지만 멤버 변수 하나를 추가하면서 문제가 발생한다.
기존에 작성된 코드에서 멤버 변수가 추가됨에 따라 생성자, 복사생성자, 복사 대입 연산자 모두를 수정해야한다.
혹여나 여기서 하나라도 빼먹게되면 부분 복사가 되버리게 된다.
특히, 상속 관계에서는 더욱 더 신경써줘야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class priorityCustomer : public Customer
{
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;
}
cs

 

겉으로 보기에는 모든 정보를 복사하고 있는 것처럼 보이지만, 그렇지 않다.
PriorityCustomer에 선언된 데이터는 모두 복사하고 있는 것은 맞지만, 부모 class인 Customer로부터 상속한 데이터 멤버들은

복사가 되지 않고 있다.
Customer의 데이터들도 엄연히 파생클래스인 PriorityCustomer에 들어있기 때문에 Customer의 데이터도 반드시 모두 복사해줘야 한다.
PriorityCustomer의 복사 생성자에는 기본 클래스 생성자에 넘길 인자들도 명시되어 있지 않아서 PriorityCustomer형 객체의 Customer 부분은

인자없이 실행되는 Customer 기본 생성자에 의해 기본적인 초기화만 된다.


또한 PriorityCustomer의 복사 대입 연산자의 경우에는 기본 클래스의 데이터 멤버를 건드릴 시도도 하지 않기 때문에,

기본 클래스의 데이터 멤버는 변경되지 않고 그래로 있게 된다.


즉, 파생 클래스에 대한 복사 함수를 직접 만든다면 기본 클래스 부분을 복사에서 빠뜨리지 않도록 주의해야 한다.
기본 클래스의 멤버 변수들은 private일 가능성이 높으므로 직접 접근하기 어렵다.


그러므로 파생 클래스의 복사 함수 안에서 기본 클래스의 복사 함수를 호출하도록 만들어서 사용하면 된다.

 

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
class priorityCustomer : public Customer
{
public:
    ...
    priorityCustomer(const priorityCustomer& rhs);
    priorityCustomer& operator=(const priorityCustomer& rhs);
    ...
private:
    int priority;
};
 
priorityCustomer::priorityCustomer(const priorityCustomer& rhs)
Customer(rhs),    //기본 클래스의 복사 생성자를 호출한다.
  priority(rhs.priority)
{
    logCall("priorityCustomer copy constructor");
}
 
priorityCustomer& priorityCustomer::operator =(const priorityCustomer& rhs)
{
    Customer::operator=(rhs);  //기본 클래스 부분을 대입한다.
 
    logCall("priorityCustomer copy assignment operator");
 
    priority = rhs.priority;
 
    return *this;
}
cs

 

또한 복사 생성자와 복사 대입 연산자의 동작은 비슷하므로 함수 본문 또한 비슷하다.
그래서 아마도 코드 중복을 피하려고 한쪽에서 다른 한쪽을 호출하는 것을 생각할 수 있는데…. 절대 그래선 안된다.
(대입 연산자에서 복사생성자를 호출한다던지 복사생성자에서 대입 연사자를 호출한다던지)

복사 대입 연산자는 이미 생성된 객체에서만 사용이 가능한데 복사 대입 연산자에서 복사 생성자를 호출하면 이미 존재하는

객체를 또 만들려는 동작이 되버린다. (이미 태어난 아이에게 엄마 뱃속으로 들어가라는 꼴임)

헷갈릴까봐 적는거지만 생성과 동시에 = 를 이용해서 대입하면 복사 대입 연산자가 아닌 복사 생성자가 호출된다.

즉, 복사 대입 연산자는 이미 생성된 객체에서만 사용 가능하다는 의미이다.

 

거꾸로 복사 생성자에서 복사 대입 연산자를 호출한다는 것 또한 말이 되지 않는다.

대입 연산자의 의미는 이미 초기화가 끝난 객체에게 값을 주는 것인데 아직 초기화가 완료되지 않은 객체에 대입을 하게되므로

프로그램이 죽을 수도 있다.

 

하고 싶다면 별도의 private 함수를 만들어서 각 함수에서 호출하도록 하자.

 

2줄 요약

 

 - 객체 복사 함수는 주어진 객체의 모든 데이터 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해라.

 

 - 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대하지 마라.

 

+ Recent posts