삶의 공유

C++에서 Shallow Copy와 Deep Copy 완벽 정리 — 복사의 진짜 차이를 이해하자 본문

Programing언어/C++

C++에서 Shallow Copy와 Deep Copy 완벽 정리 — 복사의 진짜 차이를 이해하자

dkrehd 2025. 10. 16. 22:20
728x90
반응형

📘 C++ 복사의 세계 — Shallow Copy vs Deep Copy 완벽 이해

C++에서 객체를 복사할 때, "주소만 복사되는가" 혹은 "내용까지 복사되는가" 에 따라
결과가 완전히 달라집니다.
오늘은 그 차이를 코드와 함께 하나씩 살펴보겠습니다.


1️⃣ 복사의 기본 개념

 
class Point {
    int x{0};
    int y{0};
public:
    Point() = default;
    Point(int x, int y) : x{x}, y{y} {}
};

int main() {
    Point p1{1, 2};
    Point p2 = p1;  // 복사 생성자 (생성하면서 복사)
    p2 = p1;        // 대입 연산자 (이미 있는 객체에 복사)
}
 

💡 복사 생성자와 대입 연산자

구분의미호출 시점
복사 생성자 새 객체를 기존 객체로 초기화 객체를 “생성”하면서 복사
대입 연산자 이미 존재하는 객체에 다른 객체의 값을 대입 = 연산 시

→ 사용자가 따로 정의하지 않으면 컴파일러가 기본(default) 버전을 자동 생성합니다.
이 기본 버전은 모든 멤버를 그대로 복사(얕은 복사) 합니다.

단순 값 타입(int, double, bool)만 있는 Point 클래스는 문제가 없지만,
포인터 멤버를 가진 클래스에서는 큰 문제가 발생합니다.

 


 

2️⃣ Shallow Copy (얕은 복사) — 문제가 되는 상황

 
 
#include <print>
#include <cstring>

class vector {
    int* ptr;
    std::size_t sz;
public:
    vector(std::size_t sz, int value = 0) : sz(sz) {
        ptr = new int[sz];
        for (std::size_t i = 0; i < sz; i++)
            ptr[i] = value;
    }

    ~vector() { delete[] ptr; }
};

int main() {
    {
        vector v1(4);
        vector v2 = v1;   // ⚠️ 복사 생성자 호출 (디폴트)
    }
    std::println("continue main");
}

⚙️ 코드 동작 설명

  • v1(4) → v1.ptr이 새로운 메모리를 new int[4]로 할당
  • v2 = v1 → 컴파일러가 만든 기본 복사 생성자주소만 복사
    → v2.ptr과 v1.ptr이 같은 메모리를 가리킴

이제 문제가 시작됩니다 👇

🚨 얕은 복사의 문제점

  1. 공유된 메모리
    • v2가 데이터를 변경하면 v1의 데이터도 바뀜
    • 두 객체가 같은 배열을 바라보기 때문
  2. 이중 삭제(double delete)
    • 블록이 끝나면 v2와 v1이 순서대로 소멸자 호출
    • delete[] ptr;이 두 번 실행되면서 동일한 메모리를 두 번 삭제
  3. 메모리 누수(memory leak)
    • 반대로, 복사된 객체가 덮어쓰기 되면 기존 메모리 주소를 잃어버려
      해제되지 않은 메모리가 남을 수 있음

 


 

3️⃣ Deep Copy (깊은 복사) — 문제 해결의 핵심

얕은 복사는 단순히 주소값을 복사하지만,
깊은 복사는 새로운 메모리를 할당하고 데이터를 복제합니다.

 
#include <cstring>

class vector {
    int* ptr;
    std::size_t sz;
public:
    vector(std::size_t sz, int value = 0) : sz(sz) {
        ptr = new int[sz];
        for (std::size_t i = 0; i < sz; i++)
            ptr[i] = value;
    }

    ~vector() { delete[] ptr; }

    // ✅ 깊은 복사 생성자
    vector(const vector& other) : sz(other.sz) {
        ptr = new int[sz];                                  // 새 메모리 할당
        std::memcpy(ptr, other.ptr, sizeof(int) * sz);      // 데이터 복사
    }
};

int main() {
    vector v1(4, 10);
    vector v2 = v1; // 깊은 복사 수행
}
 

💡 동작 설명

  • v2.ptr 은 v1.ptr과 완전히 다른 메모리 공간을 가리킴
  • 따라서 v2가 수정되어도 v1은 영향을 받지 않음
  • 각각의 객체가 독립적인 자원을 가지므로, 소멸자에서 안전하게 삭제 가능

 


 

4️⃣ 대입 연산자도 직접 구현해야 한다

복사 생성자를 정의했더라도, 대입 연산자(operator=) 도 따로 구현해야 합니다.
그렇지 않으면 또다시 얕은 복사가 일어납니다.

 
vector& operator=(const vector& other) {
    if (this == &other) return *this;     // 자기 자신 대입 방지

    if (sz != other.sz)
        sz = other.sz;

    delete[] ptr;                         // 기존 메모리 해제
    ptr = new int[sz];                    // 새 메모리 할당
    std::memcpy(ptr, other.ptr, sizeof(int) * sz); // 데이터 복사

    return *this;
}
 

 

⚙️ 주의할 점

  1. 자기 자신 대입 방지 (if (this == &other))
    v2 = v2; 처럼 자기 자신에게 대입하는 상황에서도 안전해야 함
  2. 메모리 누수 방지
    새로 복사하기 전에 반드시 기존 메모리를 delete[] 해야 함
  3. 동일 크기일 때도 올바른 데이터 복사
    크기가 같아도 기존 데이터를 덮어써야 하므로 memcpy 필요

 


 

5️⃣ 얕은 복사 vs 깊은 복사 비교 요약

구분얕은 복사 (Shallow Copy)깊은 복사 (Deep Copy)
복사 방식 주소만 복사 새 메모리 할당 후 데이터 복사
메모리 구조 두 객체가 같은 주소 공유 각 객체가 독립된 메모리
수정 영향 한쪽 수정 시 다른쪽도 영향 서로 독립적
소멸 시 이중 삭제 위험 안전하게 삭제 가능
대표 사례 디폴트 복사 생성자 사용자 정의 복사 생성자

 


 

6️⃣ 얕은 복사 해결책 정리

해결 방법설명
깊은 복사(Deep Copy) 새로운 메모리 공간을 만들어 데이터를 복제
참조 카운팅(Reference Counting) 같은 데이터를 공유하되 참조 횟수 관리 (std::shared_ptr)
소유권 이전(Move Ownership) 메모리 소유권을 이동 (std::unique_ptr, move semantics)
복사 및 대입 금지(Delete) 복사 자체를 막음 (= delete)

 


 

📌 핵심 요약

  • 디폴트 복사 생성자 / 대입 연산자는 얕은 복사를 수행한다.
  • 포인터 멤버가 있는 클래스는 반드시 깊은 복사 생성자와 대입 연산자를 직접 구현해야 한다.
  • 그렇지 않으면 이중 delete, 메모리 누수, 공유 데이터 오염 등의 심각한 문제가 발생한다.
  • 복사 안전성을 보장하는 법:
    • Deep Copy 구현
    • 또는 std::unique_ptr / std::shared_ptr 등 스마트 포인터 사용
반응형