Language/C++

📌 Chapter 10 - 기본 대입 연산자 오버로딩

e-cko 2025. 9. 8. 17:18
반응형
반응형

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️
이니셜라이저를 활용하면 불필요한 연산을 줄여 성능을 향상시킬 수 있다.


반응형