항목 12 : 객체의 모든 부분을 빠짐없이 복사하자 객체의 안쪽 부분만 캡슐화한 객체 지향 시스템 중 설계가 잘 된 것들을 보면, 객체를 복사하는 함수가 딱 둘만 있는 것을 알 수 있습니다. 복사 생성자와 복사 대입 연산자라고, 성격에 따라 이름도 적절히 지어져 있습니다. 이 둘을 통틀어 객체 복사 함수라고 부릅니다. 객체 복사 함수는 컴파일러가 필요에 따라 만들어내기도 합니다. 그리고 컴파일러가 생성한 복사 함수는 비록 저절로 만들어졌지만 동작은 기본적인 요구에 아주 충실합니다. 복사되는 객체가 갖고 있는 데이터를 빠짐없이 복사한다라는 것입니다. 객체 복사 함수를 여러분인 선언한다는 것은, 컴파일러가 만든 녀석의 기본 동작에 뭔가 마음에 안 드는 것이 있다는 이야기입니다. 이에 대해 컴파일러도 썩 반기..
항목 11 : operator=에서는 자기대입에 대한 처리가 빠지지 않도록 하자 동적 할당된 비트맵을 가리키는 원시 포인터를 데이터 멤버로 갖는 클래스를 하나 만들었다고 가정해 봅시다. 1 2 3 4 5 6 7 class Bitmap { ... }; class Widget { ... private : Bitmap *pb; // 힙에 할당한 객체를 가리키는 포인터 }; Colored by Color Scripter 이젠 겉보기에 멀쩡해 보이는 operator=의 구현코드를 보시겠습니다. 1 2 3 4 5 6 7 8 Widget& Widget::operator=(const Widget& rhs) // 안전하지 않게 구현된 operator= { delete pb; // 현재의 비트맵 사용을 중지합니다. pb ..
항목 10 : 대입 연산자는 *this의 참조자를 반환하게 하자 C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 재미있는 성질을 갖고 있습니다. 1 2 3 int x, y, z; x = y = z = 15; // 대입이 사슬처럼 이어집니다. 대입 연산이 가진 또 하나의 재미있는 특성은 바로 우측 연관 연산이라는 점입니다. 이렇게 대입 연산이 사슬처럼 엮이려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있을 것입니다. 1 2 3 4 5 6 7 8 9 10 class Widget { public : ... Widget& operator=(const Widget& rhs) // 반환 타입은 현재의 클래스에 대한 참조자입니다. { ... return *this; // 좌변 객체(의 참조..
항목 9 : 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자 객체 생성 및 소멸 과정 중에는 가상 함수를 호출하면 절대로 안됩니다! 기본 클래스의 생성자가 호출될 도안에는, 가상 함수는 절대로 파생 클래스 쪽으로 내려가지 않습니다. 그 대신, 객체 자신이 기본 클래스 타입인 것처럼 동작합니다, 즉, 기본 클래스 생성 과정에서는 가상 함수가 먹히지 않습니다. 이 같은 동작에는 다 이유가 있습니다. 아시다시피 기본 클래스 생성자는 파생 클래스 생성자보다 앞서서 실행되기 때문에, 기본 클래스 생성자가 돌아가고 있을 시점에 파생 클래스 데이터 멤버는 아직 초기화된 상태가 아니라는 것이 핵심입니다. 이때 기본 클래스 생성자에서 어쩌다 호출된 가상 함수가 파생 클래스 쪽으로 내려간다면 어떻게 될까..
항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자 소멸자로부터 예외가 터져 나가는 경우를 C++ 언어에서 막는 것은 아니지만, 실제 상황을 들춰보면 확실히 우리가 막을 수 밖에 없는 것 같습니다. 아래의 예를 봅시다. 1 2 3 4 5 6 7 8 class DBConnection { public : ... static DBConnection create(); // DBConnection 객체를 반환하는 함수. 매개변수는 편의상 생략. void close(); // 연결을 닫습니다. 이때 연결이 실패하면 예외를 던집니다. }; Colored by Color Scripter 보다시피 사용자가 DBConnection 객체에 대해 close를 직접 호출해야 하는 설계입니다. 자원 관리 클래스의 소멸자가 ..
항목 7 : 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자 시간 기록에 관련된 TimeKeeper이라는 클래스를 선언하겠습니다. 1 2 3 4 5 6 7 8 9 10 class TimeKeeper{ public : TimeKeeper(); ~TimeKeeper(); ... }; class AtomicClock : public TimeKeeper { ... }; class WaterClock : public TimeKeeper { ... }; class WristWatch : public TimeKeeper { ... }; 이 클래스의 혜택을 받는 사용자들은 시간 정보에 접근하고 싶어 합니다. 시간 계산이 어떻게 되는지에 대해서는 신경 쓰고 싶지 않고요. 사정이 이렇기 때문에, 어..
항목 6 : 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자 코딩을 하다보면 객체의 복사를 막고 싶은 경우가 있습니다. 하지만 복사 생성자나 복사 대입 연산자를 선언하지 않으면 컴파일러가 저절로 만들어 낼지도 모릅니다. 그렇다고 직접 선언하면 복사하는 것은 마찬가지입니다. 이를 해결하기 위한 방법은 복사 생성자 및 복사 대입 연산자를 private 멤버로 선언하는 것입니다. 여기에도 부족한 것이 있습니다. private 멤버 함수는 그 클래스의 멤버함수 및 프렌드 함수가 호출할 수 있다는 점이죠. 이것까지 막으려면, '정의'를 안 해 버리는 방법이 있습니다. 정의되지 않은 함수를 누군가가 어쩌다 실수로 호출하려 했다면 분명히 링크 시점에 에러를 보지게 될 테니 괜찮습니다. 1 2..
항목 5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자 클래스가 비어 있지만 비어 있는 게 아닌 때가 있습니다. C++(컴파일러)가 빈 클래스를 훑고 지나갈 때 입니다. C++의 어떤 멤버 함수는 여러분이 클래스 안에 직접 선언해 넣지 않으면 컴파일러가 저절로 선언해 주도록 되어 있습니다. 바로 복사 생성자, 복사 대입 연산자, 그리고 소멸자 입니다. 이때 컴파일러가 만드는 함수의 형태는 모두 기본형입니다. 이들은 모두 public 멤버이며 inline함수입니다. 여러분이 다음과 같이 썼다면 1 class Empty(); 다음과 같이 쓴 것과 근본적으로는 같다는 이야기입니다. 1 2 3 4 5 6 7 8 9 class Empty{ public: Empty(){...} // 기본 생성자..