동적할당된 메모리에 대한 포인터 변수를 맴버로써 갖고 있는 클래스의 경우 복사를 하거나 대입을 할때, 깊은 복사냐 얕은 복사냐에 대한 문제로 인해서 대입 연산자 오버로딩, 복사 생성자 구현이 까다로울 수 있다. 이 점에 대해 알아보자.
이와 같이 코드를 구성하면 어떤 문제가 발생할까? 우선 출력 결과를 보자.
음 확실히 문제가 있는 것을 알 수 있다.
new와 delete를 사용했으니 동적할당으로 코드를 구현한 것은 눈치챘을 것이다. 우선 생성자 부분의 코드는 나중에 보도록 하고, main문을 먼저 보도록 하자.
주소와 data를 출력하는 코드이다. main문을 보면 생성자를 초기화 하여 주소와 data를 출력하고 있다. 그 뒤를 이어 하나의 scope에 복사 생성자가 실행되고 있다. 저새히 보면 MyString 클래스를 대변하고 있는 hello 변수를 복사 생성자에 대입을 하고 있다. 이를 실행하면 당연히 이전에 hello가 갖고 있는 주소와 data가 그대로 복사되어 copy변수에 저장될 것이다. console의 출력과 동일한 결과를 갖는다.
근데 문제가 무엇이냐면 동적할당으로 new를 사용하였으면 소멸자의 delete를 사용하여 memory reak을 방지해야 한다. 소멸자가 발생 시점을 보면 함수가 끝나거나(내부의 프로그램이 종료될때)인데, 하나의 scope로 묶어 버렸고 scope를 나가게 되므로, 내부의 프로그램이 종료됐다고 판단하여 소멸자를 작동시킨다. 여기서 우리는 문제점을 찾을 수 있다.
console의 생성자와 복사 생성자의 출력을 보게 되면 주소와 data가 일치하는 것을 볼 수 있다. 복사를 했어도 주소와 데이터는 동일하다는 것이다. 근데 복사 생성자가 끝나는 시점에 소멸자가 발생하여 내부의 주소를 삭제시킨다. 어떻게 보면 정상작동 하는 것처럼 보이지만, 복사생성자의 주소와 생성자의 주소가 같기 때문에, 복사 생성자의 주소를 지우게 되면 생성자의 주소도 사라지게 되는 것이다. 따라서 console 출력에서 제대로 출력되지 않음을 볼 수 있다.
이를 포인터를 주소 값 자체만 복사 하는 것을 얕은 복사(shallow copy)라 한다.
그럼 깊은 복사(deep copy)는 무엇인가?
주소를 새로 할당하고 새로 할당 받은 주소에 data를 복사하고 있다. 이런 방법을 깊은 복사라 한다.
이를 대입 연산자 오버로딩으로 표현하면 다음과 같다.
대입 연산자 오버로딩을 보면 다음과 같다. 앞에서 배웠던 shallow copy 같은 경우에는 주석처리와 같이 작동을 한다. 이 또한 대입 연산자 오버로딩을 통해서 작동하는 코드이다.
다만 지금 우리가 눈여겨 봐야할 것은 깊은 복사이므로 다음 코드를 보자.
대입 연산자일 경우에는 자신이 자신한테 대입을 할 수 있으므로, 저와 같은 코드를 만들어 주는 것이 좋다. 이유는 굳이 할 필요가 없다. hello = hello
를 main문에 작성하여 대입하는 방식이 있는데, user가 보면 별 문제가 없지만 프로그램 상으론 문제가 생길 수 있기 때문에 조건문을 통해 문제를 방지하는 코드를 작성하는 버릇이 중요하다.
또한, 생성자일 경우에는 자신이 처음 생성되는 것이기 때문에 그 이전에 동적할당된 메모리를 갖고 있을리가 없다. 하지만, assignmetn operator이라면 메모리를 갖고 있을 수 있기 때문에, 새로운 메모리를 할당받는 방식이 좋다. delete를 사용하여 메모리를 지우고 할당받는 형식으로 코드를 작성한다.
다음과 같은 출력 결과를 얻을 수 있다.
그럼 언제 assignment operator가 호출이 되는가를 보면 다음과 같다. 위의 코드도 그렇지만, MyString str1 = hello;
같은 경우에는 assingment operator이 호출되지 않는다. 하지만, str2 = hello;
로 class정의를 하지 않고 대입만 있는 경우에는 assignment operator가 호출이 되어 작동하는 것을 볼 수 있다.
어떤 경우에는 복사 생성자를 별도로 구분할 여력이 없을 경우도 있다. 그럴 경우에는 shallow copy를 발생하는 것을 방지해야 하는 경우가 있기도 하다. 이는 차선책이므로 실제로 사용에는 기피하는 것이 좋다.
MyString(const MyString &source) = delete;
를 사용하여 애초에 복사 생성자를 사용할 수 없게 하는 방식도 있다.