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

업데이트:     Updated:

카테고리:

태그:

이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역

📦 2. 생성자, 소멸자 및 대입 연산자

👉🏻 항목 12: 객체의 모든 부분을 빠짐없이 복사하자


✅ 객체 복사 함수란?

C++에서 객체를 복사하는 함수는 두 가지가 있다.

  1. 복사 생성자 → 기존 객체를 기반으로 새 객체를 생성할 때 호출됨

  2. 복사 대입 연산자 → 기존 객체에 다른 객체의 값을 대입할 때 호출됨

이 둘을 묶어 객체 복사 함수(copying functions) 라 부른다.

컴파일러는 이 두 함수를 자동으로 생성해준다. 하지만 복사 방식은 단순히 멤버별 얕은 복사다. 따라서 포인터, 리소스, 또는 복사 방식이 중요한 클래스에선 직접 정의가 필요하다.


✅ 직접 정의할 때 주의할 점

다음은 복사 생성자와 대입 연산자를 명시적으로 정의한 예시다.

void logCall(const string& funcName); // 로그 기록용

class Customer {
public:
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
private:
    string name;
};

Customer::Customer(const Customer& rhs)
    : name(rhs.name) {
    logCall("Customer 복사 생성자");
}

Customer& Customer::operator=(const Customer& rhs) {
    logCall("Customer 복사 대입 연산자");
    name = rhs.name;
    return *this;
}

이 코드는 잘 작동한다. 하지만, 이 상태에서 데이터 멤버가 추가되면 반드시 복사 함수도 같이 수정해줘야 한다.


✅ 데이터 멤버가 추가되었을 때

class Customer {
public:
    Customer(const Customer& rhs);
    Customer& operator=(const Customer& rhs);
private:
    string name;
    Date lastTransaction; // 새로운 멤버
};

이때 복사 함수 코드를 업데이트하지 않으면 문제가 생긴다.

Customer::Customer(const Customer& rhs)
    : name(rhs.name) {} // ❗ lastTransaction 빠짐

Customer& Customer::operator=(const Customer& rhs) {
    name = rhs.name;     // ❗ lastTransaction 빠짐
    return *this;
}

❗ 이 경우도 컴파일은 잘 되지만, 복사는 부분적으로만 이루어진다. lastTransaction은 복사되지 않아 초기값을 그대로 가진다.

👉 데이터 멤버가 늘어나면, 복사 함수도 그에 맞게 수정해야 한다.


✅ 상속이 있는 경우 주의할 점

기본 클래스가 복사되지 않는 문제도 흔하다.

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 복사 생성자");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer 복사 대입 연산자");
    priority = rhs.priority;
    return *this;
}

이 코드는 priority는 복사되지만, 기본 클래스인 Customer는 복사되지 않는다.

  • 생성자에서는 Customer() 기본 생성자만 호출됨
  • 대입 연산자에서는 Customer의 복사 연산자가 아예 호출되지 않음 Customer는 수정된 것 없이 유지됨

✅ 해결 방법: 기본 클래스 복사도 명시적으로 호출

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
    : Customer(rhs),                // ✅ 기본 클래스 복사
      priority(rhs.priority) {
    logCall("PriorityCustomer 복사 생성자");
}

PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) {
    logCall("PriorityCustomer 복사 대입 연산자");
    Customer::operator=(rhs);      // ✅ 기본 클래스 복사
    priority = rhs.priority;
    return *this;
}

👉 기본 클래스 부분도 명시적으로 복사해야 한다. 자동으로 되지 않기 때문에 항상 직접 호출해줘야 한다.


✅ 복사 함수끼리 재활용하면 안 되는 이유

Customer::Customer(const Customer& rhs) {
    *this = rhs; // ❌ 복사 생성자에서 대입 연산자 호출
}

이런 방식은 초기화되지 않은 객체에 대입을 수행하는 위험한 행동이다. 반대로 대입 연산자에서 생성자를 호출해도 마찬가지.

두 복사 함수는 서로를 호출하지 말 것! 대신 공통 동작을 별도 함수로 분리해 공유하자.


✅ 공통 복사 코드 분리하기

class Customer {
public:
    Customer(const Customer& rhs) {
        logCall("복사 생성자");
        copyFrom(rhs);
    }

    Customer& operator=(const Customer& rhs) {
        logCall("복사 대입 연산자");
        if (this != &rhs) {
            copyFrom(rhs);
        }
        return *this;
    }

private:
    void copyFrom(const Customer& rhs) {
        name = rhs.name;
        lastTransaction = rhs.lastTransaction;
    }

    string name;
    Date lastTransaction;
};

복사할 내용은 copyFrom에 모아두면, 중복도 줄고, 안정성도 높아진다.


🧐 정리

  1. 복사 생성자와 복사 대입 연산자는 모든 데이터 멤버기본 클래스 부분까지 빠짐없이 복사하자.
  2. 상속 관계에서는 기본 클래스 복사도 명시적으로 호출해줘야 한다.
  3. 복사 함수끼리 서로 호출하지 말고, 공통된 복사 코드는 별도 함수로 분리해 사용하자.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기