본문 바로가기

Programming/C++

[C++] 복사생성자 / 깊은복사와 얕은복사

이 포스팅은 윤성우님의 열혈 C++ 프로그래밍 책을 기반으로 작성되었습니다.

디폴트 복사생성자의 생성

Polygon이라는 클래스가 있고 Square라는 객체를 생성한 뒤 Square1에 복사하려한다면 어떻게 하면될까?
직관적으로는 Square1 = Square이라고 하면 될 것 같은데.. 실제로 된다.

class Polygon
{
private:
    int NumOfLine;
    int NumOfDot;
public:
    Polygon(int line, int dot);
    ...
};

int main(){
    Polygon Square(4,4);
    Polygon Square1=Square;
    Polygon Square2(Square1);
    return 0;
}

위의 코드는 실제로 대입연산을 하는 것처럼 멤버대멤버 복사가 일어난다.
따라서 Square1과 Square2 모두 Square과 같은 멤버면수를 가지게 된다.
이러한 기능을 하는 것을 복사생성자라고 하며, 이처럼 별도로 복사 연산자를 생성안해도 자동으로 컴파일러에서 호출해주는 것을 디폴트 복사생성자라고 한다. 이 때, 복사생성자는 우리가 별도로 정의하여 기능을 넣어줄 수도 있다

이 때, Square1=Square 코드는 사실 연산자오버로딩을 통해 Square2(Square1)과 같은 기능을 호출한 것이다.
연산자 오버로딩에 관련된 내용은 뒤에 포스팅하도록 하겠다.
따라서 Polygon Square1=Square은 실제로 Polygon Square1(Sqaure)로 묵시적으로 해석이 된다.
혹시 이게 마음에 들지 않는다면, explict Polygon(const Polygon &copy)와 같은 함수를 직접 정의해줌으로써 = 연산을 통해서는 객채 복사가 되지 않도록 막아줄 수 있다.

'얕은 복사'와 '깊은 복사'

하지만 위와 같은 복사생성자는 문제를 일으킬 수 있다.
이는 단순히 멤버변수를 =으로 복사하기 때문에 생기는 문제인데 아래의 예제코드를 통해서 이해해보자.

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;
    }
    ~Person()
    {
        delete []name;
        cout << "Called Destructor" << endl;
    }
}

int main(void)
{
    Person man1("Kim Ji Wan", 27);
    Person man2 = man1
}

>>> "Called Destructor"

위의 코드의 잘못된 점이 보이는가?
잘 못찾겠다면 실행결과를 보자. 이제 뭔가 보이지 않는가?
우리가 만든 Person의 객체는 man1과 man2 두명인데 이들이 각각 소멸될 때 출력됐어야 할 소멸자가 한번밖에 출력되지 않았다.
이유는 다름아닌 디폴트 복사 생성자가 man1을 복사하는 과정에서 단순히 name포인터가 man1의 이름을 가리키게 되면서, 두 객체 모두 다 하나의 이름을 포인터로 가리키고 있는 상황이 생긴 것이다. 이것의 문제점은 man1에서 소멸자가 동적할당된 name을 delete시키고 나면 man2에서도 name을 소멸하려고 할 것인데 소멸해야할 name이 이미 소멸되어있어서 에러가 발생하게 된다.

이렇게 복사 과정에서 동적할당된 멤버변수를 새로 동적할당받은 메모리에 저장하지 않고 그냥 가리키는 것을 얕은 복사, 새로 메모리를 동적할당 받아 메모리에 저장하는 것을 깊은 복사라고 한다. 

Person(const Person& copy) : age(copy.age)
{
    name = new char[strlen(copy.name)+1];
    strcpy(name,myname);
}

깊은 복사는 위와 같이 진행하면 되는데, 사실상 기존의 디폴트 생성자와 모습이 거의 흡사하다는 것을 기억해두자.

복사 생성자가 호출되는 시점

그렇다면 복사생성자는 언제 호출될까?
얼핏 들으면 이상하게 들릴 수도 있는 질문이다. 당연히 객체를 복사할 때 일어나지 않을까?
하지만 이 외에도 예상치 못한 상황에서 복사생성자는 호출된다.

1. 위에 언급한 기존의 객체를 복사하여 객체를 초기화하는 경우이다.
2. 객체를 인자로 전달받는 경우.
3. 객체를 참조형이 아닌 채로 반환하는 경우.

우리는 앞서서 객체가 인자로도 전달될 수 있고 반환될 수도 있음을 알았다.
초간단화한 다음의 코드를 보면서 이해해보자.

Person PersonFunc(Person ob) // 객체를 인자로 넘겨받으면서 해당 객체를 ob에 복사가 일어난다.
{
    ....
    return ob; // 객체를 반환하여 넘겨줌.
}

int main(void)
{
    Person Jiwan;
    PersonFunc(Jiwan); // 객체 지완을 인자로 넘겨줌. 객체를 리턴 받으면서 또 한번의 복사가 일어남.
    return 0;
}

위의 경우 객체 복사가 일어나는 것은 이해하였다면, 또 생각해볼 수 있다.
PersonFunc(Jiwan)은 아무리 봐도 아무 의미없어보이는데..?
사실 저 객체는 당장 다음줄로만 넘어가도 존재하지 않게 된다. 이와 같은 객체를 Temporary 즉, 임시 객체라한다.

끝.