이 글은 PC 버전 TISTORY에 최적화 되어있습니다.
서론
게임 프로그래밍 패턴 중 프로토타입패턴(정리해서 올려드리겠습니다. 불친절해서 죄송합니다.)을 공부하다 깊은 복사, 얕은 복사에 대한 개념이 부실하게 나와서 정리할 겸 글을 쓰게 됬습니다. 저한테는 생소한 개념이었던 C++에서의 복사 생성자, 얕은 복사와 깊은 복사에 대해서 알아보도록 하겠습니다.
1. 생성자2. 소멸자3. 복사 생성자4. 얕은 복사5. 깊은 복사
생성자
먼저 생성자가 없이 클래스를 초기화하는 방법을 아래의 코드를 예시로 보도록 하겠습니다. 간단하게 클래스 내의 private로 선언된 멤버변수들을 SetInfo() 메소드로 초기화를 하고 GetInfo() 메소드로 정보를 가져오는 것을 보실 수 있습니다.
#include using namespace std; class Person { private: char * name; int age; char * job; public: void GetInfo(); void SetInfo(char * _name, int _age, char * _job); }; void Person::GetInfo()
{ cout << "이름: " << name << ", 나이: " << age << ", 직장: " << job << endl; } void Person::SetInfo(char * _name, int _age, char * _job)
{ name = _name; age = _age; job = _job; } int main() { Person evan;
evan.SetInfo("에반", 25, "프로그래머"); evan.GetInfo(); return 0; }
[실행 결과]이름: 에반, 나이: 25, 직업: 프로그래머
class 클래스명 {public :클래스명 (매개변수) {}}
#include using namespace std; class Person { private: char * name; int age; char * job; public: Person(char* _name, int _age, char* _job); void GetInfo(); }; person:: Person(char * _name, int _age, char * _job) { name = _name; age = _age; job = _job; } void Person::GetInfo() { cout << "이름: " << name << ", 나이: " << age << ", 직장: " << job << endl; } int main() { Person evan("에반", 25, "프로그래머"); evan.GetInfo(); return 0; }
결과는 위의 코드와 같게 나옵니다. 생성자의 개념이 더 남아 있지만 여기는 복사 생성자의 자리이기 때문에 이쯤에서 넘어가도록 하고 다음에 정리해보도록 하겠습니다.
소멸자
함수 내에 선언된 지역 변수는 함수 호출이 끝남과 동시에 사라집니다. 객체도 함수에서 선언되면 함수 호출이 끝날 때 소멸되게 됩니다. 생성자는 객체가 생성될 때 호출해서 초기화를 해주는데 소멸하면서 부를 수 있는 건 없을까? 있습니다. 바로 소멸자입니다. 객체가 소멸되면서 자동으로 소멸자를 호출해 할당된 메모리를 삭제해주는 역할을 합니다.
다음은 소멸자의 형식입니다. 생성자와의 차이점은 클래스 명 앞에 ~이 붙는다는 점과, 매개변수를 가질 수 없다는 것입니다.
class 클래스명 {public :~클래스명 (매개변수) {}}
#include using namespace std; class DynamicArray { public: int *arr; DynamicArray(int size) { arr = new int[size]; } ~DynamicArray() { delete[] arr; arr = NULL; } }; int main() { int size = 3; int i; DynamicArray dynamicArray(size); cout << size << "개 입력하세요." << endl; for(i=0; i> dynamicArray.arr[i]; } for(i=size-1; i>=0; --i) { cout << dynamicArray.arr[i] << " "; } cout << endl; return 0; }
[실행결과]
3개 입력하세요.
1 2 3
3 2 1
복사생성자
복사 생성자는 다른 객체로부터 값을 복사해서 초기화하는데 사용하는 생성자 입니다.
#include using namespace std; class TestClass { private: int num; public: TestClass(int a, int b) { num = a; } void ShowData() { cout << "num: " << num << endl; } }; int main() { TestClass tc1(50); TestClass tc2 = tc1; tc2.ShowData(); return 0; }
[실행결과]
num : 50
// 기본 생성자TestClass(int a) {num = a;}// 복사 생성자TestClass(const TestClass& tc) {num = tc.num;}
깊은 복사
그렇다면 깊은 복사가 왜 필요한지 이유를 알아보도록 하겠습니다.
#include using namespace std; class TestClass { private: char *str; public: TestClass(const char *str) { name = new char[strlen(str)+1]; strcpy(name, str); } ~ TestClass() { delete []str; cout << "소멸자 호출됨" << endl; } void ShowData() { cout << "name: " << name << endl; } }; int main() { TestClass tc1("evan"); TestClass tc2 = mc1; tc1.ShowData(); tc2.ShowData(); return 0; }
[실행결과]
name: evan
name: evan
소멸자 호출됨
얕은 복사에서 tc2 선언시 객체가 복사된다고 했지만 실제로 메모리를 새로 할당하는 것이 아닌, 포인터만 가리키게 즉 참조하기만 합니다. 여기서 문제가 발생합니다. 먼저 선언된 tc1 객체가 소멸되면서 name의 메모리를 해제시키고, tc2가 다시 name을 해제시키려고 하면 이미 tc1에 의해 소멸되었으므로 오류가 생기게 됩니다. 여기서 깊은 복사가 필요한 것입니다. 깊은 복사는 위의 예제의 생성자를 복사 생성자로 똑같이 다시 한번 구현해주면 됩니다.
#include using namespace std; class TestClass { private: char *str; public: TestClass(const char *str) { name = new char[strlen(str)+1]; strcpy(name, str); } TestClass(const TestClass *tc) { name = new char[strlen(tc.name)+1]; strcpy(name, tc.name); } ~ TestClass() { delete []str; cout << "소멸자 호출됨" << endl; } void ShowData() { cout << "name: " << name << endl; } }; int main() { TestClass tc1("evan"); TestClass tc2 = mc1; tc1.ShowData(); tc2.ShowData(); return 0; }
[실행결과]
name: evan
name: evan
소멸자 호출됨
소멸자 호출됨
깊은 복사는 위와 같이 멤버뿐 아니라 포인터로 참조하는 대상까지 깊게 복사하는 것입니다. 위와 같이 참조 대상까지 복사했으므로 에러가나지 않는 것입니다.