Copy Constructor ( 복사 생성자 )
자기 자신(객체)과 같은 자료형의 instance 를 매개변수로 받으며
매개변수로 받는 instance의 데이터들을 복사해서 그대로 자기자신(객체)의 데이터들에 대입하는 생성자 함수
어떤 클래스를 디자인하는데, Copy Constructor(복사생성자)를 구현해 놓지 않고
A a ;
A a_2 = a;
위와 같이 copy initialization 을 할 경우 , 컴파일러가 자동으로 default 복사 생성자를 만들어 준다
그러나 default copy constructor 는 Shallow Copy 만 동작하므로 유의하며 쓰던지 , copy contructor 와 assignment operator 를 따로 구현하여 , 그안에서 deep copy 가 되도록 하여야 한다
Shallow Copy ( 주소값도 그대로 복사 )
객체를 복사할때 , default 복사 생성자를 이용할 경우 , 객체안의 멤버 변수들이 전부 복사 된다 이때
변수들이 Primitive types(int, char, bool, float, double, 값형 ) 인 경우 , 값들을 그대로 복사해도 문제가 되지 않는다 , 그러나 class 나 struct, 포인터 변수 , 등등 객체 또는 메모리의 주소를 가리키는 경우는 , 가리키는 메모리 주소를 그대로 복사하는 꼴이 되버려서 , 원본 또는 복사본 둘중 하나가 delete 될 경우 , 다른 한쪽에도 똑같은 영향을 미치는 것이다 , 왜냐하면 서로 같은 주소를 바라보고 있기 때문이다
ex1)
위 코드를 보면 hello 라는 객체를 선언 한뒤 " hello" 를 생성자를 통해 초기화 했다
그 뒤 copy 라는 객체를 선언하고 hello 를 copy initialize 했는데 , 이때 copy constructor 가 구현되있지 않기 때문에 컴파일러는 default copy constructor 를 구현하여 실행시킨다
그리고 값만을 복사하는 shallow copy 가 진행된다
만약 primitive type 이라면 상관 없지만 , 주소를 대입해야 하는 포인터
타입 이라면 , 주소를 복사해버리게 된다
즉 아래와 같이 같은 주소를 공유하게 되는 사태가 벌어지는 것이다
copy.m_Data = hello.m_Data;
Debug 를 위해서 코드를 좀더 추가해서 다시 출력해보면
역시나 둘의 주소가 같다는걸 알 수 있다 .
이렇게 되면 , hello . m_Data 또는 copy.m_Data 둘중 하나가 delete 되면
다른 한 쪽에서도 delete 된것과 같은 것이다
즉 scope 를 벗어나게 되면서 copy 는 자동으로 소멸되고 , 소멸자를 실행해
delete[] copy.m_Data; 를 실행해 delete[] hello.m_Data ; 실행한 것과 같은 결과가 나오고
hello.GetString(); 을 통해 hello.m_Data 에 접근하여도 이미 delete 되어 메모리 반환 되었기 때문에 쓰레기 값이 나오게 된다
정리하자면 멤버 변수로 포인터 또는 어떤 객체의 주소를 참조하는 경우
default contructor 를 사용금지
Shallow Copy( 값만 그대로 복사 ) 금지
Deep Copy 를 해야한다
Deep Copy
( 주소를 새로 할당하고 새로 할당된 객체에 값들을 복사 )
class 나 struct, 포인터 변수 , 등등 객체 또는 메모리의 주소를 가리키는 경우는 , 가리키는 메모리 주소는 복사 하지 않고 새로 동적할당을 한다 , 그리고 원본의 주소가 가리키는 값들을 새로 할당된 객체에 복사한다 , 즉 copy constructor 와 대입연산자를 따로 구현하여 default constructor 가 실행되지 않게 하는 것이다
ex1) Copy Constructor 구현
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include < iostream>
#include < cassert>
class MyString
{
private :
char * m_Data = nullptr;
int m_length = 0 ;
public :
MyString(const char * _source = "" )
{
assert(_source);
m_length = std ::strlen(_source) + 1 ;
m_Data = new char [m_length];
strcpy_s(m_Data, m_length, _source);
m_Data[m_length - 1 ] = '\0' ;
}
//Copy Constructor
MyString(const MyString& _source)
{
std ::cout < < "User defined Copy Constructor called" < < std ::endl ;
m_length = _source.m_length;
if (_source.m_Data ! = nullptr)
{
if (m_Data)
{
delete [] m_Data;
m_Data = nullptr;
}
//Deep copy
m_Data = new char [m_length];
strcpy_s(m_Data, m_length, _source.m_Data);
m_Data[m_length - 1 ] = '\0' ;
}
else
{
m_Data = nullptr;
}
}
~MyString()
{
delete [] m_Data;
}
char * GetString() { return m_Data; }
char & GetDataRef(){ return * m_Data; }
int GetLength() { return m_length; }
};
int main()
{
MyString hello("hello" );
{
MyString copy = hello;
char & helloDataRef = hello.GetDataRef();
char & copyDataRef = copy.GetDataRef();
const char * result = & helloDataRef = = & copyDataRef ? "true" : "false" ;
printf ("Copy's &m_Data == hello's &m_Data : %s\n" , result);
printf ("Copy's &m_Data : 0x%p\n" , & helloDataRef);
printf ("hello's &m_Data : 0x%p\n" , & copyDataRef);
}
std ::cout < < hello.GetString() < < std ::endl ;
}
cs
결과는
사용자가 정의가 Copy Constructor 가 호출됐으며
hello, copy, 각각의 데이터 주소가 별개의 주소를 가리키고 있으며
copy 가 소멸되었어도 hello . m_Data 가 delete 된것은 아니기 때문에
쓰레기 값이 아닌 온전한 데이터가 출력이 된다
그러나 Shallow Copy 는
선언후 동시 초기화 뿐만 아니라
초기화만 하는 상황 즉
위 코드와 같이 같은 타입의 객체를 다음 줄에 대입할때도 실행이 된다
MyString str2; // 선언
str2 = hello; // 초기화 , 같은 타입 객체 대입
이때는 Copy Constructor 를 구현해놨다 하더라도 에러가 발생한다
따라서 아래처럼 대입연산자(Assignment operator)를 구현해야 한다
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#include < iostream>
#include < cassert>
class MyString
{
private :
char * m_Data = nullptr;
int m_length = 0 ;
public :
MyString(const char * _source = "" )
{
assert(_source);
m_length = std ::strlen(_source) + 1 ;
m_Data = new char [m_length];
strcpy_s(m_Data, m_length, _source);
m_Data[m_length - 1 ] = '\0' ;
}
MyString(const MyString& _source)
{
std ::cout < < "User defined Copy Constructor called" < < std ::endl ;
m_length = _source.m_length;
if (_source.m_Data ! = nullptr)
{
if (m_Data)
{
delete [] m_Data;
m_Data = nullptr;
}
//Deep copy
m_Data = new char [m_length];
strcpy_s(m_Data, m_length, _source.m_Data);
m_Data[m_length - 1 ] = '\0' ;
}
else
{
m_Data = nullptr;
}
}
//Assignment Operator
MyString& operator = (const MyString& _source)
{
std ::cout < < "Assignment operator" < < std ::endl ;
//
//if (this == &_source) //prevent self-assignment
// return *this;
if (m_Data)
{
delete [] m_Data;
m_Data = nullptr;
}
m_length = _source.m_length;
if (_source.m_Data ! = nullptr)
{
m_Data = new char [m_length];
strcpy_s(m_Data, m_length, _source.m_Data);
m_Data[m_length - 1 ] = '\0' ;
return * this ;
}
else
{
m_Data = nullptr;
return * this ;
}
}
~MyString()
{
delete [] m_Data;
}
char * GetString() { return m_Data; }
char & GetDataRef(){ return * m_Data; }
int GetLength() { return m_length; }
};
int main()
{
MyString hello("hello" );
{
MyString copy;
copy = hello;
char & helloDataRef = hello.GetDataRef();
char & copyDataRef = copy.GetDataRef();
const char * result = & helloDataRef = = & copyDataRef ? "true" : "false" ;
printf ("Copy's &m_Data == hello's &m_Data : %s\n" , result);
printf ("Copy's &m_Data : 0x%p\n" , & helloDataRef);
printf ("hello's &m_Data : 0x%p\n" , & copyDataRef);
}
std ::cout < < hello.GetString() < < std ::endl ;
}
cs
위 // Assignment Operator 부분을 추가 시키면 다시 정상 작동된다
그런데 대입연산자 ( Assignment Operator) 를 구현할때 주의할 점은
if (this == &_source) //prevent self-assignment
return *this;
위코드를 반드시 붙여줘야 한다 , 즉 자기 자신을 복사하게 만드는 것
hello = hello;
을 막는 것이다 , 자기 자신을 Rvalue 로 놓고 Lvalue 의 자기자신에게
대입하게 되면 Lvalue 는 원래 있던 자기 자신의 데이터를 지울 것이고
지우게 되면 Rvalue 의 데이터도 지워져서 결국 지워지는 효과가 나올 수 있다
( 물론 의도하던 안하던 , 애초에 hello = hello; 를 지우는 코드로 쓰는 사람은
없을 것이고 , 어떤 결과가 나올지 예측하기도 힘들다 )
어쨌든 Copy Constructor, Assignment Operator 둘다 구현해 놓는 것을 권장한다