생성자가 복사가 된다고

invisible_thorn·2025년 4월 15일
0
post-thumbnail

복사 생성자가 뭔지 알려면 우선 생성자가 무엇인지 알아야겠죠?
내친김에 소멸자까지 함께 알아봅시다.

길게 안합니다.

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);
// 복사 생성자 호출!

그러니 굳이 두 줄에 나누어 구현할 필요가 없겠네요. ㅇㅅㅇ
복사 생성자는 여기까쥐

0개의 댓글