본문 바로가기

C++/C, C++, STL

얕은 복사(Shallow copy)와 깊은 복사(Deep copy)

이 문서의 내용

    복사 생성자

    복사 생성자(Copy constructor)는 별도 정의하지 않더라도 컴파일러가 자동으로 생성하는 Constructor입니다.

    얕은 복사에서는 디폴트 복사 생성자를 호출합니다.

    복사 생성자를 재정의하려면 객체와 동일한 타입의 참조(&)를 인자로 받는 생성자를 구성합니다.

    얕은 복사

    얕은 복사(Shallow copy)디폴트 복사 생성자를 사용하는 객체 간 복사입니다.

    다음 코드는 얕은 복사가 실행되는 간단한 예제입니다.

    #include <iostream>
    #include <cstring>
    
    class People
    {
        public:
            char* name;
            int age;
    
            People(const char* _name, int _age)
            {
                name = new char[strlen(_name) + 1];
                strcpy(name, _name);
                age = _age; 
            }
    };
    
    int main()
    {
        People p0 = People("foo", 18);
        People p1 = p0;
     
        std::cout << reinterpret_cast<void *>(p0.name) << " " << p0.name << " " << p0.age << std::endl;
        std::cout << reinterpret_cast<void *>(p1.name) << " " << p1.name << " " << p1.age << std::endl;
    }
    코드 비고
    Line 20 People p0 = People("foo", 18) 클래스에서 정의한 생성자로 p0 객체를 생성합니다.
    Line 21 People p1 = p0; p0 객체를 p1 객체에 얕은 복사합니다.
    Line 23:24 reinterpret_cast<void *>(p0.name) p0 p1 객체의 멤버 변수 char* name의 메모리 상의 주소 값을 출력합니다.
    reinterpret_cast<void *>(p1.name)

    프로그램을 실행하고 출력 결과를 확인합니다.

    0xc96f18 foo 18
    0xc96f18 foo 18

    멤버 변수 char* name에 대해서 두 객체가 동일한 힙 메모리 주소 값을 가리키고 있습니다.

    따라서 어떤 객체가 char* name 멤버 변수를 delete하게 되면 다른 객체에도 영향을 주게 됩니다.

    테스트를 위해 코드를 수정합니다. 참조 객체가 delete 되어 값을 확인하지 못하는 것을 알 수 있습니다.

    People p0 = People("foo", 18);
    People p1 = p0;
     
    delete[] p1.name;
    std::cout << reinterpret_cast<void *>(p0.name) << " " << p0.name << " " << p0.age << std::endl;
    std::cout << reinterpret_cast<void *>(p1.name) << " " << p1.name << " " << p1.age << std::endl;

    깊은 복사

    깊은 복사(Deep copy)는 얕은 복사와 달리 디폴트 복사 생성자를 사용하지 않고 재정의합니다.

    #include <iostream>
    #include <cstring>
    
    class People
    {
        public:
            char* name;
            int age;
    
            People(const char* _name, int _age)
            {
                name = new char[strlen(_name) + 1];
                strcpy(name, _name);
                age = _age; 
            }
    
            People(const People& _source)
            {
                std::cout << "Deep copied" << std::endl;
                name = new char[strlen(_source.name) + 1];
                strcpy(name, _source.name);
                age = _source.age; 
            }
    };
    
    int main()
    {
        People p0 = People("foo", 18);
        People p1 = p0;
     
        std::cout << reinterpret_cast<void *>(p0.name) << " " << p0.name << " " << p0.age << std::endl;
        std::cout << reinterpret_cast<void *>(p1.name) << " " << p1.name << " " << p1.age << std::endl;
    
        delete[] p1.name;
        std::cout << std::endl;
        std::cout << "Post deleted" << std::endl;
        std::cout << reinterpret_cast<void *>(p0.name) << " " << p0.name << " " << p0.age << std::endl;
    }
    코드 비고
    Line 17:23 People(const People&) { } 깊은 복사를 위해 복사 생성자를 재정의합니다.
    Line 34 delete[] p1.name 깊은 복사 이후 원본 객체의 멤버 변수 char* namedelete합니다.
    Line 37 reinterpret_cast<void *>(p0.name) 원본 객체의 멤버 변수가 delete된 이후 복사된 객체의 멤버 변수를 출력합니다.

    테스트 코드를 실행하면 얕은 복사와의 차이점을 두 가지 확인 할 수 있습니다.

    Deep copied
    0x786f18 foo 18
    0x786f28 foo 18
    
    Post deleted
    0x786f28 foo 18

    우선 깊은 복사가 이뤄진 이후 두 객체의 멤버 변수 char* name이 가리키는 주소 값이 서로 다릅니다.

    따라서 한쪽이 delete 된 이후에도 남은 한쪽은 여전히 유효한 값을 나타냅니다.

    정리 및 복습

    • 복사 생성자(Copy constructor)는 임의 정의하지 않더라도 컴파일러에 의해서 자동 생성됩니다.
    • 얕은 복사(Shallow copy)는 디폴트 복사 생성자를 사용합니다.
    • 얕은 복사가 실행되면 멤버 변수의 포인터 변수가 원본과 서로 동일한 메모리 주소를 참조합니다.
    • 깊은 복사(Deep copy)는 복사 생성자를 재정의하여 사용합니다.
    • 깊은 복사가 실행되면 멤버 변수의 포인터 변수가 원보과 서로 다른 메모리 주소를 참조합니다.