Basic/C++

[C++] 복사생성자 그리고 얕은복사(Shallow Copy)와 깊은복사(Deep Copy)

에반황 2016. 11. 4. 17:50


이 글은 PC 버전 TISTORY에 최적화 되어있습니다.


서론

 게임 프로그래밍 패턴 중 프로토타입패턴(정리해서 올려드리겠습니다. 불친절해서 죄송합니다.)을 공부하다 깊은 복사, 얕은 복사에 대한 개념이 부실하게 나와서 정리할 겸 글을 쓰게 됬습니다. 저한테는 생소한 개념이었던 C++에서의 복사 생성자, 얕은 복사와 깊은 복사에 대해서 알아보도록 하겠습니다. 


1. 생성자
2. 소멸자
3. 복사 생성자
4. 얕은 복사
5. 깊은 복사



생성자

 먼저 생성자가 없이 클래스를 초기화하는 방법을 아래의 코드를 예시로 보도록 하겠습니다. 간단하게 클래스 내의 private로 선언된 멤버변수들을 SetInfo() 메소드로 초기화를 하고 GetInfo() 메소드로 정보를 가져오는 것을 보실 수 있습니다.


#include 
 
using namespace std;
 
class Person {
private:
    char * name;
    int age;
    char * job;
public:
    void GetInfo();
    void SetInfo(char * _name, int _age, char * _job);
};
 
void Person::GetInfo()
{ cout << "이름: " << name << ", 나이: " << age << ", 직장: " << job << endl; } void Person::SetInfo(char * _name, int _age, char * _job)
{ name = _name; age = _age; job = _job; } int main() { Person evan;
evan.SetInfo("에반", 25, "프로그래머"); evan.GetInfo(); return 0; }
[실행 결과]

이름: 에반, 나이: 25, 직업: 프로그래머

매번 클래스를 인스턴스화 할 때마다 이렇게 초기화 해주는 것은 매우 번거롭겠죠. 생성자를 통해서 해결할 수 있습니다. 생성자의 형식을 먼저 보도록 하겠습니다.



class 클래스명 {
public : 
클래스명 (매개변수) {

}
}
생성자는 위와 같이 클래스 명과 이름을 같게 생성합니다. 매개변수도 넘길 수 있고 반환형은 없습니다. 이번에는 위의 코드를 생성자를 사용해 구현해보도록 하겠습니다.

#include
 
using namespace std;
 
class Person {
private:
    char * name;
    int age;
    char * job;
public:
    Person(char* _name, int _age, char* _job);
    void GetInfo();
};

person:: Person(char * _name, int _age, char * _job)
{
    name = _name;
    age = _age;
    job = _job;
}
 
void Person::GetInfo()
{
    cout << "이름: " << name << ", 나이: " << age << ", 직장: " << job << endl;
}


int main()
{
    Person evan("에반", 25, "프로그래머");
 
    evan.GetInfo();
    
    return 0;
}

결과는 위의 코드와 같게 나옵니다. 생성자의 개념이 더 남아 있지만 여기는 복사 생성자의 자리이기 때문에 이쯤에서 넘어가도록 하고 다음에 정리해보도록 하겠습니다.



소멸자

 함수 내에 선언된 지역 변수는 함수 호출이 끝남과 동시에 사라집니다. 객체도 함수에서 선언되면 함수 호출이 끝날 때 소멸되게 됩니다. 생성자는 객체가 생성될 때 호출해서 초기화를 해주는데 소멸하면서 부를 수 있는 건 없을까? 있습니다. 바로 소멸자입니다. 객체가 소멸되면서 자동으로 소멸자를 호출해 할당된 메모리를 삭제해주는 역할을 합니다.


다음은 소멸자의 형식입니다. 생성자와의 차이점은 클래스 명 앞에 ~이 붙는다는 점과, 매개변수를 가질 수 없다는 것입니다.

class 클래스명 {
public : 
~클래스명 (매개변수) {

}
}


아래의 예제를 통해 소멸자의 쓰임을 보도록 하겠습니다.
#include

using namespace std;

class DynamicArray {
    public:
    
        int *arr;
        
        DynamicArray(int size) {
            arr = new int[size];
        }
        
        ~DynamicArray() {
            delete[] arr;
            arr = NULL;
        }
};

int main() {
    
    int size = 3;
    int i;
    
    DynamicArray dynamicArray(size);
    
    cout << size << "개 입력하세요." << endl;
    
    for(i=0; i> dynamicArray.arr[i];
    }
    
    for(i=size-1; i>=0; --i) {
        cout << dynamicArray.arr[i] << " ";
    }
    
    cout << endl;
    
    return 0;
}

[실행결과]


3개 입력하세요.

1 2 3

3 2 1


클래스의 멤버 변수 arr가 3개의 값을 가리키게 하고 main 함수가 종료되게 되면 dynamicArray 객체는 소멸되지만 여전히 arr는 메모리를 차지하고 있겠죠. 이 때 소멸자가 소멸 시점에 자동으로 호출되면서 arr를 메모리에서 해제해주는 것입니다.




복사생성자

 복사 생성자는 다른 객체로부터 값을 복사해서 초기화하는데 사용하는 생성자 입니다. 

#include
 
using namespace std;
 
class TestClass {
private:
    int num;
public:
    TestClass(int a, int b) {
        num = a;
    }
    
    void ShowData() {
        cout << "num: " << num  << endl;
    }
};
 
int main() {
    TestClass tc1(50);
    TestClass tc2 = tc1;
 
    tc2.ShowData();
    return 0;
}


[실행결과]


num : 50


이 결과가 어떻게 나오는 것 일까요? 클래스의 생성자에는 testClass 타입의 객체를 인수로 받도록 하는 부분은 없어보입니다. tc2 객체는 마치 tc1의 멤버들을 복사한 것으로 보입니다. 이것의 비밀은 복사 생성자를 명시적으로 생성하지 않으면 디폴트 복사 생성자가 아래와 같이 삽입됩니다. 객체를 인수로 받아 그 객체의 멤버들을 복사하는 것이죠.

// 기본 생성자
TestClass(int a) {
num = a;
}

// 복사 생성자
TestClass(const TestClass& tc) {
num = tc.num;
}

이것을 얕은 복사(Shallow Copy)라고 합니다.


깊은 복사

 그렇다면 깊은 복사가 왜 필요한지 이유를 알아보도록 하겠습니다.

#include
 
using namespace std;
 
class TestClass
{
private:
    char *str;
public:
    TestClass(const char *str)
    {
        name = new char[strlen(str)+1];
        strcpy(name, str);
    }
    ~ TestClass() {
        delete []str;
        cout << "소멸자 호출됨" << endl;
    }
    void ShowData()
    {
        cout << "name: " << name << endl;
    }
};
 
int main()
{
    TestClass tc1("evan");
    TestClass tc2 = mc1;
 
    tc1.ShowData();
    tc2.ShowData();
    return 0;
}

[실행결과]


name: evan

name: evan

소멸자 호출됨


 얕은 복사에서 tc2 선언시 객체가 복사된다고 했지만 실제로 메모리를 새로 할당하는 것이 아닌, 포인터만 가리키게 즉 참조하기만 합니다. 여기서 문제가 발생합니다. 먼저 선언된 tc1 객체가 소멸되면서 name의 메모리를 해제시키고, tc2가 다시 name을 해제시키려고 하면 이미 tc1에 의해 소멸되었으므로 오류가 생기게 됩니다. 여기서 깊은 복사가 필요한 것입니다. 깊은 복사는 위의 예제의 생성자를 복사 생성자로 똑같이 다시 한번 구현해주면 됩니다.

#include
 
using namespace std;
 
class TestClass
{
private:
    char *str;
public:
    TestClass(const char *str)
    {
        name = new char[strlen(str)+1];
        strcpy(name, str);
    }

    TestClass(const TestClass *tc)
    {
        name = new char[strlen(tc.name)+1];
        strcpy(name, tc.name);
    }

    ~ TestClass() {
        delete []str;
        cout << "소멸자 호출됨" << endl;
    }
    void ShowData()
    {
        cout << "name: " << name << endl;
    }
};
 
int main()
{
    TestClass tc1("evan");
    TestClass tc2 = mc1;
 
    tc1.ShowData();
    tc2.ShowData();
    return 0;
}

[실행결과]


name: evan

name: evan

소멸자 호출됨

소멸자 호출됨


깊은 복사는 위와 같이 멤버뿐 아니라 포인터로 참조하는 대상까지 깊게 복사하는 것입니다. 위와 같이 참조 대상까지 복사했으므로 에러가나지 않는 것입니다.




반응형