explicit声明
class B { public: explicit B(int x = 0, bool b = true) //默认构造函数,要么没有参数,要么所有参数都有默认值 { m_x = x; } public: int m_x; }; void doSomething(B bObject) { std::cout << bObject.m_x << endl; } int main() { doSomething(B()); //ok 0 doSomething(B(12)); //ok 12 //doSomething(12); //error system("pause"); return 0; }
通过在构造函数前加explict,用以避免隐式类型转换。
条目1:把C++看成多种语言的联合体
C++是一门多范型编程语言,可以将C++看成是一个由若干门语言组成的联合体:
- C
- 面向对象的C++
- 包含模板的C++
- STL
条目2:多用const、enum、inline,少用#define
#define ASPECT_RATIO 1.653替换为定义一个常量
const double AspectRatio=1.653;
对于类内部的常量,为了限制常量的份数超过一份,必须将该常量声明为static成员。
class GamePlayer { private: static const int NumTurns = 5; //常量声明 int scores[NumTurns]; };
const int GamePlayer::NumTurns; //常量定义
类内部的为常量声明,而非定义。类内部的静态常量如果是整形(整数、字符型、布尔型)则不需要定义。只要你不需要得到它们的地址,你可以声明它们、调用它们而不需要定义。
对于简单的常量,多用const对象或者枚举类型数据,少用#define
多用内联函数来代替带参宏
条目3:尽可能使用const
char greeting[] = "Hello"; char *p1 = greeting; //非const指针,非const数据 const char *p2 = greeting; //非const指针,const数据 char *const p3 = greeting; //const指针,非const数据 const char *const p4 = greeting; //const指针,const数据
const成员函数
const成员函数可以被具有相同参数列表的非const成员函数重载:
1 class TextBlock 2 { 3 private: 4 std::string text; 5 public: 6 TextBlock(std::string s) 7 { 8 text = s; 9 } 10 char& operator[](std::size_t position) 11 { 12 return text[position]; //用于非const对象 13 } 14 const char& operator[](std::size_t position) const 15 { 16 std::cout << "const func" << endl; 17 return text[position]; //用于const对象 18 } 19 }; 20 21 int main() 22 { 23 TextBlock tb("Hello"); 24 std::cout << tb[0] << endl; 25 const TextBlock ctb("World"); 26 std::cout << ctb[0] << endl; 27 return 0; 28 }
关于const成员函数的两种说法:
- 按位恒定:当且仅当一个成员函数对其所在对象的所有数据成员(static数据成员除外)都不做改动时,才需要将成员函数声明为const.但是,如果据成员是指针,则const成员函数并不能保证不修改指针指向的对象,编译器不会把这种修改检测为错误。
- 逻辑恒定:如果某个对象调用了一个const成员函数,则这个成员函数可以对对象作出内部改动,但仅仅是客户端无法察觉的方式进行。
条目4:确保对象在使用前得到初始化
读取未初始化的数据将导致未定义行为。在一些语言平台中,通常情况下读取未初始化的数据仅仅是使你的程序无法运行罢了。更典型的情况是,这样的读取操作可能会得到内存中某些位置上的半随机的数据,这些数据将会“污染”需要赋值的对象,最终,程序的行为将变得十分令人费解,你也会陷入烦人的除错工作中。
解决这类不确定的问题的最好途径是:总是在使用对象之前进行初始化。对于内置类型的非成员对象,需要手动完成这一工作。注意赋值和初始化的区别:
C++约定:一个对象的数据成员要在进入构造函数内部之前得到初始化。在进入ABEntry构造函数内部之前,这些数据成员的默认构造函数应该自动得到调用。注意对于numTimesConsulted成员不成立,因为其为内置类型,对其而言,在被赋值之前,无法确保其得到了初始化。
更好的方法是使用初始化表,这样效率会更高:
C++对象中数据成员的初始化顺序为其在类中声明的顺序,而不是成员初始化列表中的顺序。为了使读者不至于陷入困惑,应保证初始化表中的顺序与声明时的顺序保持一致。
条目5:要清楚C++后台为你书写和调用了什么函数
对于一个类来说,如果不自己手动声明一个复制构造函数、赋值运算符、析构函数,编译器会自动声明这些函数,没有声明构造函数的话,编译器也会为你声明一个默认构造函数。所有这些函数都是public和inline的。
条目6:要显式禁止编译器为你生成不必要的函数
通常情况下,如果你希望一个类不支持某种特定的功能,你需要做的仅仅是不去声明那个函数。然而这一策略对复制构造函数和拷贝赋值运算符就失效了,这是因为,即使你不做声明,而一旦有人尝试调用这些函数,编译器就会为你自动声明它们(参见条目 5)。解决问题的关键是,所有编译器生成的函数都是公共的。为了防止编译器生成这些函数,将复制构造函数和赋值运算符声明为私有的。通过显式声明一个函数,你就可以防止编译器去自动生成这个函数,同时,通过将函数声明为private的,你便可以防止人们去调用它。同时为了防止其他成员函数或者友元函数访问这些private函数,可将这些private成员函数只声明而不进行定义。
class HomeForSale { public: HomeForSale() {} private: HomeForSale(const HomeForSale&);//只有声明,无定义 HomeForSale& operator=(const HomeForSale&); //只有声明,无定义 };
条目7:要把多态基类的析构函数声明为虚函数
C++有明确的规则:如果希望通过一个基类类型的指针来删除一个派生类对象,并且基类的析构函数为非虚析构函数,则结果是未定义的。典型的后果是,运行中派生类中新派生出的部分将得不到销毁,基类部分会被销毁掉,这样就产生了一个古怪的“部分销毁”的现象。
排除这一问题的方法很简单:为基类提供一个虚拟的析构函数,这样删除一个派生类对象,程序就可以精确地按照需要进行了,这个对象都会得到销毁。任何有虚函数的类几乎都需要一个虚析构函数,如果一个类不包含虚函数,则通常情况下意味着它将不作为基类使用。当一个类不作为基类使用时,将它的析构函数声明为虚函数不是个好主意。
条目8:防止因异常中止析构函数
条目9:永远不要在构造或者析构的过程中调用虚函数
创建一个派生类的对象时,基类的构造函数优先于派生类的构造函数运行,在基类构造函数运行的时候,派生类的数据成员还未得到初始化。对于一个派生类的对象来说,在其进行基类部分构造工作的时候,这一对象的类型就是基类的。不仅仅虚函数会解析为基类的,而且 C++中“使用运行时类型信息”的部分(比如 dynamic_cast(参见条目 27)和typeid)也会将其看作基类类型的对象。
对于析构过程可以应用同样的推理方式。一旦派生类的析构函数运行完毕,对象中派生类的那一部分数据成员将取得未定义的值,所以 C++会认为它们不再存在。在进入基类的析构函数时,这个对象将成为一个基类对象,C++的所有部分——包括虚函数、dynamic_cast 等等——都会这样对待该对象。
条目10:让赋值运算符返回一个指向*this的引用
int x, y, z; x = y = z = 15; //一连串的赋值操作
这种实现的本质是:赋值时,返回一个指向运算符左边对象的引用。当为你的类实现赋值运算符时,应遵守这一惯例,这一惯例对所有的赋值运算符同样适用。
1 class Widget 2 { 3 public: 4 Widget() {} 5 Widget& operator=(const Widget& rhs) 6 { 7 //other code 8 return *this; //返回运算符左边的对象 9 } 10 Widget& operator+=(const Widget& rhs) 11 { 12 //other code 13 return *this; //返回运算符左边的对象 14 } 15 };
条目11:在operator=中要处理自赋值问题
条目12:要复制整个对象,不要遗漏人一部分
当没有手动定义拷贝成员函数时(拷贝构造函数和拷贝赋值运算符),编译器将自动生成拷贝函数,且自动生成的拷贝函数可以精确地按你所期望的方式运行,当前正在赋值的所有对象都会得到复制。然而当自己声明拷贝函数时,如果拷贝函数内只进行部分复制,编译器不会给出任何警告和错误。通过继承,这一问题可以带来更加严重却隐蔽的危害。
1 void logCall(const std::string& msg) 2 { 3 /// 4 } 5 class Customer 6 { 7 public: 8 Customer():name("unknown"){} 9 Customer(const std::string& s) :name(s) {} 10 Customer(const Customer& rhs):name(rhs.name) 11 { 12 logCall("Customer copy constructor"); 13 } 14 Customer& operator=(const Customer& rhs) 15 { 16 logCall("Customer copy assignment operator"); 17 name = rhs.name; 18 return *this; 19 } 20 private: 21 std::string name; 22 }; 23 //////////////////////////////////////// 24 class PriorityCustomer :public Customer 25 { 26 public: 27 PriorityCustomer() :priority(0) {} 28 PriorityCustomer(const PriorityCustomer& rhs); 29 PriorityCustomer& operator=(const PriorityCustomer& rhs); 30 private: 31 int priority; 32 }; 33 PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :priority(rhs.priority) 34 { 35 logCall("PriorityCustomer copy constructor"); 36 } 37 PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) 38 { 39 logCall("Customer copy assignment operator"); 40 priority = rhs.priority; 41 return *this; 42 }
拷贝时,PriorityCustomer对象从基类继承而来的成员始终没有得到复制。一旦你亲自为一个继承类编写了拷贝函数,你必须同时留心其基类的部分。当然这些部分通常情况下是私有的,所以你无法直接访问它们。取而代之的是,派生类的拷贝函数必须调用这些私有数据在基类中相关的函数。
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) :Customer(rhs), //调用基类的拷贝构造函数 priority(rhs.priority) { logCall("PriorityCustomer copy constructor"); } PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs) { logCall("Customer copy assignment operator"); Customer::operator=(rhs); //为基类部分赋值 priority = rhs.priority; return *this; }
条目13:要使用对象来管理资源
为了确保createInvestment()所返回的资源总能得到释放,我们需要将这类资源放置在一个对象中,以来C++对默认析构函数的自动调用来确保资源及时得到释放。标准库中的auto_ptr是类似于指针的对象(智能指针),其析构函数可以自动对其所指内容执行delete。
由于当一个 auto_ptr 被销毁时,它将自动删除其所指向的内容,所以永远不存在多个 auto_ptr 指向同一个对象的情况,这一点很重要。如果存在的话,这个对象就会被多次删除,这样你的程序就会立即陷入未定义行为。为了防止此类问题发生,auto_ptr 有一个不同寻常的特性:如果你复制它们(通过拷贝构造函数或者拷贝赋值运算符),它们就会被重设为 null,然后资源的所有权将由复制出的指针独占!
引用计数智能指针是auto_ptr的替代品,它可以跟踪有多少个对象指向了一个特定的资源,同时在没有指针再指向这一资源时,智能指针会自动删除该资源。可以看出引用计数智能指针的行为和垃圾回收器相似。
auto_ptr和tr1::shared_ptr在析构函数中使用的是delete语句,而不是delete[]。这就意味着对于动态分配的数组使用auto_ptr和tr1::shared_ptr不是一个好主意。但是遗憾的是,这样的代码会通过编译。
条目14:要注意资源管理类中的复制行为
条目15:要为资源管理类提供对原始资源的访问权
条目16:互相关联的new和delete要使用相同的形式
std::string *stringPtr1 = new std::string; std::string *stringPtr2 = new std::string[100]; delete stringPtr1; delete[] stringPtr2;
对stringPtr1调用delete[],或者对stringPtr2调用delete,都会导致未定义的行为。这里的规则很简单:如果你在一个 new 语句中使用了[],那么你必须在相关的delete 语句中也使用[]。如果你在一个 new 语句中没有使用[],那么在相关的delete 语句中也不应使用[]。
条目17:用智能指针存储由new创建的对象时要使用独立的语句
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
在编译器能够生成对processWidget的调用前,必须对传入的参数进行处理,编译器必须自动生成代码来解决下面三件事情:1.调用priority(),2.执行new Widget,3.调用tri::shared_ptr的构造函数。C++编译器对这三项任务完成的顺序要求的很宽松,调用priority()可能出现在第2步,如果调用priority抛出异常的话,则第一步new Widget返回的指针将会消失,这种情况下,processWidget()可能会造成资源泄露。这是因为:在资源被创建(通过 new Widget)以后和将这个资源转交给一个资源管理对象之前的这段时间内,有产生异常的可能。防止这类问题发生的办法很简单:使用单独的语句,创建 Widget 并将其存入一个智能指针,然后将这个智能指针传递给 processWidget。
条目18:要让接口易于正确使用,而不易被误用
条目19:要像设计类型一样设计class
条目20:传参时要多用“引用常量”,少用传值
默认情况下,C++为函数传入和传出对象是采用传值方式的(这是从C语言继承而来的特征)。除非你明确使用其他方法,否则函数的形参总是通过复制实参的副本来创建,而且,函数的调用者得到的也是函数返回值的副本。这些副本是由对象的拷贝构造函数创建的。
bool validateStudent(const Student& s);
通过引用传参也可以避免“截断问题”。当一个派生类的对象以一个基类对象的形式传递(传值方式)时,基类的拷贝构造函数就会被调用,此时,这一对象的独有特征——使它区别于基类对象的特征会被“截掉”。剩下的只是一个简单的基类对象,这并不奇怪,因为它是由基类构造函数创建的。通过传递常量引用,可以避免截断问题。
C++编译器中,引用通常是以指针的形式实现的,所以通过引用传递实质是传递一个指针。传递一个内置数据类型的对象,传值会比传递引用更为高效,迭代器和 STL 中的函数对象也是如此,这是因为它们设计的初衷就是能够更适于传值,这是 C++的惯例。
条目21:在必须返回一个对象时,不要尝试返回一个引用
这个函数会返回一个指向result的引用,但result为一个局部对象,局部对象在函数退出时会被销毁。事实上,任何返回局部对象的引用的函数都是灾难性的(任何返回指向局部对象的指针的函数也是灾难性的)。
条目22:将数据成员声明为私有的
条目23:多用非成员非友元函数,少用成员函数
多用非成员非友元函数,少用成员函数。这样做可以增强封装性,以及包装的灵活性和功能的扩展性。
条目24:当函数所有参数都需要进行类型转换时,要将其声明为非成员函数
条目25:最好不要让swap抛出异常
条目26:定义变量的时机越晚越好
条目27:尽量少用转型操作
条目28:不要返回指向对象内部部件的“句柄”
条目29:力求代码做到“异常安全”