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

s-nova 님의 블로그

[C++] 얕은 복사 vs 깊은 복사 본문

C++

[C++] 얕은 복사 vs 깊은 복사

s-nova 2025. 8. 31. 23:30

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