본문 바로가기

Programming Study/C++

복사 생성자(Copy Constructor)

1. '복사 생성자'와의 첫 만남

C++ 스타일의 초기화

우리는 지금까지 다음과 같은 방식으로 변수와 참조자를 선언 및 초기화해 왔다.

int num=20;

int &ref = num; 

하지만 C++에서는 다음의 방식으로 선언 및 초기화가 가능하다.

int num(20);

int &ref(num); 

위의 두 가지 초기화 방식은 결과적으로 동일하다.

즉, C++에서는 위의 두 가지 초기화 방식을 동시에 지원하고 있다.

그렇다면 이야기를 객체의 생성으로 옮겨가 보자.

그리고 이를 위해서 다음과 같이 클래스를 하나 정의하겠다.

class SoSimple

{

private:

    int num1;

    int num2;

public:

    SoSimple(int n1, int n2) : num1(n1), num(n2)

    { }

     void ShowSimpleDate()

    {

         cout<<num1<<endl;

         cout<<num2<<endl;

    }

} ;

이어서 다음 코드의 실행결과를 예상해보자.

단순히 출력결과만 예상할 것이 아니라, 객체의 생성관계를 예측해보기 바란다.

int main(void)

{

    SoSimple sim1(15,20);

    SoSimple sim2 = sim1;       //???

    sim2.ShowSimpleData();

    return 0;

위의 코드 중에서 main 함수의 두 번째 문장에 해당하는 다음 문장은 객체의 생성 및 초기화를 연상시킨다.

SoSimple sim2=sim1; 

즉, sim2 객체를 새로 생성해서, 객체 sim1과 sim2간의 멤버 대 멤버 복사가 일어난다고 예상해 볼 수 있다.

그런데 실제로 그러한 일이 일어난다.

다음 그림에서 보이듯이 sim2 객체가 생성된 다음에, sim과 sim2간의 멤버 대 멤버 복사가 일어난다.

(참고로 아래 그림에서는 객체의 멤버변수만을 대상으로 객체를 표현해 놓았다.

따라서 위의 main 함수를 실제 실행해보면, num1과 num2에 저장된 값 15와 20이 출력된다.

그리고 다음 두 문장이 동일한 의미로 해석되듯이

int num1=num2;

int num1(num2); 

다음 두 문장도 동일한 의미로 해석이 된다.

SoSimple sim2 = sim1 ;

SoSimple sim2(sim1);

그런데 한가지 이상한 생각이 들지 않는가? C++의 모든 객체는 생성자의 호출을 동반한다고 했는데,

sim2의 생성자 호출에 대해서는 언급한 바가 없다.

과연 sim2는 어떠한 과정을 거쳐서 생성되는 것일까?

 

SoSimple sim2(sim1);

이 문장을 다시 한번 관찰하자. 그리고 생성자의 호출관점에서 이를 재분석해보자.

문장에 담겨진 내용을 정리하면 다음과 같다.

  - SoSimple형 객체를 생성해라.

  - 객체의 이름은 sim2로 정한다.

  - sim1을 인자로 받을 수 있는 생성자의 호출을 통해서 객체생성을 완료한다.

즉, 위의 객체생성문에서 호출하고자 하는 생성자는 다음과 같이 SoSimple 객체를 인자로 받을 수 있는 생성자이다.

SoSimple(SoSimple &copy)

{

    .....

그리고 다음의 문장도,

SoSimple sim2 = sim1; 

실은 다음의 형태로 묵시적 변환이 되어서(자동으로 변환이 되어서) 객체가 생성되는 것이다.

SoSimple sim2(sim1); 

그런데 앞서 정의한 SoSimple 클래스에는 이러한 유형의 생성자가 정의되어 있지 않았다.

이렇게 생성자가 삽입되지 않을 경우

자동으로 멤버 대 멤버의 복사를 진행하는 디폴트 복사 생성자가 삽입되는데 형태는 대충 이러하다.

SoSimple(const SoSimple &copy)
  : num1(copy.num1), num2(copy.num2) 

멤버 대 멤버의 복사에 사용되는 원본을 변경시키는 것은 복사의 개념을 무너뜨리는 행위가 되니,

키워드 const를 삽입해서 이러한 실수를 막아 놓는 것이 좋다.

 

멤버 대 멤버의 복사가 진행되는 디폴트 복사 생성자가 자동으로 삽입되니까, 굳이 복사 생성자를 직접 정의하지 않아도 된다.

그러나 반드시 복사 생성자를 정의해야 하는 경우도 있다는 것을 잊지 말자.

 

변환에 의한 초기화! 키워드 explicit으로 막을 수 있다!

복사 생성자에 대한 첫 소개를 끝내기에 앞서 키워드 explicit를 알아보자.

앞서 다음의 문장은,

SoSimple sim2 = sim1; 

다음의 형태로 묵시적 변환이 일어나서 복사 생성자가 호출된다고 설명하였다.

SoSimple sim2(sim1); 

이는 결국, 복사 생성자가 묵시적으로 호출된 것으로 불 수 있다.

따라서 위와 같은 유형의 반환이 마음에 들지 않는다면, 복사 생성자의 묵시적 호출을 허용하지 않으면 된다.

그리고 이러한 목적으로 사용되는 키워드가 explicit이다.

이 키워드를 사용하면 더 이상 묵시적 변환이 발생하지 않아서 대입 연산자를 이용한 객체의 생성 및 초기화는 불가능하다.

 

묵시적 변환이 좋은것만은 아니다.

자료형이든, 문장이든 자동으로 변환되는 것이 늘 반가운 것만은 아니다.

묵시적변환이 많이 발생하는 코드일수록 코드의 결과를 예측하기가 어려워지기 때문이다.

따라서 키워드 explicit는 코드의 명확함을 더하기 위해서 자주 사용되는 키워드 중 하나이다.

 

 

2. '깊은 복사(Deep copy)'와 '얕은 복사(Shallow copy)'

디폴트 복사 생성자는 멤버 대 멤버의 복사를 진행한다. 그리고 이러한 방식의 복사를 가리켜 '얕은 복사'라 하는데,

이는 멤버변수가 힙의 메모리 공간을 참조하는 경우에 문제가 된다.

이러한 문제를 해결 하기 위해 '깊은 복사'를 이용한다.

'깊은 복사'는 멤버 뿐만 아니라, 포인터로 참조하는 대상까지 깊게 복사한다는 뜻으로 정해진 이름이다.

 

 

3. 복사 생성자의 호출시점

복사생성자가 호출되는 시점은?

복사생성자가 호출되는 시점은 크게 세가지로 구분할 수 있다.

  - case 1: 기존에 생성된 객체를 이용해서 새로운 객체를 초기화하는 경우

  - case 2: Call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우

  - case 3: 객체를 반환하되, 참조형으로 반환하지 않는 경우

이들은 모두 다음의 공통점을 지닌다.

"객체를 새로 생성해야 한다. 단, 생성과 동시에 동일한 자료형의 객체로 초기화해야 한다.!"

 

 

 

- 윤성우 저, 열혈강의 C++ 프로그래밍 中 -

 

'Programming Study > C++' 카테고리의 다른 글

상속(Inheritance)의 이해  (0) 2014.09.27
friend와 static 그리고 const  (0) 2014.09.26
클래스의 완성  (0) 2014.09.24
클래스의 기본  (0) 2014.09.23
C언어 기반의 C++_2  (0) 2014.09.22