복사 생성자
클래스의 call by value를 지원하기 위해 기본으로 제공되는 생성자이다.
class C_TEST
{
public:
C_TEST(); // 기본 생성자
C_TEST(const C_TEST &other); // 복사 생성자
};
복사생성자가 불리는 타이밍은
1. 대입 연산을 할 때 (직접 복사를 할 때)
2. 매개 변수로 사용될 때 (call by value)
3. 리턴 타입일 때 (call by value)
3개로 외우지 말고, 대입 연산 또는 call by value일 때 2개로 생각하면 편하다.
흔히 하는 복사 생성자 실수
1. 매개 변수의 자료형을 클래스(or 구조체) 자료형으로 받는 실수.
클래스나 구조체들은 변수에 비해 메모리의 크기가 크기 때문에, 복사 생성자로 넘기면 끔찍한 일이 벌어질 수 있다.
클래스 포인터나 레퍼런스로 받자.
#include <iostream>
class C_TEST
{
public:
C_TEST();
C_TEST(const C_TEST& other);
};
void func(C_TEST c);
int main()
{
C_TEST c1{}; // 기본 생성자 호출
func(c1);
// [출력 결과]
// 기본 생성자
// 복사 생성자
}
C_TEST::C_TEST()
{
printf("기본 생성자\n");
}
C_TEST::C_TEST(const C_TEST& other)
{
printf("복사 생성자\n");
}
void func(C_TEST c) // 함수 인자를 받을 때, 복사 생성자 호출 (call by value)
{
}
2. return을 아무 생각없이 해버리는 실수
return할 때도 복사 생성자가 호출되기 때문에, 클래스의 크기가 클수록 메모리의 스택 영역을 많이 잡아먹겠죠??
포인터를 사용해서 4Byte만 씁시다.
#include <iostream>
class C_TEST
{
public:
C_TEST();
C_TEST(const C_TEST &other);
};
C_TEST func();
int main()
{
func(); // return은 func()자리에 복사해서 값을 남기기 때문에 안된다.
// [출력 결과]
// 기본 생성자
// 복사 생성자
}
C_TEST::C_TEST()
{
printf("기본 생성자\n");
}
C_TEST::C_TEST(const C_TEST & other)
{
printf("복사 생성자\n");
}
C_TEST func()
{
C_TEST cTmp{}; // 기본 생성자 호출
return cTmp; // 복사 생성자 호출
}
복사 생성자를 막는 법
나는 이러한 실수를 안할 수 있다고 생각하겠지만, 협업하는 프로젝트에서는 내 동료가 복사생성자를 잘못 사용할수도 있다.
친절하게 막아주자.
1. 기본 복사 생성자는 public interface이기 때문에 private로 바꿔준다.
복사생성자 함수를 private로 선언만 하고 구현을 안하면 외부에서 사용이 불가능하다.
class C_TEST
{
public:
C_TEST();
// C_TEST(const C_TEST &other);
private:
C_TEST(const C_TEST &other);
};
2. 그냥 지운다.
class C_TEST
{
public:
C_TEST();
C_TEST(const C_TEST &other) = delete;
};
이렇게 막아놓으면 컴파일 과정에서 error를 띄우기에, 복사생성자를 이용한 실수를 방지할 수 있다.
(매개변수로 사용되거나 return을 클래스 그대로 사용했을 때)
얕은 복사와 깊은 복사
복사하면 빠질 수 없는 얕은 복사와 깊은 복사이다.
얕은 복사
기본 제공되는 복사 생성자는 얕은 복사로 제공된다.
얕은 복사는 멤버 변수와 멤버 변수를 단순 대입으로 복사한다.
이 때, 포인터 변수에서 문제가 되는데, 포인터 변수가 동일한 메모리를 가리키게 된다.
이렇게 되면 메모리 해제 시점이 애매해진다.
#include <iostream>
class C_TEST
{
private:
int* m_pData;
public:
C_TEST():m_pData{}
{
}
C_TEST(int nData) // 동적 할당
{
m_pData = new int(nData);
}
void printData()
{
printf("pData: %d\n", *m_pData);
}
void set_pData(int nData)
{
*m_pData = nData;
}
~C_TEST() // 동적 할당 한 것 메모리 해제
{
delete m_pData;
}
};
int main()
{
C_TEST c1(100);
C_TEST c2(c1); // 기본 복사 생성자 호출, 포인터변수가 힙에 할당된 동일한 메모리를 참조.
c2.printData(); // 100 출력
c2.set_pData(50); // c1의 pData도 바뀜.
c1.printData(); // 50 출력
c2.printData(); // 50 출력
// c1과 c2에서 소멸자가 호출되면서 m_pData를 2번 메모리 해제함.
// 프로그램 펑 터짐!!
}
깊은 복사
얕은 복사의 문제점을 해결하기 위해 깊은 복사를 사용한다.
깊은 복사는 동일한 메모리를 가리키지 않도록 하는 복사 방법이다.
(복사생성자를 오버라이딩하는 게 깊은 복사가 아니다. 나만 처음에 헷갈렷나?)
따로 함수를 만들어도 되지만, 복사생성자를 오버라이딩해서 깊은 복사를 구현해보자.
#include <iostream>
class C_TEST
{
private:
int* m_pData;
public:
C_TEST():m_pData{}
{
}
C_TEST(int nData)
{
m_pData = new int(nData); // 동적 할당
}
C_TEST(const C_TEST& other)
{
m_pData = new int(*other.m_pData); // 복사생성자가 불릴 때, 동적 할당 새로 함.
}
void printData()
{
printf("pData: %d\n", *m_pData);
}
void set_pData(int nData)
{
*m_pData = nData;
}
~C_TEST()
{
delete m_pData; // 동적 할당 된 것 메모리 해제
}
};
int main()
{
C_TEST c1(100);
C_TEST c2(c1); // 오버라이딩된 복사생성자 호출
c2.printData(); // 100 출력
c2.set_pData(50); // c1의 pData랑은 다른 메모리를 가리킨다.
c1.printData(); // 100 출력
c2.printData(); // 50 출력
// c1과 c2에서 소멸자가 호출되면서 각각의 m_pData 메모리가 해제됨.
// 프로그램 정상 작동!!
}
대입 연산자 삭제
이 게시글 초반에 복사 생성자가 불리는 타이밍을 2가지로 인지하면 된다고 했다.
중요!! (대입 연산, call by value)
복사생성자로 막은 것은 call by value의 타이밍이다.
이번에는 직접 복사를 하는 대입 연산자를 막아볼 차례이다.
#include <iostream>
class C_DATA
{
private:
int m_nData;
public:
C_DATA();
// 대입 연산은 이거 쓴다고 안 막힌다.
// C_DATA(const C_DATA& cInput) = delete;
void setData(int nData);
int getData();
// 함수 구현은 길어지므로 패쓰
};
int main()
{
C_DATA c1{};
C_DATA c2{};
C_DATA c3{};
c1.setData(100);
c3 = c2 = c1;
// c2 = c1을 하고, (c2=c1) 자리에 c2를 남김.
// 그 다음에 c3 = c2.
printf("%d\n", c1.getData());
printf("%d\n", c2.getData());
printf("%d\n", c3.getData());
}
대입 연산은 오퍼레이터 재구현을 통해 삭제해준다.
복사 생성자와 비슷하게 delete 키워드를 사용한다.
class C_TEST
{
public:
C_TEST();
C_TEST(const C_TEST &) = delete; // 기본 복사생성자 삭제
const C_TEST & operator=(const C_TEST &) = delete; // 기본 대입연산자 삭제
};
추가 이해 내용
C++은 기본자료형도 클래스로 만들어져있기 때문에, 이런 문법들이 기본으로 제공되는 것이다.
int a(10);
int b = a;
기본 자료형은 복사가 이뤄져도 크게 문제 없지만,
상대적으로 크기가 큰 클래스에서는 복사가 일어나는 문법들을 삭제해주는 것이다.
이상한 점
근데 정리하다보니 느끼는 게, 이상한 게 있다.
// 기본 복사 생성자 삭제 (이걸 막아도 대입 연산자로 인한 복사는 안 막히네?)
C_TEST(const C_TEST& other) = delete;
// 기본 대입 연산자 삭제 (그럼 이건 복사생성자의 타이밍이 아니잖아.)
const C_TEST & operator=(const C_TEST &) = delete;
내가 잘못 알고 있나 싶어서 구글링해봐도 모든 게시글에서 '복사생성자의 타이밍'을 3가지로 구분하며, 그 안에 대입 연산이 포함되어 있다.
'복사가 일어나는 타이밍 3가지'로 불러야 하는거 아닌가? 잘 이해가 안된다. ㅋㅋㅋ 모든 게시글들이 오류인 것인가!!
'기타 > C++' 카테고리의 다른 글
[C++] 상속에 대해서 (0) | 2021.01.17 |
---|---|
[C++] 클래스 간의 조립 (상속, 포함) (0) | 2020.12.29 |
[C++] 메모리 관리, 누수잡기 (0) | 2020.12.23 |
[C++] 참조자 (reference), 포인터와의 차이 (0) | 2020.12.23 |
[C++] 동적 할당과 메모리 해제 (0) | 2020.12.17 |