초기화
- 앞선 참조자와 생성자의 내용을 복습해본다면, C++는 다음과 같은 초기화를 지원한다는 것을 할 수 있다.
- int num = 20 -> int num(20)
- int &ref = num -> int &ref(num)
- 즉, 이러한 초기화를 응용해본다면, class 선언 시 class의 객체 또한 복사가 가능하다는 점을 유추할 수 있다.
int main(void)
{
SoSimple sim1(10, 100);
SoSimple sim2 = sim1;
SoSimple sim3(sim1);
}
- 이러한 별도의 생성자가 존재하는 것이 아닌, 객체를 복제하는 경우의 생성자를 '복사 생성자'라고 부른다. 또한 앞선 생성자와 마찬가지로, 복사 생성자를 정의하지 않으면, 멤버 대 멤버의 복사를 진행한느 디폴트 복사 생성자가 자동으로 삽입되어, 복사를 진행한다. 많은 경우, 복사 생성자를 직접 정의할 필요는 없다. 하지만, 복사 생상자를 반드시 정의해야 하는 경우 또한 존재한다.
- 앞선 코드를 보게 되면, SoSimple sim2 = sim1은, 묵시적 변환이 일어나서 복사 생성자가 호출되어 SoSimple sim2(sim1)으로 변환된다. 만일 이와 같은 변화가 마음에 들지 않는 경우, Class의 Constructor에 explicit을 선언할 경우, 묵시적 변환이 발생하지 않아 대입 연산자를 이용한 객체의 생성 및 초기화가 불가능해진다.
- 복사 생성자의 경우, 참조형의 선언을 의미하는 &은 반드시 삽입되어야 한다. 만일 &이 없다면, 복사 생성자의 호출은 무한루프에 빠질 것이다.
class SoSimple
{
private:
int num1;
int num2;
public:
SoSimple(int n1, int n2) : num1(n1), num2(n2)
{ }
SoSimple(const SoSimple ©) : num1(copy.num1), num2(copy.num2)
{ }
};
- 앞선 디폴드 복사 생성자는 멤버 대 멤버 복사를 진행한다. 이러한 복사 방식을 '얕은 복사(Shallow copy)'라고 한다. 이 경우 해당 객체는 동일한 포인터를 참조하게 된다.
- 이러한 복사에서 두 오브젝트 중 하나의 오브젝트를 통해 delete연산을 수행한다면, segmentation fault오류가 발생하게 된다. 따라서, 디폴트 복사 생성자가 아닌, 별도의 복사 생성자를 선언하여 '깊은 복사(Deep copy)'로 객체를 복사하는 것이 더욱 안전한 기법이라 할 수있다.
- 깊은 복사를 위해서는 복사 생성자를 정의해 줄 필요가 있다.
Person(cnost Person& copy) : age(copy.age)
{
name = new char[strlen(copy.name)+1];
strcpy(name, copy.name);
}
- 복사 생성자가 호출되는 시점은, 크게 3가지로 구분할 수 있다.
- 기존에 생성된 객체를 이용, 새로운 객체를 초기화하는 경우
- Call-by-value 방식의 함수호출 과정에서 객체를 인자로 전달하는 경우
- 객체를 반환하되, 참조형으로 반환하지 않는 경우
- 이 3가지의 경우는 "객체를 새로 생성해야하나, 생성과 동시에 동일한 자료형의 객체로 초기화해야 한다"라는 공통점을 가지고 있다.
- 복사 생성자의 호출 시점에 대해 자세히 알아보기 전에, 메모리 공간의 할당과 초기화가 동시에 일어나는 상황에 대해 알아보자
int num1 = num2;
int SimpleFunc(int n)
{
. . . .
}
int main(void)
{
int num = 10;
SimpleFunc(num); // 호출되는 순간 매개변수 n이 할당과 동시에 초기화
. . . .
}
int SimpleFunc(int n)
{
. . . .
return n; // 반환하는 순간 메모리 공강이 할당됨과 동시에 초기화
}
int main(void)
{
int num = 10;
cout << SimpleFunc(num) << endl;
. . . .
}
SoSimple SimpleFuncObj(SoSimple ob)
{
. . . . .
}
int main(void)
{
SoSimple obj;
SimpleFuncObj(obj);
}
// ob 객체가 생성과 동시에 전달되는 인자로 초기화
SoSimple SImpleFuncObj(SoSimple ob)
{
. . . .
return ob; // 반환하는 순간 메모리 공간이 할당되면서 동시에 초기화
}
- 만일, 앞선 경우 처럼, 복사와 동시에 초기화가 이루어지는 경우가 아닌, 복사 생성자의 호출과 같은 경우에서 초기화를 진행할 경우에는 초기화가 조금 다른 시간에 일어난다.
- 먼저, 함수에 인자를 전달하는 과정에서 복사 생성자가 호출된다. 이러한 흐름을 코드와 예시로 보자면 다음과 같다.
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num;
public:
SoSimple(int n) : num(n)
{ }
SoSimple(const SoSimple& copy) : num(copy.num)
{ }
void ShowData()
{
cout << "num: " << num << endl;
}
};
void SimpleFuncObj(SoSimple ob)
{
ob.ShowData();
}
int main(void)
{
SoSimple obj(7);
cout << "Before Call Func" <<endl;
SimpleFuncObj(obj);
cout << "After Call Func" << endl;
return 0;
}
- 먼저 SoSimpel obj를 통해 num이 7인 객체가 생성되었다.
- 다음, 이 obj가 매개변수로써 SimpleFuncObj가 호출되었고, 이 때, SimpleFuncObj의 ob가 선언과 동시에 초기화된다. 이러한 경우, 복사 생성자의 호출주체는 obj가 아닌, ob가 된다. 이렇듯 호출과 초기화에서 누가 호출되고 초기화 되는 지 자세히 알아 둘 필요가 있다.
- 즉, 정리하자면
- 함수에 인자가 전달된다.
- 전달된 인수는 매개변수에 '복사'된다.
- 이 과정에서 복사 생성자가 호출된다.
- 동시에 초기화 작업이 이루어진다.
- 즉, 매개변수에 어떠한 객체가 존재한다면, 해당 객체를 호출할 때에는 복사 생성자가 호출된다는 것을 이해해야한다.
- 객체의 반환에서는 위 과정에서 한단계가 추가 될 뿐이다. 만일 obj객체가 생성되어 SimpleFuncObj에 전달하였다고 가정하자, 그렇게 전달된 객체는 SoSimple ob가 초기화 되어 ob의 복사 생성자가 호출될 것이다. 그 뒤, 해당 객체는 return ob에 의해 다시금 생성되어 복사될 것이다. 이때 다시금 생성되는 객체를 '임시객체'라고 한다.
- 임시객체가 생성/초기화 되는 타이밍을 알았다면, 소멸하는 타이밍도 알아두어야 한다.
- 클래스 외부에서 객체의 멤버함수를 호출하기 위해 필요한 것은 다음 세 가지 중 하나다.
- 객체에 붙여진 이름
- 객체의 참조 값
- 객체의 주소 값
- 그런데 임시객체가 생성된 위치에는 임시객체의 참조 값이 반환된다. 그러므로 다음과 같은 경우도 허용된다.
const Temp &ref = Temp(300);
- 또한 다음과 같은 호출도 허용되는데
SimpleFuncObj(obj).AddNum(30);
- 즉, 반환을 위해 임시객체가 생성은 되지만, 해당 객체는 메모리 공간에 존재하고, 이 객체의 참조 값이 반환되어서 함수의 호출이 진행된다.
- 이러한 결과를 통해 다음 두가지 결론을 내릴 수 있다.
- 임시객체는 다음 행으로 넘어갈 시 바로 소멸된다.
- 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
- 즉 다음과 같은 코드를 보았을때,
#include <iostream>
using namespace std;
class SoSimple
{
private:
int num;
public:
SoSimple(int n) : num(n)
{ }
SoSimple(const SoSimple& copy) : num(copy.num)
{ }
~SoSimple()
{ }
};
SoSimple SimpleFuncObj(SoSimple ob)
{
return ob;
}
int main(void)
{
SoSimple obj(7);
SimpleFuncObj(obj);
SoSimple tempRef = SimpleFuncObj(obj);
return 0;
}
- 배운 내용을 바탕으로, 위 코드의 일련의 과정을 정리해 보자
- SoSimple 클래스의 obj 객체가 생성된다. 해당 객체는 7이라는 값을 가지고, constructor에 의해 7로 초기화된다.
- SImpleFuncObj를 통해 ob라는 객체가 obj로부터 복사되었고, 반환을 통해 ob라는 객체는 임시객체로 생성된다.
- 다음 행으로 넘겨지며 매개변수 ob는 소멸한다.
- 그 뒤, 반환으로 생성된 임시객체가 소멸한다.
- 그 뒤 tempRef라는 객체는 앞선 과정을 일부 반복한다.
- ob라는 매개변수를 생성하고, 반환을 통해 임시객체를 생성한다.
- ob라는 매개변수는 소멸한다.
'Study > C++' 카테고리의 다른 글
[C++] Static (0) | 2023.10.06 |
---|---|
[C++] Friend (0) | 2023.10.06 |
[C++] Class, Array and this pointer (0) | 2023.10.06 |
[C++] Constructor & Destructor (0) | 2023.10.06 |
[C++] Information Hiding (0) | 2023.10.06 |