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; } };
주요 특징:
클래스가 포인터만 포함하고 메모리를 할당하지 않는 경우, 사용자 정의 소멸자가 왜 필요한가요?
소멸자는 클래스 내에서 명시적으로 메모리(또는 기타 자원)를 할당한 경우에만 필요합니다. 포인터가 메모리를 할당하지 않는 경우 기본 제공 소멸자면 충분합니다.
동적 메모리가 있는 클래스에서 대입 연산자(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) { ... }
개발자가 깊은 복사를 구현하지 않고 내장 포인터가 있는 클래스를 복사합니다. 복사 후 여러 객체가 같은 메모리 영역을 공유하게 됩니다. 두 개의 소멸자가 호출되어 double delete가 발생하며 프로그램이 중단됩니다.
장점:
단점:
개발자가 복사 생성자, 대입 연산자 및 소멸자를 올바르게 구현합니다. 각 객체가 자신의 메모리를 소유합니다.
장점:
단점: