[ 얕은 복사 (shallow copy) ]
- 값을 복사하는 것이 아닌, 값을 가리키는 포인터(주소 값)를 복사하는 것이다.
- 변수 생성에서 대입 연산자를 이용한 값의 복사는 문제가 되지 않지만, 객체에서는 문제가 발생할 수 있다.
변수 생성 시 얕은 복사
int x = 10;
int y = x;
객체 생성 시 얕은 복사
class Simple
{
private:
int num1;
int num2;
public:
Simple(int n1, int n2) : num1(n1), num2(n2) {}
void ShowSimpleData()
{
std::cout << num1 << endl;
std::cout << num2 << endl;
}
};
int main()
{
Simple sim1(15, 20);
Simple sim2 = sim1;
sim2.ShowSimpleData();
return 0;
}
이처럼 대입 연산자를 사용할 경우, 실제 멤버 대 멤버의 복사가 일어난다.
Simple sim2 = sim1;
// 위 문장은 묵시적으로 Simple sim2(sim1);라고 해석된다.
[ 깊은 복사 (deep copy) ]
- 메모리를 새로 할당해서 내용을 복사하는 것이다.
- 복사한 대상자가 바뀌더라도 복사된 객체는 아무런 영향을 받지 않는다.
[ 복사 생성자 ]
같은 클래스의 객체로부터 복사해서 새로운 객체를 생성하는 생성자이다.
복사 생성자는 어떤 클래스 T가 있으면 아래와 같이 정의된다.
T(const T& a);
다른 T의 객체 a를 상수 레퍼런스로 받기 때문에 생성자 내부에서 객체의 정보를 변경하지 못하고 복사만 할 수 있다.
이때, const가 붙이는 이유는 함수 내에서 변경되지 않는 객체라는 것을 명시적으로 나타내기 위함이다.
예제
class Sample
{
int num1;
int num2;
public:
Sample(int a, int b);
Sample(const Sample &Sp);
};
// 일반 생성자
Sample::Sample(int a, int b)
{
std::cout << "일반 생성자" << std::endl;
num1 = a;
num2 = b;
}
// 복사 생성자
Sample::Sample(const Sample &Sp)
{
std::cout << "복사 생성자" << std::endl;
num1 = Sp.num1;
num2 = Sp.num2;
}
int main(void)
{
Sample sp1(3, 3);
Sample sp2(sp1);
Sample sp3 = sp2;
return (0);
}
실행 결과
sp1은 int x, int y를 인자로 가지는 생성자가 오버로딩 된다.
Sample sp1(3, 3);
sp2는 인자로 sp1을 넘겼으므로 복사 생성자가 호출된다.
Sample sp2(sp1);
C++ 컴파일러의 디폴 복사 생성자 기능으로, 컴파일러가 Sample sp3(sp2)로 해석하기 때문에 sp3도 복사 생성자가 호출된다.
Sample sp3 = sp2;
주의사항
디폴트 복사 생성자는 얕은 복사를 수행한다. 즉, 멤버 변수의 값과 주소가 모두 같다.
만약 일반 생성자에서 동적 할당을 수행하고 해당 메모리를 포인터로 가리키고 있는 경우, 일반적으로 소멸자에서 메모리를 해제한다.
그런데 디폴트 복사 생성자는 얕은 복사만을 수행하므로 복사받은 객체가 삭제되고 동적 할당된 메모리도 해제되고 난 후, 복사된 객체가 해당 주소에 접근하면 런타임 오류가 발생한다.
따라서 이 경우에는 복사 생성자를 개발자가 직접 지정해 새롭게 동적 할당을 해주어야 한다.
예제
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;
std::cout << "called destrucctor!" << std::endl;
}
void ShowPersonInfo()
{
std::cout << name << std::endl;
std::cout << age << std::endl;
}
};
int main(void)
{
Person man1((char *)"Rannnneey", 29);
Person man2 = man1;
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
실행결과
복사 생성자를 호출하면 man1과 man2는 name과 age 모두 같은 값을 갖게 된다.
여기서 객체 man1과 man2의 name은 같은 주소값을 가리킨다.
즉, man1의 name이 동적으로 할당받아서 가리키고 있던 메모리를 man2의 name도 같이 가리킨다.
그런데 main 함수가 종료되기 직전에, 생성되었던 객체들이 파괴되면서 소멸자를 호출하는데 만약 man1이 먼저 파괴가 되었다고 가정해 보자.
그러면 소멸자는 man1의 내용을 모두 파괴하며 name의 주소값에 할당한 메모리를 delete를 하게 된다.
그런데 man2의 name이 여전히 해제된 메모리 주소값을 가리키고 있다.
man2에서 delete[] name이 실행되고, 이미 해제된 메모리에 접근해서 다시 해제하려고 했기 때문에 런타임 오류가 발생한다.
[ 깊은 복사를 위한 복사 생성자 ]
name의 경우 메모리를 따로 할당해서 그 내용만 복사하는 깊은 복사를 수행하게 해야 한다.
Person(const Person& copy) : age(copy.age)
{
name = new char[strlen(copy.name)+1];
strcpy(name, copy.name);
}
이렇게 하면 소멸자에서도 메모리 해제 시 각기 다른 메모리를 해제하는 것이기 때문에 문제가 발생하지 않는다.