[C++] 항목 12: 객체의 모든 부분을 빠짐없이 복사하자
카테고리: Cpp
태그: Cpp
이 글은 아래의 책을 자세히 정리한 후, 정리한 글을 GPT에게 요약을 요청하여 작성되었습니다.
이펙티브 C++ 제3판, 스콧 마이어스 저자, 곽용재 번역
📦 2. 생성자, 소멸자 및 대입 연산자
👉🏻 항목 12: 객체의 모든 부분을 빠짐없이 복사하자
✅ 객체 복사 함수란?
C++에서 객체를 복사하는 함수는 두 가지가 있다.
-
복사 생성자 → 기존 객체를 기반으로 새 객체를 생성할 때 호출됨
-
복사 대입 연산자 → 기존 객체에 다른 객체의 값을 대입할 때 호출됨
이 둘을 묶어 객체 복사 함수(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
에 모아두면,
중복도 줄고, 안정성도 높아진다.
🧐 정리
- 복사 생성자와 복사 대입 연산자는 모든 데이터 멤버와 기본 클래스 부분까지 빠짐없이 복사하자.
- 상속 관계에서는 기본 클래스 복사도 명시적으로 호출해줘야 한다.
- 복사 함수끼리 서로 호출하지 말고, 공통된 복사 코드는 별도 함수로 분리해 사용하자.
댓글남기기