s-nova 님의 블로그
[C++] 얕은 복사 vs 깊은 복사 본문
C++에서 객체를 복사할 때, 멤버 변수가 포인터를 포함하고 있다면 단순히 값을 그대로 복사하는 것만으로는 문제가 생길 수 있습니다. 바로 얕은 복사$^{\text{Shallow Copy}}$와 깊은 복사$^{\text{Deep Copy}}$의 차이 때문입니다.
1. 얕은 복사 - 포인터 주소만 복사
컴파일러가 자동으로 생성해주는 기본 복사 생성자$^{\text{Default Copy Constructor}}$와 복사 대입 연산자는 멤버 변수를 그대로 복사합니다. 멤버가 포인터라면 주소값 자체를 복사하므로, 두 객체가 같은 메모리를 가리키게 됩니다.
class Knight
{
public:
Knight()
{
_pet = new Pet();
}
~Knight()
{
delete _pet; // 소멸 시 Pet 해제
}
// 복사 생성자/대입 연산자를 따로 정의하지 않으면
// 컴파일러가 아래처럼 얕은 복사를 자동 생성
// Knight(const Knight& k) { _hp = k._hp; _pet = k._pet; }
int _hp = 100;
Pet* _pet = nullptr;
};
int main()
{
Knight k1;
k1._hp = 200;
Knight k2 = k1; // 얕은 복사: k2._pet == k1._pet (같은 주소!)
// k2 소멸 → delete k2._pet (Pet 해제)
// k1 소멸 → delete k1._pet (이미 해제된 메모리를 또 해제!) → 크래시
}
k2가 먼저 소멸될 때 _pet을 해제하고, 이후 k1이 소멸될 때 이미 해제된 같은 주소를 또 해제합니다.
이를 이중 해제$^{\text{Double Free}}$라 하며, 프로그램 크래시로 이어집니다.
2. 깊은 복사 - 새 메모리를 할당해서 복사
깊은 복사$^{\text{Deep Copy}}$는 포인터가 가리키는 실제 데이터까지 새로 할당하여 복제합니다. 복사 생성자와 복사 대입 연산자를 직접 정의해서 구현합니다.
class Knight
{
public:
Knight()
{
_pet = new Pet();
}
~Knight()
{
delete _pet;
}
// 복사 생성자: 새 Pet을 따로 생성
Knight(const Knight& k)
{
_hp = k._hp;
_pet = new Pet(*k._pet); // 새 메모리에 Pet 복제
}
// 복사 대입 연산자: 기존 Pet 해제 후 새로 생성
Knight& operator=(const Knight& k)
{
if (this == &k) // 자기 자신 대입 방지
return *this;
_hp = k._hp;
delete _pet; // 기존 Pet 해제
_pet = new Pet(*k._pet); // 새 메모리에 Pet 복제
return *this;
}
int _hp = 100;
Pet* _pet = nullptr;
};
int main()
{
Knight k1;
k1._hp = 200;
Knight k2 = k1; // 깊은 복사: k2._pet != k1._pet (독립된 객체)
Knight k3;
k3 = k1; // 깊은 복사: k3._pet도 독립된 객체
// k2, k3, k1 각자 자신의 Pet을 소유 → 소멸 시 각자 안전하게 해제
}
3. 멤버가 포인터가 아닌 경우
멤버 변수가 포인터 없이 일반 객체라면 컴파일러의 기본 복사로도 각 멤버의 복사 생성자가 호출되어 안전합니다.
class Knight
{
public:
// 복사 생성자를 따로 정의하지 않아도 안전
// Knight(const Knight& k) { _hp = k._hp; _pet = k._pet; }
// → _pet은 포인터가 아니므로 Pet의 복사 생성자가 호출됨
int _hp = 100;
Pet _pet; // 포인터가 아닌 값 타입
};
📌 참고: 멤버가 값 타입 $(
Pet _pet$)$이면 기본 복사가 안전하지만, 포인터 타입 $($Pet* _pet$)$이면 반드시 깊은 복사를 직접 구현해야 합니다.
4. 얕은 복사 vs 깊은 복사 비교
| 구분 | 얕은 복사 | 깊은 복사 |
| 포인터 복사 방식 | 주소값만 복사 $($같은 메모리 공유$)$ | 새 메모리 할당 후 데이터 복제 |
| 소멸 시 | 이중 해제 위험 → 크래시 | 각자 독립적으로 안전하게 해제 |
| 구현 방법 | 컴파일러 자동 생성 | 복사 생성자 / 대입 연산자 직접 정의 |
| 비용 | 빠름 | 메모리 할당 비용 발생 |
💡 Rule of Three: 소멸자, 복사 생성자, 복사 대입 연산자 중 하나를 직접 정의해야 한다면 나머지 둘도 반드시 함께 정의해야 합니다. 포인터 멤버를 가진 클래스라면 세 가지를 항상 세트로 구현하는 습관을 들이세요. C++11 이후에는 이동 생성자와 이동 대입 연산자까지 포함한 Rule of Five가 권장됩니다.
'C++' 카테고리의 다른 글
| [C++] 스레드, CPU 캐시 (0) | 2025.09.18 |
|---|---|
| [C++] lvalue, rvalue, Perfect forwarding (0) | 2025.09.14 |
| [C++] 스마트 포인터 (0) | 2025.09.08 |
| [C++] 다형성, virtual, override (0) | 2025.07.27 |
| [C++] 매크로, constexpr, inline, template (0) | 2025.07.19 |