[C++] 디폴트 복사 생성자(Default copy constructor)

728x90
반응형

디폴트 복사 생성자는 멤버 대 멤버를 그대로 복사한다. 이러한 방식의 복사를 얕은 복사(shallow copy)라고 하는데 이는 멤버변수가 힙(Heap)의 메모리 공간을 참조하는 경우에 문제가 발생한다.

#include <iostream>
#include <cstring>

class Person
{
    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
    {
        std::cout << "이름 : " << name << std::endl;
        std::cout << "나이 : " << age << std::endl;
    }

    ~Person()
    {
        delete[]name;
        std::cout << "called destructor" << std::endl;
    }
};

int main(void)
{
    Person p1((char*)"Park", 4); //p1 객체를 생성하고 초기화, 이때는 생성자가 호출
    Person p2(p1); // p2 객체를 생성하고, p1 객체로 초기화, 이때 복사 생성자가 호출

    p1.ShowPersonInfo();
    p2.ShowPersonInfo();

    return 1;
}

위 코드에서 객체 p1p2가 생성되는 과정을 자세히 살펴보자.

먼저, p1 객체가 생성될 때, 포인터 변수 namemyname의 길이만큼 메모리를 할당받고 해당 메모리 영역을 참조하게 된다. 예를 들어, "Park "이라는 문자열이 저장된 메모리 주소 0x12F 번지를 참조하고 있을 것이다.

p2 객체를 p1으로 초기화하면 복사 생성자가 호출된다. 이 과정에서 p1nameage 값이 그대로 p2nameage에 복사된다.

이렇게 되면 p1p2name 포인터는 모두 동일한 메모리 영역(예: 0x12F)을 참조하게 된다. 여기까지는 문제가 없지만, 객체가 소멸될 때 문제가 발생한다.

p1이 먼저 소멸되고 p2가 소멸되는 시나리오를 살펴보자. p1의 소멸자가 호출되면 name이 참조하고 있는 메모리 영역의 할당이 해제된다. 따라서 p2의 소멸자가 호출될 때, p2name 포인터는 이미 해제된 메모리 영역을 참조하고 있어 런타임 에러가 발생하게 된다.

이 문제를 해결하려면 복사 생성자를 명시적으로 정의하여 name 포인터의 깊은 복사를 수행해야 한다. 이렇게 하면 각 객체가 독립적인 메모리 영역을 참조하게 되어, 소멸 시 문제가 발생하지 않는다.

#include <iostream>
#include <cstring>

class Person
{
    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(const Person& p) : age(p.age)
    {
        name = new char[strlen(p.name)+1]; 
        strcpy(name, p.name);
    }

    void ShowPersonInfo() const
    {
        std::cout << "이름 : " << name << std::endl;
        std::cout << "나이 : " << age << std::endl;
    }

    ~Person()
    {
        delete[]name;
        std::cout << "called destructor" << std::endl;
    }
};

int main(void)
{
    Person p1((char*)"Park", 4); //p1 객체를 생성하고 초기화, 이때는 생성자가 호출
    Person p2(p1); // p2 객체를 생성하고, p1 객체로 초기화, 이때 복사 생성자가 호출

    p1.ShowPersonInfo();
    p2.ShowPersonInfo();

    return 1;
}

p1이 생성되면서 생성자에 의해 p1.name에는 myname의 길이만큼 메모리를 할당하게 된다. p2를 선언하면서 전달인자로 p1을 전달했기 때문에 Person Class의 복사 생성자가 호출된다. 복사 생성자의 매개변수는 p1 객체를 참조하고 있다. 복사 생성자에서는 namep1.name의 길이만큼 메모리를 할당하고, p1.name가 참조하는 메모리 영역에 저장된 문자열을 name에 복사한다.

name = new char[strlen(p.name)+1];
strcpy(name, p.name);

복사 생성자에서는 새로운 메모리영역을 할당해 문자열을 복사했기 때문에 p1객체가 소멸해도 p2.name이 참조하는 메모리 영역에는 아무런 영향을 주지 않기 때문에 에러가 발생하지 않는다.

728x90
반응형