[05-3] 복사 생성자의 호출시점

김민성·2022년 7월 21일
post-thumbnail

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

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

이들은 모두 다음의 공통점을 가진다.

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

메모리 공간의 할당과 초기화가 동시에 일어나는 상황!

복사 생성자의 호출시기를 논하기에 앞서, 먼저 메모리 공간이 할당과 동시에 초기화되는 상황을 나열해보자. 가장 대표적인 예는 다음과 같다.

int num1=num2;

이는 num1이라는 이름의 메모리 공간을 할당과 동시에 num2에 저장된 값으로 초기화시키는 문장이다.

즉, 할당과 동시에 초기화가 이뤄진다. 그리고 다음의 경우에서도 할당과 동시에 초기화가 이뤄진다.

int SimpleFunc(int n)
{
. . . . 
}
int main(void)
{
	int num=10;
    SimpleFunc(num);	
    //호출되는 순간 매개변수 n이 할당과 동시에 초기화!
    . . . .
}

위의 코드에서 SimpleFunc 함수가 호출되는 순간에 매개변수 n이 할당과 동시에 변수 num에 저장되어 있는 값으로 초기화된다. 이렇듯 매개변수도 함수가 호출되는 순간에 할당되므로, 이 상황도 메모리 공간의 할당과 초기화가 동시에 일어나는 상황이다.

결론적으로 함수가 값을 반환하면, 별도의 메모리 공간이 할당되고, 이 공간에 반환 값이 저장된다(반환 값으로 초기화된다).


위의 코드에서 SimpleFuncObj 함수가 호출되는 순간, 매개변수로 선언된 ob객체가 생성되고(ob를 위한 메모리 공간이 할당되고), 이는 인자로 전달된 obj객체로 초기화된다. 즉, 메모리 공간이 할당되면서 동시에 초기화 된다.

할당 이후, 복사 생성자를 통한 초기화

앞서 객체가 생성 및 초기화되는 경우에 대해 정리했는데, 그렇다면 이 때 초기화는 어떻게 이뤄지겠는가? 일단 상황적으로 판단해보면, 초기화는 멤버 대 멤버가 복사되는 형태로 이뤄져야 한다.
그래서 '복사 생성자의 호출' 방식으로 초기화를 진행한다. 예제를 통해 이해해보자.

PassObjCopycon.cpp

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{  }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout<<"Called SoSimple(const SoSimple& copy)"<<endl;
	}
	void ShowData()
	{
		cout<<"num: "<<num<<endl;
	}
};
	
void SimpleFuncObj(SoSimple ob)
{
	ob.ShowData();
}

int main(void)
{
	SoSimple obj(7);
	cout<<"함수호출 전"<<endl;
	SimpleFuncObj(obj);
	cout<<"함수호출 후"<<endl;
	return 0;
}
함수호출 전
Called SoSimple(const SoSimple& copy)
num: 7
함수호출 후

우선 실행결과를 통해 함수에 인자를 전달하는 과정에서 복사 생성자가 호출됨은 확인됐다. 그리고 이로 인해 멤버변수 num에 저장된 값이 복사 됨을 확인했다.

복수 생성자의 호출주체가 누군지 확인해보자.

위 그림에서 보이듯이 초기화의 대상은 obj 객체가 아닌, ob 객체이다. 그리고 ob 객체는 obj 객체로 초기화된다. 따라서 ob 객체의 복사 생성자가 호출되면서, obj 객체가 인자로 전달되어야 한다.

다음 예제를 통해서 복사생성자가 호출되는 세 번째 경우에 대해 알아보자.

ReturnObjCopycon.cpp

#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{ }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout<<"Called SoSimple(const SoSimple& copy)"<<endl;
	}
	SoSimple& AddNum(int n)
	{
		num+=n;
		return *this;
	}
	void ShowData()
	{
		cout<<"num: "<<num<<endl;
	}
};

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout<<"return 이전"<<endl;
	return ob;
}

int main(void)
{
	SoSimple obj(7);
	SimpleFuncObj(obj).AddNum(30).ShowData();
	obj.ShowData();
	return 0;
}
Called SoSimple(const SoSimple& copy)
return 이전
Called SoSimple(const SoSimple& copy)
num: 37
num: 7


위의 그림을 보면 SimpleFuncObj라는 함수를 호출한 영역으로 ob라는 객체를 복사해서 넘긴다.


이 상황에서 매개변수로 선언된 객체 ob를 반환해야 한다.

위 그림에서 보이듯이 객체를 반환하게 되면, '임시객체'라는 것이 생성되고, 이 객체의 복사 생성자가 호출되면서 return문에 명시된 객체가 인자로 전달된다. 즉, 최종적으로 반환되는 객체는 새롭게 생성되는 임시객체이다. 따라서 함수호출이 완료되고 나면, 지역적으로 선언된 객체 ob는 소멸되고 obj 객체의 임시객체만 남는다. 다음 예제의 일부분을 살펴보자.

SimpleFuncObj(obj).AddNum(30).ShowData();

AddNum 함수의 호출로 인해, 임시객체에 저장된 값이 30 증가한다. 이어서 ShowData 함수호출을 통해 임시객체에 저장된 값을 출력하고 있다. 그리고 이때 출력된 값은, 위 코드의 다음 행과는 다르다. ShowData 함수의 호출대상인 두 객체가 서로 별개이기 때문에 이는 당연한 결과이다.

반환할 때 만들어진 객체는 언제 사라져요?

임시객체도 임시변수와 마찬가지로 임시로 생성되었다가 소멸되는 객체이다. 그리고 임시객체는 우리가 임의로 만들 수도 있다. 다음 예제를 통해 임시객체의 실체를 이해해보자.

IKnowTempObj.cpp

#include <iostream>
using namespace std;

class Temporary
{
private:
	int num;
public:
	Temporary(int n) : num(n)
	{
		cout<<"create obj: "<<num<<endl;
	}
	~Temporary()
	{
		cout<<"destroy obj: "<<num<<endl;  
	}
	void ShowTempInfo()
	{
		cout<<"My num is "<<num<<endl;
	}
};

int main(void)
{
	Temporary(100);
	cout<<"********** after make!"<<endl<<endl;

	Temporary(200).ShowTempInfo();
	cout<<"********** after make!"<<endl<<endl;

	const Temporary &ref=Temporary(300);
	cout<<"********** end of main!"<<endl<<endl;
	return 0;
}
create obj: 100
destroy obj: 100
********** after make!

create obj: 200
My num is 200
destroy obj: 200
********** after make!

create obj: 300
********** end of main!

destroy obj: 300

클래스 외부에서 객체의 맴버함수를 호출하기 위해 필요한 것은 다음 세 가지 중 하나이다.

  • 객체에 붙여진 이름
  • 객체의 참조 값(객체 참조에 사용되는 정보)
  • 객체의 주소 값

위의 코드를 분석해보면,

Temporary(100);

100으로 초기화된 Temporary임시객체 생성!

Temporary(200).ShowTempInfo();

이는 임시객체여서 다음 행으로 넘어가면 소멸된다.
임시객체가 생성된 위치에는 임시객체의 참조 값이 반환된다. 위 문장의 경우 먼저 임시객체가 생성되면서 다음의 형태가 된다.

(임시객체의 참조 값).ShowTempInfo();

그래서 이어서 멤버함수의 호출이 가능한 것이다. 또한 이렇듯 '참조 값'이 반환되기 때문에 다음과 같은 문장의 구성도 가능한 것이다.

const Temporary &ref=Temporary(300);

임시객체 생성시 반환되는 '참조 값'이 참조자 ref에 전달되어, ref가 임시객체를 참조하게 된다.

참조자에 참조되는 임시객체는 바로 소멸되지 않는다.

결론을 내리자면,

  • 임시객체는 다음 행으로 넘어가면 바로 소멸되어 버린다.
  • 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
profile
다양한 활동을 통해 인사이트를 얻는 것을 즐깁니다. 저 또한 인사이트를 주는 사람이 되고자 합니다.

0개의 댓글