Rule of Three (복사생성자, 대입연산자, 소멸자)

출처 : Acceleated C++ (Chapter 11)






1 복사생성자
2 대입 연산자


2.1 대입 연산자 와 복사 생성자 의 구분 및 차이

3 소멸자
4 디폴트 연산
5 Rule of Three
6 Comment


1 복사생성자 #

함수에 객체를 값으로 전달하거나 함수로부터 객체를 값으로 리턴하는 것은, 그 객체를 복사한다는 의미를 내포한다.
vector<int> vi;
double d;
d = median(vi); // vi를 median의 매개변수로 넘긴다.(복사생성자 호출)
string line = “어쩌구저쩌구 asdf yoway 바하무트”;
vector<string> words = split(line);
///< split의 결과값으로 words를 생성한다.(복사 생성자 호출)

또는
vector<string> vs1;

vecotr<string> vs2 = vs1; // vs2에 vs1을 복사. (복사생성자 호출)


2 대입 연산자 #



template <class T> class Vec{
public:
Vec& operator=(const Vec& );
};

과 같은 형식으로 구현되는 연산자이다. 클래스가 대입 연산자의 오버로드된 여러 인스턴스(즉, 인자들의 타입만 다른)를 정의할수 있지만 그중에서도 클래스 자체에 대한 const 레퍼런스를 취하는 버전은 좀 특별하다. 그 버전은 한 클래스 타입의 값을 다른 클래스에 대입한다는 의미를 갖는다. 그 클래스에 대한 여러 버전의 operator= 함수가 있어도, 특히 상위 버번을 대입연산자(assignment operator) 라고 부른다.

대입의 가장 기본적인 동적은 기존의 값을 제거하고, 새로운 값으로 대체한다. 이다.

또한 대입은 자기-대입(Self-Assignment)에 대한 고려를 해야한다. 동적할당한 포인터를 가지고 있는 객체의 경우 대입연산으로 자기 자신을 대입하게 될 경우 기존의 동적할당횐 포인터를 해제하는 과정에서 오류가 발생하는 경우가 있기 때문이다.


template <class T>
Vec<T>& Vec<T>::operator=(const Vec& rhs)
{
//자기 대입인지 체크
if( &rhs != this )
{
// 왼항의 메모리 해제 등등
uncreate();

// 오른항의 값으로 복사작업
…..
}
return *this;
}



2.1 대입 연산자 와 복사 생성자 의 구분 및 차이 #

복사를 수행하는 시점은 그 객체를 처음 만든 시점이기 때문에 할당을 해제할 기존의 객체는 존재하지 않지만 대입은 객체의 기존값을 존재하고 있다는 점이 다르다.

= 연산자의 경우 대입과 복사 두가지에 모두 쓰여 구별하기 어려울지 모르지만 위에서 언급했듣이 복사 생성자가 호출되는 경우는 객체가 생성되는 시점이라는 것만 기억하고 있으면 된다. (이른바 변수의 생성지 초기화가 되는 방법을 지정하는 것이다.)





vector<string> vs1;

vecotr<string> vs2 = vs1; // vs2에 vs1을 복사. (복사생성자 호출)

초기화는 다음과 같은 상황에 발생한다.

  • 변수 선언시
  • 함수 진입시, 함수 매개변수에 의해서 (call by value)
  • 함수로 부터 리턴시, 함수의 리턴값에 대해 (call by value)
  • 생성자 초기 설정자에서
그리고 대입은 = 연산을 사용할때문 발생한다.


string url = http://lagoons.net”;      // 초기화(const char* 를 취하는 생성자)
string space(url.size(), ‘ ‘); // 초기화
string s; // 초기화
string c(url); // 초기화(복사생성자)

s = c; // 대입


위의 코드를 보며 복사 생성자가 호출될때와 대입이 호출될 때를 생각해보면 된다. 함수의 호출을 통한 예를 살펴보면


stirng func(string);    // 함수 원형

string s; // 초기화
string res = func(s); // s가 매개변수로 넘어가면서 초기화(복사생성자)
// func의 결과인 임시 string 객체의 생성 후
// res가 임시 string 객체로 복사생성자 호출

string s; // 초기화
string res; // 초기화
res = func(s) // s가 매개변수로 넘어가면서 초기화(복사생성자)
// res에게 func의 결과로 생긴 임시 string 객체를 대입


복사생성자와 대입을 구분해야 하는 이유는, 실제로 그 둘은 실행시 서로 다른 연산을 수행하는 것이기 때문이다.





정리

  • 생성자는 항상 초기화를 수행한다.
  • operator= 맴버함수는 항상 대입을 수행한다.


3 소멸자 #

생성된 객체는 생성되었던 스코프를 벗어나는 직시 소멸하게 되며 이때 이 객체가 사용했던 자원을 반납하는 역할을 소멸자가 맡게 된다.

소멸자의 경우는 프로그램 예외가 발생시에도 호출을 보장받게 되므로 이 곳에서 사용한 자원을 반납한다면 비정상적인 작동시라도 리소스의 누스가 발생하는 것을 막을수 있다.


4 디폴트 연산 #

간단하게 구현된 struct의 경우 위에서 언급했던 생성자, 대입 연산자, 소멸자 등을 구현해 주지 않아도 사용하는데 무리가 없었으며 원하는 대로 작동했다. 이것은 컴파일러에서 해당 연산자가 없을 경우 기본적인 연산자를 자동으로 제공해주기 때문에 가능한것이다.


5 Rule of Three #

메모리 같은 리소소를 관리하는 클래스들은 복사를 처리하는 작업에 신경을 많이 써야 한다.
하지만 위에서 언급한 디폴트 연산이 꼭 원하는 대로만 작동하리라는 보장은 없다.
일반적으로 클래스 내부에 포인터를 가지고 동적할당을 사용하는 경우 디폴트 연산자의 대입 연산자는 포인터가 가르키는 버퍼의 크기만큼 새로 할당해서 그곳에 복사하려는 객체의 포인터가 가르키는 버퍼의 값을 복사한뒤 대입하는 포인터에 그 버퍼의 주소를 넣는 동작을 원하겠지만 디폴트 연산의 대입연산자는 그저 포인터의 값만을 복사하게 될것이다.
또한 소멸자의 경우도 동적할당한 버퍼를 해제시키지 않는다.

리소스를 할당이 필요한 객체 T에 대한 복사, 소멸을 적절히 구현하기 위해서는 다음과 같은 것들이 필요하다.





T::T() // 생성자
T::~T() // 소멸자
T::T(const T& ) // 복사 생성자
T::operator=(const T&) //대입연산자

초기화시 리소스를 할당하는 코드가 필요하다면 소멸자에서는 그 리소를 해제하는 코드가 필요하며 대입에서는 리소스의 해제와 복사에 대한 적절한 처리가 필요하다.
즉 복사 생성자, 소멸자, 대입 연산자는 밀접한 연관이 있으며 그들관의 관계를 Rule of Three 라고 표현한다.

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다