0. 拷贝构造函数和赋值运算符
copy构造函数用来“以同型对象初始化自我对象”,copy assignment操作符被用来“从另一个同型对象中”拷贝其值到自我对象
copy构造函数使用时,自我对象并没有被实例化;而copy assignment操作符使用时自我对象已经被实例化
如:
String str1("Hello");
String str2(str1); // copy constructor
String str3 = str1; // 注意:copy constructor !
String str4;
str4 = str1; // copy assignment
1. 将构造函数声明为explicit可以阻止它们被用来执行隐式类型转换。
如:
有一个函数 void doSomething(B bObject);
B有一个接受int的构造函数 B::B(int b);
B obj1(100);
doSomething(obj1); // ok
doSomething(28); // 如果没声明explicit可以,反之不行
2. 条款三:尽可能使用const. Use const whenever possible
const实施于成员函数的目的,是为了确认该成员函数可作用于const 对象身上。它承诺绝不改变其对象的逻辑状态。它可以使class的接口更加容易理解;而且,它们使操作const对象成为可能,因为改善c++效率的一个根本方法就是以pass by reference-to-const方式传递对象,而此技术的前提是我们有const成员函数可用来处理取得(并经修饰而成)的const对象
很多人都漠视的一个事实:如果两个成员函数只是常量性(const 与否)不同,它们可以被重载。
const和指针,要小心const和指针的关系
如果一个指针*p是const对象的成员,我们不改变指针值p(也就是它指向哪个对象)而改变*p(它所指向对象的值),那么编译器不会对此提出异议。这同样适用于const成员函数中的情况。
一个增加灵活性声明:声明为【mutable】的成员变量可能总是会被更改,即使在const成员函数内
const_cast<> 运算符可以用来转换掉对象的const属性
请记住:
<!--[if !supportLists]-->l <!--[endif]-->将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回值类型、成员函数本体。
<!--[if !supportLists]-->l <!--[endif]-->编译器强制实施bitwise constness,但你应该使用conceptual constness 适当的使用mutable和注意指针带来的影响。
<!--[if !supportLists]-->l <!--[endif]-->当const函数和non-con函数有实质等价的实现时,可利用non-const函数调用const函数避免重复代码。
3. 条款四:确定对象在使用前已初始化. Make sure that object are initialized before they’re used.
别混淆赋值(assignment)和初始化(initialization)
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,应该使用初始化列表实现。
在构造函数本体内进行的只是赋值而不是初始化。初始化发生的事件更早,发生于这些成员的default构造函数被自动调用之时(比进入构造函数本体的时间更早)。这样构造函数所做的一切工作都浪费了。
请立下一个规矩:总是在初始化列表中列出所有成员变量(包括内置类型),以免还得记住哪些成员变量(如果它们在初值列中被遗漏的话)可以无需初值。
当然,你可以将那些“赋值表现像初始化一样好”的成员变量,改用赋值操作并移入一个private函数,在所有的构造函数中调用它,以避免不必要的编码重复。这种做法在成员变量的初值系“由文件或数据库”读入时特别有用。
C++有着十分固定的“成员初始顺序”。Base早于derived,成员变量按声明顺序。
对于non-local static对象,使用singleton是个不错的方案
请记住:
<!--[if !supportLists]-->l <!--[endif]-->为内置型对象进行手工初始化,C++不保证初始化他们
<!--[if !supportLists]-->l <!--[endif]-->构造函数最好使用成员初始化列表,而不要在构造函数本体内赋值。其排列次序应按照声明次序
<!--[if !supportLists]-->l <!--[endif]-->Singleton实现non-local static对象
4. 条款05 了解C++默默编写并调用哪些函数. Know what functions C++ silently writes and calls.
为了驳回编译器自动提供的功能(默认构造函数、默认析构函数、默认拷贝构造函数、默认赋值操作符),可将相应的成员函数声明为private并且不予实现。
将构造函数声明为explicit可以阻止隐式类型转换.
5. 条款07:为多态基类声明virtual 析构函数. Declare destruction vritual in polymorphic base class.
C++ 明确指出:当derived class对象经由一个base class指针被删除,而该base class带有一个non-virtual析构函数,其结果是未定义——实际执行时通常发生的事对象的derived成分没被销毁。
消除这个问题的办法很简单,base class声明一个virtual析构函数。
任何函数只要带有一个virtual函数几乎确定也应该有一个virtual析构函数。
如果class不含virtual函数,通常表示它并不意图被用作一个base class。
无端地将所有class的析构函数都声明为virtual就像从未声明它们为virtual一样,都是错误的。很多人的心的事:只有当class内含有至少一个virtual函数才将它声明为virtual析构函数。
将base class的析构函数声明为纯虚函数是定义抽象类的常用手法,但是这个纯虚析构函数仍需提供定义,因为derived class需要调用base class的析构函数。如果未定义则会导致链接错误。
6. 条款12:Copy all parts of an object. 复制对象时勿忘其每一个成分
derived class 应在拷贝构造函数的初始化列表和赋值运算符的函数体内调用base class的拷贝构造函数和赋值运算符。否则将导致初始化不完整。
class Customer ...{ /**//* … */ };
class PriorityCustomer : public Customer ...{ /**//* .. */};
PriorityCustomer::PriorityCustomer(PriorityCustomer &rhs) : Customer(rhs) ...{
// 调用base class Customer拷贝构造函数
/**//*
…
*/
}
PriortiyCustomer& PriorityCustomer::operator=(PriorityCustomer &rhs)...{
Customer::operator=(rhs); // 调用base class operator=
/**//*
…..
*/
return *this;
}
7. 条款13:以对象管理资源. Use objects to manager resources.
善于使用智能指针,为了防止资源泄漏,请使用RAII对象,他们在构造函数中获得资源并在析构函数中释放资源。
两个常用的RAII classes是tr1::shared_ptr和auto_ptr。前者往往是较佳选择(通过引用计数器实现),后者不支持指针的拷贝和复制,复制动作会使它(被复制物)指向null。
类似的Boost库中的boost::scoped_array和boost::shared_array类也可以提供类似功能
void test_fun()
...{
std::auto_ptr<Child> aptr_c(new Child("Child 1"));
Child *ptr = new Child("Child 2");
aptr_c.get()->func(); // 尽量使用显示转换,防止不合适的隐式转换
(*aptr_c).func(); // 使用隐式转换,可以增强可读性
std::auto_ptr<Child> aptr_c2 = aptr_c; // will set aptr_c to null
// aptr_c->func(); failed. aptr_c is null.
delete ptr;
}
智能指针auto_ptr可以在生命期结束时自动销毁返回资源。尽量使用智能指针保存factory函数返回的指针。
此外,利用栈对象自动销毁调用析构函数的特性,可以将需要释放的资源根据作用域封装在栈对象中,此栈对象即为一个资源管理对象,对应类为资源管理类。
8、条款14:在资源管理类中小心coping行为禁止复制。许多时候允许资源管理对象被复制是不合理的。应该将coping行为声明为private。
对底层资源使用“引用计数器法”。
tr1::shared_ptr是一个绝好的实现手段,这个类在vs2005的库中还没有被加入,vs2008的c++标准库包含了这个类,当然tr1的大部分类实现来自于boost,这个也不例外。
例如:
class Lock...{
public:
explicit Lock(Mutex* pm) : mutexPtr(pm)
...{
lock(mutexPtr.get());
}
/**//*
我们并没有忘记析构函数,默认的析构函数会自动调用每个非static类变量的析构函数
当mutexPtr 的引用计数为0就会调用智能指针的删除器
*/
private:
std::tr1::shared_ptr<Mutex> mutexPtr;
};
9、条款20:宁以pass-by-reference-to-constt替换pass-by-vaule
除了熟知的,传递const引用会比单纯的值传递效率更高,应用传递还有意想不到的好处,多态。在参数为base class时,传递引用可以使虚函数被正确解析,而传递值将导致对象slicing为base class。
在编译器底层Reference往往是指针实现出来的,因此如果你有一个对象是内置类型,传递value往往比reference要高效些。对内置类型而言,当你有机会选择采用pass-by-value或pass-by-reference-to-const时选择前者并非没有道理。这个忠告也适用于STL的迭代器和函数对象,因为他们习惯上被实现为passed-by-value。
在函数返回对象时谨慎使用reference 。除了*this其他的返回引用最好仔细斟酌。
请记住:决不要返回一个pointer或者reference指向一个local stack对象,或返回一个reference指向一个heap-allocated对象,或返回pointer或者reference指向一个local static对象而有可能需要多个这样的对象。如果要,考虑singleton吧。
10、条款22:将成员变量声明为private
将成员变量声明为private。protected不比public更具有封装性。因为更改了protect变量将会影响虽有的derived class
11、条款23:宁以non-member、non-friend替换member函数(尤其是需要满足交换律的operator)
C++不是纯面向对象语言,不是所有的函数都定义在类中。
你不需要强行将所有函数定义在class内,因为这会造成class庞大的体积,而不同的用户又可能对不同的方法感兴趣。适当的拆分是有好处的。
将所有的便利函数(uitlity funcation)放在多个头文件中,但隶属于同一个命名空间是C++标准库的组织方式。
因为在意封装而让函数成为class的non-member函数不意味它不可以是另一个class的member,它可以是某个工具类的static member函数。这让纯面向对象思维的java程序员感觉比较习惯。
请记住:宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性
12、条款24:若所有参数皆需类型转换,请为此采用non-member函数
此条款一般用于实现operator时,为了满足交换律和调用隐式类型转换将operator在类外实现。是否声明为friend看需要。
13、条款34:区分接口继承和实现继承
Derived classes内的名称会遮掩base classes内的名称,他们不会被重载。在public继承下从来没有人希望如此。
如何推翻C++对继承而来名称的缺省遮掩行为:
使用using声明式使被遮掩的base class函数可见
using Base::funcation;
有时候你并不想继承base classes的所有函数,这是可以理解的。但在public继承下,这绝对不可能发生,因为它违反了public继承所暗示的base class和derived classes之间的is-a关系。(这也是为什么上述using 声明被放在derived class的public区域的原因:base class的public名称在derived class内也应该是public的)。然而在private继承之下,它却可能是有意义的。例如,假设Derived以private的形式继承Base,以一个转交函数完成对Base class函数的调用。(private是对Derived classes的一种协助)
这也是为什么在copy constructor和copy assignment中必须调用base class的内容的原因。因为它们被drived class的隐藏了,不会被继承。
见条款12
14、条款34:区分接口继承和实现继承
pure virtual函数、impure virtual函数、non-virtual函数之间的差异,使得你得以精确指定你想要derived class继承的东西:只继承接口(pure virtual),或是继承接口和一份缺省实现(virtual),或是继承接口和一份强制实现(non-virtual)。
pure virtual函数只具体指定接口继承
impure virtual函数具体指定接口继承及实现继承
non-virtual 函数具体指定接口继承以及强制性实现继承,non-virtual函数会为该class建立起一个不变性,凌驾其特异性。你最好绝不重新定义继承而来的non-virtual函数。
否则如:
class D{
public:
void mf();
}
class D : public B { / *… */ };
D x;
D *pD = &x;;
B *pB = &x;
pD->mf();
pB->mf();
......
这两个调用将调用不同的mf,因为mf是静态绑定的,而不像virtual是动态绑定的。
就是因为同样的道理,一个derived class绝不应该重新定义一个继承而来的non-virtual析构函数。
15、条款35:考虑virtual函数以外的其他选择
Non-Virtual Interface手法
一个有趣的思想流派主张virtual函数应该总是被实现为private。这一基本设计,也就是“令客户通过public non-virtual函数间接调用private virtual函数”,称为non-virtual interface手法。是template method设计模式的一个特例。Effective C++的作者把这个non-virtual函数成为virtual函数的wrapper。
NVI函数的一个优点是可以在执行virtual函数的特化行为之前和之后,做一些公共的初始化和清理工作。如果让客户直接调用virtual函数就没有任何好办法做这些事。
有些事情看似比较怪异,你在derived class中实现一个derived class从不调用的virtual函数。但这并不存在矛盾。重新定义表示某些事情如何被完成,而调用他们表示何时被完成,base class只是保留了“函数合适被调用的”权利。一开始这些听起来有些诡异,但是C++这种derived class”可重新定义被继承来的private virtual函数“的规则完全合情合理。
另外一种情况,使用Strategy模式
使用函数指针代替virtual函数,这样是Strategy模式的一个简单应用。这样做的优点是同一个类型的实体可以有不同的运算实现方式(实体作为参数传入)。但这样做,指针指向的函数只能访问public内容,如果要访问private内容只能降低封装。
便利设施:借由boost和tr1::funcation实现Strategy模式会得到高于函数指针的弹性(flexibility)。
16、条款37:绝不重新定义继承而来的缺省参数值
缺省参数是静态绑定的,如果在virtual函数内改变,将会出现函数调用和参数不符合的结果。如果在non-virtual…条款34里貌似说过不要改变non-virtual函数的实现…
18、条款39:明智而审慎地使用private继承
Private 继承意味着 is-implemented-in-terms of(根据某物实现出)。它通常比复合(组合)的级别低。但是,当derived class要重新定义继承而来的virtual函数时或者要访问protected base class的成员时,这种设计是合理的。
和复合不同,private继承可以造成empty base的最优化。这对致力于“对象尺寸最小化”的程序开发者(特别是嵌入式程序员)而言,可能很重要。
19、条款40:明智而审慎的使用多重继承
对于一个从Java阵营转入C++的程序员而言多重继承一般不会被使用。
如果确实需要,请注意“钻石”继承的出现,即一个子类的继承链上重复出现了同一个base class,这样base class的成员变量会经每一条路径被复制。解决的方式是virtual继承,但是virtual继承会增加大小、速度、初始化(及赋值)复杂度成本。如果virtual base classes不带任何数据,将是最具使用价值的情况。
多重继承的确有正当用途。其中一个情节设计“public继承某个Interface class”和“private继承协助实现某个class”的两相组合。
20、条款42:了解typename的双重意义
声明template时class和typename可以互换。
但是考虑这种情况:
template<typename C>
void print2md(const C& container)
{
C::const_iterator* x;
}
...
C是在编译器被解析的,而在编译期编译器并不知道C里面到底有什么。一种很不幸的事实,万一C中有一个变量是const_iterator,而有一个全局变量为x。这会是一场灾难。一种避免的方案就是使用typename声明这是一个类型
template<typename C>
void print2md(const C& container)
{
Typename C::const_iterator* x;
}
...
请记住:
声明template参数时,前缀关键字class和typename可互换
请使用关键字typename标示嵌套从属类型名,但不能在base class lists或member initialization list内以它作为base class修饰符。
Virtual void mf1()
...{
Base::mf1(); // 最好暗自转换成inline
}