随手笔记之Effective C++

来源:互联网 发布:免费打电话的软件 编辑:程序博客网 时间:2024/04/30 09:08

以下是 Effective C++读后总结,虽不完整但看完这些也就差不多相当于看完Effective C++了。


条款02: 尽量以const enum inline 代替 #define.

#define 定义的宏 ,在编译的时候之前预处理全部替换掉,这样存在一个问题,例如:#define ABC 100 你在编译的过程中又用到ABC ,但是此时ABC 并没用添加到记号表中,出现编译错误,还有如果这个宏是其他文件中的,你对于这个100的来源开始估计也是莫名其妙的,所以尽量替代#define,

Const 定义的常量是要分配内存的,并且定义的时候要赋值。你可以取这个变量的地址,

Enum :枚举类型可以充当一个ints 使用,例如 enum{NUM = 5};  int a[NUM];

这是可行的,还有就是,enum 取地址是不合法的,通常取#define 地址也是不合法的.

由于宏定义会存在较多的隐患,比如自增自减那么这种情况最好用inline函数来是实现.

条款03: 尽量多用const

Const 可以定义常量,可以定义常指针等,C++中的迭代器 const_iterator  指明迭代器指向的内容是常量,如果迭代器为常指针,那么可以 const xxx.iterator it = xxx;

Const 的成员函数,是用来不让改变某个对象中的值,例如 常对象,只能调用const 的成员函数,防止改变。

如果const 和 non-const 实现的代码是一样的,那么可以再non-const 中调用const 

这个函数,以减少代码量,但要进行转化, 第一添加Const 的这个转换 为了安全,static_cast ,后面去除const 这个特性,const_cast:

条款04

1. 内置内型对象手工初始化。

2. 构造函数最好使用成员初值列表,而不要在构造函数中使用赋值,这样第一是效率方面,因为构造函数中赋值,是首先调用其他类类型的默认构造函数,然后在赋值,而初始化列表中,是直接进行一个copy 的构造函数,效率相对于快,但是对于内置内型的话没多大的差别,初始化的顺序最好相同,

3. 为了避免“跨编译单元的初始化次序,用local static 来替换 non-local static对象”.

条款05:构造、析构、赋值运算

当你定义一个空类的时候,那么编译器其实是默认会有一个默认的构造函数,一个默认的拷贝构造函数,一个默认的赋值操作符,一个默认的析构函数,,他们都是默认的public 并且inline 

如果你的成员数据是引用类型的,那么赋值操作符是不行的,因为引用时不允许修改的,那么这个时候就要我们自己去写一个赋值操作符的函数,如果一个基类的赋值操作符为私有的,那么编译器是不会为基于这个基类的派生类生成一个赋值操作符,因为为派生类生成一个赋值操作符本身是想处理基类中的成分,但是因为是私有的所以无权调用。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝。

当你不想一个类能够进行一个拷贝构造或者一个赋值操作符时候,那么我们可以定义这个拷贝构造和赋值操作符函数为私有类型,就可以了,相对于说还是比较不错的选择,那么这也有一个问题,如果你定义了一个friend 函数,这是会破坏封装的一种用法,他可以访问私有的数据,所以我们聪明的话不去定义friend,或者定义一个基类实现,然后继承这个基类,那么这个时候如果你是friend调用这个派生类的拷贝或者赋值,派生类中的会去尝试调用基类的,所以是行不通的。

条款07:为多态基类声明为virtual 析构函数。

一般基类中有一个或者一个以上的virtual 函数的时候,定义析构函数为virtual ,因为如果有的时候派生类中有分配的空间,如new 分配的一段空间,假如基类中没有virtual 析构函数,那么析构的时候,由于是给了基类的指针,那么调用的是基类中的析构函数,派生类的析构函数并没有调用,所以说这样可能就会导致内存的一些泄露等情况,加上virtual 这个关键字的析构函数,会首先调用派生类中的析构函数,然后在调用基类中的析构函数。

条款08:不要让异常逃离析构函数

如果假如一个客户连接了数据库,要求close退出的时候,但是客户可能忘记了,我们要在析构函数的时候去捕捉这个异常,

如果抛出异常就结束程序,可以通过调用abort 完成。

吞下异常,可以用try  catch 

析构函数不要吐出异常,如果一个析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下或者结束程序。

条款10:令operator = 返回一个reference to *this 

为了实现“连锁赋值” 赋值操作符必须返回一个reference 指向操作符的左侧实参,如:int & operator +=(const int &rhs)

{  return  *this;}

条款11: 在operator = 中处理”自我赋值“

例如: int & A::operator =(const A & rhs)

{

If(this == rhs) return *this;

Delete mm;

Mm= new .....

Return *this;

}

这里是提前判断一下。

条款12:复制对象时候不要忘记复制每一个成分

1. Copy 函数应该确保复制”对象内的所有成员变量“及”所有base class 成分”

2.不要尝试以某个copy 函数去调用另一个copy 函数 应该将共同机能放进第三个函数中,并由两个copy 函数共同调用。

条款18: 让接口容易被正确的使用,不易被误用.

例如我创建一个日期类,其中的构造函数要填写的参数,用户使用可能会输入顺序错误,或者超过界限,所以我们用定义一个接口的使用要小心使用,我们比较好的方法是把月,年,日,全部用类封装起来,这样我们可以放心一点使用,

Std::tr1::shared_ptr 智能指针,可以帮用户省很多事情,它的引用次数为会调用“删除器”,消除了客户的内存泄露问题,另外还有一个消除了潜在的客户问题:“cross-DLL probllem”,这个问题发生在“对象在动态链接程序库中被new,却在另外一个DLLdelete,智能指针会追踪这个记录,引用的次数来决定调用的是哪个DLLdelete.

条款20:宁以pass-by-reference-to-const  替换pass-by-value

例如参数的时候如果是类,那么以传值的方式就会产生构造析构的一些动作,如果是引用的方式不必要,并且在引用的时候我们前面加上一个const,这样防止去改变传进来的东东,这样效率就提高很多,并且容易被接受,

Slicing 切割的问题。解决方案是by_reference-to-const

切割的问题是:例如一个基类win,定义了一个mywin 是继承它,那么通过一个函数传进去的参数,参数的类型是基类类型,那么我们直接用传值方式的话这样会产生切割问题,调用的肯定是基类的方法,所以避免产生用引用的方式。

所谓的切割就是把派生类的一些自有的特性给除去了,因为是传值,这样构造的对象肯定就是基类的对像。

内置类型一般采用的是传值方式,

条款21:返回对象时,不要一味的返回其reference.

绝对不要返回指针或者引用局部变量,或者返回reference 指向局部堆对象,或者返回指针或者引用局部静态对象。

局部堆由于是new了,必然要delete,谁来?没有办法取得引用后面的指针,所以会导致内存泄露,

静态的局部变量虽然可以,但是会造成多线程的困恼。

条款22:将成员变量声明为private.

 

成员数据应该最好private ,这更能体现封装性。

条款23:宁以non-member  non-friend 替换member 函数

面向对象的要求是,数据以及操作数据的那些函数应该绑在一起,意味着member 函数是较好的选择,但是这个建议是不正确的,这是基于对面向对象的真实的一个误解,面向对象的守则是要求数据应该尽可能的被封装,但是许多方面non_member 函数做法要比member 做法好,我们了解下原因:

1. Non-member , non-friend 较大的封装性是它并不增加“能够访问”类中的private成分。

2. 命名空间的好处作用是各个功能可以独立,命名空间可以跨多个文件,但是类不行,例如,list,vector 等,我们用到只要包含头文件就可以,他们都是在命名空间std 下实现的。

条款24: 若所有参数皆需类型的转换,请为此采用non-member 函数。

Explicit 一般作用于构造函数,防止“隐式转换”,如果不加的时候,可以隐式转换不会出错,如果加上则必须是要某种类的类型,否则出错。也就是说

1explicit主要用于 "修饰 "构造函数

2、使得它不用于程序中需要通过此构造函数进行 "隐式 "转换的情况

3、指定此关键字,需要隐式转换方可进行的程序将会不能通过而可通过强制转换 使它没有用

例如 乘法,本身要求是类与类相乘,但是如果你写成类*一个整形数,那么这个整形数会隐式转换成这个类的类型,就是用这个整形编译器去调用构造函数构造一个对象,加上explicit 则会出错,但是如果写成一个整形数*一个类,那么则是必然出错,因为 整形数不会调用类中的成员函数,它会在整个全局中找看有没有符合这个类型的操作函数像,operatorint, 类),这样,发现也没有,那么就认定错误,所以这种存在隐式转换,我们就用non-member 函数。 

但是我们会讨论要不要定义为friend 函数,否定的,因为这个函数完全可以由pubic 接口函数去完成。

条款26:尽可能延后变量定义式的出现时间

可以改善程序的效率和代码的清晰,有时候我们定义的变量可能没用到或者还没有用的时候出现了异常而导致没有用到,这样假如是类类型的话,就浪费了构造和析构的时间。

条款27:尽量少做转型动作

C++提供四种转型动作

1. Const_cast<T>  目的是去除常量的特性。也是唯一的一个转型操作符。

2. Dynamic_cast<T> 主要执行“安全向下转型”,决定某对象是否归属于继承体系中的某个类型。

3. Reinterpret_cast<T> 执行低级的转型。

4. Static_cast<T> 用来强迫隐式转换,唯一不能是将const 转换为non-const 

优良的C++代码很少使用转型,但是不能说完全不使用,例如一个除法int x/int y

这时候将x转换为 double 是很好的。

如果转型是必要的,那么最好是将其隐藏在某个函数的背后,客户随后可以调用该函数,而不需要将转型放进他们自己的代码内。

条款28:避免返回handles 指向对象内部成分。

和之前提到过的一样,避免返回引用局部的变量或者局部的指针。

条款29:为“异常安全”而努力是值得的。

异常是在你不知情的情况下发生,如果没有相应的异常处理函数来处理,可能会出现严重的错误,异常函数的作用是:

1. 不要泄露任何资源。例如互斥锁,你后面分配空间,万一失败,你最后的释放锁永远不会执行,

2. 不允许数据败坏。你new空间,之前你删除了一个指针指向的东西,但是万一new失败,那么这个本来是想指向new 的,但是失败,之前的东西又被删除了,这样是错误的。可以用资源管理类来解决内存泄露的问题。

3. 基本的承诺,如果抛出异常,程序中的任何事物保持在有效的状态下,没有任何的的数据或结构遭到破坏。

4. 强烈保证。如果抛出异常,程序状态不改变,函数成功就是完全成功,失败的话,程序会回复到“之前的状态”。这种一般采用 copy and swap 的策略:为你打算修改的对象做出一份副本,然后再那副本的身上做一切要改变的修改莫若没有动作抛出异常,那么原来对象和副本进行一个不抛出异常的操作中置换,如果有任何异常,那么原对象任然保持不变。这种手法是 pimpl idiom

5.  不抛掷保证。

条款30:透彻了解Inline的里里外外。

一般来说inline 关键字一加就是内联函数,主要用于短小的函数中,因为inline函数其实是把代码复制到当前的代码中去,而不是真正的函数调用,所以它像函数但是又不是函数的调用,所以一般这样长数量的代码段中不用inline函数,inline函数代替宏来说有很大的优势,例如自增自减的问题,它一般是定义于头文件中,

但是inline函数来说如果通过函数之后怎而进行的调用 可能不会inline,如:

Inline void f(){}  void (*pf)() = f;   f(); //OK   pf();  //error

但是处理inline函数的时候要小心,是否应该把这个函数当做inline函数,例如派生类中的构造函数和析构函数,该不该是inline,就算其中的代码是空,但是那只是你眼睛欺骗了你,如果派生类有3string ,成员数据,基类中也有,那么派生类的构造函数虽然为空,但是要执行的动作依然是基类的构造,然后try catch{} 看看3string 的 构造是否成功,如果基类中也有,那么这个代码量其实就是非常大的,所以inline的使用要小心的确定,用在小,频繁调用的身上,可使日后的调试过程和二进制的升级更容易。也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。大部分的inline 是发生在编译的过程中的。

条款31:将文件的编译依存关系降至最低。

在实现类中,内的成员数据只含有一个指针,指向其实现类,这样的设计一般叫做

Pimpl idiom (pointer to implementation), 内含的指针名一般叫做 pImpl

这样真正的“接口和实现分离”,当class中任何的实现细目修改的时候,不需要重新编译,客户也看不到实现的细目。

分离的关键:以“声明的依存性” 替换“定义的依存性”,那正是编译依存性的最小化本质,

1. 如果使用对象的引用或者指向对象的指针就可以,那么没有必要使用对象,声明就可以了。

2. 尽量以类的声明式替换类的定义式。

3. 为声明式和定义式提供不同的头文件。

使用pimpl idiom class  叫做 Handle classes .  还有一种interface classes

这两种设计解除了接口和实现之间的耦合关系,降低了文件间的编译依存性。

条款32:确定你的public 继承塑模出is -a 的关系

Public 继承意味着 is -a ,:适用于基类身上的每一件事情一定也适用于派生类身上,因为每一个派生类对象也都是一个基类对象。可参考的一些错误的例子如矩形正方形,鸟企鹅等。

好的接口可以防止无效的代码通过编译,因此你应该宁可采取“在编译期间拒绝“

而不是在”运行期间”。

条款33:避免遮掩继承而来的名称

当一个派生类继承基类,派生类继承了基类的所有东西,实际运作的方式是:派生类作用域被嵌套在基类的作用域内。当一个派生类中的函数A被调用它里面含有另一个函数B的时候,这个时候查找的顺序是:先从Local作用域中也就是A函数的作用域中找是否有B函数,没有再查找派生类覆盖的作用域中,还是没有,那么就查找外围的基类,如果还是没有查找内含基类的作用域,最后在全局作用域中查找。

当基类中和派生类中有相同名称的函数时候,通过派生类的对象调用的是派生类中的函数,这样就把基类中的函数给掩盖起来了,如果想让基类被掩盖的名称可见,可以使用Using 声明式。(或转交函数)

条款34:区分接口继承和实现继承

1. 成员函数的接口总是会被继承的。

2. 声明一个纯虚函数是为了让派生类只继承函数的接口。但我们可以为纯虚函数提供一份实现代码,唯一调用的方法是通过基类名称调用。

3. 非纯虚函数是为了让派生类继承该函数的接口和缺省的实现代码。

4. 声明非虚拟函数是为了让派生类继承接口和一份强制实现的代码。

条款35: 考虑virtual 函数以外的其他选择。

NVI 手法:所谓的Template Method设计模式。是用Public non-virtual函数间接的调用private virtual 函数。优点在于在做一些工作之前和工作之后能做一些事情,如加互斥锁,释放锁等一些操作。

Strategy 设计模式。

1. 将virtual 函数替换为“函数指针成员变量”,这是一部分

2. 以tr1::function成员替换virtual 函数允许使用任何可调用物搭配一个兼容于需求的签名式,
3. 将基础体系内的virtual 函数替换为另一个继承体系内的virtual函数。

条款36:绝不重新定义继承而来的non-virtual 函数

条款37:绝不重新定义继承而来的缺省参数值

缺省的参数值都是静态绑定的,而virtual 函数 唯一应该覆写的却是动态绑定的。

你想让基类和派生类都有一个共同的默认值,那么采用NVI手法。

条款38:通过复合塑模出has-a或根据某物实现出。

复合的意义和public 继承完全不同。

复合意味has-a(有一个),我想应该是我们常说的组合关系。

条款39:明智而审慎的使用private 继承。

条款40:明智而审慎的使用多重继承