항목 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(){...} // 기본 생성자..
항목 4 : 객체를 사용하기 전에 반드시 그 객체를 초기화하자 단순 선언 시, 객체의 값을 초기화하는 데 있어서 C++은 항상 초기화를 보장하지는 않습니다. 1 2 3 4 5 6 class Point { int x, y; }; ... Point p; 위와 같이 하였을 때, p의 데이터 멤버 역시 어떤 상황에서는 초기화가 보장되지만 어쩔 때는 안됩니다. C++의 C부분만을 쓰고 있으며 초기화에 런타임 비용이 소모될 수 있는 상황이라면 값이 초기화된다는 보장이 없습니다. 하지만 STL부분에서는 초기화가 됩니다. 가장 좋은 방법은 모든 객체를 사용하기 전에 항상 초기화하는 것입니다. 기본제공 타입으로 만들어진 비멤버 객체에 대해서는 초기화를 손수 해야 하겠습니다. 1 2 3 4 5 6 int x = 0; //..
항목 3 : 낌새만 보이면 const를 들이대 보자! const의 장점은 '의미적인 제약'을 소스 코드 수준에서 붙인다는 점과 컴파일러가 이 제약을 단단히 지켜준다는 점일 것입니다. const 키워드는 클래스 바깥에서는 전역 혹은 네임스페이스 유효범위의 상수를 선언(정의)하는 데 쓸 수 있습니다. 그뿐 아니라 파일, 함수, 블록 유효범위에서 static으로 선언한 객체에도 const를 붙일 수 있습니다. 클래스 내부의 경우에는, 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있습니다. 포인터에서 const는 아래와 같이 사용할 수 있습니다. 1 2 const int * a = data // 비상수 포인터, 상수 데이터 int * const a = data // 상수 포인터, 비상수 데이터 1..
항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자 #define ASPECT_RATIO 1.653 이라는 코드를 작성했을때 작성자에게는 ASPECT_RATIO가 기호식 이름으로 보이지만 컴파일러에게는 보이지가 않습니다. 선행 처리자가 숫자 상수로 바꾸어 버리기 때문입니다. 그 결과로, ASPECT_RATIO라는 이름은 컴파일러가 쓰는 기호 테이블에 들어가지 않습니다. 만약 숫자 상수로 대체된 코드에서 컴파일 에러라도 발생하면 헷갈릴 수 있는 여지가 존재합니다. 이 문제의 해결법은 매크로 대신 상수를 쓰는 것입니다. const double AspectRatio = 1.653; AspectRatio는 언어 차원에서 지원하는 상수 타입의 데이터이기 때문에 컴파일러의 눈에도 보..