1. 기본 복사 생성자 (Default Copy Constructor)
1) 자동 생성되는 복사 생성자
- 복사 생성자는 객체를 다른 객체로 초기화할 때 호출되는 생성자이다.
- 사용자가 직접 정의하지 않으면 컴파일러가 기본 복사 생성자를 자동으로 만들어준다.
- 기본 복사 생성자의 형태는 클래스명(const 클래스명 &객체) 형식이다.
⭕ 예시:
Ex(const Ex& cp) : n(cp.n) {}
- 아래 코드처럼 객체를 복사하면, 내부적으로 복사 생성자가 호출된다.
Ex ex2 = ex; // Ex ex2(ex); 와 동일하게 해석됨
2) 얕은 복사(Shallow Copy)의 문제점 ⚠️
- 얕은 복사란, 포인터 멤버의 주소 값만 복사하는 방식이다.
- 즉, 두 객체가 동일한 메모리 주소를 공유하게 된다.
⭕ 문제점:
1️⃣ 두 객체 중 하나가 소멸되면 공유된 메모리도 해제되어 다른 객체가 사용할 수 없게 된다.
2️⃣ 이로 인해 double free, 메모리 접근 오류 등이 발생할 수 있다.
3) explicit 키워드의 효과 ✳️
- 복사 생성자에 explicit 키워드를 붙이면 암시적 변환을 막을 수 있다.
explicit Ex(const Ex& cp);
✅ 효과:
- Ex ex = 1; 처럼 암시적 변환을 통한 객체 생성이 불가능해진다.
- 이는 명확한 타입 안정성을 보장한다.
2. 사용자 정의 복사 생성자
1) 복사 생성자의 정의 방법
- 복사 생성자는 다음 형식으로 직접 정의할 수 있다.
Ex(const Ex& cp) { n = cp.n; }
- 복사 생성자는 반드시 참조형으로 인자를 받아야 한다.
· 만약 값으로 인자를 받으면, 복사 생성자가 자기 자신을 호출하게 되어 무한 루프에 빠진다.
· 컴파일러는 이를 감지하고 에러를 발생시킨다. - const 키워드를 붙이지 않아도 되지만, 객체 복사의 의미를 명확히 하기 위해 붙이는 것이 좋다.
2) 깊은 복사 (Deep Copy) ⛏️
- 깊은 복사란, 동적 할당된 메모리까지 복사하는 방식이다.
- 얕은 복사와 달리, 각 객체가 독립된 메모리 공간을 소유하므로 안전하다.
⭕ 깊은 복사를 위한 복사 생성자 예:
Ex(const Ex& cp) {
n = new char[strlen(cp.n) + 1]; // 새로운 메모리 공간 확보
strcpy(n, cp.n); // 내용 복사
}
3. 복사 생성자가 호출되는 시점 ✅
복사 생성자는 다음과 같은 상황에서 호출된다:
1) 객체 초기화 시
Ex a; // 일반 생성자 호출 → a 생성
Ex b = a; // Ex b(a);로 묵시적 형변환, 클래스 Ex에 정의된 복사 생성자가 호출되어 b의 멤버를 a로부터 복사함.
2) 함수에 객체를 값으로 전달(Call by value) 할 때
void ShowEx(Ex ex); // 함수 외부에서 전달된 객체를 ex라는 새로운 지역 변수로 복사하여 전달받음
3) 함수에서 객체를 반환할 때
Ex GetEx(Ex temp) {
return temp;
// 복사 생성자 호출 (임시 객체 생성)
// 컴파일러에 따라 RVO(Return Value Optimization) 또는 Copy Elision(복사 생략)이 발생할 수 있다.
// 이 경우 복사 생성자가 호출되지 않고 최적화로 생략된다.
}
• 함수가 객체를 반환할 때, 일반적으로는 복사 생성자가 호출되어 반환용 임시 객체가 만들어진다.
• 하지만 최신 컴파일러는 RVO(반환값 최적화) 또는 복사 생략을 적용해,
👉 임시 객체를 복사하지 않고 직접 생성하여 성능을 개선한다.
• 따라서 복사 생성자 호출이 생략될 수도 있음을 반드시 기억해야 한다.
4) 임시 객체의 소멸과 참조자 바인딩
const Ex& ref = Ex(1); // 임시 객체를 const 참조자가 참조함
• Ex(1)은 임시 객체를 생성하는 표현이다.
• 임시 객체는 보통 해당 문장이 끝난 후 바로 소멸한다.
• 하지만, 위 코드처럼 임시 객체가 const 참조자에 바인딩되면,
👉 임시 객체의 수명이 참조자의 수명과 동일하게 연장된다.
• 즉, 임시 객체는 ref가 선언된 블록이 종료될 때까지 살아남는다.
✅ 마무리 정리
1️⃣ 복사 생성자는 객체를 복사할 때 호출되는 특수 생성자이다.
2️⃣ 기본 복사 생성자는 얕은 복사를 수행하며, 동적 메모리 사용 시 문제를 발생시킬 수 있다.
3️⃣ 깊은 복사를 위해서는 사용자가 복사 생성자를 직접 정의해야 한다.
4️⃣ 복사 생성자는 참조형으로 받아야 하며, const를 붙이는 것이 권장된다.
5️⃣ 함수 인자 전달, 반환 시점 등 다양한 순간에 복사 생성자가 호출된다.
6️⃣ explicit 키워드는 암시적 변환을 방지하여 객체 생성의 안정성을 높인다.
💻 예제 코드
#include <iostream>
#include <cstring> // strlen, strcpy 사용
class Ex {
char* n; // 문자열 저장용 포인터
public:
// 생성자
Ex(const char* str) {
n = new char[strlen(str) + 1]; // 메모리 동적 할당
strcpy(n, str); // 문자열 복사
std::cout << "일반 생성자 호출, 주소: " << this << std::endl;
}
// 복사 생성자 (깊은 복사)
Ex(const Ex& cp) {
n = new char[strlen(cp.n) + 1];
strcpy(n, cp.n);
std::cout << "복사 생성자 호출, 주소: " << this << std::endl;
}
// 소멸자
~Ex() {
std::cout << "소멸자 호출, 주소: " << this << std::endl;
delete[] n;
}
void Show() const {
std::cout << "문자열: " << n << std::endl;
}
};
// 함수 인자 전달 시 복사 생성자 호출 확인용
Ex ExFuncObj(Ex ob) {
std::cout << "함수 매개변수로 복사된 객체 주소: " << &ob << std::endl;
ob.Show();
return ob; // 반환 시 복사 생성자 호출 가능 (최적화로 생략 가능)
}
// 함수 반환 예시
Ex CreateEx() {
Ex temp("임시 객체");
return temp; // 복사 생성자 호출될 수 있음 (컴파일러 최적화 가능)
}
int main() {
std::cout << "=== Ex 클래스 테스트 ===" << std::endl;
Ex a("원본");
Ex b = a; // 복사 생성자 호출
std::cout << "\n-- 함수 인자 전달 --" << std::endl;
ExFuncObj(a); // 함수 매개변수 복사 생성자 호출
std::cout << "\n-- 함수 반환 --" << std::endl;
Ex c = CreateEx(); // 함수 반환 시 복사 생성자 호출 (최적화 가능)
std::cout << "\n-- 임시 객체 참조 --" << std::endl;
const Ex& ref = Ex("임시 참조"); // 임시 객체 수명 연장
ref.Show();
return 0;
}📤 출력 예시
=== Ex 클래스 테스트 ===
일반 생성자 호출, 주소: 0x7ffeefbff5b0
복사 생성자 호출, 주소: 0x7ffeefbff5a0
-- 함수 인자 전달 --
복사 생성자 호출, 주소: 0x7ffeefbff590
함수 매개변수로 복사된 객체 주소: 0x7ffeefbff590
문자열: 원본
복사 생성자 호출, 주소: 0x7ffeefbff580
소멸자 호출, 주소: 0x7ffeefbff590
소멸자 호출, 주소: 0x7ffeefbff580
-- 함수 반환 --
일반 생성자 호출, 주소: 0x7ffeefbff570
복사 생성자 호출, 주소: 0x7ffeefbff560
복사 생성자 호출, 주소: 0x7ffeefbff550
소멸자 호출, 주소: 0x7ffeefbff570
소멸자 호출, 주소: 0x7ffeefbff560
소멸자 호출, 주소: 0x7ffeefbff550
-- 임시 객체 참조 --
일반 생성자 호출, 주소: 0x7ffeefbff540
문자열: 임시 참조
소멸자 호출, 주소: 0x7ffeefbff540
소멸자 호출, 주소: 0x7ffeefbff5a0
소멸자 호출, 주소: 0x7ffeefbff5b0
✳️ 복사 생성자는 클래스 설계에서 자주 등장하는 핵심 요소이다.
포인터를 멤버로 가지는 경우 반드시 직접 정의해서 메모리 안정성을 확보해야 한다.
'Language > C++' 카테고리의 다른 글
| 📌Chapter 07 - 상속 (3) | 2025.08.27 |
|---|---|
| 📌 Chapter 06 - 특수 멤버와 한정자(Special Member & Specifier) (1) | 2025.08.25 |
| 📌 Chapter 04 - 클래스 고급 개념 - 02 (4) | 2025.08.09 |
| 📌 Chapter 04 - 클래스 고급 개념 - 01 (1) | 2025.08.09 |
| 📌 Chapter 03 – 클래스 기초 (2) | 2025.08.06 |