Exceptional C++ Syle 学习笔记

来源:互联网 发布:mac的flash 编辑:程序博客网 时间:2024/06/02 15:53

看完了《Exceptional C++ Syle》,现将书中一些重要的东西记下来,以供以后翻阅。

 

第1条        vector的使用

 使用vetor<T>∷at,该成员函数会进行下标越界检查以确保当前vector中的确包含了需要的元素。

名言:一般说来,不应该为不使用的东西付出代价。

使用operator[],标准并不强制operator[]进行越界检查,高效性。

 

size():容器中实际元素个数。

resize():在容器尾部添加或删除一些元素,使容器达到指定大小。

capacity():最少添加多少个元素才会导致容器重分配内存。

reserve():总是会使容器的内部缓冲区扩充至一个更大的容量。

 

V.reserve(2);           // 只是确保v当中有空间能放得下两个或更多的元素,但V仍是空的!

V[0] = 1; V[1] = 2;    // 错误!operator[](或at())只能用来改动确实存在于容器中的元素。

V[0]返回内部缓冲区中用于存放但尚未存放第一个元素的那块空间的引用。所以代码根本就没有往vector中添加任何东西。但输出的话,结果确实是符合预期的。

V.reserve(100);     // 如果第一次V.reserve(2)没有使内部缓冲区扩大到100或更多的话,这次将引发一次内部缓冲区的重分配。这时V就只会将它确实拥有的那些元素复制到“新家”中,而此时V认为它内部空空如也,因此不进行任何元素拷贝。另一方面,新分配的内部缓冲区最初值可能为0,严格讲不确定。因此此时输出第一个元素,将与预期不符。

 

1)  当迭代器并没有用来修改vector中的元素时,应使用const_iterator。

2)  使用!=而不是<来比较两个迭代器。<只对随机访问迭代器有效,而!=对于任何迭代器都是有效的。

3)  或许可在循环前求出V.end(),以避免无谓的重复求值。

4)  尽量使用\n而不是endl。使用endl会迫使输出流刷新其内部缓冲区,影响效率。

5)  在简单的情况下,尽量使用标准库中的copy()和for_each()。

如:copy(V.begin(), V.end(), ostream_iterator<int>(cout, “\n”));

 

 第11条 try和catch

尽量通过析构函数来进行异常环境下的自动清理工作,而不是通过try/catch。

1) 对于那些不能返回值的函数(如构造函数和操作符),对于错误抛出端和处理端相隔遥远的情况,异常是最有效的方案。

2) 只在那些侦测到错误但自己却无法处理的地方抛出异常。

3) 捕获一个汇报了较低层错误的异常,然后抛出另一个在执行转换的代码自身的(较高层面的)语义上下文中重新表达的异常。

4)将原先的异常转换为另一种表示形式,如错误代码。

 

第12条 异常安全性

        使用“资源获取即初始化”(RAII)惯用法来管理资源的所有权。使用对资源具有所有权的对象如Lock类和shared_ptr类,具有异常安全性。

        资源获取即初始化即将所有初始化操作移到对象的构造函数中,所有的释放操作都放在对象的析构函数里。这种用构造函数申请资源用析构函数释放资源的做法或者说技术通常被称为“资源获取即初始化”。这样做的优点是显而易见的:对象创建后,用户能开始正确使用对象,不用担心对象的有效性或者是否还要作进一步的初始化操作。此外,因为对象的析构函数也能释放所有使用的资源并进行重置,从而让对象的用户从繁琐的处理事务中解脱出来。注意这个技术通常要借助适当的异常处理代码来应付对象构造期间丢出的异常。

1)先在一旁将所有事情做完,然后通过不抛出异常的操作来提交整个任务。

2)尽量使用单一职守的类或函数。

3)函数应当总是支持它所能支持的最强的异常安全保证,但前提是不能给那些并不需要该保证的调用者带来额外的开销。

4)永远不要运行析构函数、释放操作(deallocation)以及swap()函数抛出任何异常,否则就没法安全且可靠地进行资源清理了。

 

第15条 访问权限的使用

         Public:公用成员可以被任何代码访问。

         Protected:保护成员可以被类自身的成员函数访问,也可以被类的友元访问,也可以被派生类的成员函数与友元访问。

         Private:私有成员只可以被类自身的成员函数以及类的友元访问。

访问类的私有成员:

1) 通过复制类定义再添加友元声明;

2) #define private public         但违反了惟一定义规则(ODR),如果类的底层内存格局没有改变,该手法有效。

3) 模拟原对象的内存布局,使用reinterpret_cast。

以上都是非标准且不可移植的方案。

4) 提供成员模板函数特化。这是一种符合标准且可移植的技术,绕过了类的访问控制机制,有时在调试的时候能够给出更好的诊断输出,但这绝不是个好习惯。

附:reinterpret_cast介绍

http://www.cnblogs.com/ider/archive/2011/07/30/cpp_cast_operator_part3.html

 

第16条 私有

一个私有的虚函数可以被任何派生类中的相应虚函数重写,但不能被派生类访问。

私有成员对于任何能够看到其所属类定义的代码来说都是可见的。这意味着…它会参与名字查找和重载决议,因而可能会使调用变得无效或具有二义性,即便它本身可能永远不被调用。

具有对某个成员的访问权的代码可以通过泄露该成员的指针(摆脱了名字的束缚)的方式将其访问权限授予给其他任何代码。

Private 成员的名字只对其所属类的其他成员或友元来说是可访问的,而这里的其他成员也包括成员模板的任何显式特化(不管它的某个给定的显式特化是否为意料之中的)。

class X{public:template<class T>void f(const T& t){//…}//…Private:Int private_;};//使用代码Namespace{Struct Y {};}Template<>Void X::f(const Y&){Private_ = 2;} 

 

第17条 封装

封装几乎总是意味着数据隐藏。

运行时多态,使用虚函数,更彻底地将内部实现(由派生类提供)与对外接口(由基类提供)分离,提供内部实现的派生类甚至可以在最终用到它的客户代码之后编写。

编译时多态,使用模板,将接口与实现完全分离,任何具有必备操作的类都可以替换使用,而无需具有任何继承或其他关系。

封装并不总是数据隐藏,不过数据隐藏总是某种形式的封装。封装并不总是多态,但多态则总是某种形式的封装。

面向对象定义:数据以及操作这些数据的函数的组合。另一个方面,通过由一组函数构成的接口将调用代码与内部数据隔离开来(由接口来负责操纵内部数据)。简单地说,面向对象就是关于将接口与实现分离的方法学。

1)功能完整的类至少得有自己的接口、行为以及不变式。

2)数据成员永远都应当为私有的。

3)大多数调用代码永远都不应该直接去操纵类的内部。

4)公用数据、保护数据都是危险的。(为什么当初C++语言要加入保护数据?)

5)总是将所有数据成员放在私有区段。惟一的例外是C风格的struct,后者的意图并不在于封装什么东西,因而其所有成员都可以是公用的。

6)接口是最需要在第一时间做对的事情。其他东西都可以在后期进行修正。如果你一开始就没有把接口做对的话,那么以后你可能就永远没有机会去改正它了。

 

第18条 虚拟

虚函数什么时候应当为public,什么时候应当为protected,什么时候又应当为private?

答:很罕见(如果有的话)、有时、默认情况下。

准则:尽量让接口成为非虚的。

一个公用的虚函数被强制做两件事:其一是给出接口,由于它是公用的,因此就成为了呈现给外部世界的接口的直接的一部分。其二是给出实现细节,即其内部可定制的行为,由于它是虚的,因而就提供给了派生类一个替换其基类中的实现的机会。公用虚函数天生就会做两件截然不同的事情,它们的客户相互之间是一种竞争关系。

我们应当尽量使用非虚接口模式来使接口稳定和非虚,将可定制的工作放在那些负责实现可定制行为的非公用虚函数当中。优点——

第一,基实现现在对它的接口及策略拥有了完全的控制权,因而可以很方便地在非虚接口函数中实施接口的前条件跟后条件、插入一些设备,或者做一些类似的事情等。

第二,由于我们更好地分离了接口与实现,所以我们可以让它们分别具有自然而然的形式,而不是费力寻找一个强迫它们看起来一模一样的折中形式。

第三,采用非虚接口模式可以让基类在变化面前更为稳固。

准则:尽量将虚函数置为私有的,只有当派生类需要调用基类中实现的虚函数的时候,我们才需要将后者设为保护的。

准则:基类的析构函数要么应当为公用虚函数,要么应当为保护的非虚函数。

让继承树中的非叶子节点成为抽象类。

 

第20条 内存中的容器之一:内存管理的层次

         C++中流行的内存管理策略:

1)通用意图的分配策略,提供调用方可能要求的任意大小的内存块。a)性能,需要做更多的工作;b)碎片,随着内存块的不断分配和释放,导致许多小的、不连续的未分配内存区域。

2)固定大小的分配策略,返回固定大小的内存块。不灵活,但可以在短时间内完成分配,不会产生那样的内存碎片。

3)垃圾收集,与C/C++指针、malloc和new不完全兼容。

内存管理层次:

1)操作系统内核提供了最为基础的内存分配服务。

2)编译器的默认运行时库也建立了内存分配服务。

3)标准库容器和标准分配器进而又利用运行时库提供的服务来实现他们自己的策略和优化。

vector<int>使用allocator<int>而vector<int,MyAllocator>的MyAllocator则可以是任何你喜欢的分配策略。

 

第21条 内存中的容器之二:它到底有多大

通用内存管理器会将实际分配的内存块的大小存放在该内存块的头部,而返回给你的指针就是从实际分配的内存块头部向后偏移系统维护信息所耗大小之后的指针。

有些平台要求某些特定类型的数据满足特定的字节边界对齐条件。

在每一个数据成员前都加上访问控制符,就等于允许编译器自由地调整每个数据成员的顺序。

一个结构的对齐要求就是该结构中最大的成员的对齐要求。

 

Vector<T>:除了用于内存对齐的填充字节外,不用为每个元素花任何额外开销。

Deque<T>:分页式存储,要求deque为每一页都保存一个额外的指针以便管理其相关信息。

List<T>:双向链表,对于每个T元素,list<T>都要额外存储两个指针,一个指向前一节点,一个指向后继节点。

Set<T>,multiset<T>,map<key,T>,multi-map<key,T>:set通常的内部实现是树,每个节点都有三个指针的额外开销,一个指向左子节点,一个指向右子节点,一个指向父节点。Set还有其他可行的内部实现方案,例如可以使用跳表,不过每个元素仍需要至少三个指针的额外开销。

Sizeof(string)是16,没有将该string对象可能分配的任何数据缓冲区算进去。

附:跳表介绍

http://blog.csdn.net/desilting/article/details/6946248

 

第23条 进行new操作,也许会跑出异常之二:内存管理中的实际问题

大多数形式的new通过抛出bad_alloc异常来报告分配失败,nothrow new则通过历史悠久的malloc方式来报告失败,即返回空指针。对那些并不检查错误的客户代码来说:一个未被捕获的bad_alloc异常会粗暴地结束程序(不管程序栈是否会展开至main),在栈开解的过程中至少会尝试销毁一部分对象从而令它们持有的资源得到释放;一个未被检查的空指针则会导致一次内存非法访问,当用户试图对一个空指针进行解引用时程序会立即崩溃。

不过极少数情况下nathrow new也能带来好处:1.一些非常老旧的C++应用程序会通过检查new的返回值是否为null来确定它是否失败。这类代码移植到现代编译器上时,可以将new全局替换为nothrow new。2.在时间关键(tiem-critical)的函数内或内层循环中大量使用nothrow new,能够给代码带来性能提升。

无论如何,检查new是否失败通常都没有多大意义——

1) 有些操作系统直到内层需要被实际使用到的时候才会提交实际的物理或虚拟内存。原因是在这类系统上分配只不过是记录一下对内存的请求而已,直到它们被实际使用到的时候操作系统才交出物理或虚拟内存,甚至提交也是根据进程实际访问的页面来逐页提交。

为了防止任何对已分配内存的访问而可能导致的程序崩溃:惟一途径是在分配内存之后立即对其进行“地毯式”读或写(memset)。对于non-POD类类型的对象或数组,编译器隐式生成的构造函数会自动“踏遍”这些内存。

POD类型:指不含用户自定义的构造函数(包括拷贝构造函数)、析构函数、复制赋值操作符,以及不含引用、指向成员的指针以及非POD类型的(非静态)数据成员的类或union。

2)倘若内存分配失败,则会导致访问违规进而使进程崩溃,对此我们的代码无能为力(除非使用某些平台相关的手段来截获访问违规)。

3)在现实中,new失败的可能性本就相当小,而又因为系统颠簸(thrash)的存在而变得几近绝迹了。要是真的没内存剩下了的话,那么你也就做不了什么了。

4)当你检测到new失败的时候并不总能有办法应付。如果内存真的被耗尽了,根本无法依靠抛出一个非平凡的(例如非内建类型)异常来汇报错误;即便抛出像throw string(“failed”);这样的异常也不行,因为它通常会通过new来进行一次动态分配。

 

在某些特定情况下,检查内存是否耗尽并试图恢复是有意义的——

1)在程序一开始就一次性分配以后将会用到的所有内存,然后自己去管理它们。

2)分配的缓冲区大小是从外部输入中获取的。考虑一个通信应用,每个包头部保存了该包的大小,当数据流被破坏或编程错误导致表达包长度的字节混乱的时候,就应当对这类破坏进行检查,一旦发现数据无效或缓冲区大小无意义就中止操作。

3)当程序试图分配一个巨大的工作缓冲区时,如果分配无法完成时,只需降低标准,重试获取一个较小的缓冲区,直到得到一个合适大小的。

 

附:栈开解介绍

http://www.learncpp.com/cpp-tutorial/153-exceptions-functions-and-stack-unwinding/