[C++] 6.복사 생성자


복사 생성자



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

int num=20;
int &ref=num;



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


int num(20);
int &ref(num);



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

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


//클래스 example 정의

class example
{
private:
	int num1;
	int num2;
public:
	example(int n1,int n2) :num1(n1),num2(n2){}
	...
};

//다음과 같은 코드는 무엇을 의미할까요??

int main(void)
{
	example ex1(2,3);
	example ex2=ex1; //어떠한 형태의 대입연산이 진행될까요?
	
	return 0;
}



위의 대입연산은 객체 ex1과 ex2간의 멤버 대 멤버 복사가 일어납니다~

따라서 ex2의 멤버 변수 num1과 num2에 각각 2와 3이 저장됩니다


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

int num1=num2;
int num1(num2);

//다음 두 문장도 동일한 의미로 해석됩니다.

example ex2=ex1;
example ex2(ex1);



C++의 모든 객체는 생성자의 호출을 동반한다고 했는데, 그렇다면,

객체 ex2는 어떤 생성자를 호출하면서 생성되는 것일까요?

바로 ‘복사 생성자’ 입니다.


//복사 생성자
example(example &copy)
	:num1(copy.num1),num2(copy.num2){}



복사 생성자는 객체 간의 복사가 이루어질 때 호출되는 생성자인데,

example ex2=ex1;



위의 문장은 다음과 같이 묵시적으로 변환되고(자동으로 변환)

example ex2(ex1);



ex2는 복사 생성자를 호출하여 생성됩니다.



복사 생성자가 정의가 되어있지 않으면 다음과 같은 디폴트 생성자가 자동으로 호출됩니다.


//디폴트 복사 생성자

example(const example &copy)	:num1(copy.num1),num2(copy.num2){};



멤버 대 멤버의 복사에 사용되는 원본을 변경시키면 안되기에 키워드 const를 삽입하여,

실수를 막아놓아야합니다.



디폴트 복사 생성자가 자동으로 삽입되기에,

굳이 복사 생성자를 직접 정의할 필요는 없다고 생각할지도 모르지만,

반드시 복사 생성자를 정의해야 하는 경우도 있습니다.

‘깊은 복사’를 해야할 때인데요.. 조금 뒤에 알아보겠습니다.



키워드 explicit



다음 문장은,

example ex2=ex1;



다음과 같이 묵시적 변환이 일어나서 복사 생성자가 호출된다고 설명하였습니다.

example ex2(ex1);



이런 복사 생성자의 묵시적 호출을 허용하고 싶지 않다면,

키워드 ‘explicit’을 이용하면 됩니다~~

//복사 생성자 explicit이용해서 새로 정의


explicit example(const example &copy)
	:num1(copy.num1),num2(copy.num2){}



그러면 다음과 같은 대입 연산자를 이용한 객체의 생성 및 초기화는 불가능합니다.

example ex2=ex1; (X)


example ex2(ex1); (O)



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

explicit 은 코드의 명확함을 더하기 위해서 자주 사용되는 키워드입니다!



‘깊은 복사’와 ‘얕은 복사’



디폴트 복사 생성자는 멤버 대 멤버의 복사를 진행합니다.

그리고 이러한 방식의 복사를 가리켜 ‘얕은 복사’라고 합니다~

하지만 이는 멤버변수가 힙의 메모리 공간을 참조하는 경우에 문제가 됩니다!

예를 들어 다음 예제를 볼까요??

class example
{
private:
	char *name; //멤버 변수가 힙 영역의 메모리 공간 참조
public:
	example(char *n) //name 동적할당
	{
		int len=strlen(n)+1;
		name=new char[len];
		strcpy(name,n);
	}
	...
};


int main(void)
{
	example ex1("꾸리");
	example ex2=ex1; //디폴트 복사 생성자 호출
	
	return 0;
}



위와 같은 복사는 문제가 있습니다.

디폴트 복사 생성자는 멤버 대 멤버 간의 복사가 이루어지는데,

그 때문에 ex2의 name이 ex1의 name을 가리키게 됩니다.

즉, ex1과 ex2의 name이 공유되고 있는거죠~

우리가 원하는 것은 ex1의 값을 그대로 ex2에 복사하되

ex1과 ex2의 name이 서로 다른 곳을 참조하게 하는 것입니다.

이 때 ‘깊은 복사’가 필요합니다~

깊은 복사란 멤버뿐만 아니라, 포인트로 참조하는 대상까지 깊게 복사한다는 뜻입니다.

깊은 복사가 이뤄지도록 복사 생성자를 새로 정의해보겠습니다.


example(const example& copy) //복사 생성자를 통한 동적할당
{
	name=new char[strlen(copy.name)+1];
	strcpy(name,copy.name);
}



깊은 복사를 하고싶을 때 직접 복사 생성자를 정의해야하므로,

복사 생성자를 제대로 알고 계셔야합니다~



복사 생성자의 호출시점



이제 우리는 클래스 별로 필요한 복사 생성자를 정의할 수 있게 되었습니다.

그럼 복사 생성자가 호출되는 세 가지 시점에 대해 이야기해보겠습니다.

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

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

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

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



세 가지 경우에 각각 언제 복사 생성자가 호출되는 지 살펴보겠습니다.


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



class example
{
private:
	int num;
public:
	example(int n) :num(n){}
	...
};

int main(void)
{
	example ex1(3);
	example ex2=ex1;
	
	return 0;
}



//먼저 ex2 객체가 생성되면서 ex2를 위한 메모리 공간이 할당되고,

//동시에 객체 ex2의 디폴트 복사 생성자가 호출되어 ex1값으로 초기화가 이루어집니다.




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



class example{...};

example function(example ex) //객체를 인자로 받는 함수
{
	....
}

int main(void)
{
	example ex1(3);
	function(ex1); //객체 ex1을 인자로 전달
	
	return 0;
}



함수 function이 호출되는 순간, 매개변수로 선언된 ex 객체가 생성되고,

동시에 ex의 디폴트 복사 생성자가 호출되어 ex1값으로 초기화가 이루어집니다.

여기서 함수 functuin의 호출이 끝나면 객체 ex도 소멸됩니다~




//3. 객체를 반환하되, 참조형으로 반환하지 않는 경우



class example {...};

example function(example ex)
{
	...
	return ex;
}

int main(void)
{
	example ex1;
	example &ref=function(ex1);
	
	return 0;
}



일단, 2번과 같이 함수 function의 매개변수 ex의 객체가 생성되고,

동시에 ex의 디폴트 복사 생성자가 호출되어 ex1값으로 초기화가 됩니다.

그리고 return 문이 실행되는 순간, 임시객체가 생성되고,

임시객체의 디폴트 복사 생성자가 호출되어 ex값으로 초기화 됩니다.

함수 function()의 호출이 종료되었으므로 객체 ex는 소멸되고,

참조자 ref는 임시객체를 참조하게됩니다.



위의 예제에서 임시객체라는 것이 등장하는데요~

임시변수처럼 임시로 생성되었다가 소멸되는 객체입니다~

임시변수에 대해 좀 더 알아볼까요??


class example
{
private:
	int num;
public:
	example(int n) :num(n){}
	example & Add(int n)
	{
		num+=n;
		return *this;
	}
};

example function(example ex)
{
	return ex;
}

int main(void)
{
	example ex1(3);
	cout<<function(ex1).Add(3)->num<<endl; //(1)
	cout<<ex1.num<<endl; //(2)
	

	return 0;
}



위의 예제에서 함수 function(ex1)에 의해 임시객체가 생성되고,

임시객체의 Add(3) 함수를 호출합니다.

따라서 (1)의 결과값은 임시객체의 num인 6이 출력됩니다.

반면 (2)의 결과값은 ex1의 num인 3입니다.

그리고 (1)문장이 끝나면 임시객체는 소멸됩니다~



이와 같이 임시객체는 다음 행으로 넘어가면 바로 소멸되어 버립니다.

하지만 참조자에 참조되는 임시객체는 바로 소멸되지 않습니다~


example &ref=function(ex1); //임시객체가 참조되고 있으므로 바로 소멸되지 않습니다.