개요
빈 클래스의 경우, 상황에 따라 컴파일러는 기본 생성자 / 소멸자 / 복사 생성자 / 복사 대입 연산자를 public, inline으로 선언해 놓는다. 어떤 상황에서 자동으로 생성되는지 살펴보자.
// 자동 생성 예시
class Empty{
public:
// 생성자
Empty() { }
// 소멸자 : 가상 여부에 따라 다름
~Empty() { }
// 복사 생성자
Empty(const Empty& rhs) { }
// 복사 대입 연산자
Empty& operator=(const Empty& rhs) { }
};
1. 기본 생성자 / 소멸자
Empty e1; // 기본 생성자, 소멸자 선언
클래스의 인스턴스를 생성하는 경우 해당 클래스에 생성자, 소멸자가 선언되어 있지 않으면 컴파일러는 자동으로 기본 생성자와 소멸자를 public, inline으로 선언한다.
소멸자의 경우 클래스가 상속한 기본 클래스의 소멸자가 가상 소멸자로 되어 있지 않으면 비가상 소멸자로 만들어진다는 점을 유의하자.
2. 복사 생성자
Empty e2(e1); // 복사 생성자 선언
클래스의 인스턴스를 복사 생성자를 통해 초기화하는 경우 해당 클래스에 복사 생성자가 선언되어 있지 않으면 컴파일러는 자동으로 복사 생성자를 public, inline으로 선언한다.
이때 복사 생성자는 원본 객체의 비정적 데이터를 사본 객체 쪽으로 복사하는 일을 한다.
세부적인 작동원리를 살펴보자.
// NamedObject.h
template<typename T>
class NamedObject {
public:
NamedObject(char* name, const T& value);
NamedObject(string& name, const T& value);
// 암시적으로 선언된 복사 생성자
NamedObject(const NamedObject& rhs)
: nameValue(rhs.nameValue), objectValue(rhs.objectValue)
{ }
private:
string nameValue;
T objectValue;
};
// main.cpp
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1); // 복사 생성자 선언 및 호출
no1.nameValue, no1.objectValue를 no2에 복사하는 과정에서, no2.nameValue의 초기화는 string의 복사 생성자에 no1.nameValue를 인자로 넘겨 호출함으로써 이루어진다. 반면에, objectValue는 기본제공 타입인 int이므로 복사 생성자 호출 없이 no1.objectValue의 각 비트를 그대로 복사해오는 것으로 끝난다.
자동으로 선언된 복사 생성자의 동작 방식을 보면, 초기화 리스트를 활용하여 초기화를 진행하고 있기 때문에 nameValue, objectValue가 const 혹은 참조형태여도 문제가 발생하지 않는다.
3. 복사 대입 연산자
Empty e1;
Empty e2;
e2 = e1; // 복사 대입 연산자 선언
복사 대입 연산자의 경우 복사 생성자와 다르게, 멤버 변수가 참조 혹은 const 형태인 경우 문제가 발생한다.
// NamedObject.h
template<typename T>
class NamedObject {
public:
NamedObject(string& name, const T& value);
// 암시적으로 선언된 복사 대입 연산자
NamedObject& operator=(const NamedObject& rhs)
{
nameValue = rhs.nameValue; // string의 복사 대입 연산자 호출
objectValue = rhs.objectValue;
return *this;
}
private:
string& nameValue; // 참조 형태
const T objectValue; // const
};
// main.cpp
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2("Any Number", 10);
no1 = no2; // 컴파일 에러 발생
nameValue는 참조 형태이고, objectValue는 const 형 변수이다. 암시적으로 선언된 복사 대입 연산자의 작동 방식을 보면, no1.nameValue에 no2.nameValue를 대입하려고 하는 것을 볼 수 있다. 이는 참조 형태, const 형 변수인 경우 위배되는 방식이다. 따라서 복사 생성자와 달리 복사 대입 연산자에서는 멤버 변수가 참조 형태, const인 경우 컴파일 에러가 발생한다.
4. 암시적 생성 방지
(1) private 정의
class NoAutoCreate{
private:
NoAutoCreate() { };
~NoAutoCreate() { };
NoAutoCreate(const NoAutoCreate& rhs) { };
NoAutoCreate& operator=(const NoAutoCreate& rhs) { };
};
private으로 생성자, 소멸자, 복사 생성자, 복사 대입 연산자를 선언하는 경우 컴파일러는 암시적으로 해당 함수들을 선언하지 않는다. 하지만 위의 경우 class 내부 혹은 friend 함수/클래스에서는 활용이 가능하므로 더 확실하게 차단하고자 한다면 정의를 배제하거나 다음과 같이 delete를 활용하여 작성하면 된다.
(2) delete
class NoAutoCreate{
public:
NoAutoCreate() = delete;
~NoAutoCreate() = delete;
NoAutoCreate(const NoAutoCreate& rhs) = delete;
NoAutoCreate& operator=(const NoAutoCreate& rhs) = delete;
};
(3) Uncopyable 클래스 상속
class Uncopyable{
protected:
Uncopyable() { }
~Uncopyable() { }
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const UnCopyable&);
};
class Something : private Uncopyable{
...
};
Uncopyable 클래스는 데이터 멤버가 전혀 없기 때문에 공백 기본 클래스 최적화(empty base class optimization, EBO) 기법이 먹혀 들어갈 여지가 있다.하지만, Uncopyable을 상속받는 구조로 구현하게 될 경우 다중 상속으로 갈 가능성이 있으니 유의해야 한다. 부스트 라이브러리에도 Uncopyable과 똑같은 구실을 하는 클래스인 noncopyable이 있다고 하니 취향에 맞게 사용하면 될 것 같다.
'C++ > [서적] Effective C++' 카테고리의 다른 글
| [Effective C++] 06. 대입 연산자는 *this의 참조자를 반환하게 하자 (0) | 2025.11.28 |
|---|---|
| [Effective C++] 05. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 (0) | 2025.11.28 |
| [Effective C++] 04. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2025.11.28 |
| [Effective C++] 02. 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2025.11.28 |
| [Effective C++] 01. #define을 쓰려거든 const, enum, inline을 떠올리자 (1) | 2025.11.26 |