Skip to Content
💻 코리아IT아카데미 신촌 - 프로그래밍 학습 자료
C++ 프로그래밍Unit 11: 생성자와 소멸자Topic 2: 복사 생성자와 대입 연산자

Topic 2: 복사 생성자와 대입 연산자 📋

🎯 학습 목표

  • 복사 생성자의 개념과 필요성을 이해할 수 있다
  • 얕은 복사와 깊은 복사의 차이를 설명할 수 있다
  • 대입 연산자를 오버로딩할 수 있다
  • 자가 대입 문제를 해결할 수 있다

📷 사진 복사로 이해하는 복사 생성자

복사 생성자를 이해하는 가장 쉬운 방법은 사진 복사를 생각하는 것입니다!

두 가지 복사 방법 📸

  1. 얕은 복사 (Shallow Copy): 사진 링크만 복사

    • 원본과 복사본이 같은 사진 파일을 가리킴
    • 한쪽이 수정하면 다른 쪽도 영향받음 ⚠️
  2. 깊은 복사 (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의 규칙)

클래스가 다음 중 하나를 정의한다면, 세 가지 모두 정의해야 합니다:

  1. 소멸자 (Destructor)
  2. 복사 생성자 (Copy Constructor)
  3. 대입 연산자 (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