[Effective C++] 항목5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자

Jangmanbo·2023년 2월 8일
0

Effective C++

목록 보기
5/33

사용자가 직접 클래스 안에 선언하지 않아도 컴파일러가 알아서 선언하는 함수에는 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자가 있다. 모두 public이자 inline 함수로 선언된다.

class Empty{};

즉, 이 Empty 클래스와

class Empty{
public:
	Empty() { ... }
    Empty(const Empty& rhs) { ... }
    
    ~Empty() { ... }
    
    Empty& operator=(const Empty& rhs) { ... }
};

이 Empty 클래스는 근본적으로 동일하다.

Empty e1;		// 기본 생성자, 소멸자

Empty e2(e1);	// 복사 생성자

e2 = e1;		// 복사 대입 연산자

단 이 함수들은 컴파일러가 필요하다고 판단할 때만 만들어진다.
예를 들어 Empty e1; 코드가 존재하는 경우에만 기본 생성자와 소멸자가 만들어진다.


  • 기본 생성자
    • 새로운 객체를 메모리에 만드는 데 필요한 과정을 제어하고 객체를 초기화
    • 기본 클래스 및 비정적 데이터 멤버의 생성자 호출
  • 소멸자
    • 객체가 메모리에서 해제될 수 있도록 하는 과정을 제어
    • 기본 클래스 및 비정적 데이터 멤버의 소멸자 호출
    • 해당 클래스가 상속한 기본 클래스의 소멸자가 virtual이 아니라면 마찬가지로 비가상 소멸자
  • 복사 생성자
    • 원본 객체의 비정적 데이터를 사본 객체로 복사
  • 복사 대입 연산자
    • 원본 객체의 비정적 데이터를 사본 객체로 복사

복사 생성자

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<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);		// 복사 생성자 호출

컴파일러가 만든 복사 생성자는 no1nameValueobjectValue를 사용해서 no2nameValueobjectValue를 각각 초기화한다.

  • string nameValue의 경우 string 타입은 자체적으로 복사 생성자가 있으므로 string의 복사 생성자를 호출하면서 초기화한다.
  • T, 즉 int objectValue는 기본 제공 타입이므로 no1.objectValue의 각 비트를 그대로 복사하여 초기화한다.

복사 대입 연산자

컴파일러가 만든 복사 대입 연산자 또한 근본적으로 복사 생성자와 동일하다.

// 아까 정의한 NamedObject와 비교하며 보기
template<typename T>
class NamedObject {
public:
	// const char* name을 인자로 취하는 생성자 삭제
    // string& nameValue이기 때문
	NamedObject(const 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;	// 대입 연산

psnameValue는 string 객체를 참조하고 있다.
이때 대입 연산이 일어나면

  • p.nameValues.nameVlaue가 참조하는 string을 참조하게 될까?
    • C++에서 참조자는 원래 자신이 참조하고 있는 것과 다른 객체를 참조할 수 없다.
  • p.nameValue가 참조하는 객체 자체가 바뀌는 걸까?
    • 해당 string에 대한 포인터나 참조자를 품은 다른 객체들(대입 연산에 직접적으로 관여하지 않는 객체)까지 영향을 받게 된다.

결국 컴파일러는 이런 상황에 대해 컴파일을 거부한다.

따라서 참조자(ex. nameValue)를 데이터 멤버로 갖는 클래스의 경우 직접 대입 연산자를 정의해야 한다.
상수(ex. objectValue)를 데이터 멤버로 갖는 경우도 마찬가지이다. 상수 객체를 수정하는 것은 문법에 어긋나기 때문이다.



정리
1. 컴파일러는 경우에 따라 클래스의 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 생성할 수 있다.
2. 참조자나 상수를 데이터 멤버로 갖는 클래스는 직접 대입 연산자를 정의해야 한다.

0개의 댓글