1. 기본(Default) 대입 연산자
1) 자동 생성되는 기본 대입 연산자
· 동일한 자료형의 두 객체 간에는 대입 연산(=) 이 가능하다.
· 사용자가 별도로 정의하지 않으면, 컴파일러가 자동으로 기본 대입 연산자를 생성한다.
· 기본 대입 연산자는 얕은 복사(Shallow Copy) 방식으로 동작한다.
· 얕은 복사란 멤버 변수를 단순히 복사하는 것으로, 포인터 멤버가 있는 경우 주소 값만 복사된다.
2) 얕은 복사의 문제점 ⚠️
· 객체가 동적 할당을 통해 메모리를 사용한다면 얕은 복사로 인해 문제가 발생한다.
· 예를 들어, 두 객체가 서로 다른 문자열을 참조하는 경우를 살펴보자.
Ex ex1; // 객체 ex1 생성
Ex ex2; // 객체 ex2 생성
ex2 = ex1; // ex2.operator=(ex1) 호출
⭕ 메모리 구조 예시:
· ex1.name → 0xAA
· ex2.name → 0xBB
· ex2 = ex1; 실행 후 → ex2.name → 0xAA, 기존 0xBB는 해제 불가 상태
· 이때 ex2의 멤버 name이 원래 가리키던 문자열 주소는 잃어버린다 → 메모리 누수 발생
· 또한 두 객체가 동일한 메모리 주소를 공유하므로, 소멸자 호출 시 이중 해제(Double Free) 오류가 발생한다.
· 따라서 생성자에서 동적 할당(new, malloc 등) 을 사용하는 경우, 반드시 깊은 복사(Deep Copy) 와 함께 대입 연산자 오버로딩을 직접 정의해야 한다.
2. 깊은 복사를 위한 대입 연산자 정의
Ex& operator=(const Ex& ref) {
delete []name; // 기존 메모리 해제
name = new char[strlen(ref.name) + 1]; // 새로운 메모리 할당
strcpy(name, ref.name); // 깊은 복사 수행
return *this; // 자기 자신 반환
}
- 기존에 참조하던 메모리를 delete[] 로 해제해야 한다.
- 새로운 공간을 new char[] 로 할당한 후 문자열을 복사한다.
- 마지막에 *this 를 반환해야 연속 대입이 가능하다.
⭕ 예시: a = b = c; → (a = b) = c; 와 같이 연속적으로 수행된다.
3. 상속 관계에서의 대입 연산자
1) 유도 클래스에서 기본 대입 연산자가 정의되지 않은 경우
· 자동으로 기초 클래스의 기본 대입 연산자가 호출된다.
2) 유도 클래스에서 대입 연산자를 정의한 경우
- 기초 클래스의 대입 연산자가 자동으로 호출되지 않는다.
- 따라서 기초 클래스의 대입 연산자를 명시적으로 호출해야 한다.
Ex2& operator=(const Ex2& ref) {
Ex1::operator=(ref); // 기초 클래스 대입 연산자 직접 호출
// Ex2 멤버에 대한 복사 작업 추가
return *this;
}
💻 예제 코드
#include <iostream>
#include <cstring>
using namespace std;
// 📌 기초 클래스 Ex1
class Ex1 {
protected:
char* name; // 동적 할당된 문자열 멤버
public:
// 생성자
Ex1(const char* str = "none") : name(new char[strlen(str) + 1]) {
strcpy(name, str);
cout << "Ex1 생성자 호출: " << name << endl;
}
// 복사 생성자 (깊은 복사)
Ex1(const Ex1& ref) : name(new char[strlen(ref.name) + 1]) {
strcpy(name, ref.name);
cout << "Ex1 복사 생성자 호출: " << name << endl;
}
// 대입 연산자 오버로딩 (깊은 복사)
Ex1& operator=(const Ex1& ref) {
if (this != &ref) { // 자기 자신 대입 방지
delete []name; // 기존 메모리 해제
name = new char[strlen(ref.name) + 1];
strcpy(name, ref.name);
}
cout << "Ex1 대입 연산자 호출: " << name << endl;
return *this;
}
// 소멸자
virtual ~Ex1() {
cout << "Ex1 소멸자 호출: " << name << endl;
delete []name;
}
};
// 📌 유도 클래스 Ex2
class Ex2 : public Ex1 {
private:
int value; // 유도 클래스 고유 멤버
public:
// 생성자
Ex2(const char* str = "none", int val = 0)
: Ex1(str), value(val) {
cout << "Ex2 생성자 호출: " << name << ", value=" << value << endl;
}
// 복사 생성자
Ex2(const Ex2& ref) : Ex1(ref), value(ref.value) {
cout << "Ex2 복사 생성자 호출: " << name << ", value=" << value << endl;
}
// 대입 연산자 오버로딩
Ex2& operator=(const Ex2& ref) {
if (this != &ref) {
Ex1::operator=(ref); // ⚠️ 반드시 기초 클래스의 대입 연산자를 명시적으로 호출
value = ref.value; // 유도 클래스 고유 멤버 복사
}
cout << "Ex2 대입 연산자 호출: " << name << ", value=" << value << endl;
return *this;
}
// 소멸자
~Ex2() {
cout << "Ex2 소멸자 호출" << endl;
}
};
int main() {
cout << "=== 객체 생성 ===" << endl;
Ex2 ex1("객체1", 10); // 생성자 호출
Ex2 ex2("객체2", 20); // 생성자 호출
cout << "\n=== 복사 생성자 호출 ===" << endl;
Ex2 ex3(ex1); // 복사 생성자 호출
cout << "\n=== 대입 연산자 호출 ===" << endl;
ex2 = ex1; // 대입 연산자 호출
cout << "\n=== 프로그램 종료 ===" << endl;
return 0;
}
🖥️ 실행 결과 (예시)
=== 객체 생성 ===
Ex1 생성자 호출: 객체1
Ex2 생성자 호출: 객체1, value=10
Ex1 생성자 호출: 객체2
Ex2 생성자 호출: 객체2, value=20
=== 복사 생성자 호출 ===
Ex1 복사 생성자 호출: 객체1
Ex2 복사 생성자 호출: 객체1, value=10
=== 대입 연산자 호출 ===
Ex1 대입 연산자 호출: 객체1
Ex2 대입 연산자 호출: 객체1, value=10
=== 프로그램 종료 ===
Ex2 소멸자 호출
Ex1 소멸자 호출: 객체1
Ex2 소멸자 호출
Ex1 소멸자 호출: 객체1
Ex2 소멸자 호출
Ex1 소멸자 호출: 객체1
4. 이니셜라이저를 활용한 성능 향상
- 생성자, 복사 생성자, 대입 연산자에서 이니셜라이저(initializer list) 를 사용하면 성능을 향상시킬 수 있다.
⭕ 생성자 본문에서 대입할 경우:
· 멤버 변수가 먼저 기본 생성자로 생성된다.
· 이후에 대입 연산자가 호출된다.
· 따라서 불필요한 임시 객체 생성과 추가 연산이 발생한다.
⭕ 초기화 리스트를 사용할 경우:
· 멤버 변수가 처음부터 원하는 값으로 초기화된다.
· 중간 과정(기본 생성 → 대입)이 없으므로 성능이 더 효율적이다.
💻 예제 코드
#include <iostream>
using namespace std;
class Ex1 {
private:
int data;
public:
Ex1(int d = 0) : data(d) {
cout << "Ex1 생성자 호출: " << data << endl;
}
Ex1(const Ex1& ref) : data(ref.data) {
cout << "Ex1 복사 생성자 호출: " << data << endl;
}
Ex1& operator=(const Ex1& ref) {
data = ref.data;
cout << "Ex1 대입 연산자 호출: " << data << endl;
return *this;
}
};
// ✅ 이니셜라이저 사용
class Ex2 {
private:
Ex1 ex;
public:
Ex2(const Ex1& ref) : ex(ref) {
cout << "Ex2 생성자 호출" << endl;
}
};
// ⚠️ 대입 연산 사용
class Ex3 {
private:
Ex1 ex;
public:
Ex3(const Ex1& ref) {
ex = ref; // 기본 생성자 → 대입 연산자 호출
cout << "Ex3 생성자 호출" << endl;
}
};
int main() {
cout << "--- ex 생성 ---" << endl;
Ex1 ex1(12);
cout << "\n--- Ex2 객체 생성 (이니셜라이저 사용) ---" << endl;
Ex2 ex2(ex1);
cout << "\n--- Ex3 객체 생성 (대입 사용) ---" << endl;
Ex3 ex3(ex1);
return 0;
}
🖥️ 실행 결과 (예시)
--- ex 생성 ---
Ex1 생성자 호출: 12
--- Ex2 객체 생성 (이니셜라이저 사용) ---
Ex1 복사 생성자 호출: 12
Ex2 생성자 호출
--- Ex3 객체 생성 (대입 사용) ---
Ex1 생성자 호출: 0
Ex1 대입 연산자 호출: 12
Ex3 생성자 호출
✅ 정리
1️⃣ Ex2 (이니셜라이저) → 멤버가 ref로 곧바로 초기화 → 복사 생성자 1회 호출
2️⃣ Ex3 (대입) → 멤버가 기본 생성 → 그 후 ref로 대입 → 생성자 + 대입 연산자 총 2회 호출
3️⃣ 따라서 이니셜라이저 리스트를 사용하면 불필요한 연산을 줄이고 성능이 향상된다.
✅ 마무리 정리
1️⃣ 기본 대입 연산자는 자동 생성되며 얕은 복사를 수행한다.
2️⃣ 동적 할당 멤버가 있을 경우, 깊은 복사를 직접 정의해야 한다.
3️⃣ 대입 연산자 오버로딩 시 기존 메모리 해제 후 새로운 공간에 복사한다.
4️⃣ 상속 관계에서는 기초 클래스의 대입 연산자를 명시적으로 호출해야 한다.
5️⃣ 이니셜라이저를 활용하면 불필요한 연산을 줄여 성능을 향상시킬 수 있다.
'Language > C++' 카테고리의 다른 글
| 📌 Chapter 10 - 나머지 연산자 오버로딩 (0) | 2025.09.16 |
|---|---|
| 📌 Chapter 10 - 첨자 연산자 오버로딩 (Subscript Operator) (0) | 2025.09.09 |
| 📌 Chapter 10 – 연산자 오버로딩 (0) | 2025.09.03 |
| 📌 Chapter 09 - 가상 (0) | 2025.09.02 |
| 📌 Chapter 08 - 객체 포인터와 가상 함수의 이해 (1) | 2025.08.28 |