프로그래밍C++ 중급 개발자

C++에서 동적 메모리를 사용하는 컨테이너를 예로 들어 얕은 복사(shallow copy)와 깊은 복사(deep copy)의 차이를 설명하십시오. 깊은 복사를 수동으로 구현하는 방법은 무엇입니까?

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

답변.

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는 복사되지 않음

일반적인 오류 및 안티패턴

  • Rule of Three를 구현해야 한다는 필요성을 무시함.
  • 객체의 복사본이라고 생각하며 포인터를 복사함.
  • 소멸자에서 동적 자원을 해제하지 않음.
  • 소유 자원을 가진 클래스에 대해 얕은 복사를 사용함.

실제 사례

부정적인 케이스

프로그래머가 복사 생성자/할당 연산자를 재정의하지 않고 배열 래퍼 클래스를 구현합니다. 그 결과 두 객체가 동일한 메모리를 소유하게 되어 하나를 삭제하면 두 번째 객체에 접근할 때 충돌이 발생합니다.

장점:

  • 빠르게 작동함 (복사 없음).

단점:

  • 매우 추적하기 어려운 런타임 오류; 이중 해제/세그멘테이션 오류.

긍정적인 케이스

개발자가 깊은 복사를 구현합니다: 배열의 내용이 복사되고, 고유한 소멸자와 자기 할당 방지 기능이 있는 할당 연산자가 있습니다.

장점:

  • 안전한 복사 및 메모리 해제.
  • 코드가 유지 관리 가능하고 확장 가능함.

단점:

  • 약간 더 많은 코드와 메모리 비용.
  • 여러 동적 자원을 가진 클래스의 경우 더 복잡함.