열한번째 열혈강의
오늘은 복사생성자에 대해 알아볼 시간이다. 복사생성자를 한 파트 자체로 때서 설명하고 있다. 그만큼 복사생성자에 대해 헤깔려 하는 사람이 많다는 뜻이다. 한번 제대로 알아 보도록 하자.
복사생성자
물론 겁부터 먹을 필요는 없다. 생소한 개념일지도 모르지만 생성자의 한 형태라고 쉽게 접근하자.
우리가 앞서 변수와 참조자를 선언, 초기화 해온 방식은 이렇다.
int num = 20;
int $ref = num;
그러나 C++에서는 다음 방식으로 선언 및 초기화도 가능하다.
int num(20)
int &ref(num)
이러한 방식은 결과적으로 동일하다. 즉, C++은 두가지 방식으로 초기화를 지원하고 있다. 그럼 객체의 생성으로 화두를 옮겨 보자.
위와 같은 클래스가 있다.
그리고 다음 코드 실행결과를 예상해보자. 단순 출력이 아니라 객체 생성관계를 예측해보자.
여기서 두번째 문장은 객체 생성 및 초기화를 연상할 수 있다.
SoSimple sim2 = sim1;
즉, sim2 객체를 새로 생성해서, 객체 sim1과 sim2간의 멤버 대 멤버 복사가 일어난다고 예상해 볼 수 있다. 그런데 실제로 그런 일이 일어난다.
객체의 멤버변수만을 대상으로 표현해 보면,
sim2의 객체에서 num1은 sim1 객체의 num1을 복사하고, num2 역시 같다.
따라서 위의 main 함수를 실제 실행해보면 num1과 num2에 저장된 값 15와 20이 출력된다. 그리고 다음 두 문장이 동일한 의미로 해석되듯이,
int num1 = num2;
int num1(num2);
다음 두 문장도 동일한 의미로 해석이 된다.
SoSimple sim2 = sim1;
SoSimple sim2(sim1);
근데 한가지 이상한 생각이 들어야 한다.
C++의 모든 객체는 생성자의 호출을 동반한다고 했는데, sim2의 생성자 호출에 대해서는 언급한 바가 없다. 그럼 어떤 과정을 거치게 되는 것일까?
SoSimple sim2(sim1);
위 소제목을 다시 한번 보도록 하자.
이 문장에 담겨있는 내용을 정리하면
SoSimple 형 객체를 생성하라
객체의 이름은 sim2로 정한다
sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체생성을 완료한다
위 내용과 관련해서는 이미 인지하고 있다. 즉, 위의 객체생성문에서 호출하고자 하는 생성자는 다음과 같은 생성자이다.
그리고 다음의 문장도
SoSimple sim2 = sim1;
실은 다음의 형태를 묵시적 변환이 되어서(자동 변환) 객체가 생성되는 것이다.
SoSimple sim2(sim1);
그런데 앞서 정의한 SoSimple 클래스는 이런 생성자는 없었다. 그럼 앞서 제시한 클래스와 main 함수는 제대로 컴파일되지 않는, 문제 있는 코드가 아닐까? 이에 대해 이해를 위해 예제를 관찰할 것이다. 그리고 나서 디폴트 복사 생성자에 대한 이해를 해보자.
위 예제는
SoSimple sim2 = sim1;
부분을 제외하고는 특별할 게 없다. 특히 copy하는 생성자 역시 이해할 수 있다. 그런데 이런 생성자를 별도로 복사 생성자(copy constructor) 라고 부른다. 이는 생성자의 정의형태가 독특해서 붙은 이름이 아니다. 다만 이 생성자가 호출되는 시점이 다른 일반 생성자와 차이가 있기 때문에 붙은 것이다. 즉, 복사 생성자를 정확하기 이해하기 위해서는 복사 생성자의 호출시점을 확실히 이해해야 한다.
그럼 좀더 이쁜 정의를 보자.
멤버 대 멤버의 복사에 사용되는 원본을 변경시키는 것은 복사의 개념을 무너뜨리는 행위가 되니 (실수가 되니) const를 해당 사항을 막아두도록 하자.
자동 삽입되는 디폴트 복사 생성자
앞서 우리는 복사 생성자의 삽입 없이도 멤버 대 멤버의 복사가 이루어 진다는 걸 알았다. 그렇다면 어떻게 가능한 것일까?
복사 생성자를 정의하지 않으면, 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 자동으로 삽입된다.
위의 문장이 이야기하듯이 생성자가 존재하더라도, 복사 생성자가 정의되어 있지 않으면 디폴트 복사 생성자라는 것이 삽입되어 멤버 대 멤버의 복사를 진행한다.
그럼 다음과 같이 생각될 수 있다.
자동 삽입이 되니 굳이 복사 생성자를 직접 정의하지 않아도 되지 않을까?
대부분의 경우는 그러하다. 그러나 반드시 복사 생성자를 재정의해야 하는 경우도 있다.
변환에 의한 초기화, explicit
SoSimple sim2 = sim1;
SoSimpe sim2(sim1);
위 두가지는 복사 생성자가 묵시적으로 호출된 것이라고 볼 수 있다. 따라서 이런 변환이 마음에 안든다면 복사 생성자의 묵시적 호출을 허용하지 않으면 된다. 그리고 이런 목적으로 사용되는 것이 explicit 이다.
explicit SoSimple(const SoSimple ©) : num1(copy.num1), num2(copy.num2){}
더 이상 묵시적 변환이 발생하지 않아서 대입 연산자를 이용한 객체의 생성 및 초기화는 불가능해졌다.
참고로 묵시적 변환이 좋은 것만은 아니다.
자료형이든, 문장이든 자동으로 변환되는 것이 늘 반가운 것만은 아니다. 묵시적 변환이 많이 발생하는 코드일수록 코드의 결과를 에측하기가 어려워지기 때문이다. 따라서 키워트 explicit는 코드의 명확함을 더하기 위해서 자주 사용되는 키워드 중 하나이다.
그리고 이런 묵시적 변환은 복사 생성자에서만 일어나는 것이 아니다. 파라미터가 하나인 생성자가 있다면 이 역시 묵시적 변환이 발생한다. 다음 클래스를 보자
다음의 문장을 통해서 객체 생성이 가능하다.
AAA obj = 3; // AAA obj(3);
그리고 이 경우에도 마찬가지로 키워드 explicit가 생성자에 선언되면, 묵시적 변환을 허용하지 않게 된다.
그전에 앞서 예제에서도 봤지만 복사생성자의 매개변수 선언에 const는 필수가 아니다. 그러나 참조형 선언을 의미하는 &는 반드시 삽입해야 한다. 그 이유는 아직 이해하지 못할 것이다. 그러나 후에 복사 생성자의 전체적인 내용을 이해하고 정확히 이해한다면 이유를 알 수 있을 것이다. 결론만 말하자면 &가 없다면 복사 생성자 호출은 무한루프에 빠져버린다. 다행히 C++은 선언이 없다면 컴파일 에러를 발생시킨다.
그럼 여기서 기본적인 설명을 마무리 하고 복사 생성자의 별도 정의에 대해서는 다음 포스팅에서 이어서 하도록 하겠다.