您的位置:首页 > 编程语言 > C语言/C++

<<Effective C++>>读书笔记(三)

2016-03-19 21:04 232 查看

条款27 尽量少做转型动作

C++ 中提供的四种新式转型

1.const_cast<T> (expression)
2.dynamic_cast<T> (expression)
3.reinterpret_cast<T> (expression)
4.static_cast<T> (expression)


const_cast通常被用来将对象的常量性转除.

dynamic_cast 主要用来执行安全向下转型. (上行转换是安全的,下行转换则不一定是安全的)

reinterpret_cast 执行低级转型

static_cast 用来强迫隐式转换

旧式转换仍然合法,但是新式转换较受欢迎, 其原因如下:

很容易在代码中被辨识出来

各转型操作的目标愈窄化,编译器愈可能诊断出错误的运用

如果你认为转型操作其实什么也没做,只是告诉编译器把某种类型视为另外一种类型.这其实是错误的.比如int和double其实在底层表示是不一样没办法把一个double,用int的方式解释一下就变成int类型了.底层还是做了一定的转换的.带虚函数的父子类之间的转换,也不是仅仅从编译器层面,把一种类型解释为另外一种类型而已.

如果可以请尽量避免转型操作

宁可使用C++ style(新式)转型,也不要使用旧式转型.

条款28 避免返回handlers指向对象内部成分

返回handlers指向对象的内部成分,等于是破坏了类的封装性

返回handlers可能会发生dangling handlers(空悬handler)

条款29 为”异常安全”而努力是值得

异常安全的两个条件:

不泄露任何资源

不允许数据败坏

下面是一个非异常安全实现的函数:

class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc);  //改变背景图像
...
private:
Mutex mutex;                                  //互斥器
Image *bzImage;                               //目前的背景图像
int imageChanges;                             //背景图像被改变的次数
};

void Pretty::changeBackground(std::istream& imgSrc)
{
lock(&mutex);
delete bgImage;
++imageChanges;
bzImage = new Image(imgSrc);
unlock(&mutex);
}


上面的代码是不具备异常安全的,首先如果new Image发生了异常,那么会导致永远无法unlock锁资源了.导致了资源泄露并且new Image的异常也会导致数据被破坏,因为原来的bgImage已经被delete了,并且imageChanes被递增了.因此又导致了数据败坏,两个异常安全的条件都不满足.资源泄露的问题很好解决,可以利用对象来管理资源,在离开作用域后利用对象的析构函数来释放资源,代码如下:

Lock ml(&mutex);
delete bgImage;
++imageChanges;
bzImage = new Image(imgSrc);


现在只剩下做好一个问题了,数据败坏的问题.条款11中提供了一种方法来避免数据败坏的问题.在删除数据的时候,应该先保存原来的数据,然后再new一个新的数据,只有在new成功的时候,才会delete原来的数据.此外还可以使用copy-and-swap来避免数据败坏的问题.

异常安全函数的三个基本保证:

基本承诺 如果异常被抛出,程序内的任何事物仍然保持在有效状态下,没人任何对象或者数据结构会因此而败坏.可以不是调用函数之前的状态.

强烈保证 如果异常被抛出,程序状态不改变,程序会回复到”调用函数之前”的状态.

不抛出异常保证 承诺绝不抛出异常

一个异常安全的函数,必须满足上面三个基本保证中的一个.一个强烈保证的实现:

//将bzImage按照shared_ptr来管理,只有在new Image成功的情况下,才会delete内内部的指针,因此下面这段代码达到了强烈保证的需求.
Lock ml(&mutex);
bzImage.reset(new Image(imgSrc));
++imageChanges;


除了利用上面的方法来达到强烈保证外,还有一个一般化的设计策略可以达到强烈保证,那就是copy-and-swap,原则很简单,为你打算修改的对象做出一份副本,然后在那副本上做一切必要的修改,若有任何修改动作抛出了异常,原对象保持未改变的状态.待所有的改变完成后,再与原对象在一个不抛出异常的操作中置换.基于copy-and-swap达到强烈保证,代码如下:

//这里把imageChanges和bgImage通过pimpl idiom手法管理起来
using std::swap
Lock ml(&mutex);
shared_ptr<PMImpl> pNew<new PMImpl(*pImpl);
pNew->bgImage.reset(new Image(ImageSrc));    //修改副本
++pNew->imageChanges;
swap(pImpl,pNew);                            //置换数据,释放mutex


异常安全函数即使发生了异常也不会泄漏资源或允许任何数据结构败坏.这样的函数区分为三种可能的保证:基本型,强烈型,不抛出异常型.

强烈保证
往往能够以
copy-and-swap
实现出来,但是”强烈保证”并非对所有函数都可能.

函数提供的
异常安全保证
通常最高只等于其所调用的各个函数的
异常安全保证
中的最弱者.

条款30 透彻了解inlining的里里外外

inline函数通常置于头文件内,因为大多数编译环境都是在编译过程中进行inline.因为需要知道函数长什么样.

所有的virtual函数都不是inlining,因为virtual函数需要等到运行时才知道调用哪个.

函数声明为inline,无法随着程序库的升级而升级.一旦程序库中的inline函数改变,那么所有用到程序库中的inline函数的程序都要重新编译

大部分调试器对inline函数都束手无策

不要因为function template出现在头文件中,就将他们声明为inline.

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

如果使用object reference或object pointers可以完成任务,就不要使用objects

如果能够,尽量以class声明式替换class定义式.

为声明式,和定义式,提供不同的头文件(和标准库一样,只包含声明式的头文件命令为
<classname>fwd
),例如标准库中的iosfwd

包含了iostream各个组件相关的声明式.

编译依存最小化的一般构想是,相依于声明式,不要相依于定义式.基于此构想的两个手段是handler classes和Interface classes

程序库的头文件应该以
完全且仅有声明式
的形式存在,这种做法不论是否涉及到templates都适用.

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

在C++进行面向对象编程,最重要的一个规则是:public inheritance(公开继承)意味着”is-a”的关系.public继承”意味着is-a”适用于base classes身上的每一件事情也一定适用于derived classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象.

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

名称查找规则:

1. local作用域也就是使用变量的成员函数所在的栈幁区域.

2. 该成员函数所在类的作用区域

3. 基类作用域内

4. 基类所在namespace内

5. global namespace内

derived classes内的名称会遮掩base classes内的名称,在public继承下从来没有人希望如此.

为了让被遮掩的名称再见天日,可使用using声明式,或转交函数.

class Base {

private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int) {cout << "Base::mf1(int)" << endl;}
virtual void mf2() {cout << "Base::mf2()" << endl;}
void mf3() {cout << "Base::mf3()" << endl;}
void mf3(double) {cout << "Base::mf3(double)" << endl;}
};

class Derived:public Base {
public:
virtual void mf1() {cout << "Derived::mf1()" << endl;}
void mf3() {cout << "Derived::mf3()" << endl;}
void mf4() { cout << "Derived::mf4()" << endl;}
};

int main()
{
Derived d;
int x;
d.mf1();        //运行没问题,调用的是Derived::mf1()
//d.mf1(x);     //运行错误,Derived遮掩了Base的mf1的名称,因此调用的是Derived::mf1,但是找不到对应的函数
d.mf2();        //运行没问题 调用的是Base::mf2,名称没有被遮掩
d.mf3();        //运行没问题 调用的是Derived::mf3()
//d.mf3(x);     //运行错误,Derived遮掩了Base的mf3的名称,因此调用的是Derived::mf3,但是找不到对应的函数
}


通过在Derived类中添加下面的using声明可以解决这个问题

class Derived:public Base {
public:
//让Derived在其作用域内看到Base类中的mf1和mf3函数,避免了遮掩问题.
using Base::mf1;
using Base::mf3;
....


上面的using Base::mf1,会将Base类中所有的名为mf1的重载函数都让其在Derived作用域内可见,如果你只想让mf1中的某一个重载函数在Derived中可见,就可以使用转交函数,下面是转交函数的使用方法:

比如:对于Base中只想让Base::mf1(int)可见,那么可以在Derived中写上一个mf1(int)的函数,然后在内部显示调用Base::mf1(int)
class Derived:public Base {
........
virtual void mf1(int x)
{
Base::mf1(x);
}
........
};


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

声明一个pure virtual函数的目的是为了让derived classes只继承函数接口

声明一个inpure virtual函数的目的,是让derived classes继承该函数的接口缺省实现

有的时候带缺省实现的inpure virtual,并不适用与某些派生类,推荐pure virtual+default成员函数

通过inpure virtual函数实现缺省实现,让每个derived classess有一份缺省实现.但是后续的derived classes并不是都适用与这个缺省实现,因此有了一种更好的办法就是使用pure virtual函数来实现,强迫派生类实现重新实现接口,同时提供一个default成员函数,作为缺省实现.可以让派生类调用. 

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

NVI(non-virtual interface)手法,它是所谓的Template Method设计模式的一个独特表现形式.这个手法就是令客户通过public non-virtual成员函数间接调用private virtual函数.这个手法的优点在于,可以在正式调用private virtual函数之前做一些准备工作和善后工作.virtual函数也可以不比是私有的,甚至在有些场景下,virtual函数应该是public的,否则会导致不能实施NVI手法.

使用non-virtual interface(NVI)手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹

较低访问性的virtual函数

将virtual函数替换为函数指针成员变量,这是Strategy设计模式的一种分解表现形式

以function成员变量替换virtual函数,因而允许使用任何可调用物.这也是Strategy设计模式的某种形式

将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是strategy设计模式传统实现手法

stragtegy设计模式的目的就是将机能从成员函数移到class外部.但是这带来的缺点就是非成员函数无法访问class的non-public成员

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

public继承意味着is-a的关系,适用于基类的每一件事,也适用于派生类.因为每一个派生类对象都是一个基类对象,派生类一定会继承基类的non-virtual函数,如果派生类重新定义了继承过来的non-virtual函数,那么就会出现矛盾,派生类不再是一个基类对象了.因此建议绝不要重新定义继承而来的non-virtual函数.

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

绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数,你唯一需要覆写的东西确实动态绑定的,如果派生类将继承而来的缺省参数修改了,那么会导致调用的是派生类的virtual函数,但是缺省参数用的是基类的缺省参数,因为静态类型是基类,动态类型是派生类.

条款38 通过塑模出has-a或”根据某物实现出”

has-a和is-a具有完全不同的意义,has-a实际有两个意义,有一个或根据某物实现出.is-a和has-a的关系还是很好区别的,比如一个人,一个地址,还有电话号码,你不能说一个人是一个地址,这里只能是has-a的关系一个人含有地址,电话号码等信息.当has-a发生在应用域内,是有一个的意义,像车子,人,这些都是应用域的部分,像互斥器,缓冲区查找树等发生在实现域内,表现的是根据某物实现的含义.

条款39 明智而审慎地使用private继承

private继承意味根据某物实现出的含义,它通常比复合的级别低.

和复合不同,private继承可以造成empty base最优化,这对致力于对象尺寸最小化的程序库开发者而言,可能很重要.

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

多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要

virtual继承会增加大小,速度,初始化复杂度等等成本,如果virtual base classes不带任何数据,将是最具使用价值的情况
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息