[C++] 객체 지향 프로그래밍 - 얕은 복사와 깊은 복사 (Heap 메모리, Stack 메모리)
객체 지향 프로그래밍 (Object Oriented Programming, 이하 OOP)에서의 데이터의 단위는 객체이다.
OOP에서의 객체 복사는 기존 객체의 사본을 만드는 일이다.
복사의 방식은 얕은 복사(참조 형식)와 깊은 복사(값을 복사)가 존재하게 된다.
예시 코드
/*
* OOP - 얕은 복사와 깊은 복사
*/
#include <iostream>
using namespace std;
class MyObject {
public:
string* name;
int age;
//기본 생성자
MyObject(int age, string name) {
this->name = new string(name);
this->age = age;
}
//기본 복사 생성자와 같음. 레퍼런스로 만들지 않으면 복사생
//MyObject(const MyObject &myObj) {
// cout << "얕은복사 생성자 호출" << "\n";
// this->name = myObj.name;
// this->age = myObj.age;
//}
//깊은 복사 생성자.
//MyObject(const MyObject &myObj) {
// cout << "깊은복사 생성자 호출" << "\n";
// this->name = new string(*myObj.name);
// this->age = myObj.age;
//}
//소멸자
~MyObject() {
cout << *name << "의 소멸자 호출" << "\n";
delete name;
}
};
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
cout.tie(NULL);
MyObject obj1(42, "Mike");
MyObject obj2(obj1);
//MyObject obj2 = obj1;
*obj2.name = "John";
obj2.age = 36;
cout << "obj1's name's addr:" << &(*obj1.name) << "\n";
cout << "obj2's name's addr:" << &(*obj2.name) << "\n";
cout << "obj1's name:" << *obj1.name << "\n";
cout << "obj2's name:" << *obj2.name << "\n";
cout << "obj1's age:" << obj1.age << "\n";
cout << "obj2's age:" << obj2.age << "\n";
return 0;
}
위와 같은 코드가 있을 때, 얕은 복사와 깊은 복사의 차이에 대해 비교해 볼 것이다.
main 함수에서의 코드의 로직은 다음과 같다.
- MyObject 객체 obj1을 기본 생성자로 생성.
- MyObject 객체 obj2를 복사 생성자로 생성.
- obj2의 name과 age 수정.
- obj1, obj2의 정보 출력.
MyObject 클래스의 기본 생성자는 정수형 변수 age와
문자열형 변수 name을 입력받아서 클래스의 멤버변수(age, name)에 대입하게 된다.
이 때, 포인터 문자열 변수 name에는
new 키워드를 사용해서 동적할당을 해서 만든 문자열의 주소를 가리키도록 만들었다.
그러면 위와 같은 형태로 만들어 질 것이다.
힙 메모리에는 동적 할당으로 생성된 객체가 생성된다.
(이 객체는 프로그래머가 메모리를 해제하기 전 까지 남아있다.)
그리고 스택 메모리에는 해당 객체, 변수가 호출된 메서드가 종료되면 자동으로 사라지게 된다.
우리는 MyObject의 객체를 기본 생성자로 생성할 때 포인터 변수 name에
새로운 문자열을 new 키워드로 동적할당해서 해당 문자열의 주소를 name 변수가 참조하도록 만들었다.
이렇게 만든 것이 obj1이 된다.
MyObject obj1(42, "Mike");
MyObject obj2(obj1);
//MyObject obj2 = obj1;
obj2는 복사 생성자의 방식으로 만들어진다.
복사 생성자란, 생성할 객체와 같은 타입의 객체를 참조해서 객체를 초기화 하는 방식이다.
위와 같은 구조일 때 얕은 복사와 깊은 복사의 차이는 무엇일까?
얕은 복사(Shallow Copy)
얕은 복사란, 복사 생성자를 이용해서 객체를 복사할 때 원본 객체를 그대로 복사해서 생성한다.
이 때, 원본 객체 내부에 주소 참조 변수(포인터)가 있을 경우,
복사 객체의 주소 참조 변수는 해당 주소를 그대로 참조하게 된다.
즉 원본 객체의 주소 참조 변수와 복사한 객체의 주소 참조 변수의 주소가 같게 된다.
그러면 obj2의 age는 42, name의 주소는 obj1 객체의 name 포인터 변수가 가리키는 문자열의 주소와 같아질 것이다.
그러면 이렇게 될 경우 name을 수정했을 때 어떻게 되는지 확인해보자.
*obj2.name = "John";
obj2.age = 36;
cout << "obj1's name's addr:" << &(*obj1.name) << "\n";
cout << "obj2's name's addr:" << &(*obj2.name) << "\n";
cout << "obj1's name:" << *obj1.name << "\n";
cout << "obj2's name:" << *obj2.name << "\n";
cout << "obj1's age:" << obj1.age << "\n";
cout << "obj2's age:" << obj2.age << "\n";
위와 같이 코드를 짜서 실행해 보면 결과는 다음과 같다.
- obj1의 name과 obj2의 name 포인터 변수가 가리키는 문자열의 주소는 같다.
- obj2의 age를 수정했지만 obj1의 age는 수정이 되지 않았다.
obj1의 age와 obj2의 age는 각각 별개의 Stack Memory에서 생성된다.
따라서 별개의 주소를 가지므로 각 객체에 영향을 주지 않게된다. - 문제점 1. obj2의 name을 수정했더니 obj1의 name도 수정이 됐다.
obj1의 name과 obj2의 name이 가리키는 주소가 같기 때문에
obj2의 name을 수정해도 obj1의 name이 수정된 것이다. - 문제점 2. 소멸자 호출이 한 번 밖에 되지 않았다.
이는 Exception이 발생한 것이다.
코드에서 소멸자를 호출할 때 delete키워드를 호출해서
name이 가리키는 주소를 Heap 메모리에서 해제시켰다.
그러나 obj2의 name이 가리키는 주소는 정상적으로 해제가 됐지만,
obj1에서 name이 가리키는 주소는 이미 obj2의 소멸자에서 해제가 돼 있으므로
Exception이 발생한 것이다.
(소멸자는 생성자의 역순으로 호출이 된다.)
이 문제를 어떻게 해결할 수 있을까?
-> 객체마다 name이 가리키는 주소를 다르게 설정하면 되지 않을까?
깊은 복사(Deep Copy)
깊은 복사란, 복사 생성자를 이용해서 객체를 복사할 때 주소 참조 변수(포인터)가 있을 경우
주소를 복사하는 것이 아닌, 해당 주소가 가리키는 변수 값을 역참조해서 복사하게 되는 것을 의미한다.
이렇게 하기 위해서는 우리가 생성자를 새로 정의하듯,
복사 생성자도 새로 정의해야 한다.
//기본 복사 생성자와 같음. 레퍼런스로 만들지 않으면 복사생
//MyObject(const MyObject &myObj) {
// cout << "얕은복사 생성자 호출" << "\n";
// this->name = myObj.name;
// this->age = myObj.age;
//}
//깊은 복사 생성자.
MyObject(const MyObject &myObj) {
cout << "깊은복사 생성자 호출" << "\n";
this->name = new string(*myObj.name);
this->age = myObj.age;
}
깊은 복사 생성자의 로직은 다음과 같다.
- age는 그냥 값 복사를 하면 된다.
어차피 각 객체마다 개별적으로 stack 메모리에 생성된다. - name 역시 역참조를 해서 값 복사를 하게 되는데,
문자열 변수를 동적할당으로 원본 객체의 name 값으로 생성해서
복사할 객체가 해당 문자열 변수의 주소를 참조하도록 만들어준다.
그러면 얕은복사에서 했던 name 수정 후 출력이 어떻게 변하는지 알아보자.
*obj2.name = "John";
obj2.age = 36;
cout << "obj1's name's addr:" << &(*obj1.name) << "\n";
cout << "obj2's name's addr:" << &(*obj2.name) << "\n";
cout << "obj1's name:" << *obj1.name << "\n";
cout << "obj2's name:" << *obj2.name << "\n";
cout << "obj1's age:" << obj1.age << "\n";
cout << "obj2's age:" << obj2.age << "\n";
깊은 복사에서는 obj1과 obj2의 name이 가리키는 주소가 다르므로
얕은 복사에서 발생했던 문제점들이 사라지게 된다.
코드 전문은 위에서 확인이 가능하다.