Computer >> 컴퓨터 >  >> 프로그래밍 >> C++

C++에서 대입 연산자를 직접 작성해야 하는 경우와 그 이유

C++에서 클래스를 설계할 때 대입 연산자(operator=)를 직접 작성해야 하는 경우가 언제인지 알아보겠습니다. 핵심 기준은 간단합니다. 클래스 내부에 포인터 멤버 변수가 있는지 없는지에 따라 달라집니다.

클래스에 포인터 멤버가 없다면 컴파일러가 자동으로 생성해 주는 기본 복사 생성자와 대입 연산자만으로 충분하기 때문에 굳이 직접 작성할 필요가 없습니다. 하지만 포인터 멤버가 존재하는 경우에는 기본 연산자가 얕은 복사(shallow copy)를 수행하면서 예기치 않은 문제를 일으킬 수 있으므로, 상황에 맞게 직접 구현해야 합니다.

문제 상황: 기본 대입 연산자의 한계

먼저 사용자 정의 대입 연산자나 복사 생성자가 없는 클래스의 예제를 살펴보겠습니다.

#include<iostream>
using namespace std;

class MyClass { // 사용자 정의 대입 연산자 및 복사 생성자 없음
    int *ptr;
public:
    MyClass(int x = 0) {
        ptr = new int(x);
    }
    void setValue(int x) {
        *ptr = x;
    }
    void print() {
        cout << *ptr << endl;
    }
};

main() {
    MyClass ob1(50);
    MyClass ob2;
    ob2 = ob1;
    ob1.setValue(100);
    ob2.print();
}

실행 결과

100

main() 함수에서 ob1 객체의 값을 setValue() 메서드로 100으로 변경했을 뿐인데, ob2의 출력 결과도 함께 100으로 바뀌었습니다. 이처럼 의도하지 않은 값 변경은 심각한 버그로 이어질 수 있습니다.

원인은 다음과 같습니다. 사용자 정의 대입 연산자가 없으므로 컴파일러가 자동 생성한 기본 대입 연산자가 호출되는데, 이때 오른쪽 피연산자(RHS)의 ptr 포인터 값이 그대로 왼쪽 피연산자(LHS)에 복사됩니다. 결과적으로 두 객체의 포인터가 동일한 메모리 위치를 가리키게 되어, 한쪽의 변경이 다른 쪽에도 영향을 미치는 것입니다.

해결 방법 두 가지

이 문제를 해결하는 방법은 크게 두 가지입니다.

  • 복사 제한 방식: private 영역에 더미(dummy) 대입 연산자를 선언하여 객체 복사 자체를 금지합니다.
  • 깊은 복사(deep copy) 방식: 사용자 정의 대입 연산자를 직접 구현하여 독립적인 메모리 공간에 값을 복사합니다.

해결 예제: 깊은 복사를 수행하는 대입 연산자

#include<iostream>
using namespace std;

class MyClass {
    int *ptr;
public:
    MyClass(int x = 0) {
        ptr = new int(x);
    }
    void setValue(int x) {
        *ptr = x;
    }
    void print() {
        cout << *ptr << endl;
    }
    // 사용자 정의 대입 연산자
    MyClass &operator=(const MyClass &ob2) {
        // 자기 자신에 대한 대입(self assignment) 여부 확인
        if(this != &ob2)
            *ptr = *(ob2.ptr);  // 포인터가 아닌 '값'을 복사
        return *this;
    }
};

main() {
    MyClass ob1(50);
    MyClass ob2;
    ob2 = ob1;
    ob1.setValue(100);
    ob2.print();
}

실행 결과

50

이번에는 ob1의 값을 100으로 변경한 후에도 ob2는 처음 할당한 값인 50을 그대로 유지합니다. 사용자 정의 대입 연산자가 깊은 복사를 수행하여 각 객체가 서로 다른 메모리 공간을 가리키도록 만들었기 때문입니다.

주의 사항: 자기 참조 검사

대입 연산자를 직접 구현할 때 반드시 기억해야 할 것이 있습니다. 바로 자기 자신에 대한 대입(self assignment) 여부를 확인하는 것입니다. if(this != &ob2) 같은 검사를 생략하면, obj = obj 형태의 코드에서 현재 객체의 값이 잘못 변경되거나 리소스가 손상될 위험이 있습니다.

정리

  • 클래스에 포인터 멤버가 없으면 컴파일러가 생성한 기본 대입 연산자로 충분합니다.
  • 포인터 멤버가 있다면 얕은 복사로 인해 두 객체가 같은 메모리를 공유하는 문제가 발생합니다.
  • 객체 복사를 허용하고 싶다면 깊은 복사를 수행하는 대입 연산자를 직접 작성하고, 허용하지 않으려면 private 더미 연산자로 복사를 차단합니다.
  • 구현 시에는 반드시 자기 참조 검사를 포함해야 안전합니다.