프로그래밍C++ 개발자

얕은 복사(shallow copy)와 깊은 복사(deep copy) 간의 차이를 설명하세요. 깊은 복사 생성자가 필요한 경우와 그 이유는 무엇인가요? 깊은 복사를 수동으로 구현하는 방법은 무엇인가요?

Hintsage AI 어시스턴트로 면접 통과

답변.

질문의 역사

C++에서 객체 복사 시 기본적으로 사용되는 메커니즘은 멤버별 복사(memberwise copying)입니다: 각 객체의 멤버에 개별적인 복사 연산이 호출됩니다. 내장형 타입에 대해서는 안전하지만 동적 자원에는 문제가 발생합니다: 포인터만 복사되고 실제 데이터는 복사되지 않습니다.

문제

객체가 힙 메모리에 할당된 포인터를 포함하고 있다면, 두 객체를 복사한 후 이들이 같은 메모리 영역을 가리키게 됩니다. 그러면 하나의 객체가 파괴될 때 메모리가 해제되고 다른 객체의 포인터는 유효하지 않게 됩니다("야생" 포인터). 이것은 런타임 오류와 메모리 릭으로 이어질 수 있습니다.

해결책

복사본이 독립적이기 위해서는 깊은 복사(deep copy) — 바이트 단위 복사 및 자체 버퍼 할당이 필요합니다. 이를 위해 사용자 정의 복사 생성자와 대입 연산자를 작성해야 합니다.

코드 예시:

class MyString { char* data; public: MyString(const char* s) { data = new char[strlen(s)+1]; strcpy(data, s); } // 깊은 복사 생성자 MyString(const MyString& src) { data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } // 깊은 복사 대입 연산자 MyString& operator=(const MyString& src) { if (this != &src) { delete[] data; data = new char[strlen(src.data) + 1]; strcpy(data, src.data); } return *this; } ~MyString() { delete[] data; } };

주요 특징:

  • 외부 자원을 관리하는 클래스에 필요함
  • 사용자 정의 복사 생성자 및 대입 연산자 구현이 필요함
  • 올바른 Rule of Three 구현 없이는 메모리 릭이 발생할 수 있음

함정 질문들.

클래스가 포인터만 포함하고 메모리를 할당하지 않는 경우, 사용자 정의 소멸자가 왜 필요한가요?

소멸자는 클래스 내에서 명시적으로 메모리(또는 기타 자원)를 할당한 경우에만 필요합니다. 포인터가 메모리를 할당하지 않는 경우 기본 제공 소멸자면 충분합니다.


동적 메모리가 있는 클래스에서 대입 연산자(operator=)를 구현하지 않으면 복사 생성자만 선언된 경우 어떻게 되나요?

복사 생성자를 수동으로 정의한 경우, 컴파일러는 자동으로 대입 연산자(operator=)를 구현하지 않습니다; 암시적으로 선언되거나, 컴파일러가 오류/경고를 발생시킵니다 (표준에 따라 다름). 이는 대입 시 잘못 정의된 동작으로 이어지며, 멤버별 복사가 발생하여 double free나 메모리 릭이 발생할 수 있습니다.

코드 예시:

MyString a("hi"); MyString b = a; // 괜찮음: 사용자의 복사 생성자 MyString c("bye"); c = a; // 문제! operator=가 수동으로 구현되지 않으면 얕은 복사가 발생함

손수 구현한 operator=에서 자기 자신에게 할당하는 것은 왜 위험한가요?

자원을 나누는 경우 this!=&rhs를 확인하지 않으면, 자기 자신에게 할당할 때 delete[] data가 실행된 후 이미 파괴된 배열을 복사하게 되어 segfault가 발생할 수 있습니다. 자기 보호: 항상 self-assignment을 확인하세요.

if (this != &rhs) { ... }

전형적인 오류 및 앤티 패턴

  • Rule of Three 메소드(복사 생성자, operator=, 소멸자) 중 하나가 구현되지 않음
  • self-assignment 확인이 이루어지지 않음
  • delete[]를 잊어버려서 메모리 릭 발생
  • 포인터가 공유된 경우 double free 발생

실제 사례

부정적인 케이스

개발자가 깊은 복사를 구현하지 않고 내장 포인터가 있는 클래스를 복사합니다. 복사 후 여러 객체가 같은 메모리 영역을 공유하게 됩니다. 두 개의 소멸자가 호출되어 double delete가 발생하며 프로그램이 중단됩니다.

장점:

  • 코드 작성이 간단함 ("복사가 작동함"에서 처음 보기에는)

단점:

  • 프로그램 중단, 예측할 수 없는 버그 발생
  • 메모리 릭 또는 double-free 발생

긍정적인 케이스

개발자가 복사 생성자, 대입 연산자 및 소멸자를 올바르게 구현합니다. 각 객체가 자신의 메모리를 소유합니다.

장점:

  • 복사 및 파괴 시 안전함
  • 메모리 릭 없음

단점:

  • 복사본이 많아질 경우 복사의 오버헤드 증가