프로그래밍/cpp

복사 생성자의 인수는 왜 레퍼런스를 사용하나?

콘파냐 2015. 10. 5. 16:25

복사 생성자는 클래스의 객체를 생성할 때 이미 존재하는 동일 클래스의 다른 객체를 그대로 복사하기 위해 필요하다. 클래스는 일종의 데이터 집합이다. 이 데이터를 다루는 도구인 멤버함수는 동일 클래스의 객체라면 공유를 하는 개념이기 때문에 객체의 복사에 있어서는 데이터(멤버변수)의 복사만 생각하면 된다. 왜냐면 데이터의 경우는 객체마다 고유하기 때문이고, 이를 공유하면 문제가 발생하기 때문이다.(예를 들어 특정 객체의 파괴 시 데이터를 공유할 경우 문제 발생). 따라서 C++문법에서 복사 생성자를 다룰 때 항상 따라다니는 문제인 얕은 복사와 깊은 복사에 관한 이야기가 중요한 문법적 주제가 된다. 초심자에게는 포인터와 메모리에 대한 개념이 확실히 이해되고 난 후에 이해할 수 있는 난코스라고 생각하면 된다. 이번 글은 복사 생성자의 인수가 왜 꼭 레퍼런스여야 하는지에 대해 분석한 글이다. 위 내용을 다 이해하지 못한다면 다음을 참고하길 바란다.

2014/03/18 - [프로그래밍/c++] - 복사 생성자 - 얕은복사 깊은복사 C++

 

레퍼런스의 특징

C언어를 처음 배울 때 레퍼런스를 많이 무시했었다. 그만큼 포인터를 공부하는데 애를 먹었고, 레퍼런스를 사용하지 않고도 포인터로 해결볼 수 있기 때문이다. (사실 포인터는 알고 보면 어려운 내용이 아니다. 그만큼 포인터를 제대로 설명한 책을 찾기 힘들기도 하고, 하도 다들 어렵다고 하니, 지레 어렵다고 겁먹고 들어가기 때문이라고 생각한다.) 아무튼 한 때 포인터를 어렵게 익혔으니 레퍼런스 같은 녀석을 쓰지말자 라는 포인터 우월주의에 빠졌었다.

그런데 C++의 연산자 오버로딩이나 C++11에서 등장한 우측값 참조 등의 새로운 문법들이 나오면서 레퍼런스가 편의적인 면(가독성)에서도 좋고 새로운 문법적인 측면에서 중요하다고 새삼 깨달았다.

아무튼 레퍼런스를 사용하면 포인터 보다 깔끔하고 가독성 있게 코딩을 할 수 있다. 우선 이런 특징 때문에 복사 생성자의 인수는 포인터 대신 레퍼런스를 사용한다.(포인터를 사용할 수도 있다는 것만 기억해 두자.)

 

우선 다음 코드를 보자

3개의 생성자가 있다. 디폴트 생성자, 복사 생성자, 그리고 int 형 인수를 갖는 생성자.

여기서 복사 생성자의 형식인수는 레퍼런스 타입이다. 한번 이와 비슷한 간단한 코드를 만들고 복사생성자의 &를 없애보고 컴파일을 해보길 바란다.

1> c:\visual studio 2015\projects\mytest\mytest\main.cpp(63): note: 복사 생성자가 모호하거나 사용할 수 있는 복사 생성자가 없어서 class 'myTest'을(를) 복사 생성할 수 없습니다.

(친절하게도 비주얼 스튜디오에서는 에러메세지가 한글이다.)

아무튼 이런 에러가 발생은 했지만 왜? 그런지에 대해서 연구를 해보자. 다음은 복사 생성자의 인수에 &를 없앤 경우다.

myTest c = a+c; 는 사실 myTest c(a+b); 와 동일하다. 복사 생성자가 호출되며 a+b는 실인수가 되고 클래스 내부의 형식인수는 const myTest other 가 된다. 즉 형식인수 other에 실인수 a+b가 대입되는데 other = a+b라는 동작이 내부적으로 이루어진다.

other는 myTest클래스의 객체고 a+b또한 연산자 오버로딩을 통해 myTest타입을 반환한다.

즉 내부적으로 동작하는 코드 const myTest other = a+b; 는 또다시 복사생성자를 호출 할 수 밖에 없다.

다시 const myTest other(a+b); //결국 이 동작은 끝이 없게 이루어진다.

따라서 형식인수를 레퍼런스로 선언해서 해결하는 것이다.

 

좀 헷갈릴 것 같아서 비교 예를 들어보겠다.

연산자 오버로딩의 인수에 사용하는 &

이 경우는 좀 다르다. 위에 코드에서 복사 생성자의 경우는 어쩔 수 없이 &를 사용해야 하지만 연산자 오버로딩의 경우는 사용하지 않아도 상관없다. 즉 사용하지 않아도 무한 호출이 일어나지 않는다는 말인데, 한번 살펴보면..

내부적으로 a.operator+(b); 가 동작하는데

const myTest other = b;//형식인수에 실인수 b가 대입된다. 이 표현은 실제로

const myTest other(b); 라는 뜻이고 복사 생성자가 호출된다.

따라서, other라는 객체(+연산자의 형식인수)는 복사 생성자를 호출하고 이는 다음과 같다.

const myTest& other = b;가 호출된다. other가 레퍼런스변수 이므로 여기서 무한 호출은 이뤄지지 않는다.

즉, 복사 생성자를 제대로 정의했다면 문제는 없다.

기본적으로 보통 복사생성자를 정의하지 않는다면 아무런 문제가 없이 동작하지만, 복사 생성자를 새로 정의 하는 이유는 얕은 복사와 깊은 복사 문제로 돌아가므로 얕은복사와 깊은복사 파트를 살펴보면 된다.

반응형