얕은 복사 vs 깊은 복사

Jaemyeong Lee·2024년 9월 24일

게임 서버1

목록 보기
58/220

이 Step에서 다루는 것

  • 얕은 복사(Shallow Copy)가 포인터 멤버에서 왜 위험한지
  • 깊은 복사(Deep Copy)로 독립 객체를 만드는 방법
  • Rule of 3(소멸자/복사 생성자/복사 대입 연산자)를 왜 함께 고려해야 하는지
  • 실전 대안: 복사 금지 또는 스마트 포인터로 Rule of 0 지향

학습 목표

  • Knight k2 = k1;에서 포인터 주소만 복사되면 어떤 문제가 생기는지 설명할 수 있다.
  • 깊은 복사 구현 시 복사 생성자뿐 아니라 복사 대입 연산자까지 필요한 이유를 설명할 수 있다.
  • “깊은 복사 vs 복사 금지 vs 스마트 포인터” 중 상황에 맞는 선택을 할 수 있다.

얕은 복사(Shallow Copy): 주소만 복사된다

컴파일러가 만들어주는 기본 복사는 멤버를 그대로 복사합니다.
포인터 멤버가 있으면 “객체”가 아니라 “주소”가 복사됩니다.

k1._pet ----┐
            ├----> Pet 객체(힙)
k2._pet ----┘

결과:

  • k1k2가 같은 Pet 하나를 공유하게 됩니다.
  • 한쪽에서 delete하면 다른 쪽 포인터는 댕글링이 됩니다.

왜 위험한가: 이중 delete + 해제 후 사용

전형적인 사고 흐름:

  • k1 소멸 -> delete _pet
  • k2 소멸 -> 이미 해제된 같은 주소를 다시 delete (이중 delete, UB)

또는:

  • k1이 먼저 delete한 뒤
  • k2._pet->... 접근 시 Use-After-Free 발생(UB)

핵심:

  • 얕은 복사 자체가 “공유 의도”가 아니라면 거의 버그의 시작점입니다.

깊은 복사(Deep Copy): 각 객체가 자기 자원을 소유

깊은 복사는 복사 시점에 새 객체를 따로 만들어 독립 소유하게 합니다.

class Knight {
public:
    Knight() = default;
    ~Knight() { delete _pet; }

    // 복사 생성자: 깊은 복사
    Knight(const Knight& other)
        : _hp(other._hp),
          _pet(other._pet ? new Pet(*other._pet) : nullptr)
    {
    }

    // 복사 대입 연산자: 깊은 복사 + 자기대입 방지
    Knight& operator=(const Knight& other)
    {
        if (this == &other)
            return *this;

        Pet* newPet = other._pet ? new Pet(*other._pet) : nullptr;
        delete _pet;
        _pet = newPet;
        _hp = other._hp;
        return *this;
    }

private:
    int _hp = 0;
    Pet* _pet = nullptr;
};

깊은 복사 구조(개념)

k1._pet ----> Pet 객체 A (힙)
k2._pet ----> Pet 객체 B (힙)  // 서로 독립

Rule of 3 / 실전 선택지

포인터 같은 “직접 관리 자원”이 멤버로 있으면 보통 3개를 같이 고민해야 합니다.

  • 소멸자
  • 복사 생성자
  • 복사 대입 연산자

실전 선택지:

  • 깊은 복사 구현: 위처럼 Rule of 3를 정확히 구현
  • 복사 금지: 의도적으로 복사를 막기
    • Knight(const Knight&) = delete;
    • Knight& operator=(const Knight&) = delete;
  • 스마트 포인터 사용: std::unique_ptr 등으로 Rule of 0에 가깝게 설계
    • 수동 new/delete 자체를 줄여 버그 여지를 크게 줄임

profile
李家네_공부방

0개의 댓글