Rule of three : 이 규칙은 다음 중 하나라도 클래스에서 정의한다면, 아마도 반드시 모두 정의해야 할 것이라 주장합니다:
- 소멸자 (Destructor)
- 복사 생성자 (Copy Constructor)
- 복사 대입 연산자 (Copy Assignment Operator)
●C++는 다음과 같은 멤버 함수를 자동으로 제공한다. (더 있지만, Rule of three에 쓰이는 것만 설명하겠다)
- 디폴트 소멸자(파괴자) : 객체의 모든 클래스 타입 멤버의 소멸자를 호출합니다.
- 복사 생성자 : 객체의 모든 멤버를 복사 생성자의 인수로부터 해당 멤버의 복사 생성자를 호출하여 생성하며, 비-클래스 타입 (예: int나 포인터와 같은)의 데이터 멤버에는 평범한 대입을 수행합니다.
- 복사 대입 연산자 : 객체의 모든 멤버를 대입 연산자의 인수로부터 해당 멤버의 복사 대입 연산자를 호출하여 대입하며, 비-클래스 타입 데이터 멤버에는 평범한 대입을 수행합니다.
Rule of three는 위에서 설명한 자동으로 제공되는 디폴트 함수가 클래스에 적절하지 않아 1개라도 명시적으로 작성한다면, 나머지도 작성해야 한다는 것을 의미한다.
왜 그러한지 예시를 통해서 이야기해보자. 아래 사진은 이름 변수를 char * 포인터로 가지고 있는 클래스 Cat이다.
(구현은 생략하였다.)
임의로 작성을 하지 않는다면 아래 기본 복사 생성자, 기본 복사 대입 연산자, 기본 소멸자가 생성될 것이다.
위 기본 함수들의 문제는 변수들을 낮은 복사(swallow copy)하는 것이 문제를 발생시킨다.
아래의 코드는 모든 고양이의 이름을 "nabi"로 바꾼다. 여기서 사용되는 것은 기본 복사 생성자, 기본 복사 대입 연산자이다.
문제는 기본 복사 생성자, 기본 복사 대입 연산자를 쓸 때 낮은 복사를 사용함에 따라,
nabi의 변수 name이 가르키는 주소를 black, red, blue의 name변수가 똑같이 가리킨다.
아래의 코드는 추가된 부분만 보여주는 코드이다.
showName 함수를 추가하여 "객체의 이름, 이름, 이름이 가리키는 주소"를 출력해 주었다.
4마리 고양이 모두 *name이 가르키는 주소가 0x404016으로 같은 것을 알 수 있다!
따라서 4마리 중 한 마리라도 소멸자가 호출되어 *name을 소멸시킨다면, 나머지 3마리는 이름을 잃게 되는 상황이 벌어진다.
따라서 우리는 포인터가 변수로 존재한다면, 명시적으로 복사 생성자, 복사 대입 연산자, 소멸자를 선언하고 깊은 복사(deep copy)를 구현해야 할 책임을 가지게 된다.
nabi가 소멸자에 의해 삭제될 때, 나머지 3마리가 이름을 잃는 것이 원하는 바였다면 그냥 기본 함수를 쓰면 되나, 그것이 아니라면 저 3가지를 구현해야 하기 때문에 Rule of three 가 필요하다.
코드 전문
#include <iostream>
using namespace std;
class Cat
{
private:
char *name; // 고양이의 이름(포인터이다.)
public:
Cat(char *name) : name(name) {} // 작성한 생성자
void showName(char *originName)
{
cout << originName << " : " << name << ", *name이 가르키는 주소 : " << static_cast<void *>(name) << endl;
}
// Cat(const Cat &t) {} // 기본 복사 생성자
// Cat &operator=(const Cat &t) {} // 기본 복사 대입 연산자
// ~Cat() {} // 기본 소멸자
};
int main()
{
Cat nabi("nabi"); // 작성한 생성자 사용
Cat black(nabi); // 복사 생성자 사용
Cat red = nabi; // 복사 생성자 사용
Cat blue("blue");
blue = nabi; // 복사 대입 연산자 사용
nabi.showName("nabi");
black.showName("black");
red.showName("red");
blue.showName("blue");
}
잡담
사실 스마트 포인터를 사용하면 신경 쓰지 않아도 되는 문제이긴 하다. (따라서 이 규칙이 중요한 건지 아직 모르겠다.)
char 포인터 쓰지말고 string 쓰라고 컴파일러에서 경고를 보내기도 했는데 주소를 가르키는 것의 의미를 잘 보이게 하기 위해 포인터로 작성하였다.
추후에 Rule of five도 작성해야겠다.(C++11 이후 LRvalue의 개념 추가로 탄생한 규칙)
'C++ > C++' 카테고리의 다른 글
16. String 클래스와 표준 템플릿 라이브러리 (미완, 일반화 프로그래밍) (1) | 2023.09.18 |
---|---|
13. 클래스의 상속 (0) | 2023.09.14 |
Overloading (0) | 2023.07.28 |
12. 클래스와 동적 메모리 대입, +추가 필요 (0) | 2023.07.25 |
11. 클래스의 활용 (0) | 2023.07.19 |