복사 생성자는 객체의 복사본을 생성할 떄 호출되는 생성자다
복사 생성자는 기본적으로 컴파일러가 알아서 만든다 그렇기에 필요한 경우가 아니라면
만들 필요가 없다 하지만 필요한 경우에 만들지 않으면 대참사가 발생한다
필요한 경우란 클래스 내부에서 메모리를 동적 할당 및 해제하고 이를 멤버 포인터 변수로 관리하는 경우이다 말이 굉장히 어렵다 쉽게 쉽게 설명해 보겠다
복사 생성자의 문법은 간단하다
클래스이름(const 클래스이름& rhs);
기본적인 약속이 존재한 먼저 const는 무조건 붙여라 외워라
두번쨰로 매개변수 이름으로 rhs는 for int i 같은 존재다.
//제작자 코드
class test
{
public:
test(int a)
{
number = a;
}
test(const test& rhs)
{
cout << "복사 생성자 호출 !!" << endl;
this->number = rhs.number;
cout << number << endl;
}
private:
int number;
};
//사용자 코드
int main(void)
{
test a(10);
test b(a);
return 0;
}
출력결과
복사 생성자 호출 !!
10
복사 생성자도 1장에서 배운 생성자 처럼 클래스 인스턴스가 복사 생성될때 호출 되는것을 확인 할수 있다.
그리고 우리가 생각 했던 대로 10이 출력 된것을 볼수 있다
그렇다면 다음 코드를 살펴보자
//제작자 코드
class test
{
public:
test(int a)
{
number = new int(a);
}
~test()
{
delete number;
}
int GetData()
{
return *number;
}
private:
int* number = nullptr;
};
//사용자 코드
int main(void)
{
test a(10);
test b(a);
cout << a.GetData() << endl;
cout << b.GetData() << endl;
return 0;
}
출력결과
10
10
우리가 생각한 출력결과가 호출 되었지만 위에 코드를 실행하면 오류가 발생한다
깊은 복사와 얕은 복사의 문제를 해결하지 못한 코드라서 메모르 해제 과정에서 문제가 생긴다
깊은 복사 얕은 복사는 매우 중요하기에 천천히 그림을 통해 설명하겠다
//사용자 코드
int main(void)
{
int pa = new int;
*pa = 10;
int pb = new int;
*pb = pa;
delete pa;
delete pb;
return 0;
}
위에 코드가 실행되면 메모리 에서 그림과 같은 구조가 형성된다
pa와 pb가 각각의 동적 생성한 int 인스턴스가 있다. 설명을 위해 a,b라 부르겠다
1번 그림처럼 pa 가 a를 가리키고 pb가 b를 가리키면 정상적이다 하지만
pb가 pa를 얕은 복사 하면서 인스턴스 b는 접근 불가능한 메모리 영역이 되어버렸다.
심지어 그림 2에서 a가 메모리 영역을 해제한 다음 pb가 메모리를 해제하면 심각한 오류가 발생한다
(해제할수 있는 메모리가 없는데 해제를 하니깐)
그렇다면 어떤식으로 깊은 복사를 할수 있을까 간단하다
*pb = pa; 를 *pb = *pa;로 바꾸면 된다
그러면 이제 앞선 코드를 복사 생성자를 이용해 수정해 보자
//제작자 코드
class test
{
public:
test(int a)
{
number = new int(a);
}
test(const test& rhs)
{
number = new int;
*this->number = *rhs.number;
}
~test()
{
delete number;
}
int GetData()
{
return *number;
}
private:
int* number = nullptr;
};
//사용자 코드
int main(void)
{
test a(10);
test b(a);
cout << a.GetData() << endl;
cout << b.GetData() << endl;
return 0;
}
대입 연산자 = 클래스 에도 기본적으로 적용된다.
int main(void)
{
test a(10);
test b(5);
b = a;
cout << a.GetData() << endl;
cout << b.GetData() << endl;
return 0;
}
b = a; 처럼 대입을 할수있다 하지만 컴파일러가 기본적으로 제공하는 대입 연산자는 얕은 복사를 하기 떄문에 문제가 발생한다. 그렇기에 대입 연산자를 정의해줘야 한다
대입 연산자의 기본 문법은 단순하다
클래스이름& operator=(const 클래스이름& rhs)
현재 코드에서 대입 연산자를 정의하면 다음과 같다
test& operator=(const test& rhs)
{
*number = *rhs.number;
return *this;
}
위와 같이 대입 연산을 정의해주면 클래스 인스터스에 대입 연산이 제대로 적용된다.
변환 생성자는 개발자 모르게 호출되면서 동시에 불필요한 임시객체를 만들어 버린다
다음 예제를 살펴보자
//제작자 코드
class CTestData
{
public:
CTestData(int nParam)
{
cout << "CTestData(int)" << endl;
}
CTestData(const CTestData & rhs )
{
cout << "CTestData(const CTestData &)" << endl;
}
int GetData() const
{
return m_nData;
}
void SetData(int nParam)
{
m_nData = nParam;
}
~CTestData()
{
cout << "소멸자 생성" << endl;
}
private:
int m_nData = 0;
};
void TestFunc(CTestData param)
{
cout << "TestFunc(): " << param.GetData() << endl;
}
//사용자 코드
int main(void)
{
TestFunc(5);
return 0;
}
출력결과
CTestData(int)
TestFunc(): 5
소멸자 생성
천천히 코드를 살펴보면 출력결과가 굉장히 이상하다는 것을 느낄수 있다
사용자 코드 어디에도 클래스 인스턴스를 생성한 적이 없는데 생성자와 소멸자가 호출 된것이다
어떻게 이런일이 가능할까? 지금 이러한 현상이 바로 변환 생성자 때문에 일어난 일이다
그렇다면 언제 변환 생성자가 호출 되었을까?
TestFunc(5); 에서 Test의 원형을 보면 void TestFunc(CTestData param) 매계변수가 클래스 형식임을 확인할수 있다 즉 변환 생성자 는 TestFunc(5);을 TestFunc(CTestData(5));로 변환 시켜 버린다
그렇기 떄문에 변환 생성자는 내가 원치 않아도 임시객체를 만들어 생성 소멸자를 호출한다
그렇기에 우리는 지금부터 2가지 약속을 할것이다
1.매개변수로 클래스를 넘길때는 참조형으로 넘길것
2.변환 생성자에는 explicit을 붙여서 묵시적 형변환을 차단해라
그렇다면 다음과 같이 코드를 수정해야한다
void TestFunc(CTestData& param)
{
cout << "TestFunc(): " << param.GetData() << endl;
}
explicit CTestData(int nParam)
{
cout << "CTestData(const CTestData &)" << endl;
}
클래스가 변환 생성자를 제공하면 두 형식 사이에 호환성이 생긴다 예시로 int 5가 형변환을 통해 CTest 클래스로 형변환 된것을 앞서 살펴 봤다 하지만 CTest가 int 형이 될수 없으므로 반쪽짜리 형변환이 된다 그렇기에 지금부터 CTest가 int형 으로 형변환 시키는 방법에 대해 알아보자
형변환 연산자 문법
operator int(void){return m_ndata}
그러면 다음과 같이 형변환이 가능하다
//사용자 코드
int main(void)
{
CTestData a(5);
cout << a;
return 0;
}
출력결과
5
이처럼 클래스를 int형으로 형변환 시킬수 있다