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

·2024년 12월 19일
0

C++

목록 보기
26/26

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

컴파일러가 선언해 주는 함수

  • 기본 생성자
  • 소멸자
  • 복사 생성자
  • 복사 대입 연산자
class Empty
{
public:
	// 기본 생성자
	Empty() { ... } // 생성자를 하나라도 만들면 컴파일러는 기본 생성자를 선언하지 않음
    // 소멸자
    ~Empty() { ... }
    // 복사 생성자
    Empty(const Empty& rhs) { ... }
    // 복사 대입 연산자
    Empty& operator=(const Empty& rhs) { ... }
};

참조자를 데이터 멤버로 갖고 있는 클래스에 대입 연산을 지원하기 위해서는 직접 복사 대입 연산자를 정의해 주어야 함

template<class T>
class NamedObject
{
public:
	NamedObject(std::string& name, cosnt T& value);
    
private:
	std::string& nameValue;
    const T objectValue;
};

int main()
{
	std::string newDog("Persephone");
	std::string oldDog("Satch");
    
    NamedObject<int> p(newDog, 2);
    NamedObject<int> s(oldDog, 36);
    
    p = s; // 컴파일 에러
}

데이터 멤버가 상수 객체인 경우에도 C++ 컴파일러가 비슷하게 동작함
상수 멤버를 수정하는 것은 문법에 어긋나기 때문에 자동으로 만들어진 복사 대입 연산자 내부에서는 상수 멤버를 어떻게 처리해야할지 모호해짐

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

컴파일러에서 자동으로 제공하는 기능을 허용치 않으려면 대응되는 멤버 함수를 private으로 선언한 후에 구현은 하지 않은 채로 두면 됨
하지만 이 경우에는 friend멤버 함수를 통해서는 접근이 가능하기 때문에 delete를 사용하여 함수를 삭제하는 것이 좋음

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

소멸자는 파생 클래스 -> 기본 클래스 순으로 호출됨
기본 클래스를 사용하여 파생 클래스의 메모리를 해제하는 경우 virtual 키워드를 사용해야만 파생 클래스의 소멸자부터 호출됨
이렇게 하지 않으면 파생 클래스의 힙 영역에 저장된 메모리가 있는 경우 제대로 해제가 되지 않아 누수가 발생될 수 있음

가상 함수를 C++에서 구현하려면 클래스에 별도의 자료구조(vptr)가 하나 들어가야 함
vptr은 가상 함수의 주소, 즉 포인터들의 배열을 가리키고 있으며 가상 함수 테이블 포인터의 배열은 vtbl이라고 불림

vptr이 추가 되면 객체의 크기가 커지기 때문에 모든 객체의 소멸자를 virtual로 선언해서는 안 됨
가상 소멸자를 선언하는 것은 그 클래스에 가상 함수가 하나라도 들어 있는 경우로 한정해야 함

경우에 따라 순수 가상 소멸자를 두면 편리하게 사용할 수 있음
순수 가상 함수는 해당 클래스를 추상 클래스로 만드는데 마땅히 넣을 만한 순수 가상 함수가 없는 경우가 종종 생김
이럴 때 순수 가상 소멸자를 선언하면 됨

예외가 소멸자를 떠나지 못하도록 붙들어 놓자

소멸자에서는 예외가 빠져나가면 안 됨
만약 소멸자 안에서 호출되 함수가 예외를 던질 가능성이 있다면 어떤 예외이든지 소멸자에서 모두 받아낸 후에 삼켜 버리던지 프로그램을 끝내든지 해야 함
어떤 클래스의 연산이 진행되다가 던진 예외에 대해 사용자가 반응해야 할 필요가 있다면, 해당 연산을 제공하는 함수는 반드시 보통의 함수(즉, 소멸자가 아닌 함수)이어야 함

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

기본 클래스의 생성자가 호출될 동안에는 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않음
그 대신 객체 자신이 기본 클래스 타입인 것처럼 동작함
기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아님
어쩌다 호출된 가상 함수가 파생 클래스 쪽으로 내려간다면 파생 클래스 버전의 가상 함수는 초기화 되지 않은 데이터 멤버를 건드리게 되므로 미정의 동작이 이루어짐

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

int x, y, z;
x = y = z = 15;

대입이 사슬처럼 이루어지는 이유는 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있기 때문임
단순히 대입형 연산자 말고도 모든 형태의 대입 연산자에서 지켜져야 함
관례이기 때문에 지키지 않는다고 컴파일이 안 된다거나 하지는 않지만 모든 기본제공 타입들이 따르고 있을 뿐만 아니라 표준 라이브러리에 속한 모든 타입에서도 따르고 있으므로 지키는 것이 좋음

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

자기 대입

어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것
같은 타입으로 만들어진 객체 여러 개를 참조자 혹은 포인터로 물어 놓고 동작하는 코드를 작성할 때는 같은 객체가 사용될 가능성을 고려하는 것이 일반적으로 바람직한 자세임

Widget& Widget::operator=(const Widget& rhs)
{
	Bitmap& pOrig = pb;
    pb = new Bitmap(*rhs.pb);
    delete pOrig;
    
    return *this;
}

이렇게 코드를 짜면 new Bitmap 부분에서 예외가 발생하더라도 pb는 변경되지 않은 상태로 유지되기 때문에 안전함
또한 일치성 검사 같은 것이 없음에도 불구하고 이 코드는 자기대입 현상을 완벽히 처리하고 있음

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

객체 복사 함수는 주어진 객체의 모든 데이터 멤버 및 모든 기본 클래스 부분을 빠뜨리지 말고 복사해야 함
클래스의 복사 함수 두 개를 구현할 때, 한쪽을 이용해서 다른 쪽을 구현하려는 시도는 하지 말고 공통된 동작을 제3의 함수에다 분리해 놓고 양쪽에서 이것을 호출하게 만들어서 해결해야 함

0개의 댓글

관련 채용 정보