![[C++] 복사 생성자와 임시객체](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fdh33ow%2Fbtsak9zZp6w%2FJLVLwhSIF5RbTjb61m6yNK%2Fimg.png)
이번글은 C++의 복사 생성자와 임시객체에 대한 글입니다!
#include <iostream>
using namespace std;
class Person
{
private:
int val;
public:
Person(int n) : val(n)
{
cout << "Call Default Constructor!" << endl;
}
Person(const Person& p) : val(p.val)
{
cout << "Call Copy Constructor!" << endl;
}
Person& AddVal(int n)
{
val += n;
return *this;
}
void PrintVal()
{
cout << "val : " << val << endl;
}
~Person()
{
cout << "Call Destructor!" << endl;
}
};
Person PersonFunc(Person p)
{
cout << "p ptr : " << &p << endl;
return p;
}
int main()
{
Person p1(3);
PersonFunc(p1);
cout << endl;
Person TempRef = PersonFunc(p1);
cout << "Return person : " << &TempRef << endl;
return 0;
}
여러분들은 혹시 위의 코드에서 임시객체가 어디서 생성되고 어떻게 반환되며 복사생성자는 어디서 호출되는지 그리고 해당 코드의 생성자호출 시점을 알고 계신가요?
먼저 복사 생성자가 무엇인지 부터 알아보도록 하겠습니다.
복사생성자란?
멤버대 멤버로 데이터가 복사가 일어나는 생성자입니다.
class 내부에 따로 코드를 적어주지 않으면 Defualt Copy Constructor가 자동으로 호출되어 멤버 대 멤버 복사가 일어납니다.
가령 Person이라는 클래스가 있을 경우
Person p1;
Person p2 = p1; // A
Person p3(p1); // B
이런식으로 복사생성자를 호출할 수 있습니다. A의 경우와 B의 경우의 차이점을 아시나요?
A의 경우에는 묵시적 변환(또는 암시적 변환)이 되어 객체가 생성되는 것이고
B의 경우 기본 복사생성자가 호출된 경우입니다.
(A의 경우와 같은 묵시적 변환을 막고싶은 경우 int 값을 받는 생성자에 "explicit"키워드를 통해 묵시적 변환을 막을 수 있습니다.)
이 맴버 대 멤버 복사가 Shallow Copy(얕은 복사)를 뜻합니다.
"복사 생성자에서 Shallow Copy를 할 때 문제가 발생할 수 있다"라는 말을 들어보신적 있으실 텐데요.
대표적으로 클래스의 멤버 변수로 포인터를 가지고 있는 경우 복사생성자를 통해 생성된 객체가 멤버 대 멤버복사를 하여 같은 주소를 가르키는 포인터 변수가 생겨서 소멸자 호출시 "Double Free" 현상이 발생할 수 있다는 것입니다.
따라서 A라는 클래스를 설계할 경우 아래와 같이 복사생성자를 구현을 해야 "double free"현상을 방지하는 "깊은 복사"가 이루어 집니다.
#include <iostream>
#include <cstring>
#define _CRT_SECURE_NO_WARNINGS
#pragma warning(disable:4996)
using namespace std;
class A
{
private:
char* name;
int age;
public:
A(char* sm, int age) : age(age)
{
int len = strlen(sm) + 1;
name = new char[len];
strcpy(name, sm);
cout << "기본 생성자 호출 " << endl;
}
A(const A& a) : age(a.age)
{
this->name = new char[strlen(a.name) + 1];
strcpy(this->name, a.name);
cout << "복사 생성자 호출 : " << &a <<endl;
}
~A()
{
cout << "소멸자 호출" << endl;
delete[] name;
}
};
int main()
{
char* c = new char[3];
c[0] = 'a'; c[1] = 'b'; c[2] = 'c';
A a1(c, 13);
A a2 = a1;
A a3(a1);
return 0;
}
(참고로 strlen(a.name) + 1]에서 +1은 \0 값이 들어가기 때문입니다)
위의 코드에서 복사생성자를 주석처리한뒤 호출하면 error가 납니다.
간략하게 나마 기본 복사생성자, 복사 생성자의 얕은복사, 깊은 복사를 알아보았습니다.
그러면 이제 "임시객체"와 복사생성자가 무슨 연관이 있는지 정리를 해보도록 하겠습니다.
임시객체
가장 간단한 예로
int num1 = 10;
int num2 = num1;
위의 코드는, num2라는 메모리 공간을 할당과 동시에 num1에 저장된 데이터(값)으로 초기화 시키는 코드입니다.
즉, 할당과 동시에 초기화가 이루어 지는 것입니다.
int Func(int n)
{
return n;
}
int main()
{
int num = 10;
cout << Func(num) << endl;
return 0;
}
위와 같은 경우 Func가 호출됨과 동시에 매개변수 n이 할당과 동시에 변수 num에 저장되어있는 값으로 초기화가 됩니다.
매개변수 n도 함수가 호출되는 동시에 할당되므로, 메모리 공간의 할당과 초기화가 동시에 발생하는 상황입니다.
또한 return n;에서 "반환 하는 순간 메모리 공간이 할당되면서 동시에 초기화"가 또 진행이 됩니다.
조금 난해할 수 있는데요. "함수가 값을 반환하면, 별도의 메모리 공간이 할당되고, 이 공간에 반환값이 저장된다(반환 값으로 초기화 된다) 정도로 이해하시면 될거같습니다.
그렇지 않고서야 어떻게 cout << Func(num) << endl; 이 가능할까요?
이는 class 복사생성자도 마찬가지 인데요
#include <iostream>
using namespace std;
class Person
{
private:
int val;
public:
Person(int n) : val(n)
{
cout << "Call Default Constructor!" << endl;
}
Person(const Person& p) : val(p.val)
{
cout << "Call Copy Constructor!" << endl;
}
Person& AddVal(int n)
{
val += n;
return *this;
}
void PrintVal()
{
cout << "val : " << val << endl;
}
~Person()
{
cout << "Call Destructor!" << endl;
}
};
Person PersonFunc(Person p)
{
cout << "p ptr : " << &p << endl;
return p;
}
int main()
{
Person p1(3);
PersonFunc(p1); // 1번
cout << endl;
Person TempRef = PersonFunc(p1); // 2번
cout << "Return person : " << &TempRef << endl;
return 0;
}
main함수의 1번의 경우 PersonFunc를 호출하게되면 p1이 PersonFunc의 매개변수로 들어와 p의 복사생성자가 호출되어 p1을 바탕으로 복사생성자를 호출한뒤 이를 "임시객체" p에 넘겨준뒤 p는 소멸됩니다.
2번의 경우 1번과 마찬가지로 p의 복사생성자가 호출된 후 이를 임시객체에 전달 후 p는 소멸됩니다.
이후 TempRef가 이를 대입연산자로 받는것 처럼 보이는데요. 사실은
임시객체가 생성된 위치에는 임시객체의 참조값이 반환됩니다. TempRef는 임시객체의 참조를 나타냅니다.
임시객체는
- 임시객체는 다음행으로 넘어가면 바로 소멸되어 버린다.
- 참조자에게 참조되는 임시객체는 바로 소멸되지 않는다.
라는 특성을 가지고있는데
TempRef가 참조를 하고있기 때문에 소멸되지 않고 해당 코드를 실행하고 출력력과를 보면
main에서 기본생성자로 호출한 p1생성후 1번의 PersonFunc호출시 PersonFunc의 매개변수인 p의 복사생성자가 호출 됩니다.
이후 return p를 하면서 임시객체가 만들어지면서 임시객체에 대한 복사생성자를 호출 합니다.
그리고 1번에서 참조를 하는 친구들이 없기때문에
p에대한 소멸자 호출 -> 임시객체 소멸자 호출이 진행되었습니다.
이후 2번에서는 PersonFunc(p1)으로 매개 변수 p에 대한 복사생성자 호출후 임시객체에 대한 복사 생성자를 호출 합니다.
이후 반환되었기 때문에 p는 소멸되고 임시객체는 참조값을 TempRef에게 반환한뒤 소멸되지 않습니다.
이후 return 0을 만나고 나서야 p1에대한 소멸자와 임시객체 TempRef가 가르키는 임시객체가 소멸된 것을 확인할 수 있습니다.
틀린 부분이 있다면 수정하도록 하겠습니다 :)
'CPP' 카테고리의 다른 글
[C++] 전달참조 (0) | 2023.07.31 |
---|---|
[C++] 오른값 참조 (0) | 2023.07.30 |
[C++] Effective C++ 항목 1~27 정리 (0) | 2023.07.03 |
[C++] C++ 컴파일 의존성 (2) | 2023.05.28 |
[C++] vftable (0) | 2023.04.21 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!