Topic 2: 복사 생성자와 대입 연산자 📋
🎯 학습 목표
- 복사 생성자의 개념과 필요성을 이해할 수 있다
- 얕은 복사와 깊은 복사의 차이를 설명할 수 있다
- 대입 연산자를 오버로딩할 수 있다
- 자가 대입 문제를 해결할 수 있다
📷 사진 복사로 이해하는 복사 생성자
복사 생성자를 이해하는 가장 쉬운 방법은 사진 복사를 생각하는 것입니다!
두 가지 복사 방법 📸
-
얕은 복사 (Shallow Copy): 사진 링크만 복사
- 원본과 복사본이 같은 사진 파일을 가리킴
- 한쪽이 수정하면 다른 쪽도 영향받음 ⚠️
-
깊은 복사 (Deep Copy): 사진 파일 자체를 복사
- 완전히 독립된 두 개의 사진
- 서로 영향을 주지 않음 ✅
class Photo {
string* imageData; // 동적 할당된 이미지 데이터
// 얕은 복사 - 위험! ❌
// imageData가 같은 메모리를 가리킴
// 깊은 복사 - 안전! ✅
// 새로운 메모리를 할당하고 데이터 복사
};🔍 복사 생성자란?
복사 생성자는 같은 클래스의 다른 객체로부터 새 객체를 만드는 특별한 생성자입니다!
class MyClass {
public:
// 복사 생성자의 형태
MyClass(const MyClass& other) {
// other 객체의 내용을 복사
}
};💥 얕은 복사의 문제점
#include <iostream>
#include <cstring>
using namespace std;
// 문제가 있는 클래스 - 기본 복사 생성자 사용
class DangerousString {
private:
char* data;
int length;
public:
// 생성자
DangerousString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
cout << "✅ 생성: " << data << endl;
}
// 소멸자
~DangerousString() {
cout << "🗑️ 소멸: " << data << endl;
delete[] data;
}
void print() {
cout << "내용: " << data << endl;
}
void changeData(const char* newStr) {
strcpy(data, newStr);
}
};
void demonstrateShallowCopyProblem() {
cout << "=== 얕은 복사 문제 시연 ===" << endl;
DangerousString str1("Hello");
DangerousString str2 = str1; // 기본 복사 생성자 (얕은 복사)
str1.print();
str2.print();
// 한쪽을 수정하면...
str1.changeData("Bye");
cout << "\n수정 후:" << endl;
str1.print(); // Bye
str2.print(); // Bye (같이 바뀜! 😱)
// 소멸자에서 문제 발생! (같은 메모리를 두 번 해제)
}✅ 깊은 복사로 해결하기
#include <iostream>
#include <cstring>
using namespace std;
class SafeString {
private:
char* data;
int length;
public:
// 일반 생성자
SafeString(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
cout << "✅ 생성: " << data << " (주소: " << (void*)data << ")" << endl;
}
// 복사 생성자 - 깊은 복사! ⭐
SafeString(const SafeString& other) {
length = other.length;
data = new char[length + 1]; // 새로운 메모리 할당!
strcpy(data, other.data); // 데이터 복사
cout << "📋 복사 생성: " << data << " (새 주소: " << (void*)data << ")" << endl;
}
// 대입 연산자 오버로딩 ⭐
SafeString& operator=(const SafeString& other) {
cout << "= 대입 연산자 호출" << endl;
// 자가 대입 검사 (중요!)
if (this == &other) {
return *this;
}
// 기존 메모리 해제
delete[] data;
// 새로운 메모리 할당 및 복사
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
return *this;
}
// 소멸자
~SafeString() {
cout << "🗑️ 소멸: " << data << " (주소: " << (void*)data << ")" << endl;
delete[] data;
}
void print() const {
cout << "내용: " << data << " (주소: " << (void*)data << ")" << endl;
}
void changeData(const char* newStr) {
delete[] data;
length = strlen(newStr);
data = new char[length + 1];
strcpy(data, newStr);
}
};
int main() {
cout << "=== 깊은 복사 해결책 ===" << endl;
SafeString str1("Hello");
SafeString str2 = str1; // 복사 생성자 호출
cout << "\n초기 상태:" << endl;
str1.print();
str2.print();
// 한쪽을 수정해도...
str1.changeData("Bye");
cout << "\n수정 후:" << endl;
str1.print(); // Bye
str2.print(); // Hello (영향 없음! ✅)
cout << "\n대입 연산자 테스트:" << endl;
SafeString str3("World");
str3 = str1; // 대입 연산자 호출
str3.print();
return 0;
}🎮 실습: 게임 인벤토리 복사
#include <iostream>
#include <string>
using namespace std;
class Item {
public:
string name;
int value;
Item(string n = "Empty", int v = 0) : name(n), value(v) {}
};
class Inventory {
private:
Item** items; // 아이템 포인터 배열
int capacity; // 최대 용량
int count; // 현재 아이템 수
string owner; // 소유자
public:
// 생성자
Inventory(string ownerName, int cap = 10)
: owner(ownerName), capacity(cap), count(0) {
items = new Item*[capacity];
for (int i = 0; i < capacity; i++) {
items[i] = nullptr;
}
cout << "🎒 " << owner << "의 인벤토리 생성 (용량: " << capacity << ")" << endl;
}
// 복사 생성자 - 깊은 복사
Inventory(const Inventory& other)
: owner(other.owner + "_복사본"),
capacity(other.capacity),
count(other.count) {
cout << "📋 인벤토리 복사 중..." << endl;
items = new Item*[capacity];
for (int i = 0; i < capacity; i++) {
if (other.items[i] != nullptr) {
// 각 아이템도 새로 생성 (깊은 복사)
items[i] = new Item(other.items[i]->name, other.items[i]->value);
} else {
items[i] = nullptr;
}
}
cout << "✅ " << owner << "의 인벤토리 복사 완료!" << endl;
}
// 대입 연산자
Inventory& operator=(const Inventory& other) {
cout << "= 인벤토리 대입 연산" << endl;
// 자가 대입 검사
if (this == &other) {
return *this;
}
// 기존 아이템들 삭제
for (int i = 0; i < capacity; i++) {
delete items[i];
}
delete[] items;
// 새로운 데이터 복사
owner = other.owner + "_대입본";
capacity = other.capacity;
count = other.count;
items = new Item*[capacity];
for (int i = 0; i < capacity; i++) {
if (other.items[i] != nullptr) {
items[i] = new Item(other.items[i]->name, other.items[i]->value);
} else {
items[i] = nullptr;
}
}
return *this;
}
// 소멸자
~Inventory() {
cout << "🗑️ " << owner << "의 인벤토리 소멸" << endl;
for (int i = 0; i < capacity; i++) {
delete items[i];
}
delete[] items;
}
// 아이템 추가
void addItem(string itemName, int value) {
if (count < capacity) {
items[count] = new Item(itemName, value);
count++;
cout << "➕ " << owner << ": " << itemName << " 획득!" << endl;
} else {
cout << "❌ " << owner << ": 인벤토리가 가득 찼습니다!" << endl;
}
}
// 인벤토리 표시
void showInventory() const {
cout << "\n=== " << owner << "의 인벤토리 ===" << endl;
for (int i = 0; i < count; i++) {
cout << i+1 << ". " << items[i]->name
<< " (가치: " << items[i]->value << " 골드)" << endl;
}
if (count == 0) {
cout << "비어있음" << endl;
}
cout << "==================" << endl;
}
};
int main() {
cout << "=== 게임 인벤토리 복사 시스템 ===" << endl;
// 원본 인벤토리 생성
Inventory player1("용사", 5);
player1.addItem("전설의 검", 1000);
player1.addItem("체력 포션", 50);
player1.addItem("마법 반지", 500);
player1.showInventory();
// 복사 생성자로 복사
cout << "\n--- 복사 생성자 테스트 ---" << endl;
Inventory player2 = player1; // 복사 생성자
player2.showInventory();
// 원본 수정
cout << "\n--- 원본 수정 ---" << endl;
player1.addItem("드래곤 갑옷", 2000);
cout << "\n수정 후 비교:" << endl;
player1.showInventory();
player2.showInventory(); // 영향 없음!
// 대입 연산자 테스트
cout << "\n--- 대입 연산자 테스트 ---" << endl;
Inventory player3("마법사", 3);
player3.addItem("마법 지팡이", 800);
player3 = player1; // 대입 연산자
player3.showInventory();
return 0;
}📝 Rule of Three (3의 규칙)
클래스가 다음 중 하나를 정의한다면, 세 가지 모두 정의해야 합니다:
- 소멸자 (Destructor)
- 복사 생성자 (Copy Constructor)
- 대입 연산자 (Assignment Operator)
class MyClass {
public:
// 1. 소멸자
~MyClass() { /* 자원 해제 */ }
// 2. 복사 생성자
MyClass(const MyClass& other) { /* 깊은 복사 */ }
// 3. 대입 연산자
MyClass& operator=(const MyClass& other) {
/* 자가 대입 검사 + 깊은 복사 */
}
};⚠️ 자가 대입 문제
class DataHolder {
int* data;
public:
// 잘못된 대입 연산자 ❌
DataHolder& operator=(const DataHolder& other) {
delete data; // 자기 자신일 때 문제!
data = new int(*other.data);
return *this;
}
// 올바른 대입 연산자 ✅
DataHolder& operator=(const DataHolder& other) {
if (this != &other) { // 자가 대입 검사!
delete data;
data = new int(*other.data);
}
return *this;
}
};💡 핵심 정리
- 복사 생성자: 객체를 복사하여 새 객체를 만드는 생성자
- 얕은 복사: 포인터 값만 복사 (위험!)
- 깊은 복사: 실제 데이터를 새로 할당하여 복사 (안전!)
- 대입 연산자: 이미 존재하는 객체에 다른 객체를 대입
- Rule of Three: 소멸자, 복사 생성자, 대입 연산자는 함께!
✅ 실습 체크리스트
🚀 다음 시간 예고
다음 시간에는 소멸자와 자원 관리에 대해 알아볼 거예요!
- 소멸자의 역할
- 메모리 누수 방지
- RAII 패턴
“깊은 복사로 안전한 프로그램을 만드세요! 🛡️”
Last updated on