복사 생성자(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++ 코드를 작성하는 데 필수적입니다.