이펙티브 C++
https://www.yes24.com/Product/Goods/17525589
페이지 80p ~ 89p
요약
2. 생성자, 소멸자 및 대입 연산자
- 거의 모든 C++ 클래스에 한 개 이상 꼭 들어 있는 것들이 생성자, 소멸자, 대입 연산자이다.
- 생성자는 새로운 객체를 메모리에 만드는 데 필요한 과정을 제어하고 객체의 초기화를 담당한다.
- 소멸자는 객체를 없앰과 동시에 그 객체가 메모리에서 적절히 사라질 수 있도록 하는 과정을 제어한다.
- 대입 연산자는 기존의 객체에 다른 객체의 값을 줄 때 사용한다.
항목 5: C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
- 클래스가 비어 있지만(empty) 비어 있는게 아닌 때가 언제일까?
- 일단은 C++ 컴파일러가 빈 클래스를 훑고 지나갈 때라고 답할 수 있다.
- 어떤 멤버 함수는 클래스 안에 직접 선언해 넣지 않으면 컴파일러가 저절로 선언해 주도록 되어 있는데, 다음의 함수들은 그러한 대상들이다.
- 복사 생성자
- 복사 대입 연산자
- 소멸자
- 컴파일러가 자동으로 만들어 주는 함수들은 모두 기본형이며, 생성자조차도 선언되어 있지 않으면 컴파일러가 기본 생성자를 대신 선언해 놓는다.
- 이 함수들은 모두 public이며, inline이다.
// 이렇게 선언된 클래스는
// class Empty {};
// 컴파일러에 의해 이런 코드가 추가된다.
class Empty {
public:
// 기본 생성자
Empty() {}
// 복사 생성자
Empty(const Empty& rhs) {}
// 소멸자
~Empty() {}
// 복사 대입 연산자
Empty& operator=(const Empty& rhs) {}
};
//Empty e1; // 기본 생성자
//Empty e2(e1); // 복사 생성자
//
//e2 = e1; // 복사 대입 연산자
- 기본 생성자와 소멸자가 하는 일은 일차적으로 컴파일러에게 배후의 코드를 깔 수 있는 자리를 마련하는 것이다.
- 기본 클래스 및 비정적 데이터 멤버의 생성자와 소멸자를 호출하는 코드가 여기서 생겨난다.
- 소멸자는 이 클래스가 상속한 기본 클래스의 소멸자가 가상 소멸자로 되어 있지 않으면 비가상 소멸자로 만들어진다.(중요!)
- 복사 생성자와 복사 대입 연산자는 원본 객체의 비정적 데이터를 사본 객체 쪽으로 복사하는 것이 전부이다.
template<typename 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("SmallestPrimeNumber", 2);
NamedObject<int> no2(no1); // 복사 생성자 호출됨
- 컴파일러가 만든 복사 생성자는
no1.nameValue
,no1.objectValue
를 이용해no2.nameValue
,no2.objectValue
를 각각 초기화한다.nameValue
의 타입은 string이고, 표준 string 타입은 자체적으로 복사 생성자를 갖고 있으므로no2.nameValue
의 초기화는 string의 복사 생성자에no1.nameValue
를 인자로 넘겨 호출함으로써 이루어진다.objectValue
는 현재 int이며 기본제공 타입이기 때문에 값 복사가 진행된다.
- 하지만 컴파일러는 합리적인 경우에만 자동으로 코드를 생성해준다. 다음 케이스를 보자.
template<typename T>
class NamedObject {
public:
// nameValue가 비상수 string의 레퍼런스이기 때문에 name은 상수 타입이 아니다.
// 참조할 string을 가져야 하기 때문에 char*는 없앰
NamedObject(std::string& name, const T& value);
private:
std::string& nameValue; // 참조자
const T objectValue; // 상수
};
- 만약 여기에서 다음과 같은 상황이 발생하면 어떨까?
std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
// ???
p = s;
- 대입 연산이 과연 자동으로 만들어질까?
nameValue
는 레퍼런스 타입이기 때문에 다른 레퍼런스 타입을 대입 연산자로 처리할 수 없다.objectValue
는 생성자에서 값이 초기화되고 나면 값이 변할 수 없는 상수이기 때문에 평범하게 대입 연산자를 사용할 수 없다.
- 이렇게 애매한 경우에는 C++ 컴파일러가 컴파일 거부를 하게 된다.
- 레퍼런스는 원래 자신이 참조하고 있는 것과 다른 객체는 참조할 수 없다.
- 상수는 값을 변경할 수 없다.
- 따라서 레퍼런스를 데이터 멤버로 갖고 있는 클래스에 대입 연산을 지원하려면 직접 대입 연산자를 정의해 주어야 한다.
- 또한, 복사 대입 연산자를 private으로 선언한 기본 클래스로부터 파생된 클래스는 컴파일러가 자동으로 생성해주는 복사 대입 연산자를 가질 수 없다.(컴파일러가 거부!)
컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있다.
항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
- 복사 생성자, 복사 대입 연산자 등이 컴파일러에 의해 자동으로 만들어지는 것을 막기 위해 함수를 직접 선언해야 하는 것은 맞지만, 이것들을 private으로 선언하면 자동 생성된 함수를 사용하지 않게 막을 수 있다.
- 일단 함수가 명시적으로 선언된다.
- 그렇기 때문에 컴파일러가 기본형으로 함수를 만들 수 없다.
- private 접근 제한자를 가지기 때문에 외부로부터의 호출을 차단할 수 있다.
- 그러나 friend 함수가 호출할 수 있다는 허점이 있다.
- friend 함수에 의한 호출까지 막으려면 선언만 하는 방법이 있다.
- 이는 C++의 iostream라이브러리에 속한 몇몇 클래스에서도 복사 방지책으로 사용되는 기법이다.
- 이렇게 되면 링크 시점 에러가 발생한다.
class HomeForSale {
public:
private:
// 선언만 있음
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
}
- 더 좋은 방법은 링크 시점 에러를 컴파일 시점 에러로 옮기는 것이다.
- 복사 생성자와 복사 대입 연산자를 해당 클래스 자체에 넣지 말고 별도의 기본 클래스에 넣고, 기본 클래스로부터 클래스를 파생시키는 것이다.
class Uncopyable {
protected:
// 파생 클래스에 생성과 소멸 허용
Uncopyable() {}
~Uncopyable() {}
private:
// 복사 생성자, 복사 대입연산자 방지
Uncopyable(const Uncopyable&);
Uncopyable&(const Uncopyable&);
}
// 복사를 방지하고 싶은 클래스
class HomeForSale: private Uncopyable {}
HomeForSale
객체의 복사를 외부(멤버 함수, friend 함수 포함)에서 시도하려고 할 때 컴파일러는HomeForSale
클래스만의 복사 생성자와 복사 대입 연산자를 만들려고 할 것이다.- 컴파일러가 생성하려는 복사 함수는 기본 클래스의 대응 버전을 호출하게 되어 있다.
- 복사 함수들이 기본 클래스에서 private으로 선언되어 있기 때문에 컴파일러는 복사 함수를 생성할 수 없다.
Uncopyable
클래스는 private으로 상속해도 문제 없으며, 가상 소멸자가 아니어도 된다.
컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private으로 선언한 후에 구현은 하지 말자. Uncopyable과 비슷한 베이스 클래스를 쓰는 것도 좋은 방법이다.
항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
- tbd
메모
'책 > 이펙티브 C++' 카테고리의 다른 글
이펙티브 C++(99p ~ 111p) (0) | 2024.01.14 |
---|---|
이펙티브 C++(89p ~ 99p) (1) | 2024.01.12 |
이펙티브 C++(71p ~ 79p) (1) | 2024.01.10 |
이펙티브 C++(61p ~ 70p) (0) | 2024.01.09 |
이펙티브 C++(51p ~ 60p) (1) | 2024.01.08 |