
복사 생성자가 뭔지 알려면 우선 생성자가 무엇인지 알아야겠죠?
내친김에 소멸자까지 함께 알아봅시다.
길게 안합니다.
class TestClass()
{
private:
int m_i;
public:
TestClass(){}
~TestClass(){}
}
int main()
{
TestClass tc; // 객체 생성
};
클래스는 보통 이런 형태로 이루어집니다.
멤버변수와 멤버함수
그리고 생소멸자는 멤버함수 그 클래스의 멤버함수 입니다.
TestClass();
생성자입니다
객체가 생성될 때 호출되는 함수입니다.
초기화 함수 등을 따로 만들 필요없이 이곳에 작성하는 것이 효율적입니다.
반환 타입을 적지 않으며, 함수명은 반드시 자료형 이름(클래스 이름)과 동일해야합니다.
~TestClass();
소멸자입니다
객체가 소멸될 때 호출되는 함수입니다
메모리 해제를 하는 함수 등을 이곳에 작성하면 효율적입니다
생성자와 같이 반환 타입을 적지 않고 함수명은 자료형 이름을 따릅니다
모든 멤버함수에는 보이지 않는 지역변수 *this를 갖는다
주소 변수를 매개변수로 받지 않아도 되는 이유가 이것
해당 멤버함수를 호출시킨 객체의 주소값을 받아온다
네 그렇습니다 아주 중요한 내용입니다.
기본적인 성질을 알아보았으니, 돈복사 대신 생성자를 복사 해봅시다.
classs TestClass()
{
private:
float m_data;
public:
// 기본 생성자(Default construction)
TestClass()
: m_data(0.f)
{}
TestClass(float _data)
: m_data(_data)
{}
~TestClass()
{}
}
int main()
{
TestClass tsc;
TestClass tsc2(10.f); // 오버로딩된 생성자를 가리킴
// 기본 생성자를 호출할까?
// Nope
// 컴파일러는 반환 타입이 TestClass이며 인자가 없는 함수의 전방선언으로 봄
TestClass tsc2();
// 따라서 객체 생성이 안됨
// setData().. 등의 함수를 만들어서 값을 넣어주어야함
};
만약 클래스에 생성자가 정의되어 있지 않으면 기본 생성자를 생성합니다.
TestClass(){}
이니셜라이즈도 하지 않은 아무 의미를 담지 않은 생성자 일지라도 규칙이라 호출하게 됩니다.
그러나 생성자 오버로딩으로 인자를 가진 생성자가 있다면 기본 생성자를 호출하지 않습니다.
TestClass(int _data)
:m_data(_data)
{}
// main
TestClass tsc;
이 코드는 컴파일 오류가 납니다.
왜일까요?
여기서 tsc의 객체는 기본 생성자를 호출하는 객체이기 때문입니다.
생성자에 맞추어 인자를 갖게끔 오버로딩된 객체를 만들어줘야 합니다.
따라서
TestClass tsc(3.f);
이런 식으로 작성해줘야합니다.
그래야 오버로딩된 생성자를 잘 호출하겠죠?
네 그럼 이제 진짜 복사 생성자를 알아봅시다.
class TestClass
{
private:
float m_data;
public:
void setData(float _data)
{
m_data = _data;
}
public:
TestClass()
: m_data(0.f)
{}
TestClass(float _data)
: m_data(_data)
{}
~TestClass() {}
};
int main()
{
TestClass tsc;
tsc.setData(2.f);
// 디폴트 복사 생성자
TestClass tsc2(tsc);
}
디폴트 복사 생성자라는 걸 작성했을 때 컴파일 오류가 나지 않습니다.
그말인 즉 컴파일러가 자동으로 생성해준다는 이야기인데요,
만약 복사 생성자가 구현되어있다면 어떻게 작성 되어있을지 확인해 봅시다.
// 복사 비용을 줄이기 위해 원본 값을 참조한다.
// 원본을 수정할 수 없게 const 키워드를 추가
// 복사하고자 하는 값과 동일하게 시작하기 위함
TestClass(const TestClass& _Other)
: m_Data(_Other.m_Data)
{}
복사 생성자가 구현되어있으면?
기본 생성자는 구현되지 않습니다.
그니까 어떤 생성자건 본인이 직접 구현한 생성자가 있다면 기본 생성자는 만들어지지 않습니다!
개헷갈림
깊은 복사와 얕은 복사를 사용할 때 특히 중요합니다.
이 부분은 나중에...
디폴트 복사 생성자가 문제가 되어 금지하는 경우도 있답니다.
TestClass(const TestClass& _Other) = delete;
이어서 봅시다.
class MyClass
{
private:
int m_i;
public:
void setData(int _a) { m_i = _a;}
}
;
int main()
{
// 기본 생성자 생성
MyClass mc1;
mc1.setData(10);
// 복사 생성자 생성
MyClass mc2(mc1);
// 대입 연산자 -> 디폴트 대입 연산자도 있음
MyClass mc3;
mc3 = mc1;
}
디폴트 대입 연산자를 직접 구현하자면
void operator=(const MyClass& _Other)
{
m_i = _Other.m_i;
}
mc3이 호출 객체가 되는 연산자 함수입니다.
근데 사실 컴파일러가 만들어주는 디폴트 대입 연산자는 MyClass& 타입을 반환하는 함수여야합니다.
얘는 걍 러프하게 작성해본 거임
MyClass& operator=(const MyClass& _Other)
{
m_i = _Other.m_i;
return *this;
}
이렇게 되어야 주소를 갖는 자기자신을 반환하면서 대입 대입 대입... 이 되어 값이 결정되는 것 입니다.
int i = 0;
int i1 = 10;
int i2 = 100;
i2= i1 = i;
// i의 0이 i1으로 전달
// i1은 0을 가지고 i2에 전달
// i2는 0을 값으로 가짐
이와 같이
int main()
{
// 기본 생성자 생성
MyClass mc1;
mc1.setData(10);
// 복사 생성자 생성
MyClass mc2(mc1);
// 대입 연산자
MyClass mc3;
mc3 = mc2 = mc1;
}
대입이 연달아 이어 진행될 수 있도록, operator=의 반환타입은 호출자 자기 자신이 되면 됩니다.
+추가
MyClass mc4 = mc3;
// 생성자가 호출되기 전에 대입이 일어남
// 복사 생성자로 해석해서 처리함
// ->
MyClass mc4;
mc4 = mc3;
// 생성자 호출 후 대입 연산자 호출됨
MyClass mc4(mc3);
// 복사 생성자 호출!
그러니 굳이 두 줄에 나누어 구현할 필요가 없겠네요. ㅇㅅㅇ
복사 생성자는 여기까쥐