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

C++ 복사 생성자는 언제 호출될까? 호출 시점과 예제 총정리

복사 생성자(Copy Constructor)란?

복사 생성자는 같은 클래스의 기존 객체를 사용해 새로운 객체를 초기화하는 특수한 생성자입니다. C++에서 복사 생성자는 주로 다음 세 가지 상황에서 호출됩니다.

  • 동일한 타입의 다른 객체로 새 객체를 초기화할 때
  • 객체를 함수의 인수로 전달하기 위해 복사할 때 (값에 의한 전달)
  • 함수의 반환값으로 객체를 복사할 때

클래스에 복사 생성자가 정의되어 있지 않으면, 컴파일러가 자동으로 기본 복사 생성자를 만들어 줍니다. 하지만 클래스에 포인터 변수나 동적 메모리 할당(new/delete)이 포함되어 있다면, 반드시 직접 복사 생성자를 정의해야 합니다. 그렇지 않으면 얕은 복사(shallow copy)로 인해 두 객체가 같은 메모리를 공유하게 되어, 소멸 시 이중 해제(double free) 오류가 발생할 수 있습니다.

복사 생성자의 가장 일반적인 형태는 다음과 같습니다.

classname (const classname &obj) {
    // 생성자 본문
}

여기서 obj는 다른 객체를 초기화하는 데 사용되는 객체에 대한 참조(reference)입니다. 매개변수가 const 참조인 이유는, 복사 과정에서 원본 객체가 변경되지 않도록 보장하기 위해서입니다.


예제 1: 함수에 객체를 값으로 전달하는 경우

다음 예제는 객체가 함수의 인수로 전달될 때 복사 생성자가 호출되는 것을 보여줍니다.

#include <iostream>
using namespace std;

class Line {
    public:
        int getLength( void );
        Line( int len ); // 일반 생성자
        Line( const Line &obj); // 복사 생성자
        ~Line(); // 소멸자

    private:
        int *ptr;
};

// 멤버 함수 정의 (생성자 포함)
Line::Line(int len) {
    cout << "Normal constructor allocating ptr" << endl;
    ptr = new int; // 포인터용 메모리 할당
    *ptr = len;
}

Line::Line(const Line &obj) {
    cout << "Copy constructor allocating ptr." << endl;
    ptr = new int;
    *ptr = *obj.ptr; // 값 복사
}

Line::~Line(void) {
    cout << "Freeing memory!" << endl;
    delete ptr;
}

int Line::getLength( void ) {
    return *ptr;
}

void display(Line obj) {
    cout << "Length of line : " << obj.getLength() << endl;
}

// 프로그램 메인 함수
int main() {
    Line line(10);
    display(line);
    return 0;
}

실행 결과

Normal constructor allocating ptr
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Freeing memory!

display(line)을 호출하면 객체가 값으로 전달되므로 복사 생성자가 한 번 실행됩니다. 함수가 끝나면 원본 객체와 복사된 객체 각각에 대해 소멸자가 차례로 호출되어 메모리가 두 번 해제되는 것을 확인할 수 있습니다.


예제 2: 기존 객체로 새 객체를 초기화하는 경우

이번에는 이미 생성된 객체를 이용해 같은 타입의 새 객체를 만들 때 복사 생성자가 호출되는 예제입니다.

#include <iostream>
using namespace std;

class Line {
    public:
        int getLength( void );
        Line( int len ); // 일반 생성자
        Line( const Line &obj); // 복사 생성자
        ~Line(); // 소멸자

    private:
        int *ptr;
};

// 멤버 함수 정의 (생성자 포함)
Line::Line(int len) {
    cout << "Normal constructor allocating ptr" << endl;
    ptr = new int; // 포인터용 메모리 할당
    *ptr = len;
}

Line::Line(const Line &obj) {
    cout << "Copy constructor allocating ptr." << endl;
    ptr = new int;
    *ptr = *obj.ptr; // 값 복사
}

Line::~Line(void) {
    cout << "Freeing memory!" << endl;
    delete ptr;
}

int Line::getLength( void ) {
    return *ptr;
}

void display(Line obj) {
    cout << "Length of line : " << obj.getLength() << endl;
}

// 프로그램 메인 함수
int main() {
    Line line1(10);
    Line line2 = line1; // 이 코드 역시 복사 생성자를 호출합니다

    display(line1);
    display(line2);
    return 0;
}

실행 결과

Normal constructor allocating ptr
Copy constructor allocating ptr.
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Copy constructor allocating ptr.
Length of line : 10
Freeing memory!
Freeing memory!
Freeing memory!

Line line2 = line1; 구문은 대입 연산처럼 보이지만, 실제로는 새 객체를 초기화하는 구문이므로 복사 생성자가 호출됩니다. 또한 display() 함수가 두 번 호출될 때마다 값 전달에 의한 복사가 발생해, 총 세 번의 복사 생성자 호출이 일어난 것을 출력 결과에서 확인할 수 있습니다.


정리: 복사 생성자가 호출되는 시점

  • 객체 초기화 시: Line line2 = line1; 처럼 기존 객체로 새 객체를 초기화할 때
  • 값에 의한 전달 시: 함수 매개변수로 객체가 값(pass-by-value)으로 전달될 때
  • 값 반환 시: 함수가 객체를 값으로 반환할 때 (컴파일러 최적화에 따라 생략될 수 있음 — RVO/NRVO)

참고로, 최신 C++ 컴파일러는 불필요한 복사를 제거하는 복사 생략(Copy Elision) 및 이동 시맨틱(move semantics)을 적용하므로, 실제 빌드 환경에서는 위 예제의 복사 횟수가 줄어들 수 있습니다. 그럼에도 복사 생성자의 개념과 동작 원리를 이해하는 것은 안전하고 효율적인 C++ 코드를 작성하는 데 필수적입니다.