도순씨의 코딩일지
C++ :: 복사 생성자(Copy Constructor) 본문
🌼 복사 생성자의 개념
C부터 시작하여 우리는 다음과 같은 방법으로 변수와 참조자를 선언하고 초기화했습니다.
1
2
|
int num(20);
int &ref = num;
|
cs |
하지만 C++에서는 이러한 방식으로도 선언과 초기화가 가능합니다.
1
2
|
int num(20)
int &ref(num)
|
cs |
두 초기화 방식은 결과적으로 동일합니다. 다음 코드를 살펴봅시다.
1
2
3
4
5
6
7
8
9
10
11
|
class SoSimple{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2): num1(n1), num2(n2){}
void ShowSimleData(){
cout << num1 << endl;
cout << num2 << endl;
}
};
|
cs |
이어서 다음 코드의 실행 결과를 예상해봅시다.
1
2
3
4
5
6
|
int main(void){
SoSimple sim1(15, 20);
SoSimple sim2 = sim1;
sim2.ShowSimleData();
return 0;
}
|
cs |
위 코드중에서 3번째 줄의 코드는 객체의 생성 및 초기화를 연산시킵니다. 객체 대 객체의 복사가 나타납니다.
실제로 코드를 실행시켜 보면, num1과 num2에 저장된 값 15와 20이 출력됩니다.
그런데 sim2는 어떠한 과정을 거쳐서 생성되는 것일까요? 더 자세하게 알아보도록 합시다.
세 번째 줄의 코드에 담겨있는 내용을 정리하면 다음과 같습니다.
💡 SoSimple형 객체를 생성하라
💡 객체의 이름은 sim2로 정한다
💡 sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체 생성을 완료한다.
따라서 이 코드가 묵시적 변환 과정을 거쳐서 객체를 생성시킨다는 것을 알 수 있습니다.
1
|
SoSimple sim2(sim1)
|
cs |
다음 예제를 살펴보도록 합시다.
⭐️ ClassInit.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#include <iostream>
using namespace std;
class SoSimple{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2) {}
SoSimple(SoSimple ©): num1(copy.num1), num2(copy.num2){
cout << "Called SoSimple(SoSimple ©)" << endl;
}
void ShowSimpleData(){
cout<<num1 <<endl;
cout << num2 << endl;
}
};
int main(void){
SoSimple sim1(15, 20);
cout << "생성 및 초기화 직전"<<endl;
SoSimple sim2 = sim1; // SoSimple sim2(sim1)
cout << "생성 및 초기화 직후" << endl;
sim2.ShowSimpleData();
return 0;
}
|
cs |
⭐️ ClassInit.cpp 실행 결과
1
2
3
4
5
|
생성 및 초기화 직전
Called SoSimple(SoSimple ©)
생성 및 초기화 직후
15
20
|
cs |
위 예제의 23줄에 제시된 코드를 복사 생성자(Copy constructor)라고 합니다.
다음과 같은 것이 일반적인 형태의 복사생성자입니다.
1
2
3
|
SoSimple(const SoSimple ©) : num1(copy.num1, copy.num2){
cout << "Called SoSimple(SoSimple ©)" << endl;
}
|
cs |
const를 사용하여 원본을 변경시키는 실수를 막아주었습니다.
🌼 디폴트 복사 생성자
복사 생성자를 정의하지 않으면 자동으로 디폴트 복사 생성자가 생성됩니다. 다음 두 코드는 동일한 내용입니다.
1
2
3
4
5
6
7
8
|
class SoSimple{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2) {}
...
};
|
cs |
1
2
3
4
5
6
7
8
|
class SoSimple{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2) {}
SoSimple(const SoSimple ©): num1(copy.num1), num2(copy.num2);
};
|
cs |
🌼 explicit : 변환에 의한 초기화 차단
1
2
3
|
SoSimple sim2 = sim1;
SoSimple sim2(sim1)
|
cs |
앞에서 언급한 것 처럼 첫 번째 라인의 코드는 묵시적 변환이 일어나서 세 번째 라인의 코드와 같은 형태로 바뀝니다.
이런 변환을 막고 싶다면
1
2
3
|
explicit SoSimple(const SoSimple ©) : num1(copy.num1), num2(copy.num2){
//empty
}
|
cs |
다음과 같이 선언하면 됩니다. 묵시적 변환이 좋은 것만은 아니므로 자주 사용되는 키워드 중 하나입니다.
🌼 깊은 복사와 얕은 복사
디폴트 복사 생성자는 얕은 복사가 일어나며 문제를 야기시키는 경우가 많습니다. 다음 코드를 봅시다.
⭐️ ShallowCopyError.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
#include <iostream>
#include <cstring>
using namespace std;
class Person{
private:
char * name;
int age;
public:
Person(char * myname, int myage){
int len = strlen(myname) + 1;
name = new char[len];
strcpy(name, myname);
age = myage;
}
void ShowPersonInfo() const{
cout << "이름: "<< name << endl;
cout << "나이: " << age << endl;
}
~Person(){
delete []name;
cout << "called destructor!"<<endl;
}
};
int main(void){
Person man1("Lee dong woo", 29);
Person man2 = man1;
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
|
cs |
⭐️ ShallowCopyError.cpp 실행 결과
1
2
3
4
5
|
이름: Lee dong woo
나이: 29
이름: Lee dong woo
나이: 29
called destructor!
|
cs |
분명 객체는 두 개가 생성되었는데 소멸자가 한 번만 실행된 것을 확인할 수 있습니다. 이 문제점은 얕은 복사때문에 일어난 일입니다. 디폴트 복사 생성자는 단순히 복사만 하기 때문에 같은 주소를 참조하는 형태를 보입니다.
이러한 문제가 발생하기 위해서 깊은 복사(deep copy)를 사용합니다. 깊은 복사는 멤버뿐만 아니라 포인터로 참조하는 대상까지 복사합니다.
깊은 복사를 사용하여 ShallowCopyError.cpp를 수정하면 다음과 같이 수정할 수 있습니다.
⭐️ 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#include <iostream>
#include <cstring>
using namespace std;
class Person{
private:
char * name;
int age;
public:
Person(char * p_name, int p_age) {
age = p_age;
name = new char[strlen(p_name)+1];
strcpy(name, p_name);
}
Person(const Person& copy) : age(copy.age){
name = new char[strlen(copy.name) + 1];
strcpy(name, copy.name);
}
void ShowPersonInfo() const{
cout << "이름: "<< name << endl;
cout << "나이: " << age << endl;
}
~Person(){
delete []name;
cout << "called destructor!"<<endl;
}
};
int main(void){
Person man1("Lee dong woo", 29);
Person man2 = man1;
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
|
cs |
⭐️ 실행 결과
1
2
3
4
5
6
|
이름: Lee dong woo
나이: 29
이름: Lee dong woo
나이: 29
called destructor!
called destructor!
|
cs |
소멸자가 두 번 실행된 것을 보아 깊은 복사가 실행되었음을 알 수 있습니다.
📜 출처
윤성우(2010). 윤성우 열혈 C++ 프로그래밍. 오렌지미디어.
'𝐏𝐑𝐎𝐆𝐑𝐀𝐌𝐌𝐈𝐍𝐆 > 𝐂++' 카테고리의 다른 글
C++ :: 상속(Inheritance) (0) | 2020.08.25 |
---|---|
C++ :: const, friend 키워드 (0) | 2020.08.16 |
C++ :: 클래스와 배열, this 포인터 (0) | 2020.08.15 |
C++ :: 생성자(Constructor)와 소멸자(Destructor) (0) | 2020.08.15 |
C++ :: 캡슐화(Encapsulation) (0) | 2020.08.14 |