본문 바로가기
프로그래밍/Effective C++

Chapter 2 생성자, 소멸자 및 대입 연산자

by Ohdumak 2018. 3. 28.

2018/03/19 - [프로그래밍/Effective C++] - Chapter 1 C++에 왔으면 C++의 법을 따릅시다


Chapter 2 생성자, 소멸자 및 대입 연산자


C++ 클래스에 한 개 이상 꼭 들어 있는 것들이 생성자와 소멸자, 대입 연산자이다.


생성자: 새로운 객체를 메모리에 만드는 데 필요한 과정을 제어하고 객체의 초기화를 맡는 함수

소멸자: 객체를 없앰과 동시에 그 객체가 메모리에서 적절히 사라질 수 있도록 하는 과정을 제어하는 함수

대입 연산자: 기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수


클래스를 제대로 쓰려면 이들이 우선 우뚝 서 있어야 함은 분명하고도 중요한 요구사항이다.



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


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) { ... }	// 복사 대입 연산자
};

컴파일러가 만들어주는 복사 대입 연산자도 근본적으로는 동작 원리가 똑같다. 하지만 일반적인 것만 놓고 보면, 이 복사 대입 연산자의 최종 결과 코드가 '적법해야(legal)' 하고 '이치에 닿아야만(resonable)' 한다. 둘 중 어느 검사도 통과하지 못하면 컴파일러는 operator=의 자동 생성을 거부한다.


이것만은 잊지 말자!

- 컴파일러는 경우에 따라 클래스에 대해 기본 생성자, 복사 생성자, 복사 대입 연산자, 소멸자를 암시적으로 만들어 놓을 수 있습니다.



항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자


복사 생성자와 복사 대입 연산자는 외부에서 호출하려고 하면 컴파일러가 대신 선언한다.


public 멤버로 두지 말고, 복사 생성자 및 복사 대입 연산자를 private 멤버로 선언하도록 하자. private 멤버 함수는 그 클래스의 멤버 함수 및 프랜드(friend)함수가 호출 할 수 있기 때문에 '정의(define)'를 하지 않는다.

[멤버 함수를 private 멤버로 선언하고 일부러 정의(구현)하지 않는 방법]은 꽤 널리 퍼지면서 하나의 '기법'으로 굳어지기 까지 했다.

class HomeForSale {
public:
	...
private:
	...
	HomeForSale(const HomeForSale&);		// 선언만
	HomeForSale& operator=(const HomeForSale&);
};


복사 생성자와 복사 대입 연산자를 private로 선언하되, 이것을 HomeForSale 자체에 넣지 말고 별도의 기본 클래스에 넣고 이것으로 부터 HomeForSale을 파생시키는 방법

class Uncopyable {
protected:					// 파생된 객체에 대해서
	Uncopyable() {}		// 생성과 소멸 허용
	~Uncopyable() {}
private:
	Uncopyable(const Uncopyable&);	// 복사 방지
	Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale: private Uncopyable { // 복사 생성자도, 복사 대입 연산자도 선언 안됨
	...
};

부스트 라이브러리의 noncopyable로 대체해서 사용 가능


이것만은 잊지 말자!

- 컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면, 대응되는 멤버 함수를 private로 선언한 후에 구현은 하지 않으 채로 둔다. Uncopyable과 비슷한 기본 클래스를 쓰는 것도 한 방법이다.



항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가성 소멸자로 선언하자


기본 클래스의 소멸자 앞에 virtual을 붙이면 객체 전부가 소멸된다.


기본 클래스의 손에 가상 소멸자를 쥐어 주자는 규칙은 다형성(polymorphic)을 가진 기본 클래스 인터페이스를 통해 파생 클래스 타입의 조작을 허용하도록 설계된 기본 클래스에만 적용된다.


모든 기본 클래스가 다형성을 갖도록 설계된 것은 아니다. 표준 string 타입, STL 컨테이너 타입은 다형성의 흔적조차 볼 수 없다.


이것만은 잊지 말자!

- 다형성을 가진 기본 클래스에는 반드시 가상 소멸자를 선언해야 한다. 즉, 어떤 클래스가 가상 함수를 하나라도 갖고 있으면, 이 클래스의 소멸자도 가상 소멸자이어야 한다.

- 기본 클래스로 설계되지 않았거나 다형성을 갖도록 설계되지 않은 클래스에는 가상 소멸자를 선언하지 말아야 한다.



항목 8: 예외 소멸자를 떠나지 못하도록 붙들어 놓자


소멸자로부터 예외가 터져 나가는 경우를 C++ 언어에서 막는 것은 아니지만, 실제 상황을 들춰보면 확실히 막을 수밖에 없다.


C++는 예외를 내보내는 소멸자를 좋아하지 않는다.


어떤 동작이 예외를 일으키면서 실패할 가능성이 있고 또 그 예외를 처리해야 할 필요가 있다면, 그 예외는 소멸자가 아닌 다른 함수에서 비롯된 것이어야 한다는 것이 포인트다.

어찌 되었든 문제 해결의 칼자루를 먼저 쥔 쪽은 사용자이고, 칼을 뽑지 않기로 결정한 쪽 역시 사용자이다.


이것만은 잊지 말자!

- 소멸자에서는 예외가 빠져나가면 안된다. 만약 소멸자 안에서 호출된 함수가 예외를 던질 가능성이 있다면, 어떤 예외이든지 소멸저에서 모두 받아낸 후에 삼켜 버리든지 프로그램을 끝내든지 해야 한다.

- 어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 한다.



항목 9: 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자


호출 결과가 여러분이 원하는 대로 돌아가지 않을 것이다.


파생 클래스 객체의 기본 클래스 부분이 생성되는 동안은, 그 객체의 타입은 바로 기본 클래스이다. 


이것만은 잊지 말자!

- 생성자 혹은 소멸자 안에서 가상 함수를 호출하지 마세요. 가상 함수라고 해도, 지금 실행중인 생성자나 소멸자에 해당되는 클래스의 파생 클래스 쪽으로는 내려가지 않는다.



항목 10: 대입 연산자는 *this의 참조자를 반환하게 하자


C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 재미있는 성질을 갖고 있다.


int x, y, z;

x = y = z = 15;    // 대입이 사슬처럼 이어진다.


대입 연산이 가진 또 하나의 재미있는 특성은 바로 우측 연관(right-associative) 연산이다. 즉, 위의 대입 연산 사슬은 다음과 같이 분석된다.


x = (y = (z = 15));


이렇게 대입 연산이 사슬처럼 엮이려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있을 것이다. 이런 구현은 일종의 관례(convention)인데, 여러분이 나름대로 만드는 클래스에 대입 연산자가 혹시 들어간다면 여러분도 이 관례를 지키는 것이 좋다.

class Widget {
public:
	...
	Widget& operator=(const Widget& rhs)	// 반환 타입은 현재의 클래스에 대한 참조자이다.
	{
		...
		return *this;						// 좌변 객체(의 참조자)를 반환한다.
	}
	...
};

+=, -=, *= 등에도 동일한 규약을 적용하자.

대입 연산자의 매개변수 타입이 일반적이지 않은 경우에도 동일한 규약을 적용하자.


이것만은 잊지 말자!

- 대입 연산자는 *this의 참조자를 반환하도록 만들자



항목 11: operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자


자기대입(self assignment)은 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 말한다.


언뜻 보기에  명확하지 않은 이러한 자기대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태, 다시 말해 중복(aliasing)라고 불리는 것 때문이다. 같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세가 된다.


	Widget& operator=(const Widget& rhs) // 안전하지 않게 구현된 operator=
	{
		delete pb;
		pb = new Bitmap(*rhs.ps);
		
		return *this;
	}

전통적인 방법은 operator=의 첫머리에서 일치성 검사(identity test)를 통해 자기 대입을 점검하는 것이다.

	Widget& operator=(const Widget& rhs)
	{
		if (this == &rhs) return *this;	// 객체가 같은지 , 즉 자기대입인지 검사
											// 자기대입이면 아무것도 안 한다.
		delete pb;
		pb = new Bitmap(*rhs.ps);
		
		return *this;
	}

"많은 경우에 문장 순서를 세심하게 바꾸는 것만으로 예외에 안전한(동시에 자기대입에 안전한) 코드가 만들어 진다.

	Widget& operator=(const Widget& rhs)
	{
		Bitmap *pOrig = pb;		// 원래의 pb를 어딘가에 기억
		pb = new Bitmap(*rhs.ps);	// 다음, pb가 *pb의 사본을 가리키게 만든다
		delete pOrig;				// 원래의 pb를 삭제한다
		
		return *this;
	}

예외 안전성과 자기대입 안전성을 동시에 가진 operator=을 구현하는 방법으로, 방금 본 예처럼 문정의 실행 순서를 수작업으로 조정하는 것 외에 다른 방법은 '복사 후 맞바꾸기(copy and swap) 기법이다.


이것만은 잊지 말자!

- operator=을 구현할 때, 어떤 객체가 그 자신에 대입되는 경우를 제대로 처리하도록 만든다. 원본 객체와 복사대상 객체의 주소를 비교해도 되고, 문장의 순서를 적절히 조정할 수도 있으며, 복사 후 맞바꾸기 기법을 써도 된다.

- 두 개 이상의 객체에 대해 동작하는 함수가 있다면, 이 함수에 넘겨지는 객체들이 사실 같은 객체인 경우에 정확하게 동작하는지 확인해 보자



항목 12: 객체의 모든 부분을 빠짐없이 복사하자



객체의 안쪽 부분을 캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함는 복사 생성자와 복사 대입 연산자 딱 둘만 있다. 이 둘을 통틀어 객체 복사 함수(copying function)이라 부른다.


객체의 복사 함수를 작성할 때는 다음의 두 가지를 꼭 확인하라.

1. 해당 클래스의 데이터 멤버를 모두 복사

2. 클래스가 상속한 기본 클래스의 복사 함수도 꼬박꼬박 호출해 주도록 한다.


클래스의 양대 복사 함수는 본문이 비슷하게 나오는 경우가 자주 있어, 한쪽에서 다른 쪽을 호출하게 만들어서 코드 판박이를 피하면 좋겠다는 생각은 하지 말자.


양쪽에서 겹치는 부분을 별도의 멤버 함수에 분리해 놓은 후 이 함수를 호출하게 만들자.


이것만은 잊지 말자!

- 객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 한다.

- 클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 절대로 하지 말자. 그 대신, 공통된 동작을 제 3의 함수에다 분리해 놓고 야쪽에서 이것을 호출하게 만들어서 해결하자.

728x90

'프로그래밍 > Effective C++' 카테고리의 다른 글

Chapter 5 구현  (0) 2018.05.23
Chapter 4 설계 및 선언  (0) 2018.04.17
Chapter 3 자원 관리  (0) 2018.04.11
Chapter 1 C++에 왔으면 C++의 법을 따릅시다  (0) 2018.03.19
독자 여러분 반갑습니다.  (0) 2018.03.15

댓글