C++에서 객체 복사 메커니즘은 얕은 복사(shallow copy)와 깊은 복사(deep copy)로 나뉩니다. 이 차이는 동적으로 할당된 메모리를 사용하는 클래스에서 특히 중요합니다.
C++의 많은 데이터 구조는 동적 메모리(new/delete)와 함께 작동합니다. 기본적으로 컴파일러는 바이트별 복사를 수행하는 복사 생성자와 할당 연산자를 생성합니다(얕은 복사). 이는 빠르지만 객체가 외부 자원을 관리하는 경우 위험할 수 있습니다.
얕은 복사는 동적으로 할당된 자원의 주소만 복사합니다. 하나의 객체를 삭제하면 메모리가 해제되고 다른 인스턴스는 "dangling" 포인터(메모리를 가리키고 있지만 더 이상 유효하지 않은 포인터)를 갖게 됩니다. 결과적으로 이중 삭제(double delete), 메모리 누수, 충돌이 발생합니다.
깊은 복사는 모든 동적 자원의 복사본을 명시적으로 만듭니다. 이를 위해 클래스에서 복사 생성자와 할당 연산자를 수동으로 구현하여 각 요소의 복사본을 보장해야 합니다.
배열을 가진 클래스의 코드 예:
class DynArray { int* data; size_t size; public: DynArray(size_t n) : size(n), data(new int[n]) {} ~DynArray() { delete[] data; } // 깊은 복사 생성자 DynArray(const DynArray& other) : size(other.size), data(new int[other.size]) { for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } // 깊은 복사 할당 연산자 DynArray& operator=(const DynArray& other) { if (this != &other) { delete[] data; size = other.size; data = new int[size]; for (size_t i = 0; i < size; ++i) data[i] = other.data[i]; } return *this; } };
주요 특징:
컴파일러는 항상 정확하게 복사 생성자와 할당 연산자를 생성합니까?
답변:
아닙니다. 동적 자원이 있는 클래스의 경우 기본 복사는 부정확합니다: 두 객체가 동일한 자원을 소유하게 됩니다. 외부 자원을 소유할 때는 깊은 복사를 명시적으로 구현해야 합니다.
깊은 복사 생성자/할당 연산자만 작성하면 소멸자를 구현할 필요가 있습니까?
답변:
네, 그렇지 않으면 메모리 누수가 발생합니다: 사용자 정의 복사 생성자에서 메모리를 해제하더라도 소멸자를 구현하지 않으면 객체가 파괴될 때 메모리를 해제할 방법이 없습니다.
std::vector는 포인터를 저장할 수 있으며 왜 복사할 때 메모리 누수가 발생할 수 있습니까?
답변:
네, std::vector는 포인터를 자유롭게 저장할 수 있습니다. 이러한 std::vector를 복사하면 포인터 자체가 복사되고 그들이 가리키는 객체는 복사되지 않습니다. 이는 얕은 복사입니다: 모든 내용을 깊은 복사하려면 각 객체를 수동으로 복사하고 메모리에 독립적으로 배치해야 합니다.
예:
std::vector<int*> v1; v1.push_back(new int(42)); std::vector<int*> v2 = v1; // 포인터가 복사되고 *int는 복사되지 않음
프로그래머가 복사 생성자/할당 연산자를 재정의하지 않고 배열 래퍼 클래스를 구현합니다. 그 결과 두 객체가 동일한 메모리를 소유하게 되어 하나를 삭제하면 두 번째 객체에 접근할 때 충돌이 발생합니다.
장점:
단점:
개발자가 깊은 복사를 구현합니다: 배열의 내용이 복사되고, 고유한 소멸자와 자기 할당 방지 기능이 있는 할당 연산자가 있습니다.
장점:
단점: