객체 지향 프로그래밍 (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이 가리키는 주소가 다르므로
얕은 복사에서 발생했던 문제점들이 사라지게 된다.
코드 전문은 위에서 확인이 가능하다.
'코딩 > C++' 카테고리의 다른 글
[C++] End Of File 처리. (0) | 2021.10.13 |
---|---|
[C++] algorithm STL의 sort()와 queue STL의 priority_queue의 정렬 방식의 차이 - less와 greater (0) | 2021.09.01 |
[C++] priority_queue STL 비교 구조체 comparator 사용하기 (0) | 2021.09.01 |
[C++] 문자열(문자)을 정수형처럼 다루는 메서드 (0) | 2021.08.17 |
[C++] Map 자료구조 사용하기 (0) | 2021.08.17 |