본문 바로가기
SWE/C++ OOP

[C++] 언제 복사생성자와 복사대입연산자를 오버로딩해야할까? | 얕은복사와 깊은복사

by S나라라2 2023. 2. 24.
반응형

 

컴파일러는 디폴트로 복사 생성자와 복사대입연산자를 만들어준다.

그러나 개발자가 직접 재정의하는 경우가 있는데, 언제 그래야하는건지 왜 그래야하는건지 궁금했다.

 


복사 함수들(복사 생성자와 복사 대입 연산자)을 언제 따로 정의해야 하는가?

결론부터 말하자면 클래스에 멤버변수로 포인터를 가지고 있을 때, 복사함수들을 재정의해야 한다.

 

클래스가 포인터를 포함하고 있지 않으면, 복사함수들을 재정의할 필요가 없다. 컴파일러가 모든 클래스에 대한 기본 복사함수들을 만들어주기 때문이다. 그러나 우리가 런타임 자원할당(파일 핸들러, 네트워크 연결)이나 포인터를 사용한다면, 컴파일러가 생성해주는 디폴트 복사 함수들이 충분하지 않을 수 있다.

 

- 예시

#include <iostream>

using namespace std;

// 복사 함수들을 재정의하지 않은 클래스 Test
class Test {
    int* ptr;
 public:
    Test(int i = 0) {
        ptr = new int(i);
    }
    void SetValue(int i) {
        *ptr = i;
    }
    void Print() {
        std::cout  << *ptr << std::endl;
    }
};

int main() {
    // t1의 값을 5로 설정한다.
    Test t1(5);
    // t1을 t2에 복사한다.
    Test t2 = t1;
    // t1의 값을 10으로 변경한다.
    t1.SetValue(10);
    // t2의 값을 확인한다.
    t2.Print();
    
    return 0;
}

프로그램의 실행 결과로 10이 나왔다. 

main함수를 살펴보면, t1.SetValue()함수를 통해 t1의 값을 변경하였으나 결과는 t2 객체에 영향을 주었다. 이렇게 예측하지 못한 문제가 발생할 수 있다.

위의 프로그램에서는 사용자가 재정의한 복사 연산자가 없었기 때문에, 컴파일러가 기본적으로 복사연산자를 만들었다. 컴파일러가 만든 복사 연산자는 right hand포인터에서 left hand로 복사하였다. 따라서 두 개의 포인터가 모두 같은 위치를 가리키고 있다. 이러한 복사를 "얕은 복사"라고 한다.

 


얕은 복사란?

얕은 복사를 이해하기 위해 동적 할당을 살펴보자

클래스 변수 = new 클래스()

위와 같이 new연산자를 사용하여 인스턴스를 생성하면, 인스턴스는 메모리의 heap영역에 저장된다.

그리고 변수는 stack 영역에 있고, heap 영역의 참조값을 new 연산자를 통해서 전달 받는다.

여기서 얕은 복사는 heap영역에 메모리는 한 곳인데, stack영역의 변수는 두 곳에서 참조하는 복사이다.

깊은 복사가 이루어지면 heap영역의 메모리도 함께 복사되어 두 쌍을 가지게 된다.

얕은 복사의 문제점은 위의 테스트 코드처럼 t1객체를 수정했는데 t2객체에 영향을 준다는 점이다.

 


 

깊은 복사

테스트 코드에 깊은 복사를 하는 복사 함수들을 재정의하여 문제점을 수정해보자

 

- 코드 예시) 복사 생성자와 복사 대입연산자 재정의한 Test 클래스

// 복사 함수들을 재정의하지 않은 클래스 Test
class Test {
    int* ptr;
 public:
    Test(int i = 0) {
        ptr = new int(i);
    }
    void SetValue(int i) {
        *ptr = i;
    }
    void Print() {
        std::cout  << *ptr << std::endl;
    }
    // 복사 대입 연산자 재정의
    Test& operator=(const Test& rhs) {
        if (this != &rhs)
            *ptr = *(rhs.ptr);
        return *this;
    }
    // 복사 생성자 재정의
    Test(const Test& rhs) {
        ptr = new int(*(rhs.ptr));
    }
};

int main() {
    // t1의 값을 5로 설정한다.
    Test t1(5);
    // t1을 t2에 복사한다.
    Test t2 = t1;
    // t1의 값을 10으로 변경한다.
    t1.SetValue(10);
    // t2의 값을 확인한다.
    t2.Print();
    
    return 0;
}

Test클래스에서 복사대입연산자와 복사생성자를 재정의하였다. 복사함수들 내부에서는 new를 통해 heap에 새로운 메모리를 할당하고 그 안에 들은 값만 복사하고 있다.

프로그램의 실행결과로 5가 나왔다. 객체t1의 수정이 t2에 영향주지 않은 것을 확인할 수 있다.

 


추가로 복사 함수들을 재정의하지 않아서 컴파일러가 생성한 디폴트 복사함수를 수행해 발생하는 문제(얕은 복사 문제)의 다른 해결방법은 객체 복사를 막는 것이 있다. 복사 연산자를 재정의하고 그걸 private하면 된다.

 

 

 

geeksforgeeks 번역하기

참고 : https://www.geeksforgeeks.org/assignment-operator-overloading-in-c

반응형