반응형

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 둘다 구현해 놓는 것을 권장한다

반응형

'C++' 카테고리의 다른 글

c++ Singleton Pattern 기본 구현 방법  (0) 2020.03.03
const  (0) 2020.02.29
Static Member Function(정적 멤버 함수)  (0) 2020.02.27
std::function (c++ 11)  (0) 2020.02.23
Input Output library  (0) 2020.02.21

+ Recent posts