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

인트로

깊은 복사와 얕은 복사를 학습하기 전에 알아두면 좋은 지식은 복사 생성자(Copy Constructor)이다.

 

복사 생성자란 다른 객체로부터 값을 복사해서 새로운 객체를 초기화하는데 사용되는 생성자이다. 복사 생성자를 정의하지 않으면 컴파일러가 자동으로 만들어주며 기본적으로 자신과 동일한 타입의 객체에 대한 레퍼런스(&)를 인자로 받는 생성자이다. 보다 쉬운 이해를 위해 몇 가지 간단한 예제를 보자.

복사 생성자 : 얕은 복사

앞서 언급했듯 복사 생성자를 정의하지 않으면 컴파일러가 자동으로 생성해 준다. 다음 코드는 사용자 정의 복사 생성자로 s2 객체의 초기화가 이루어지는 과정을 보여준다.

#include <iostream>
#include <string>

using namespace std;

class Student
{
public :
	char* name;
	int age;

	Student(const char *_name, int _age)
	{
		name = new char[strlen(_name) + 1];
		strcpy(name, _name);
		age = _age;
	}
};

int main(void)
{
	Student s1 = Student("Kim", 18);
	Student s2 = s1;

	cout << s1.name << " " << s1.age << endl;
	cout << s2.name << " " << s2.age << endl;
}

여기서 한 가지 중요한 사실은 s2의 포인터 변수 name이 가리키는 공간이 s1의 name이 가리키는 곳과 동일하며 우리는 이러한 복사 메커니즘을 얕은 복사(Shallow Copy)라고 부른다. 

 

그렇다면 얕은 복사가 항상 괜찮은 걸까?

 

[문제점 1]

만약 s1에서 name을 delete한 뒤 s2의 name에 접근하려 한다면 delete된 힙 공간에 접근하는 문제가 발생한다.  

Student s1 = Student("Kim", 18);
Student s2 = s1;

cout << s1.name << " " << s1.age << endl;
delete[] s1.name;
cout << s2.name << " " << s2.age << endl;

 

 

[문제점 2]

나아가 두 객체의 포인터 변수 name이 같은 메모리 공간을 참조하기에 어느 한 곳에서 값을 변경하면 변경된 값이 모두에게 반영된다.

Student s1 = Student("Kim", 18);
Student s2 = s1;

strcpy(s1.name, "Lee");
cout << s1.name << " " << s1.age << endl;
cout << s2.name << " " << s2.age << endl;

 

 

비단 이러한 문제가 얕은 복사에 국한된 문제는 아니다. 포인터를 다루는 대부분의 상황에서 발생할 수 있는 문제들로 우리의 마음을 아프게 한다. 하지만 언제나 해결책은 있는 법. 얕은 복사에 대응되는 방법론이 바로 깊은 복사(Deep Copy)로 앞서 언급했던 포인터와 관련된 문제들을 해결해 준다.

복사 생성자 : 깊은 복사

얕은 복사와 다르게 포인터 변수 name이 서로 다른 메모리 공간을 참조하고 있다. 

#include <iostream>
#include <string>

using namespace std;

class Student
{
public :
	char* name;
	int age;

	Student(const char *_name, int _age)
	{
		name = new char[strlen(_name) + 1];
		strcpy(name, _name);
		age = _age;
	}
	
	//Student(const Student& s)
	//{
	//	cout << "얕은 복사 생성자 호출" << endl;
	//	name = s.name;
	//	age = s.age;
	//}

	Student(const Student& s)
	{
		cout << "깊은 복사 생성자 호출" << endl;
		name = new char[strlen(s.name) + 1];
		strcpy(name, s.name);
		age = s.age;
	}
};

int main(void)
{
	Student s1 = Student("Kim", 18);
	Student s2 = s1;

	cout << s1.name << " " << s1.age << endl;
	cout << s2.name << " " << s2.age << endl;
}

 

 

s1의 name을 "Lee"로 변경해도 s2의 name에 영향을 주지 않는 것을 확인할 수 있다.

Student s1 = Student("Kim", 18);
Student s2 = s1;

strcpy(s1.name, "Lee");
cout << s1.name << " " << s1.age << endl;
cout << s2.name << " " << s2.age << endl;

 

 

나아가 s1의 name을 delete해도 에러가 발생하지 않는다.

Student s1 = Student("Kim", 18);
Student s2 = s1;

cout << s1.name << " " << s1.age << endl;
delete[] s1.name;
cout << s2.name << " " << s2.age << endl;

마무리

깊은 복사와 얕은 복사가 꼭 복사 생성자에만 사용되는 것은 아니다. 어디까지나 깊은 복사와 얕은 복사는 방법론이기에 필요한 상황에 적절히 사용하면 된다.