1. 컴파일러가 저절로 선언하는 함수들
컴파일러는 직접 선언하지 않으면 자동으로 선언해주는 함수들이 있습니다.
- 복사 생성자
- 생성자
- 복사 대입 연산자
- 소멸자
class Yeoul {
private:
int* data;
public:
// 생성자
Yeoul(int val) { data = new int(val); }
// 복사 생성자
Yeoul(const Yeoul& rhs) { data = new int(*rhs.data); }
// 복사 대입 연산자
Yeoul& operator=(const Yeoul& rhs) {
if (this != &rhs) { delete data; data = new int(*rhs.data); }
return *this;
}
// 소멸자
~Yeoul() { delete data; }
};
위는 각 함수가 무엇인지 보여주기 위한 예시이고, 실제 컴파일러가 자동으로 생성해주는 함수들은 코드상으로는
보이지 않습니다.
그리고 컴파일러가 이렇게 자동으로 생성해주는 함수들의 접근 지정자는 전부 public입니다. 그리고 inline함수입니다.
물론 이 함수들은 필요하다고 판단 될 때에만 컴파일러가 자동 생성합니다.
그렇다면 자동으로 생성해줄때는 언제일까요?
바로 아래와 같은 경우에 자동으로 생성됩니다.
Yeoul y1; // 기본생성자, 소멸자 생성 (미리 정의하지 않았다면)
Yeoul y2(y1); // 복사 생성자
y2 = y1; // 복사 대입 연산자
기본생성자, 기본 소멸자는 컴파일러에게 "배후의 코드를" 까는 자리는 마련해줍니다.
이것이 무슨 말이냐하면 한마디로 컴파일러가 코드를(생성자와 소멸자) 생성해줄 자리는 만들어 준다는 것입니다.
class Parent { };
class Member { };
class Yeoul : public Parent {
private:
Member m;
};
//컴파일러는 다음과 같은 "배후 코드"를 포함한 생성자를 생성합니다:
Yeoul::Yeoul() : Parent(), m() {
// 비어 있음
}
Yeoul::~Yeoul() {
// 비어 있음
// 컴파일러가 자동으로 추가하는 "배후 코드":
// m.~Member(); // 멤버 변수 소멸자 호출
// Parent::~Parent(); // 부모 클래스 소멸자 호출
}
2. 소멸자
이때 자동으로 생성된 소멸자는 부모 클래스의 소멸자의 특성을 따라가게 됩니다.
부모가 virtual로 소멸자를 생성했다면 자식도 virtual이고 virtual이 아니면 자식도 아닙니다.
이게 상당히 중요한 이유는 만약 부모의 소멸자가 virtual이 아니면 문제가 생길 수 있기 때문입니다.
Base* ptr = new Derived();
delete ptr;
만약 이러한 코드가 있다고 하면
소멸자를 Base클래스에서 virtual로 선언한 경우라면 자식 클래스의 소멸자 > 부모 클래스의 소멸자 순으로
정상적으로 호출되지만 virtual이 아니라면 Base클래스의 소멸자만 호출하게 되고 결국 메모리 누수로 이어집니다.
(항목 7)
3. 복사 생성자/ 복사 대입 연산자
자동으로 생성된 복사 생성자와 복사대입 연산자는 원본 객체의 비정적 데이터를 사본으로 복사하는 것이 전부입니다.
아래는 템플릿 예제입니다.
template<class T>
class NamedObject {
public:
NamedObject(const char* name, const T& value);
NamedObject(const std::string& name, const T& value);
// 복사 생성자와 복사 대입 연산자는 명시적으로 선언되지 않음
private:
std::string nameValue;
T objectValue;
};
여기서 중요한 점은 NamedObject 템플릿에는 생성자가 선언되어서 컴파일러가 기본 생성자를 만들지 않을 것입니다.
프로그래머의 예상과 다르게 새로운 생성자가 생겨서 프로그래머가 예상치 못한 결과가 일어나지는 않는다는 것입니다.
그러나 복사 생성자 / 복사 대입 연산자는 NamedObject에 선언되어 있지 않아서
필요시에 컴파일러에 의해 함수의 기본형이 만들어지게 됩니다.
NamedObject <int> no1("I am String",3);
NamedObject<int> no2(no1); // 복사 생성자 호출됩니다.
컴파일러가 만들어준 복사 생성자는 no1.nameValue와 no1.objectValue를 사용해서 no2.nameValue와 no2.objectValue를
각각 초기화해야 합니다.
string은 자체적으로 복사 생성자가 있어서 string내부의 복사 생성자에 no1.nameValue를 넘겨서 복사가 이루어지고
int같은 경우에는 각 비트를 단순히 복사해오는 것으로 복사가 이루어 집니다.
중요한 것은
복사 대입 연산자가 위에서 설명한 것처럼 적절하게 작동하려면 최종 결과 코드가 legal하고 resonable해야 합니다.
둘중 하나라도 충족 시키지 못하면 컴파일러가 operator=의 자동생성을 거부합니다.
그렇다면 legal하고 resonable한게 무슨말일까요? 표현이 너무 모호합니다.
그것은 예제를 보면서 알아보도록 합시다.
3-1. 멤버에 참조객체가 있는 경우
NamedObject 클래스가 이렇게 선언되어 있고
template<class T>
class NamedObject {
public:
// 이제 이 생성자는 const name을 받지 않습니다. nameValue가
// 이제 non-const string에 대한 참조이기 때문입니다.
// char* 생성자는 삭제되었습니다. 우리는 반드시 참조할 string이 있어야 하기 때문입니다.
NamedObject(std::string& name, const T& value);
... // 위와 같이, operator=가 선언되지 않았다고 가정합니다
private:
std::string& nameValue; // 이제 이것은 참조입니다
const T objectValue; // 이제 이것은 const입니다
};
아래와 같이 코드를 쓴다면 무슨 문제가 발생 할까요?
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2); // 이 코드를 처음 작성했을 때, 우리 개
// Persephone은 곧 두 번째 생일을 맞이할
// 예정이었습니다
NamedObject<int> s(oldDog, 36); // 가족 개 Satch(내 어린 시절의)는
// 아직 살아있다면 36살이 되었을 것입니다
p = s; // p의 데이터 멤버들에게는 어떤 일이 일어나야 할까요?
자 위의 코드를 바탕으로 일어나는 일들을 세세하게 알아보도록 합시다.
우선 p=s가 일어나기전 p.NameValue와 s.NameValue는 각각 stringn 객체를 참조하고 있습니다.
이때 p=s를 실행하면 어떻게 될것일까요?
p.NameValue가 s.NameValue를 가리켜야할까요?
C++의 참조자는 자신이 참조하고 있는 것과 다른 객체는 참조가 불가능 합니다.
(참조는 초기화 이후 다른 객체를 참조하도록 변경할 수 없음 )
이 예시를 보면 이해가 좀 편하실겁니다.
int a = 5;
int b = 10;
// 참조자 r은 a를 참조하도록 초기화
int& r = a;
// 이 시점에서 r은 a의 또 다른 이름이 됨
r = 7; // a의 값이 7로 변경됨
// 중요: 다음 코드는 r이 b를 참조하게 바꾸는 것이 아님!
r = b; // a의 값이 b의 값(10)으로 변경됨
// a는 10, b는 10이 됨
// r은 여전히 a를 참조함
그러면 p.NameValue가 참조하는 객체 자체가 바뀌어야 하나요?
그러면 기존에 p.NameValue에 대한 포인터, 참조자를 가진 객체까지 영향을 받게 됩니다.
한마디로 재앙이 시작되는 상황인것이죠
그래서 컴파일러는 컴파일 거부를 하게 됩니다.
참조자를 데이터 타입으로 가지고 있는 클래스의 대입연산을 하려면 직접 복사 연산자를 정의해야합니다.
그래야 위와같은 문제를 피할수있습니다.
멤버에 const(상수객체)가 있어도 이러한 문제가 발생하게 됩니다.
상수 멤버를 수정하는 것은 문법에 어긋나서 안됩니다.
복사 대입 연산자를 private로 선언한 기본 클래스에서 파생된 클래스는 암시적 복사 대입 연산를 가질 수 없습니다.
파생 클래스에서 복사 대입 연산자를 제대로 구현하려면:
- 파생 클래스 자신의 멤버 변수들을 복사해야 하고
- 기본 클래스(부모 클래스)로부터 상속받은 부분도 올바르게 복사해야 합니다
그런데 기본 클래스의 복사 대입 연산자가 private이면, 파생 클래스에서 이를 호출할 수 없습니다.
다시 말하면 복사를 제대로 하려면 부모 클래스의 부분도 제대로 복사를 해야하는데 priavte이면 접근이 불가해서
안된다는 의미입니다.
이것만은 잊지 말자!
- 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다.
항목 6
https://yeoul0714.tistory.com/32
[Effective C++] 항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
1. 개요블로그의 주인장인 여울은 게임을 사랑하는 개발자이다. 여울은 게임을 소개하는 프로그램을 만들었데 이 프로그램에는 게임을 나타내는 클래스가 있다.class Game {}; 그런데 이세상에 똑
yeoul0714.tistory.com
'Effective C++ > Chapter 2: 생성자, 소멸자 및 대입 연산자' 카테고리의 다른 글
[Effective C++] 항목 10: 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2025.04.23 |
---|---|
[Effective C++] 항목 9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2025.04.21 |
[Effective C++] 항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 (0) | 2025.04.19 |
[Effective C++] 항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2025.04.18 |
[Effective C++] 항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2025.04.18 |