Notice
Recent Posts
Recent Comments
Link
삶의 공유
C++에서 Shallow Copy와 Deep Copy 완벽 정리 — 복사의 진짜 차이를 이해하자 본문
728x90
반응형
📘 C++ 복사의 세계 — Shallow Copy vs Deep Copy 완벽 이해
C++에서 객체를 복사할 때, "주소만 복사되는가" 혹은 "내용까지 복사되는가" 에 따라
결과가 완전히 달라집니다.
오늘은 그 차이를 코드와 함께 하나씩 살펴보겠습니다.
1️⃣ 복사의 기본 개념
class Point {
int x{0};
int y{0};
public:
Point() = default;
Point(int x, int y) : x{x}, y{y} {}
};
int main() {
Point p1{1, 2};
Point p2 = p1; // 복사 생성자 (생성하면서 복사)
p2 = p1; // 대입 연산자 (이미 있는 객체에 복사)
}
💡 복사 생성자와 대입 연산자
구분의미호출 시점
| 복사 생성자 | 새 객체를 기존 객체로 초기화 | 객체를 “생성”하면서 복사 |
| 대입 연산자 | 이미 존재하는 객체에 다른 객체의 값을 대입 | = 연산 시 |
→ 사용자가 따로 정의하지 않으면 컴파일러가 기본(default) 버전을 자동 생성합니다.
이 기본 버전은 모든 멤버를 그대로 복사(얕은 복사) 합니다.
단순 값 타입(int, double, bool)만 있는 Point 클래스는 문제가 없지만,
포인터 멤버를 가진 클래스에서는 큰 문제가 발생합니다.
2️⃣ Shallow Copy (얕은 복사) — 문제가 되는 상황
#include <print>
#include <cstring>
class vector {
int* ptr;
std::size_t sz;
public:
vector(std::size_t sz, int value = 0) : sz(sz) {
ptr = new int[sz];
for (std::size_t i = 0; i < sz; i++)
ptr[i] = value;
}
~vector() { delete[] ptr; }
};
int main() {
{
vector v1(4);
vector v2 = v1; // ⚠️ 복사 생성자 호출 (디폴트)
}
std::println("continue main");
}
⚙️ 코드 동작 설명
- v1(4) → v1.ptr이 새로운 메모리를 new int[4]로 할당
- v2 = v1 → 컴파일러가 만든 기본 복사 생성자는 주소만 복사
→ v2.ptr과 v1.ptr이 같은 메모리를 가리킴
이제 문제가 시작됩니다 👇
🚨 얕은 복사의 문제점
- 공유된 메모리
- v2가 데이터를 변경하면 v1의 데이터도 바뀜
- 두 객체가 같은 배열을 바라보기 때문
- 이중 삭제(double delete)
- 블록이 끝나면 v2와 v1이 순서대로 소멸자 호출
- delete[] ptr;이 두 번 실행되면서 동일한 메모리를 두 번 삭제
- 메모리 누수(memory leak)
- 반대로, 복사된 객체가 덮어쓰기 되면 기존 메모리 주소를 잃어버려
해제되지 않은 메모리가 남을 수 있음
- 반대로, 복사된 객체가 덮어쓰기 되면 기존 메모리 주소를 잃어버려
3️⃣ Deep Copy (깊은 복사) — 문제 해결의 핵심
얕은 복사는 단순히 주소값을 복사하지만,
깊은 복사는 새로운 메모리를 할당하고 데이터를 복제합니다.
#include <cstring>
class vector {
int* ptr;
std::size_t sz;
public:
vector(std::size_t sz, int value = 0) : sz(sz) {
ptr = new int[sz];
for (std::size_t i = 0; i < sz; i++)
ptr[i] = value;
}
~vector() { delete[] ptr; }
// ✅ 깊은 복사 생성자
vector(const vector& other) : sz(other.sz) {
ptr = new int[sz]; // 새 메모리 할당
std::memcpy(ptr, other.ptr, sizeof(int) * sz); // 데이터 복사
}
};
int main() {
vector v1(4, 10);
vector v2 = v1; // 깊은 복사 수행
}
💡 동작 설명
- v2.ptr 은 v1.ptr과 완전히 다른 메모리 공간을 가리킴
- 따라서 v2가 수정되어도 v1은 영향을 받지 않음
- 각각의 객체가 독립적인 자원을 가지므로, 소멸자에서 안전하게 삭제 가능
4️⃣ 대입 연산자도 직접 구현해야 한다
복사 생성자를 정의했더라도, 대입 연산자(operator=) 도 따로 구현해야 합니다.
그렇지 않으면 또다시 얕은 복사가 일어납니다.
vector& operator=(const vector& other) {
if (this == &other) return *this; // 자기 자신 대입 방지
if (sz != other.sz)
sz = other.sz;
delete[] ptr; // 기존 메모리 해제
ptr = new int[sz]; // 새 메모리 할당
std::memcpy(ptr, other.ptr, sizeof(int) * sz); // 데이터 복사
return *this;
}
⚙️ 주의할 점
- 자기 자신 대입 방지 (if (this == &other))
v2 = v2; 처럼 자기 자신에게 대입하는 상황에서도 안전해야 함 - 메모리 누수 방지
새로 복사하기 전에 반드시 기존 메모리를 delete[] 해야 함 - 동일 크기일 때도 올바른 데이터 복사
크기가 같아도 기존 데이터를 덮어써야 하므로 memcpy 필요
5️⃣ 얕은 복사 vs 깊은 복사 비교 요약
구분얕은 복사 (Shallow Copy)깊은 복사 (Deep Copy)
| 복사 방식 | 주소만 복사 | 새 메모리 할당 후 데이터 복사 |
| 메모리 구조 | 두 객체가 같은 주소 공유 | 각 객체가 독립된 메모리 |
| 수정 영향 | 한쪽 수정 시 다른쪽도 영향 | 서로 독립적 |
| 소멸 시 | 이중 삭제 위험 | 안전하게 삭제 가능 |
| 대표 사례 | 디폴트 복사 생성자 | 사용자 정의 복사 생성자 |
6️⃣ 얕은 복사 해결책 정리
해결 방법설명
| 깊은 복사(Deep Copy) | 새로운 메모리 공간을 만들어 데이터를 복제 |
| 참조 카운팅(Reference Counting) | 같은 데이터를 공유하되 참조 횟수 관리 (std::shared_ptr) |
| 소유권 이전(Move Ownership) | 메모리 소유권을 이동 (std::unique_ptr, move semantics) |
| 복사 및 대입 금지(Delete) | 복사 자체를 막음 (= delete) |
📌 핵심 요약
- 디폴트 복사 생성자 / 대입 연산자는 얕은 복사를 수행한다.
- 포인터 멤버가 있는 클래스는 반드시 깊은 복사 생성자와 대입 연산자를 직접 구현해야 한다.
- 그렇지 않으면 이중 delete, 메모리 누수, 공유 데이터 오염 등의 심각한 문제가 발생한다.
- 복사 안전성을 보장하는 법:
- Deep Copy 구현
- 또는 std::unique_ptr / std::shared_ptr 등 스마트 포인터 사용
반응형
'Programing언어 > C++' 카테고리의 다른 글
| C++ STL 핵심 파헤치기: vector, list, deque... 대체 언제, 무엇을 써야 할까? (0) | 2025.11.03 |
|---|---|
| C++ 상속에서의 복사 생성자와 대입 연산자 완전 정리 (0) | 2025.10.18 |
| C++ ADL (Argument Dependent Lookup) 완벽 정리 (0) | 2025.10.15 |
| C++ 함수 객체(Function Object) 완전 정리 (0) | 2025.09.29 |
| C++ 스마트 포인터(Smart Pointer) 완전 정리 (0) | 2025.09.29 |