얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)
객체를 생성하고 대입하게 되면 복사 생성자가 호출되어 값을 복사하게 됩니다.
클래스의 복사 생성자는 별도로 정의하지 않아도 생성되는 특수한 함수입니다.
이렇게 기본적으로 내부에 생성되는 복사 생성자는 int 등의 타입에는 정상적으로 동작합니다.
하지만 다음과 같이 포인터 등을 사용할 경우에는 문제가 발생할 소지가 있습니다.
#include <string> #include <iostream> class Person { public: Person(int nAge, char* pName) { m_Age = nAge; int nLen = strlen(pName) + 1; m_Name = new char[nLen]; strncpy(m_Name, pName, nLen); } ~Person() { if (nullptr != m_Name) { delete[] m_Name; m_Name = nullptr; } } void ShowPerson() { std::cout << m_Age << " " << m_Name << std::endl; } private: int m_Age; char* m_Name; }; int main() { Person aMan{20, "Kim"}; aMan.ShowPerson(); Person bMan = aMan; bMan.ShowPerson(); }
값을 출력하는 것은 정상적으로 되지만 종료될 때 에러가 발생합니다.
이것은 동적할당된 메모리의 위치를 공유하고 있기 때문에 발생하는 문제입니다.
자동 생성된 복사생성자에서는 포인터를 값으로 복사하고 있기 때문에 다음과 같은 상태가 됩니다.
동적할당 메모리를 각 객체가 동시에 참조하고 있습니다.
그리고 소멸자에서는 동적할당된 메모리를 해제하려고 시도를 합니다.
첫 번째로 소멸되는 객체가 메모리를 해제하는 것은 정상적으로 진행됩니다.
다음으로 소멸되는 객체는 이미 해제된 메모리를 여전히 포인터로 가지고 있습니다.
이 상태에서 메모리 해제를 다시 시도하려고 합니다.
이 과정에서 에러가 발생하기 때문에 문제가 생기는 것입니다.
이것이 포인터를 단순하게 대입하기 때문에 발생하는 얕은 복사의 문제점입니다.
이 문제를 해결하기 위해서 복사 생성자를 다음과 같이 추가합니다.
class Person { public: Person(int nAge, char* pName) { m_Age = nAge; int nLen = strlen(pName) + 1; m_Name = new char[nLen]; strncpy(m_Name, pName, nLen); } // Add copy constructor Person(const Person& oldP) { m_Age = oldP.m_Age; int nLen = strlen(oldP.m_Name) + 1; m_Name = new char[nLen]; strncpy(m_Name, oldP.m_Name, nLen); } ~Person() { if (nullptr != m_Name) { delete[] m_Name; m_Name = nullptr; } } void ShowPerson() { std::cout << m_Age << " " << m_Name << std::endl; } private: int m_Age; char* m_Name; };
복사생성자에서는 메모리를 동적할당하는 코드를 작성하였습니다.
이렇게 되면 두 객체의 m_Name이 각각 다른 메모리를 가리키기 때문에 문제가 발생하지 않습니다.
그림으로 표시하면 다음과 같은 상태가 됩니다.
각각 다른 메모리 공간을 참조하고 있습니다.
한 쪽에 메모리를 해제해도 다른 쪽 객체는 어떤 영향도 받지 않습니다.
복사생성자 뿐만 아니라 복사대입연산자에서도 동일한 문제가 발생할 수 있습니다.
이렇게 얕은 복사로 발생하는 문제를 깊은 복사를 통해서 해결하면 됩니다.
포인터가 2중, 3중으로 바뀔 경우에도 동일하게 전체를 깊은 복사로 진행하면 됩니다.
이런 상황에서는 STL의 string을 사용하는 것도 문제를 해결하는 방법입니다.
원하는 방식으로 문제를 해결하면 됩니다.