Effective C++笔记(转)

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和指针的关系

如果一个指针*pconst对象的成员,我们不改变指针值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 classestr1::shared_ptrauto_ptr。前者往往是较佳选择(通过引用计数器实现),后者不支持指针的拷贝和复制,复制动作会使它(被复制物)指向null

类似的Boost库中的boost::scoped_arrayboost::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时,传递引用可以使虚函数被正确解析,而传递值将导致对象slicingbase class

 在编译器底层Reference往往是指针实现出来的,因此如果你有一个对象是内置类型,传递value往往比reference要高效些。对内置类型而言,当你有机会选择采用pass-by-valuepass-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

将成员变量声明为privateprotected不比public更具有封装性。因为更改了protect变量将会影响虽有的derived class

 
11、条款23:宁以non-membernon-friend替换member函数(尤其是需要满足交换律的operator)

C++不是纯面向对象语言,不是所有的函数都定义在类中。

你不需要强行将所有函数定义在class内,因为这会造成class庞大的体积,而不同的用户又可能对不同的方法感兴趣。适当的拆分是有好处的。

 将所有的便利函数(uitlity funcation)放在多个头文件中,但隶属于同一个命名空间是C++标准库的组织方式。

 因为在意封装而让函数成为classnon-member函数不意味它不可以是另一个classmember,它可以是某个工具类的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 classderived classes之间的is-a关系。(这也是为什么上述using 声明被放在derived classpublic区域的原因:base classpublic名称在derived class内也应该是public的)。然而在private继承之下,它却可能是有意义的。例如,假设Derivedprivate的形式继承Base,以一个转交函数完成对Base class函数的调用。(private是对Derived classes的一种协助)

 

 这也是为什么在copy constructorcopy 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;

*pD = &x;;

*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的双重意义

声明templateclasstypename可以互换。

但是考虑这种情况:

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参数时,前缀关键字classtypename可互换

请使用关键字typename标示嵌套从属类型名,但不能在base class listsmember initialization list内以它作为base class修饰符。

 

 

Virtual void mf1()
{

       Base::mf1();           
// 最好暗自转换成inline

}

posted on 2009-03-29 10:54 弱水一瓢 阅读(307) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


<2009年3月>
22232425262728
1234567
891011121314
15161718192021
22232425262728
2930311234

导航

统计

文章分类

最新评论