[Effective C++] 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
비어있지만, 비어있지 않은 class
C++에서는 직접 선언하지 않아도 복사 생성자(copy constructor), 복사 대입 연산자(copy assignment operator), 소멸자(destructor), 생성자를 기본형으로 선언해준다. 이들은 모두 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("Smallest Prime Nunber", 2);
NamedObject<int> no2(no1); // 복사 생성자 호출
즉, 위의 샘플 코드의 no2에서 복사 생성자를 호출하므로 기본 복사 생성자가 호출되며, no1.nameValue(string 복사 생성자 호출)와 no1.objectValue(int 비트 복사)는 각각 no2 멤버 변수에 복사된다.
복사 대입 연산자도 동작원리가 같지만 최종 결과 코드가 적법하고 합리적이어야만 operator=(복사 대입 연산자)가 생성된다.
복사 대입 연산자 주의점
위에서 말했던 것처럼, 적법하고 합리적인 조건에 부합하지 않으면 컴파일러는 복사 대입 연산자를 기본적으로 생성하지 않는다.
예를 들어보자.
template<class T>
class NamedObject
{
public:
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;
위의 코드는 불가능하다.
이유는 멤버 변수는 string& (참조자) 형이며 두 객체는 각각 다른 string 참조형을 가지고 있기 때문에 대입 연산이 불가능하다. C++ 참조자는 자신이 참조하고 있는 것과 다른 객체를 참조할 수 없기 때문이다.
즉, 참조자를 데이터 멤버로 갖고 있는 클래스에 대입 연산을 지원하려면 직접 복사 대입 연산자를 정의해주어야 하며, 이 외의 경우 컴파일러는 암시적 생성을 거부한다.
또한 데이터 멤버가 상수 객체인 경우에도 상수 멤버를 수정하는 것이 문법에 어긋나기 때문에 위와 동일하게 컴파일러는 암시적 생성을 거부한다.
마지막으로 복사 대입 연산자를 private로 선언한 기본 클래스로부터 파생된 클래스의 경우에도 암시적 복사 대입 연산자를 가질 수 없다. 파생 클래스의 경우 보통 기반 클래스를 참고하는데, private영역에 있어 접근이 불가능하기 때문이다.
요약
- 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있다.
'Books > Effective C++' 카테고리의 다른 글
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 (0) | 2022.05.13 |
---|---|
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 (0) | 2022.05.09 |
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자 (0) | 2022.05.07 |
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자! (0) | 2022.05.07 |
[Effective C++] 2. #define을 쓰려거든 const, enum, inline을 떠올리자 (0) | 2022.05.06 |
댓글
이 글 공유하기
다른 글
-
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
2022.05.13 -
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
2022.05.09 -
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
2022.05.07 -
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자!
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자!
2022.05.07