안녕하세요 귀신입니다.
군대갔다오고 2년만에 대학을 다니다보니 정신없이
시간이 흘러가고 있는 것 같습니다.
그래도 오랜만에 학교 생활 하니까 기분은 좋네요 ㅎㅎ
오늘은 클래스를 공부할 때 시험문제 단골인 복사생성자를 다뤄보려 합니다.
사실 개념 자체가 어렵진않습니다.
그럼 바로 시작해 보겠습니다.
1. 복사생성자
복사생성자는 말그대로 어떠한 객체를 복사해서 생성한다는 뜻입니다. 21번 라인을 보면 CTest의 인스턴스 A를 B가 그대로 복사하는 형태를 볼 수 있습니다. 저렇게 객체를 생성하면 어떤 일이 벌어질까요? 클래스의 메서드로 printData()함수를 생성했습니다. 멤버변수 nData를 출력하는 메서드인데요. A와 B에서 각각 호출하면 놀랍게도 멤버변수 nData의 값이 10으로 동일하다는 것을 알 수 있습니다.
클래스 자체에서 기본 복사생성자를 호출하기 때문인데요. 쉽게 말해 복사생성은 멤버변수의 값을 그대로 대입하는 것이라고 생각하면 됩니다.
20번 라인을 보면 매개변수로 CTest의 참조자를 매개변수로 받는 생성자가 바로 복사생성자 입니다. 저렇게 생긴 것이 사실상 복사생성자를 따로 기술하지 않아도 자동으로 불리는 복사 생성자 입니다. 그저 멤버변수를 대입하는 것이 다입니다. 여기서 꼭 짚고 넘어가야할 것이 바로 매개변수 형태입니다. const CTest &rhs 우선 왜 const키워드를 붙였을까요? 복사를 하려하는데 원본이 훼손되는 일을 방지하기 위해서 입니다. 복사를 하려했는데 원본이 망가져버리면 큰일이니까요. 그리고 참조자를 사용합니다. 참조자를 사용하는 이유는 굉장히 중요한데요. 우선 기본적으로 포인터 변수를 아시는 분들은 이해가 편하실 겁니다. 포인터 변수는 그저 메모리만 담고 있기 때문에 변수의 크기가 그냥 클래스를 넘기는 것보다 작습니다. 즉 메모리 효율을 올릴 수 있습니다. 주소의 크기는 그저 int변수 하나의 크기이기 때문에 클래스 하나와는 비교과 안될정도로 적은 메모리를 사용하는 것이죠. 또 중요한 것이 있습니다. CTest &rhs말고 CTest rhs라고 선언하게 되면 함수 내에 새로운 객체 rhs를 복사생성으로 하나 더 만드는 꼴이 됩니다. 무슨말이냐면 참조로 넘기면 그저 주소를 넘겨 새로운 객체를 생성하지 않고도 객체를 이용할 수 있는데 CTest rhs라고 매개변수를 기술해버리면 rhs라는 이름의 객체하나를 생성하게 됩니다. 이렇게 되면 클래스가 하나 더 만들어져서 메모리 낭비가 발생하게 되죠. 그러니 꼭 객체를 매개변수로 넘길 때는 참조자로 넘기시기 바랍니다.
그렇다면 이제 얕은 복사에 대해 알아봅시다.
2. 얕은 복사
얕은 복사는 기본적으로 멤버 변수에 포인터가 있을 때 사용하는 용어입니다. 위를 보면 또 다시 A와 A를 복사한 B가 있고, 두 객체의 멤버변수 값이 모두 똑같은 걸 알 수 있습니다. 멤버 변수끼리 단순 대입이 되기 때문이죠. 이 단순히 대입만 한다는 행위에서 눈치채셨을 분도 있는데 현재 객체 A의 멤버 포인터 p와 B의 p 모두 똑같은 주소를 가리키고 있습니다. 이것이 얕은 복사인데요. 제가 직접 기본적으로 불렸을 복사생성자를 기술해 보겠습니다.
위의 코드에서 불렸을 복사생성자의 모습입니다. 중요한 것이 24번 라인입니다. 복사생성자는 그저 멤버변수끼리 단순하게 대입을 진행하므로, 멤버변수 p에 그냥 원본 p가 가리키는 주솟값을 그대로 전달합니다. 따라서 두개의 포인터가 한 곳을 가리키게 되는 것이죠. 이를 얕은 복사라 합니다. 하나의 주소를 여러개가 가리키는 것을 얕은 복사라고 부릅니다. 즉 원본은 하나, 사본이 여러개인 것이죠. 그러나 이러한 얕은 복사는 치명적인 문제를 일으킬 수 있습니다. 한번 살펴볼까요?
3. 얕은 복사의 문제점
얕은 복사의 문제점은 멤버 포인터 변수를 동적할당 했을 때 발생합니다. CTest의 생성자를 보면 포인터 멤버변수 p를 동적할당 한 후 매개변수로 값을 넣습니다. 이 때 소멸자는 할당한 메모리를 해제합니다. 이 메모리 해제에서 에러가 발생하게 됩니다. 이유가 무엇일까요?
위의 코드에서 자동으로 호출된 복사생성자는 아마 위와 같을 것입니다. 그저 멤버변수를 대입하는게 다인 복사생성자이므로 멤버변수 p에 복사하고자 하는 원본 p를 대입합니다. 여기서 아주 치명적인 문제가 발생하게 되는데요. 두 개의 포인터가 서로 하나의 주소를 가리키게 되기 때문입니다. 이것이 왜 문제냐면, 하나의 포인터를 두 번 해제하기 때문입니다. 객체 A와 B의 p변수 모두 동일한 주소를 가리키고 있는데, 소멸자는 A, B 모두 불리게 되고, 한번 해제된 메모리를 또 한번 해제하므로 치명적인 에러가 발생하는 것입니다. 이는 원본하나에 사본을 여러개 만든 얕은 복사의 단점이라고 볼 수 있습니다. 이럴 때, 원본, 즉 주소를 새로 만들고 값만 복사하는 형식이 바로 깊은 복사입니다. 살펴봅시다.
4. 깊은 복사
위와 같은 복사생성이 깊은 복사 입니다. 단순히 p변수를 대입한 것과 달이 똑같이 메모리 할당을 하여 새로운 메모리 영역을 할당한 후 원본의 p변수의 값만 따로 대입해주는 것입니다. 이렇게 하면 소멸자가 두번 불려도 각각 다른 메모리를 가리키고 있으므로 아무런 문제가 없습니다. 이처럼 멤버 포인터 변수를 동적할당해야 한다면, 메모리 해제 오류를 피하기 위해 꼭 깊은 복사를 기술 하셔야 합니다.
5. 마치며
오늘 간단하게 복사생성자와 깊은복사 얕은 복사의 개념에 대해 배웠습니다. 개념이 어렵진 않습니다. 그냥 그렇구나 하면서 미래에 에러를 없애기 위한 하나의 수단으로 꼭 알고가시면 되겠습니다. 읽어주셔서 감사합니다. 그럼 이만
참고자료
https://youtube.com/playlist?list=PLXvgR_grOs1DFOWF65X0Zqnd_264x41u-
'C++' 카테고리의 다른 글
C++ 세번째, 생성자와 소멸자 그리고 this 포인터 (0) | 2022.03.08 |
---|---|
C++ 두번째, 클래스(Class) 기본 개념과 접근 제어 지시자 (0) | 2022.02.23 |
C++ 첫번째, 간단하게 보는 C와의 차이점 (0) | 2022.02.12 |