作者通过一系列条款的方式讲述了开发经验,看了以后受益匪浅,下面还保留原有条款目录进行笔记记录。通过块引用方式增加了自己的想法。
条款1:视C++为一个语言联邦
C++是多种范式汇集的语言,包括C的过程、C++面向对象、Template泛型(STL模板库)、模板元编程、lambda。(后面自己补充可能由曲解原文意思)
条款2:尽量以const、enum、inline替换#define
单纯变量用const或enum替换define,形势函数的宏用inline函数替换。
class ttt{ enum{NumTurns=5}; int scores[NumTurns]; }
用枚举替换更像define,因为枚举和define均不可取地址,而const的变量可以被取地址。
条款3:尽可能使用const
const char *p;指针非常量,数值为常量–我还是喜欢声明时把*和变量放一起,构成一个整体,这样也容易理解,const就是对这个整体修饰的这个整体不可变也就是*p也就是值不可变。char * const p;指针常量,值非常量
对函数指定const:void a() const;此时函数不可调用非const成员(可以读非const数据成员),如某个数据成员需要被const方法操作,可以用mutable修饰:mutable boos bbb;
const char& operator[](std::size_t position)const { XXXXX return pText[position]; } char& operator[](std::size_t position){ return const_cast<char&>(static_cast<const CTextBlock&>(*this)[position]); }
避免代码重复可用const_cast去掉const符号。先将非const调用过程的this转换为const class object调用,实现对const函数调用,并将返回值的const去掉。注意可以用非const函数使用此方式,不要用const函数通过此方式调用非const函数。
条款4:确定对象使用前已先被初始化
内置(class、struct内)对象需要手工初始化。
构造函数最好用初始化列表,而不是在函数体内进行赋值(函数体内赋值前会自动先对成员进行构造,若没有初始化列表构造时的值是随机的,这样等于两次操作),初始化列表次序随意,但初始化顺序以声明顺序为准。
不同文件中的外部对象初始化顺序不一定,若跨文件调用(以extern方式引用全局变量)不保证其已经被初始化。尽量使用内置对象。
条款5:了解C++默认编写并调用哪些函数
这个对么?这里说编译器会自动创建构造、拷贝、析构函数,但《深度探索C++对象模型》中说的是只有必要的时候才会创建出来。文中说如果有自定义的构造函数则不会创建,但探索中说的是也会在自定义构造前面加东西如果必要。。。。我更原因相信深度探索里写得,因为那样更合理,更高效,通过编译期的复杂判断保证了runtime时的高效。
条款6:若不想使用编译器自动生成的函数,就该明确拒绝
将拷贝构造、赋值操作符声明为私有可以禁止此行为。可通过建立一个具有私有拷贝和赋值函数的类并继承此类以避免每个类都重复私有化函数的撰写。
条款7:为多态基类声明virtual析构函数
带多态性质的base class应具有虚析构函数,而不是普通析构函数(这样会导致以base class指针释放派生类时不调用派生类的析构函数,派生类空间未释放),也不是纯虚析构函数(析构调用过程是最深层次的base class的析构最先被调用,若为纯虚将无法调用)
继承stl中的也要注意是否有虚析构,若没有不建议继承。
可以用组合,感觉还是多用组合好。接口要保证虚析构+所有其他均为纯虚函数,没有构造函数。
条款8:别让异常逃离析构函数
析构函数不应该抛出异常:对象在函数结束时自动析构,若出现异常会中断后续对象的析构,若未中断后续析构再出现异常会有多个异常同时出现,这些行为都不可控。
析构函数中若有可能出异常的调用,应自己try catch,并在catch中消除异常不继续上传,或直接调用std::abort结束程序。对于可能发生异常的方法最好提供普通接口供用户直接调用处理,并在析构时亦自动调用以减少一部分错误可能。
条款9:绝不在构造和析构过程中调用virtual函数
对于C++:在构造和析构中不要调用虚函数,因为这样的调用不会真正调用到派生类。java C#没事。
编译期间会自动添加代码完成vptr的指向,并完成基类、成员对象的构造代码的添加以及数据成员构造代码,这些都会增加到函数代码之前(初始化列表的初始化值对应添加到相应构造初始值中)。这样来说构造函数中的代码执行时vptr已经正确指定了,只要派生类重写了就已经指向派生类了。
析构是先调用函数本体,然后进行从深层次到当前的内存释放。
也就是说,如果保证这个构造函数是最后的派生类的构造函数时,是可以在构造、析构中调用虚函数的。如果是基类的构造和析构那么真出现了调用绝对不会调用到派生类。
条款10:令operator=返回一个reference to *this
赋值采用的右结合律 x=(y=(z=15)));
赋值操作符要返回一个对当前对象的引用:
MyStr& operator =(const MyStr& str) { return *this; }
条款11:在operator=中实现“自我赋值”
避免x=x发生时出现自己的资源被自己释放的问题。要精心处理复制过程的new delete以及拷贝过程,包括对源和目标的分析。其余的任何函数也应注意被操作的多个对象实质是一个对象时的问题。
MyStr& operator =(const MyStr& str) { if(this == &str) return *this; return *this; }
条款12:复制对象时勿忘其每一部分
赋值、拷贝函数应确保对所有成员变量及base class成分的复制。不要一个copy调用另一个copy而应该共同机制放在第三个函数。
条款13:以对象管理资源
通过对象的构造函数创建资源,并在析构函数释放。尽量不要在一个函数内new后delete以防止delete之前return导致资源未释放。(资源取得时机便是初始化时机“Resource Acquisition Is Initialization,RAII”)。
可以使用std::auto_ptr,auto_ptr在调用赋值/拷贝函数时,会将源中存储的指针改为null,避免共同管理同一个对象,否则析构时会多次释放导致不可控。
sharedptr通过引用计数方式记录(引用计数型智慧指针reference-counting smart pointer,RCSP),而不是在赋值/拷贝时直接转移所有权(源定义为null),以避免对源智能指针的操作导致错误问题。注意环状引用时会出错,两个指针互指且不再使用也不会自动释放。
std::tr1::shared_ptr的析构函数是delete不是delete []不要对动态分配得到的array使用,std::tr1::shared_ptr<int> spi(new int[10]);此时最后只会释放int[0]这一个空间。boost中的boost::scoped_array和boost::shared_array可以进行动态分配空间的delete,但一般用string、vector等容器即可。
c++11中auto_ptr已经被弃用。应使用shared_ptr。
C++ Technical Report 1 (TR1)是ISO/IEC TR 19768, C++ Library Extensions(函式库扩充)的一般名称。TR1是一份文件,内容提出了对C++标准函式库的追加项目。这些追加项目包括了正则表达式、智能指针、哈希表、随机数生成器等。TR1自己并非标准,他是一份草稿文件。然而他所提出的项目很有可能成为下次的官方标准。这份文件的目标在于「为扩充的C++标准函式库建立更为广泛的现成实作品」。
C++ tr1是针对C++标准库的第一次扩展。即将到来的下一个版本的C++标准c++0x会包括它,以及一些语言本身的扩充。tr1包括大家期待已久的smart pointer,正则表达式以及其他一些支持范型编程的东东。草案阶段,新增的类和模板的名字空间是std::tr1。
C++11,先前被称作C++0x,即ISO/IEC 14882:2011,是目前的C++编程语言的正式标准。它取代第二版标准ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998公开于1998年,第二版于2003年更新,分别通称C++98以及C++03,两者差异很小)。新的标准包含核心语言的新机能,而且扩展C++标准程序库,并入了大部分的C++ Technical Report 1程序库(数学的特殊函数除外)。最新的消息被公开在 ISO C++ 委员会网站(英文)。
条款14:在资源管理类中小心copying行为
对RAII对象复制要同时复制器管理的资源。常见的复制行为是抑制复制、引用计数。
条款15:在资源管理类中提供对原始资源的访问
应提供对原始资源获取的接口,有些需要直接操作原始资源的情况。可通过get函数或者通过operator A() const {}方式提供隐式转换函数。
条款16:成对使用new和delete时要采取相同形式
new和delete要同时使用[]或者都不用。
条款17:以独立语句将newed对象置入智能指针
对于智能指针的使用应独立一个语句,而不是把对象创建、智能指针构建作为一个其他函数的参数,这样可能出现内存泄漏。
如双参数函数,第一个参数是智能指针,第二个是其他操作,当new出新的对象以后,先执行了第二个参数操作并出现异常,此时new的新对象并未作为参数使智能指针构造,process(std::tr1::shared_ptr<Wideget>(new Widget), priority());应分为两个函数。
条款18:让接口容易被正确使用,不容易被误用
shared_ptr可以定制删除器(第二个参数),防止DLL问题,可用于自动解除互斥所。cross-DLL problem对象在动态链接库dll中被new创建,却在另一个dll内被delete销毁。
保证接口一致性、误用上要有足够的约束。
条款19:设计class犹如设计type
每个类都是一个类型,应充分考虑一个类型的各方面,包括重载函数和操作符、内存分配于释放、对象的创建和销毁。
条款20:宁以pass-by-reference-to-const替换pass-by-value
用const引用替换传值,这样高效也能避免切割问题,对于内置类型传值更好。
多态只有在引用、指针才有效,若将派生类以值的方式给予父类可能出现切割情况。
条款21:必须返回对象时,别妄想返回其reference
不要返回函数中new而存在于堆中对象的引用(不知道谁负责delete),不要返回函数局部变量的引用(栈中,这样函数结束实例已经被销毁)。不要反悔局部静态成员的引用,对于多线程不安全,对于连续调用行为也不可控,当真的需要返回一个对象时就返回对象而不是引用。
任何一个引用必然存在一个和他不同名的相同实例,不得不考虑另一个名字的行为。
条款22:将成员变量声明为private
protected并不比public更具封装性。数据成员的操作应具有一致性,public下无法保证调用者行为。
条款23:宁以non-member、non-friend替换member函数
可以把成员函数放到另一个class做为静态函数,以保证一个class就是对数据的封装,超出目的范围的提出来,保证封装性。
条款24:若所有参数皆需要类型转换,请为此采用non-member函数
能避免friend就避免,变成非成员函数以后若非必要不要加friend,朋友带来的麻烦往往多余价值。
条款25:考虑写出一个不抛出异常的swap函数
当swap对自定义类型效率不高时,应提供swap成员函数,并确保不发生异常。并提供一个非成员函数调用他。
对于建立的非成员函数,std命名空间中不能够扩充新的函数,重载也不行,对于函数的非成员swap要用全特化。template<> void swap<TT>(xx& a, xx& b){};
调用swap时神队std::swap使用using声明式,然后调用swap并不带任何命名空间资格修饰
条款26:尽可能延后变量定义式的出现时间
尽量在用的时候建立对象,不要提前建立。
对于循环,除非明确知道复制成本小于“构造+析构”成本,并且正在处理效率高度敏感的代码,否则不要把变量初始化放到循环外面。
确实从来没考虑过赋值成本与构造+析构的大小问题,单纯的只考虑了提出循环避免构造+析构。。。。从来都是写到外面的
条款27:尽量少做转型动作
提倡使用四个C++风格转型,不要用T(a)/(T)a这样的C方式转型。
const_cast常量性转换,唯一有此能力的C++转型符号
dynamic_cast主要用来进行安全向下转型,从父类引用或指针向派生类转换。派生类转父类用static_cast就行。这个需要消耗重大的运行成本
reinterpret_cast低级转型,行为和结果取决于编译器,不可移植。
static_cast强制转型,但无法进行const、non-const转换
static_cast也不能去掉volatile、__unaligned
reinterpret_cast以二进制形式对原有对象进行重新解释(可进行无关类型的转换),可以把指针转换为int等等,对一段区域的二进制内容进行赋值构成新的值并赋予新的类型解释,不会对二进制上的内容做任何修改,可用于:
- 从指针类型到一个足够大的整数类型
- 从整数类型或者枚举类型到指针类型
- 从一个指向函数的指针到另一个不同类型的指向函数的指针
- 从一个指向对象的指针到另一个不同类型的指向对象的指针
- 从一个指向类函数成员的指针到另一个指向不同类型的函数成员的指针
- 从一个指向类数据成员的指针到另一个指向不同类型的数据成员的指针
条款28:避免返回handles指向对象内部成分
对于const函数避免返回指针、引用、迭代器指向内部成员(包括const的指针引用迭代器),使一个const函数更像一个常量函数,并避免dangling handles虚吊号码牌。
条款29:为“异常安全”而努力是值得的
异常安全函数即使发生异常也不会泄露资源或允许任何数据结构破坏。这样的函数分为三种保证:
基本型:发生时,程序内任何事物仍然保持在有效状态下,不破坏对象和数据结构。但当前状态无法预料。
强烈类型:发生时,程序状态不变。
不抛异常型:int aa() throw();告知它宗科正常运行,不是永远不抛异常,而是如果发生将是致命错误。
绝大部分都在基本和强烈之间。一个系统有一个函数不具备异常安全性则整个系统都不具备。
一个函数的异常安全性不会超过所调用函数的最低异常安全性级别。
条款30:透彻了解inlining的里里外外
inline应用在小型、被频繁调用的函数上,对后续调试过程、二进制升级更好也可降低代码膨胀问题。
不要因为是函数模板就一定要声明为inline。
条款31:将文件间的编译依存关系降至最低
1想依赖于声明式,而不是定义式。头文件中应以完全且仅有声明式的形势存在,无论是否涉及模板。也就是私有成员用指针,前置声明对应class并在源文件中include。
2进一步的将私有成员过多情况下可以构建一个针对此class的实现类,并在声明类中的private只提供一个实现类的指针(用智能指针),将所有需要涉及到多个私有成员的实现操作转移到实现类。
3还可以用接口类。
对于2,类似于Qt库的实现,每个公开调用的类只有一个私有成员类名+private类的指针。
对于3,接口类要求无构造、虚析构、其他全部纯虚函数。无数据成员。
谷歌C++编程规范(当然这部分说了不要前置声明任何std内容):
尽可能地避免使用前置声明。使用
#include
包含需要的头文件即可。优点:
- 前置声明能够节省编译时间,多余的
#include
会迫使编译器展开更多的文件,处理更多的输入。- 前置声明能够节省不必要的重新编译的时间。
#include
使代码因为头文件中无关的改动而被重新编译多次。缺点:
前置声明隐藏了依赖关系,头文件改动时,用户的代码会跳过必要的重新编译过程。
前置声明可能会被库的后续更改所破坏。前置声明函数或模板有时会妨碍头文件开发者变动其 API. 例如扩大形参类型,加个自带默认参数的模板形参等等。
前置声明来自命名空间
std::
的 symbol 时,其行为未定义。很难判断什么时候该用前置声明,什么时候该用
#include
。极端情况下,用前置声明代替includes
甚至都会暗暗地改变代码的含义:// b.h: struct B {}; struct D : B {} // good_user.cc: #include "b.h" void f(B*); void f(void*); void test(D* x) { f(x); } // calls f(B*)如果#include
被B
和D
的前置声明替代,test()
就会调用f(void*)
.
- 前置声明了不少来自头文件的 symbol 时,就会比单单一行的
include
冗长。- 仅仅为了能前置声明而重构代码(比如用指针成员代替对象成员)会使代码变得更慢更复杂.
结论:
- 尽量避免前置声明那些定义在其他项目中的实体.
- 函数:总是使用
#include
.- 类模板:优先使用
#include
.
条款32:确定你的public继承塑模出is-a关系
条款33:避免遮掩继承而来的名称
继承时对基类替换只看名字,不看参数、类型。也就是派生类中一个函数会覆盖掉基类中多个同名的重载函数,同时派生类中一个数据对象会覆盖掉基类的其他类型的同名对象。
可以通过派生类中写using BaseClassName::funciton_nam;方式让基类的所有function_name的重载函数都可见。
话说所有函数不是都被重命名了么?命名过程考虑了函数名和参数列表,那这样了话重载的两个函数实际上不是一个名字,派生过程为什么会覆盖掉同名的所有重载函数?
条款34:区分接口继承和实现继承
public继承下派生类继承了基类所有接口。
纯虚函数:只继承了接口
常规虚函数:继承了接口和一个缺省实现,可以重写。
非虚函数:继承了接口和强制性实现。
非虚函数不应该重写,但可以有这个函数(不算重写了),意义不一样。最后生成的object中实际上没有存储派生类的非虚函数的指针(没再vptl中存储),不支持多态,对应的函数是在class中的代码段存储了。这样当用多态切换成基类时会直接去基类的代码段找实现(vptl中没有),也就是多态性质不存在。
条款35:考虑virtual函数以外的其他选择
找virtual函数替代方式,可以用NVI收发及Strategy设计模式。NVI是特殊形式的template method设计模式。缺点是函数移到了class外部成为非成员函数无法访问class的非public成员。
tr1::function行为像函数指针,但可接纳与指定目标签名式兼容的所有可调用物。
调用一个virtual函数是有成本的,要经过最少一次vptr跳转到vtbl然后找到对应的函数指针
条款36:绝不重新定义继承而来的non-virtual函数
34条补充写了。。。。
条款37:绝不要重新定义继承而来的缺省参数值
缺省函数值是静态绑定的,继承以后不应该再给任何一样/不一样的缺省值。
继承后重写只应该做动态绑定的事情。(就是vtbl中的函数指针指向的一个新的function object的实现)
条款38:通过复合塑模树has-a 或“根据某物实现出”
复合?composition我还是喜欢说组合。。。public继承是is-a关系,在私有成员中包含其他class的object是组合。
is-implemented-in-terms of根据某实物出现
条款39:明智而审慎的使用private继承
private继承不是is-a关系。而是类似于组合的形势,内部可以调用base class,不会有外部公开的接口。尽可能用组合而不是private继承。相比于组合可以造成empty base最优化,对于对象尺寸较友好。
谷歌规范里好像禁止还是不建议private继承,总会有办法解决不需要base公开接口,但想调用base的protected接口的情况。
组合 > 实现继承 > 接口继承 > 私有继承
条款40:明智而审慎的使用多重继承
多重继承可能需要虚基类。。。就这个就可以尽可能拒绝了
条款41:了解隐式接口和编译期多态
类对接口是显示的,以函数前名为中心,多态通过virtual发生于运行期。
对template参数而言,接口是隐式的,基于有效的表达式。多态通过模板具现化和函数重载解析并发生于编译器。
条款42:了解typename的双重意义
声明模板参数时class和typename同义。对嵌套从属类型名称需要用typename标识但不得在base class lists或初始化列表内作为基类的修饰符
嵌套从属类型
template<typename C> void xx() {
typename C::const_iterator* x;
}
必须这么声明告诉编译器C::const_iterator是个类型,这就是个声明。否则可能出现:const_iterator是C的一个静态数据成员,或者x是全局变量这就是个乘法运算。
条款43:学习处理模板化基类内的名称
模板支持继承,但这个继承不符合面向对象的概念。因为模板类有全特化特性,所以在派生模板类中调用基模板类时,并不知道基类是否有这个函数,所以编译解析函数过程中不会检索基类是否有,而是直接报错。可以通过this->或者直接写基类名称::或者通过using包含基类的接口实现在对函数编译时顺利通过。
但上述所有指定方式只是对编译器的一个承诺,若最后因为全特化而破坏了承诺,那么在解析具体实例化模板时会出现错误。
全特化、偏特化:模板类支持任意类型,不同类型传入均会获得所有模板类的方法。但全特化可对指定类型赋予单独的类实现。偏特化是模板类中有多个模板参数,但是只对个别几个参数指定固定值并赋予特别的实现。
条款44:将与参数无关的代码抽离templates
避免代码膨胀,与类型参数无关的抽离,与参数有关的可通过完全相同二进制表述的局限类型共享实现码。
条款45:运用成员函数模板接受所有兼容类型
请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。
如果声明member templates用于泛化copy构造函数或泛化assignment操作,还是要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
当编写一个class template时,它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为class template内部的friend函数。
条款47:请使用traits class表现类型信息
STL源码中5中迭代器。
- input迭代器,它是read only,只能读取它指向的对象,且只能读取一次。它只能向前移动,一次一步。它模仿指向输入文件的阅读指针(read pointer);C++程序中的istream_iterators就是这类的代表。
- output迭代器,和input迭代器相反,它是write only。它也是只能向前移动,一次一步,且只能涂写一次它指向的对象,它模仿指向输出文件的涂写指针(write pointer);ostream_iterators是这一类代表。
- forward迭代器。这个迭代器派生自input迭代器,所以有input迭代器的所有功能。并且他可以读写指向的对象一次以上。
- bidirectional迭代器继承自forward迭代器,它的功能还包含向后移动。STL中的list、set、multiset、map、和multimap迭代器就是这一类迭代器。
- random access迭代器继承自bidirectional迭代器。它厉害的地方在于可以向前或向后跳跃任意距离,这点类似原始指针,内置指针就可以当做random access迭代器使用。vector、deque和string的迭代器就是这类。
traits是一种技术,是C++程序员共同遵守的协议。这个技术要求之一就是,它对内置类型和自定义类型表现的一样好。
traits classes使得类型相关信息在编译器可用,以模板和模板特化完成实现。并整合重载技术后使其有可能在编译器对类型进行if..else测试。
条款48:认识template元编程
将运行期代码提前到编译器。
条款49:了解new-handler的行为
class Widget{
public:
static std::new_handler set_new_handler(std::new_handler p) throw();
static void* operator new(std::size_t size) throw(std::bad_alloc);
private:
static std::new_handler currentHandler;
};
std::new_handler Widget::currentHandler=0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw()
{
std::new_handler oldHandler=currentHandler;
currentHandler=p;
reutrn oldHandler;
}
Widget的operator new做以下事情:
- 调用标准set_new_handler,告知Widget错误处理函数。这会将Widget的new-handler安装为global new-handler。
- 调用global operator new,如果失败,global operator new会调用Widget的new-handler,因为第一步。如果global operator new最终无法分配足够内存,会抛出一个bad_alloc异常。这时Widget的operator new要恢复原本的global new-handler,之后在传播异常。
- 如果global operator new调用成功,Widget的operator new会返回一个指针,指向分配的内存。Widget析构函数会管理global new-handler,它会将Widget’s operator new被调用前的那个global new-handler恢复回来。
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用。
nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是有可能抛出异常。
以后好好研究一下
条款50:了解new和delete的合理替换时机
- 用来检测运用上的错误。如果delete new的内存失败,会导致内存泄漏。如果在new所得内存多次delete会导致不确定行为。使用编译器提供的operator new和operator delete不能检测上述行为。如果operator new持有一个链表,其存储动态分配所得内存,operator delete则将内存从链表删除,这样就能呢检测上述错误用法。如果编程错误,可能在分配内存的之前区域或之后区域写入数据;这时可以自己定义operator new分配超额内存,在多出部分写上特定byte patterns(即签名,signature),自己定义operator delete检测签名是否更改。
- 为了强化效能。operator new和operator delete如果开辟大内存、小内存,持续这样做会造成内存碎片,这在服务器的后台程序上,可能会导致无法满足大区快内存需求,即使有足够但分散的小区块自由内存。使用自己定制的operator new和operator delete可以避免这样的问题。针对特定的需求,有时还可以提升性能。
- 为收集使用上的统计数据。在定制operator new和operator delete之前,应该首先了解软件如何使用动态内存。分配区块如何分布?寿命如何?它们是FIFO先进先出还是LIFO后进先出,或随机分配和归还?软件在不同执行阶段有不同的分配归还形态吗?任何时刻使用的最大动态分配量是多少?自己定义的operator new和operator delete可以轻松收集到这些信息。
- 为了增加分配和归还的速度。使用定制的针对特定类型对象的分配器,可以提高效率。例如,Boost提供的Pool程序库便是。如果在单线程程序中,你的编译器所带的内存管理具备线程安全,你可以写个不具备线程安全的分配器而大幅度改善速度。
- 为了降低缺省内存管理器带来的空间额外开销。泛用型分配器往往(虽然并非总是)不只比定制型慢,还使用更多空间,因为它们常常在每一个分配区块上招引某些额外开销。针对小型对象开放的分配器,例如Boost库的Pool,本质上消除了这样的额外开销。
- 为了弥补缺省分配器的非最佳对齐(suboptimal alignment)。X86体系结构上的double访问最快–如果它们是8-byte对齐。但是编译器自带的operator new并不保证分配double是8-byte对齐。
- 为了将相关对象成簇集中。如果特定的某个数据结构往往被一起使用,我们希望在处理这些数据时将“内存页错误”(page faults)的频率降至最低,那么为此数据结构创建另一个heap就有意义,这样就可以将它们成簇集中到尽可能少的内存也上。
- 为了获得非传统的行为。有时候我们需要做operator new和delete没做的事。例如,在归还内存时将其数据覆盖为0,以此增加应用程序的数据安全。
有许多理由需要写个自定的new和delete,包括改善效能、对heap运用错误进行调试、收集heap使用信息。
条款51:编写new和delete时需固守常规
operator new应该内涵死循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0 bytes申请。class专属版本的还应该处理“比正确大小更大的(错误)申请”。
operator delete应该在收到指针时不做任何事。class专属版本则还应该处理“比正确大小更大的(错误)申请”。
条款52:写了placement new也要写placement delete
当编写一个placement operator new时,也要编写对应版本的placement operator delete。否则就可能造成隐蔽的内存泄露。
当声明了placement new和placement delete时,就会掩盖正常版本。
条款53:不要轻忽编译器的警告
严肃对待编译器发出的警告信息。努力在你的编译器最高警告级别下做到无任何警告。
不要过度依赖编译器的报警能力,因为不同编译器对待事情的态度不相同。一段有警告的代码,移植到另一个编译器上,可能没有任何警告。
条款54:让自己熟悉包括TR1在内的标准程序库
C++98加入的:
STL,即Standard Template Library标准模板库。
iostreams,包含用户自定义缓冲功能、国际化I/O,以及先定义好的对象cin、cout、cerr和clog。
国家化支持,包括多区域能力。
数值处理,包括复数模板(complex)和纯数值数值(valarray)。
异常阶层体系(exception hierarchy)。
C89标准程序库。
TR1
- 智能指针
- tr1::function,可以表示任何callable entity(可调用物,即任何函数或函数对象),只要签名复合目标。
- tr1::bind,它能做STL绑定器(binders)bind1st和bind2nd所做的每一件事,且更多。
把TR1组件划分为2组,第一组提供彼此互不相干的机能:
- Hash table,可以用来实现set、map等。
- 正则表达式(Regular expression),包括以正则表达式为基础的字符串查找和替换等。
- Tuples(变量组),这是标准程序库中的pair template的新一代制品。
- tr1::array,本质是一个支持成员函数begin和end的数组。
- tr1::mem_fn,这是个语句构造上与成员函数指针(member function pointer)一致的东西。
- tr1::reference_wrapper,一个让reference的行为更像对象的设施。
- 随机数(random number)生成工具,它大大超越了rand。
- 数学特殊函数。
- C99兼容扩充
第二组TR1组件由更精巧的template编程技术构成
- Type traits,一组traits classes(**条款**47),用以提供类型的编译期信息。
- tr1::result_of,这是个template,用来推导函数调用的返回类型。
总结
C++的标准程序的主要机能由STL、iostream、locales组成。并包含C99标准程序库。
TR1添加了智能指针、一般化函数指针、hash-based容器、正则表达式以及另外10个组件的支持。
TR1只是一份规范,为获得TR1提供的好处,需要一个实物,例如Boost。
最新评论