안녕하세요.
생성자에도 몇 가지 종류가 있는데 그중에 복사 생성자에 대해서 조금 더 알아보겠습니다.
C++에서 복사 생성자는 객체를 복사하여 새로운 객체를 생성할 때 호출되는 특별한 멤버 함수입니다. 복사 생성자를 적절히 사용하는 것은 C++ 프로그래밍의 핵심이며, 특히 객체의 메모리 관리를 이해하는 데 중요합니다. 이 글에서는 복사 생성자의 기본 개념부터 고급 활용 부분까지 살펴보겠습니다.
1. 변수 초기화에 스타일
변수 초기화할 때 C++ 에는 C와 조금 다른 방식이 추가되어 있습니다. 바로 ( ) 괄호입니다. 여기서 명시적(직접) 초기화 방식과 묵시적(암시적) 방식에 따라 조금 차이가 있어서 미리 조금 알고 있으면 도움 될 것이 있어서 적어보았습니다.
<C 스타일 초기화 경우>
int count = 0;
int &refCount = count;
<C++ 스타일 초기화 경우(C++에서는 C 스타일도 가능)>
int count(0); // 명시적(직접) 초기화 방식
int &refCount(count);
2. 복사 생성자란 무엇인가?
복사 생성자는 동일한 클래스 타입의 다른 객체를 인자로 받아 새로운 객체를 초기화하는 생성자입니다. 기본 형태는 다음과 같습니다.
ClassName(const ClassName& other);
ClassName: 클래스 이름
const ClassName& other: 복사할 원본 객체 (상수 참조)
3. 디폴트 복사 생성자
클래스에서 복사 생성자를 명시적으로 정의하지 않으면 컴파일러는 자동으로 디폴트 복사 생성자를 생성합니다. 디폴트 복사 생성자는 멤버 대 멤버 복사(member-wise copy)를 수행합니다. 즉, 원본 객체의 각 멤버 변수를 새로운 객체의 동일한 멤버 변수에 복사합니다.
#include <iostream>
using namespace std;
class MyClass {
public:
int data;
MyClass(int value) : data(value) {}
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 디폴트 복사 생성자 호출
cout << obj2.data << endl;
return 0;
}
4. 명시적 복사 생성자
디폴트 복사 생성자가 모든 경우에 적합한 것은 아닙니다. 특히, 동적 할당된 메모리를 멤버 변수로 가지는 클래스에서는 깊은 복사(deep copy)를 수행하기 위해 명시적으로 복사 생성자를 정의해야 합니다.
class MyClass {
public:
int* data;
MyClass(int value) {
data = new int(value);
}
// 명시적 복사 생성자
MyClass(const MyClass& other) {
data = new int(*(other.data)); // 깊은 복사
}
~MyClass() {
delete data;
}
};
5. 깊은 복사와 얕은 복사
깊은 복사(Deep Copy) : 원본 객체의 멤버 변수가 가리키는 메모리까지 새로운 객체에 복사합니다. 즉, 원본 객체와 복사된 객체는 완전히 독립적인 메모리 공간을 가지게 됩니다.
얕은 복사(Shallow Copy) : 원본 객체의 멤버 변수가 가리키는 메모리의 주소만 새로운 객체에 복사합니다. 즉, 원본 객체와 복사된 객체는 같은 메모리 공간을 공유하게 됩니다.
동적 할당된 메모리를 멤버 변수로 가지는 클래스에서 얕은 복사를 수행하면, 원본 객체와 복사된 객체가 같은 메모리 공간을 공유하게 되므로, 한 객체에서 메모리를 변경하면 다른 객체에도 영향을 미치게 됩니다. 또한, 메모리 해제 시 이중 해제 오류가 발생할 수 있습니다.
6. 복사 생성자 호출 시점
복사 생성자는 다음과 같은 경우에 호출됩니다.
- 객체를 다른 객체로 초기화할 때 : MyClass obj2 = obj1;
- 함수에 객체를 값으로 전달할 때 : void func(MyClass obj);
- 함수가 객체를 값으로 반환할 때 : MyClass func();
7. 복사 생성자와 대입 연산자
복사 생성자는 객체 초기화에 사용되고, 대입 연산자(=)는 이미 생성된 객체에 값을 할당할 때 사용됩니다. 두 연산자는 비슷한 역할을 하지만, 호출되는 시점이 다르므로 구분하여 사용해야 합니다.
MyClass obj1(10);
MyClass obj2(20);
MyClass obj3 = obj1; // 복사 생성자 호출
obj2 = obj1; // 대입 연산자 호출
8. explicit 키워드
explicit 키워드는 암시적 형변환을 막기 위해 생성자 앞에 사용됩니다. 복사 생성자를 explicit으로 선언하면 다른 타입의 객체로부터 암시적 복사를 방지할 수 있습니다.
class MyClass {
public:
int data;
MyClass(int value) : data(value) {}
explicit MyClass(const MyClass& other) : data(other.data) {} // explicit 복사 생성자
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 오류: explicit 복사 생성자이므로 암시적 복사 불가
MyClass obj3(obj1); // 명시적 복사는 가능
return 0;
}
9. 복사 생성자 활용 시 주의사항
1) 자기 대입 방지 : 대입 연산자를 오버로딩할 때 자기 대입을 방지하는 코드를 추가해야 합니다.
#include <iostream>
using namespace std;
class MyClass {
public:
int* data;
MyClass(int value) {
data = new int(value);
}
MyClass(const MyClass& other) {
data = new int(*(other.data));
}
MyClass& operator=(const MyClass& other) {
if (this == &other) { // 자기 대입 방지
return *this;
}
delete data;
data = new int(*(other.data));
return *this;
}
~MyClass() {
delete data;
}
};
int main() {
MyClass obj1(10);
obj1 = obj1; // 자기 대입 발생
cout << *(obj1.data) << endl;
return 0;
}
2) 예외 안전성 : 복사 생성자에서 예외가 발생할 수 있는 경우, 예외 안전성을 고려해야 합니다.
#include <iostream>
#include <stdexcept>
using namespace std;
class MyClass {
public:
int* data;
MyClass(int value) {
data = new int(value);
}
MyClass(const MyClass& other) {
try {
data = new int(*(other.data));
}
catch (const bad_alloc& e) {
cerr << "메모리 할당 실패: " << e.what() << endl;
throw; // 예외 다시 처리 요청
}
}
~MyClass() {
delete data;
}
};
int main() {
try {
MyClass obj1(10);
MyClass obj2 = obj1;
cout << *(obj2.data) << endl;
}
catch (...) {
// 예외 처리
}
return 0;
}
10. 결 론
복사 생성자는 C++ 객체 지향 프로그래밍에서 중요한 개념입니다. 복사 생성자의 작동 방식을 이해하고 적절히 활용하면 메모리 관리와 객체 복사를 효율적으로 처리할 수 있습니다. 특히, 동적 할당된 메모리를 사용하는 클래스에서는 복사 생성자를 명시적으로 정의하여 깊은 복사를 수행해야 합니다.
감사합니다.
'Programming > C, C++' 카테고리의 다른 글
[C++] 생성자(Constructor)에서 조금 알아두면 좋은 읽을거리 (0) | 2025.04.01 |
---|---|
Visual Studio(VC++)에서 빌드 시 한글 때문에 에러가 발생과 콘솔(디버깅) 창에 한글이 깨져 나올 경우 (0) | 2025.03.29 |
[C++] struct와 class 차이 (0) | 2025.03.28 |