복사 생성자와 복사 대입 연산자

이오싶·2025년 12월 21일

내용 출처: 문톰의 긍정코딩

기본 개념 정리

  • 생성자: 객체 생성 시 호출, 명시적 정의 가능
    • 사용자 정의 생성자 없을 경우 기본 생성자 자동 생성
    • 사용자 정의 생성자 있을 경우 기본 생성자 자동 생성되지 않음!
  • 소멸자: 객체 소멸 시 호출, 명시적 정의 가능
    • 동적 할당된 멤버 리소스를 동시에 제거해 줄 필요 있음

      class Object {
      	~Object() {
      			cout << "Object 소멸!" << endl;
      	}
      }

복사 생성자 & 복사 대입 연산자

정의 및 특성

  • 복사 생성자
    • 다른 객체를 사용해(인자로 받아) 객체 생성 시 호출

      Object(const Object& obj);
    • 복사 생성자 정의 없을 경우 자동 생성

      • 멤버별 얕은 복사만 실행… 동적 할당 멤버의 경우 소멸자 호출 시 Double Destruction 문제
  • 복사 대입 연산자
    • 대상 객체가 이미 초기화 된 상태에서 객체 할당 시 호출
    • 대입 연산자 오버로딩 없을 경우 자동 생성

예제

class SimpleObject {
    int id;
    char* name;

    static int objectCount;     // 생성된 객체 수
    static int idCounter;       // 고유 ID 생성용 카운터
    
public:
    SimpleObject();                                     // 기본 생성자
    SimpleObject(const char* name);                     // 매개변수가 있는 생성자
    **SimpleObject(const SimpleObject& obj);              // 복사 생성자
    SimpleObject& operator=(const SimpleObject& obj);   // 복사 대입 연산자**

    static int getObjectCount();
    void printInfo() const;
    ~SimpleObject();
};

int SimpleObject::objectCount = 0;
int SimpleObject::idCounter = 0;

int SimpleObject::getObjectCount()
{
    return objectCount;
}

void SimpleObject::printInfo() const
{
    // 메서드 뒤 const: 이 메서드가 객체의 멤버를 수정하지 않음을 나타냄
    cout << "SimpleObject [ID: " << id << ", Name: " << name << "]" << endl;
}

SimpleObject::SimpleObject() : SimpleObject("Default")
{
}

SimpleObject::SimpleObject(const char* name)
{
    objectCount++;
    this->id = idCounter++;

    this->name = new char[strlen(name) + 1];
    strcpy(this->name, name);
    cout << "[" << id << "] SimpleObject " << name <<" Constructor Called!" << endl;
}

**SimpleObject::SimpleObject(const SimpleObject& obj)
{
    objectCount++;
    this->id = idCounter++;
    //obj.id = 3; const 인자의 멤버 수정 불가

    this->name = new char[strlen(obj.name) + 1];
    strcpy(this->name, obj.name);
    // 명시적 복사 생성자 정의가 없으면 컴파일러가 자동으로 생성
    // 자동 복사 생성자는 멤버별 얕은 복사 수행; name의 메모리 주소만 복사
    cout << "[" << id << "] SimpleObject " << name <<" Copy Constructor Called!" << endl;
}

SimpleObject &SimpleObject::operator=(const SimpleObject &obj)
{
    if (this == &obj) return *this;
    delete[] this->name;
    this->name = new char[strlen(obj.name) + 1];
    strcpy(this->name, obj.name);
    cout << "[" << id << "] SimpleObject " << name <<" Copy Assignment Operator Called!" << endl;
    return *this;
}**

SimpleObject::~SimpleObject()
{
    cout << "[" << id << "] SimpleObject " << name <<" Destructor Called!" << endl;
    delete[] name;
    objectCount--;
}

호출 시점

1. 대입 연산자 사용

void test_simple_object()
{
    SimpleObject obj1;
    auto ptr = new SimpleObject("Dynamic Object");
    SimpleObject obj2("Object 2");

    delete ptr;

    SimpleObject obj4 = obj2; // 복사 생성자 호출

    obj2 = obj1;     // 복사 대입 연산자 호출
    
    cout << "End of test_simple_object()" << endl;
}
  • 실행 결과
    [0] SimpleObject Default Constructor Called!
    [1] SimpleObject Dynamic Object Constructor Called!
    [2] SimpleObject Object 2 Constructor Called!
    [1] SimpleObject Dynamic Object Destructor Called!
    [3] SimpleObject Object 2 Copy Constructor Called!
    [2] SimpleObject Default Copy Assignment Operator Called!
    End of test_simple_object()
    [3] SimpleObject Object 2 Destructor Called!
    [2] SimpleObject Default Destructor Called!
    [0] SimpleObject Default Destructor Called!

2. 함수 인자 Call by Value 전달

Call by Value로 인자 전달 시 복사 생성자 호출

void SimpleObject::printInfo() const
{
    // 메서드 뒤 const: 이 메서드가 객체의 멤버를 수정하지 않음을 나타냄
    cout << "SimpleObject [ID: " << id << ", Name: " << name << "]" << endl;
}

**void getReferenceAndPrint(const SimpleObject& obj)
{
    cout << "Inside getReferenceAndPrint()" << endl;
    obj.printInfo();
}

void getValueAndPrint(SimpleObject obj)
{
    cout << "Inside getValueAndPrint()" << endl;
    obj.printInfo();
}**

void test_call_by_value() 
{
    SimpleObject obj1;
    **getReferenceAndPrint(obj1); // 참조로 전달, 복사 생성자 호출 안 됨
    getValueAndPrint(obj1);     // 값으로 전달, 복사 생성자 호출 됨**
    cout << "End of test_call_by_value()" << endl;
}
  • 실행 결과
    [0] SimpleObject Default Constructor Called!
    Inside getReferenceAndPrint()
    SimpleObject [ID: 0, Name: Default]
    [1] SimpleObject Default Copy Constructor Called!      
    Inside getValueAndPrint()
    SimpleObject [ID: 1, Name: Default]
    [1] SimpleObject Default Destructor Called!
    End of test_call_by_value()
    [0] SimpleObject Default Destructor Called!

3. 반환 값이 참조형이 아닌 경우

참조 값이 아닌 객체 반환 시 복사 생성자가 호출

단, RVO(Return Value Optimization) 비활성화 되어있어야 함

**SimpleObject getNewObject(const char* name)
{
    SimpleObject obj(name);
    return obj; // 복사 생성자 호출; RVO(복사 생략 최적화)로 무시될 수 있음
}**

void test_return_by_value() 
{
    **SimpleObject obj1 = getNewObject("Returned Object");** // 복사 생성자 호출; RVO로 무시될 수 있음
    // RVO 비활성화 옵션: g++ -fno-elide-constructors
    cout << "End of test_return_by_value()" << endl;
}
  • 실행 결과; -fno-elide-constructors 옵션 사용 시
    [0] SimpleObject Returned Object Constructor Called!   
    [1] SimpleObject Returned Object Copy Constructor Called!
    [0] SimpleObject Returned Object Destructor Called!    
    [2] SimpleObject Returned Object Copy Constructor Called!
    [1] SimpleObject Returned Object Destructor Called!    
    End of test_return_by_value()
    [2] SimpleObject Returned Object Destructor Called! 
    • 실행 플로우
      1. SimpleObject obj(name); getNewObject 스택에서 0번 객체 생성 - 일반 생성자 호출
      2. return obj; return을 통해 0번 객체를 복사하여 함수 바깥 스택(즉 test_return_by_value 스택)에 1번 객체 생성; 복사 생성자 호출
      3. getNewObject 스코프 종료; 0번 객체 소멸
      4. SimpleObject obj1 = getNewObject("Returned Object");
        1번 객체가 obj1 변수에 대입… 1번 객체 복사하여 2번 객체 생성; 복사 생성자 호출
      5. rvalue(임시 반환값)인 1번 객체 소멸
      6. test_return_by_value 스택 종료; 2번 객체 소멸
  • 실행 결과; RVO 활성화 시
    [0] SimpleObject Returned Object Constructor Called!   
    End of test_return_by_value()
    [0] SimpleObject Returned Object Destructor Called! 
    • 스택 내 생성 및 반환된 값이 별도로 복사 및 할당 해제되지 않고 함수 밖으로 전달된 값과 같은 메모리 공간 공유복사 생성자 호출되지 않음

파생 클래스의 복사 생성자/복사 대입 연산자 호출 순서

  • 자식 클래스의 복사 생성자를 정의 하면 컴파일러는 부모 클래스의 복사 생성자를 먼저 호출 한 후 자식 클래스의 복사 생성자를 호출한다.
  • 복사 대입 연산자도 위와 동일하다.

암시적 복사 생성자/복사 대입 연산자의 호출 방지

private

복사 생성자/대입 연산자 private으로 선빵치기.

class Example_Private {
public:
    Example_Private(int x);

private:
    Example_Private(const Example_Private& other);
    Example_Private& operator=(const Example_Private& other);

    int value;
};

void example_test() {
    Example_Private ex1(10);
    // Example1 ex2 = ex1; // 오류: 복사 생성자가 private이므로 접근 불가
    Example_Private ex3(20);
    // ex3 = ex1; // 오류: 복사 대입 연산자가 private이므로 접근 불가
}

Example_Private::Example_Private(int x) : value(x) {}

// 복사 생성자는 private으로 선언되어 외부에서 접근 불가
Example_Private::Example_Private(const Example_Private& other) : value(other.value) {}

// 복사 대입 연산자도 private으로 선언되어 외부에서 접근 불가
Example_Private& Example_Private::operator=(const Example_Private& other) 
{
    if (this != &other) {
        value = other.value;
    }
    return *this;
}

delete

C++11부터의 기능; 해당 메서드가 필요치 않은 경우 기본 생성 메서드 삭제 가능

class Example_Delete {
public:
    Example_Delete(int x);

    // 복사 생성자와 복사 대입 연산자를 삭제
    Example_Delete(const Example_Delete& other) = delete;
    Example_Delete& operator=(const Example_Delete& other) = delete;
private:
    int value;
};

void example_test() {
    Example_Delete ex4(30);
    // Example_Delete ex5 = ex4; // 오류: 복사 생성자가 삭제되었으므로 접근 불가
    Example_Delete ex6(40);
    // ex6 = ex4; // 오류: 복사 대입 연산자가 삭제되었으므로 접근 불가
}

Example_Delete::Example_Delete(int x) : value(x) {
profile
Iosif2510

0개의 댓글