복사 생성자는 기존 객체를 복사하여 새로운 객체를 생성할 때 호출되는 특수한 생성자이다.
#include <iostream> using namespace std; class Person { public: string name; int age; // 일반 생성자 Person(string n, int a) : name(n), age(a) {} // 복사 생성자 Person(const Person& other) { name = other.name; age = other.age; cout << "복사 생성자 호출!" << endl; } }; int main() { Person person1("철수", 25); // 일반 생성자 호출 // 복사 생성자가 호출되는 경우들: Person person2 = person1; // 1. 객체로 다른 객체 초기화 Person person3(person1); // 2. 명시적 복사 생성 return 0; }복사 생성자가 호출되는 경우는 2가지가 있다.
- 객체로 다른 객체를 선언과 동시에 초기화 할때 복사생성자가 호출
- 명시적 복사 생성자 호출
기본 복사 생성자는 복사 생성자를 정의하지 않으면, 컴파일러가 자동으로 기본 복사 생성자를 만든다. 기본 복사 생성자는 모든 멤버 변수를 얕은 복사 한다.
대입 연산자는 이미 존재하는 객체에 다른 객체의 값을 대입할 때 사용
#include <iostream> using namespace std; class Person { public: string name; int age; Person(string n, int a) : name(n), age(a) {} // 복사 생성자 Person(const Person& other) { name = other.name; age = other.age; cout << "복사 생성자 호출!" << endl; } // 대입 연산자 Person& operator=(const Person& other) { // 자기 자신 대입 방지 if (this != &other) { name = other.name; age = other.age; } cout << "대입 연산자 호출!" << endl; return *this; } void Print() { cout << "이름 : " << name << ", 나이 : " << age << endl; } }; int main() { Person person1("철수", 25); Person person2 = person1; // 복사 생성자 호출 (새 객체 생성) person1.Print(); person2.Print(); Person person3("영희", 30); person3 = person1; // 대입 연산자 호출 (이미 존재하는 객체에 대입) person3.Print(); return 0; }
- 복사 생성자: 새로운 객체를 생성하면서 초기화
- 대입 연산자: 이미 존재하는 객체의 값을 변경
여러 상황에서 자기 자신을 대입하는 경우가 존재할 수 있다.
#include <iostream> #include <string> using namespace std; void assign(string& a, string& b) { a = b; // a와 b가 같은 객체를 참조할 수 있음 } int main() { // 직접적인 경우 string str1("Hello"); str1 = str1; // 간접적인 경우 string str2 = "aa"; assign(str2, str2); // 배열이나 컨테이너 string arr[10]; int i = 5; int j = 5; arr[i] = arr[j]; // i와 j가 같으면 자기자신을 대입 // 포인터, 참조 string *ptr1 = new string("11"); string *ptr2 = ptr1; *ptr1 = *ptr2; return 0; }
#include <iostream> #include <cstring> using namespace std; class String { private: char* data; int length; public: String(const char* str) { length = strlen(str); data = new char[length + 1]; strcpy(data, str); cout << "생성자: " << data << " (주소: " << (void*)this << ")" << endl; } // 자기 자신 체크가 있는 올바른 대입 연산자 String& operator=(const String& other) { cout << "대입 연산자 호출" << endl; cout << " this 주소: " << (void*)this << endl; cout << " other 주소: " << (void*)&other << endl; if (this != &other) { cout << " -> 다른 객체, 복사 진행" << endl; delete[] data; length = other.length; data = new char[length + 1]; strcpy(data, other.data); } else { cout << " -> 같은 객체, 복사 생략" << endl; } return *this; } ~String() { cout << "소멸자: " << data << endl; delete[] data; } void print() { cout << "데이터: " << data << endl; } }; int main() { String str1("Hello"); String str2("World"); cout << "\n=== 다른 객체 대입 ===" << endl; str1 = str2; // 다른 객체 대입 cout << "\n=== 자기 자신 대입 ===" << endl; str1 = str1; // 자기 자신 대입 cout << "\n=== 프로그램 종료 ===" << endl; return 0; }위에서 대입 연산자를 보면 자기 자신을 경우는 같은 객체이기 떄문에 생략 처리를한다.
만약 자기 자신의 대한 처리를 하지 않으면 자기 자신을 대입하면 메모리 할당을 해제 했기때문에 해제된 메모리를 strcpy 진행 하기때문에 크래시가 발생하거나 쓰레기 값이 복사 된다.
멤버를 복사할 때 포인터가 가리키는 데이터가 아닌 포인터가 저장하고 있는 주소값만 복사하는 것을 의미한다.즉, 복사된 객체와 원본 객체가 동일한 메모리 공간을 참조한다.
특징
#include <iostream> using namespace std; int main() { // 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정 int* A = new int(30); // 포인터 B가 A가 가리키는 메모리를 공유 int* B = A; cout << "A의 값: " << *A << endl; // 출력: 30 cout << "B의 값: " << *B << endl; // 출력: 30 // A가 동적 메모리를 해제 delete A; // 이제 B는 Dangling Pointer(해제된 메모리를 가리키는 포인터) // 이 시점에서 B를 통해 접근하면 Undefined Behavior 발생 cout << "B의 값 (dangling): " << *B << endl; // 위험: 정의되지 않은 동작 return 0; }위 코드는 포인터A가 메모리를 할당하고 포인터B는 얕은 복사로 같은 메모리 주소를 참조하게 된다. 포인터A가 메모리를 할당을 해제하면 포인터B 해제된 메모리를 가리키는 포인터가 된다.(Dangling Pointer)
#include <iostream> #include <cstring> class ShallowCopyExample { public: char* data; ShallowCopyExample(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } // 얕은 복사 생성자 ShallowCopyExample(const ShallowCopyExample& other) { data = other.data; // 포인터 주소 복사 } ~ShallowCopyExample() { delete[] data; // 메모리 해제 } }; int main() { ShallowCopyExample obj1("Hello"); ShallowCopyExample obj2(obj1); // 얕은 복사 // obj1과 obj2는 같은 메모리를 가리킴 obj2.data[0] = 'h'; // obj1의 데이터도 변경됨 std::cout << obj1.data << std::endl; // "hello" 출력 return 0; }위 코드에서는 메모리 크래시가 발생한다. 이유는 얕은 복사를 수행한 후 두 객체가 같은 메모리를 가리키기 때문이다. 이로 인해 두 객체가 소멸될 때 동일한 메모리를 해제하게 되어 문제가 발생
클래스의 포인터 멤버가 가리키는 동적 데이터를 새로 할당된 독립적인 메모리 영역에 복제하는 것을 의미한다. 따라서 원본 객체와 복사된 객체는 서로 독립적인 메모리 공간을 소유하므로 Dangling Pointer가 발생하지 않는다.
특징
1. 메모리 사용 : 깊은 복사는 추가 메모리를 사용하지만, 원본 객체와 복사된 객체가 서로 독립적이므로 안전하게 사용할 수 있다.
2. 복사 동작 : 모든 멤버 변수를 개별적으로 복사하고, 포인터 멤버는 새로운 메모리 공간에 복사하여 서로 독립적인 데이터 구조를 만든다.
#include <iostream> using namespace std; int main() { // 포인터 A가 동적 메모리를 할당하고 값을 30으로 설정 int* A = new int(30); // 포인터 B가 A가 가리키는 값을 복사 (깊은 복사) int* B = new int(*A); cout << "A의 값: " << *A << endl; // 출력: 30 cout << "B의 값: " << *B << endl; // 출력: 30 // A가 동적 메모리를 해제 delete A; // B는 여전히 독립적으로 자신의 메모리를 관리 cout << "B의 값 (깊은 복사 후): " << *B << endl; // 출력: 30 // B의 메모리도 해제 delete B; return 0; }위 코드는 포인터A가 메모리를 할당하고 포인터B는 깊은 복사로 메모리를 할당받고 포인터A의 값을 복사한다. 포인터A,B는 서로 독립적인 메모리 공간을 소유하므로 포인터 A가 delete를 해도 포인터B는 문제가 되지않는다.
#include <iostream> #include <cstring> class DeepCopyExample { public: char* data; DeepCopyExample(const char* str) { data = new char[strlen(str) + 1]; strcpy(data, str); } // 깊은 복사 생성자 DeepCopyExample(const DeepCopyExample& other) { data = new char[strlen(other.data) + 1]; // 새로운 메모리 할당 strcpy(data, other.data); // 데이터 복사 } ~DeepCopyExample() { delete[] data; // 메모리 해제 } }; int main() { DeepCopyExample obj1("Hello"); DeepCopyExample obj2 = obj1; // 깊은 복사 obj2.data[0] = 'h'; // obj1의 데이터는 변경되지 않음 std::cout << obj1.data << std::endl; // "Hello" 출력 std::cout << obj2.data << std::endl; // "hello" 출력 return 0; }obj2 객체도 독립적인 메모리를 할당 받기 때문에 두 객체가 소멸 할때 문제가 되지않는다.
메모리 사용의 효율성을 중시한다면 얕은 복사를 사용할 수 있지만, 데이터의 안전성을 중요시한다면 깊은 복사를 선택해야 한다.