第一章 @
- 声明式(declaration)告诉编译器某个东西的名称和类型,定义式(definition)提供编译器的一些声明式所遗漏的细节。
- 所谓default构造函数是一个可被调用而不带任何实参者,这样的构造函数要么没有参数,要么每个参数都有缺省值。
- explicit可禁止编译器执行非预期的类型转换,除非有一个好理由允许构造函数被用于隐式类型转换,否则最好声明为explicit。
- copy构造函数被用来以同类型对象初始化自我对象,copyAssignment操作符被用来从另一个同型对象中拷贝其值到自我对象。
- 如果一个新对象被定义,一定会有一个构造函数被调用,不可能调用赋值操作。
第二章 @
-
视C++为一个语言联邦
C++是一个多重范型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式。
简单的方法是将C++视为一个由相关语言组成的联邦而非单一语言。 -
尽量以
const、enum、inline替换#define-
例如
define ASPECT_RATIO 1.653因为define不被视为语言的一部分,记号名称可能没进入记号表内,编译错误时可能会提示1.653而不是记号名称。
此外对于浮点常量而言,使用常量可能比使用#define导致较小量的code,因为预处理器盲目替换可能出现多份1.653.const char* const authorname = "Meyers"; //定义常量指针 const std::string authorname("Meyers"); //更好一些 class GamePlayer { private: static const int NumTurns = 5; //class专属常量,赋初始值 } const int GamePlayer::NumTurns = 5; //如果不允许类内赋予初值,可以类外定义 // the enum hack的补偿做法,但取const的地址合法,而取enum和#define的地址不合法。 class GamePlayer { private: enum { NumTurns = 5}; //无法让别人获得一个pointer或reference指向整数常量 int scores[NumTurns]; } -
#define误用的例子如下
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b)) //无论何时写这种宏,都必须为宏参数加上括号,但依然会有问题 // CALL_WITH_MAX(++a,b) 将会被预处理器处理为 f((++a) > (b) ? (++a) : (b)); a有可能被加两次 // 写出template inline函数可以获得宏一样的效率以及一般函数所有可预料行为和类型安全性。 template<typename T>inline void callWithMax(const T& a, const T& b){ //最好改用inline函数替换define
-
-
尽可能使用const,const允许指定一个语义约束。
如果const出现在型号左侧,表示被指物是常量;如果const出现在星号右侧,表示指针自身是常量。
对于迭代器来说,声明迭代器为const表示这个迭代器不得指向不同的东西,如果希望迭代器所指的东西不被改变,应该使用const_iterator
令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。-
将const实施于成员函数的目的是为了确定该成员函数可作用于const对象。
两个成员函数如果只是常量性不同,可以被重载!
成员函数如果是const,有两个流行概念:bitwise constness、logical constness。
bitwise constness正是c++对常量性的定义,因此const成员函数不能更改对象内任何non-static成员变量。class CTextBlock{ public: char& operator[](std::size_t position) const{ return pText[position]; //该成员函数声明为const,但返回一个reference指向对象内部值 } //函数不改变pText,但可以通过其返回值更改pText } const CTextBlock cctb("Hello"); char* pc = &cctb[0]; *pc = 'J'; //pText为Jello,这就是所谓的logical constness // 因此需要将此函数的返回类型声明为const reference ! //利用mutable释放掉non-static成员变量的bitwise constness约束 class CTextBlock{ public: std::size_t length() const{ if(!lengthIsValid){ textLength = std::strlen(pText); lengthIsValid = true; } } private: mutable std::size_t textLength; //这些成员变量可能总是会被改变,即使在const成员函数内 mutable bool lengthIsValid; } -
当const和non-const成员函数有实质等价的实现时,令non-const版本调用const版本可以避免代码重复。
class TextBlock{ public: const char& operator[](std::size_t position) const { return text[position];} char& operator[] (std::size_t position){ return const_cast<char&>( //移除op[]返回值的const static_cast<const TextBlock&>(*this) [position] //为*this加上const调用const op[] ) } }
-
-
确定对象被使用前已先被初始化,内置类型应该手动初始化!
c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,而在构造函数体内则是赋值操作。
应该使用member initialization list替换赋值操作,通常此做法效率较高
Object:Object(): oneThing(),twoThing(),threeThing() {} 将调用各个成员的default构造函数
c++成员初始化次序,base classes更早与其derived classes被初始化,class的成员变量总是以其声明次序被初始化。
不同编译单元内non-local static对象的初始化次序无明确定义。
应该将每个non-local static对象搬到自己的专属函数内,这些函数返回reference指向它所含的对象。
non-local static对象其实被local static对象替换了,c++保证函数内的local static对象会在被调用期间被初始化。class FileSystem{...}; FileSystem& tfs(){ static FileSystem fs; return fs; } class Directory{...}; Directory::Directory(params){ std::size_t disks = tfs().numDisks(); } Directory& tempDir(){ static Directory td; return td; }
第三章 @
-
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment构操作符,以及析构函数。
惟有这些函数被需要,它们才会被编译器创建出来。
编译器产出的析构函数是个non-virtual,除非它的base-class自身声明有virtual析构函数。
如果在一个内含reference成员的class内支持赋值操作,你必须自己定义copy assignment操作符。
如果某个base-class将copy assignment声明为private,编译器拒绝为其derived class生成一个copy assignment操作符。 -
如果不想使用编译器自动生成的函数,应该明确拒绝。
class Uncopyable { protected: Uncopyable(){} //允许derived对象构造和析构 ~Uncopyable(){} private: Uncopyable(const Uncopyable&); //阻止copying Uncopyable& operator=(const Uncopyable&); } class HomeForSale: private Uncopyable {...}; //继承Uncopyable实现对默认构造和赋值的拒绝 -
为多态基类声明virtual析构函数。
如果derived class对象经由一个base class指针被删除,而该base class带有一个non-virtual析构函数,其结果未定义。
任何class只要带有virtual函数都几乎确定应该也有一个virtual析构函数。
实现virtual函数,对象必须携带某些信息,这个信息则是vptr指针指出,如果class内含virtual函数,其对象体积会增加,其结构与C不同,也就无法移植。class AWOV { public: virtual ~AWOV() = 0; //析构声明为pure virtual函数,其类也为抽象类 } -
析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构应该捕捉任何异常,并吞下它们或结束程序。
如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。 -
绝不在构造和析构过程中调用virtual函数。
如果base-class的构造函数内调用了virtual函数,那么dervied class的构造需要先构造base-class,而该virtual函数绝对不会下降到dervied class阶层。
由于base class构造函数的执行更早于dervied class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。
如果此期间调用的virtual函数下降至derived class阶层,几乎必然取用local成员变量,将导致不明确行为。 -
令operator=返回一个reference to *this。
由于赋值可以写成连锁形式:x = y = z = 15,且赋值采用右结合律。
为了实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参。 -
在operator中处理自我赋值,技术包括:比较地址、精心的语句顺序、copy-and-swap
-
复制对象时勿忘其每一个成分,当为derived class写copying函数时,必须也复制其base部分。
应该让derived class的copying函数调用相应的base class函数。
不该令copy assignment操作符调用copy构造函数,如果有相同的代码,应该将其放入private函数,然后两个函数共同调用。
第四章 @
-
应该以对象管理资源,以对象管理资源的观念被称为RAII,因为几乎总是在获得一笔资源后于同一语句内以它初始化某个管理对象。
管理对象运用析构函数确保资源被释放!
auto_ptr是一个智能指针,其析构函数自动对所指对象调用delete,如果多个auto_ptr指向同一个对象,那么理论上可能会多次delete。
因此在设计上若通过copy构造函数或copy assignment复制auto_ptr,它们会变为null,而复制所得的指针将取得资源唯一拥有权。
shared_ptr通过reference-counting来解决这个问题,只有无任何shared_ptr指向该对象,对象才会被删除。
auto_ptr和shared_ptr在析构函数内做delete操作而不是delete[]操作,意味着它们无法管理动态分配所得的array,但仍可通过编译。
boost::scoped_array和boost::shared_array classes可以提供动态分配的array的管理 -
在资源管理类中小心copying行为,当一个RAII对象被复制通常有以下几种可能:
- 禁止复制,将copying操作声明为private
- 对底层资源使用引用计数法,可以使用shared_ptr实现,指定其删除器。
- 复制底部资源,进行深度拷贝
- 转移底部资源的拥有权,也就是类似auto_ptr的用法
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为
普遍的RAII class copying行为是:抑制copying、实施引用计数法。
-
在资源管理类中提供对原始资源的访问,因为某些API往往要求访问原始资源。
shared_ptr和auto_ptr都提供get成员函数,返回指能指针内部的原始指针。
对原始资源的访问可能经由显示转换或隐式转换,一般而言显示转换比较安全,但隐式转换更加方便。 -
成对使用new和delete时要采取相同形式,调用new[]时,删除也应该为delete[]
使用typedef定义类型时应该说明以哪一种形式delete它,最好尽量不要对数组形式做typedef动作。 -
应该以独立语句将newed对象置入智能指针内。
processWidget(std::shared_ptr<Widget>(new Widget),priority());
上述调用语句,参数的执行顺序可能不同
如果在"资源被创建"和"资源被转换成资源管理对象"两个时间点之间发生异常干扰,就可能发生资源泄露。
应该分开写:std::shared_ptr<Widget> pw(new Widget); processWidget(pw,priority());
第五章 @
-
让接口容易被正确使用,不易被误用。
std::shared_ptr<Investment> pInv(static_cast<Investment*>(0),DeleteFunc);建立null的shared_ptr并指定删除器
shared_ptr会自动使用它的每个指针专属的删除器!
促进正确使用接口的办法包括接口的一致性,以及与内置类型的行为兼容
阻止误用的办法包括建立新类型、限制类型上的操作、束缚对象值、消除客户的资源管理责任
shared_ptr指定删除器,可防范DLL问题,可被用来自动解除互斥锁。 -
设计class应该思考的问题:
- 新type应该如何创建和销毁
- 对象的初始化和赋值应该有什么区别
- 新type的对象如果被passed by value意味着什么
- 规范新type的合法值
- 新type继承某个体系受到什么影响
- 新type需要什么样的转换
- 什么样的操作符和函数对此type而言是合理的
- 应该拒绝什么样的函数自动生成
- 谁该取用新type的成员
- 它对效率、异常安全性、资源运用提供何种保证?
- 新的type是否一般化,是否应该定义为template
- 你真的需要一个新type吗?
-
宁以pass-by-reference-to-const替换pass-by-value,前者通常比较高效,并且可以避免切割问题。
以上规则并不适用于内置类型,以及STL的迭代器和函数对象,对他们而言pass-by-value往往比较适当。 -
必须返回对象时,别妄想返回其reference。
绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,
或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。 -
将成员变量声明为private!
这可以赋予客户访问数据的一致性,可细微划分访问控制,允诺约束条件获得保证、并提供class作者以充分的实现弹性。
protected并不比public更具封装性,其实只有两种访问权限:private(提供封装)和其他(不提供封装)。 -
宁可拿non-member、non-firend函数替换member函数,这样可以增加封装性,包裹弹性和机能扩充性。
因为non-member、non-firend函数并不增加能够访问class内private成分的函数数量,所以有较大封装性。 -
若所有参数皆需类型转换,请为此采用non-member函数。
-
考虑写出一个不抛异常的swap函数。
如果swap的缺省实现代码对你的class或template提供可接受的效率,则不需要做任何事。
如果swap缺省实现版的效率不足,则可以尝试以下做法:- 提供一个public swap成员函数,高校的置换你的类型的两个对象值,确保函数不抛出异常。
- 在你的class或template所在的命名空间内提供一个non-member swap,并令它调用上述swap函数。
- 如果你正在编写class,则为你的class特化std::swap,并令它调用你的swap成员函数。
- 如果你调用swap,请确定包含一个using声明式,让std::swap在函数内曝光,然后赤裸裸调用swap!
-
尽可能延后变量定义式的出现时间,延后到能给它初值实参为止,避免不必要的构造和析构!
-
尽可能避免转型,特别是注重效率的代码中避免dynamic_cast。
如果转型是必要的,试着将它隐藏于某个函数背后,客户不必将转型放进他们自己的代码。
宁可使用c++-style转型,也不要使用旧式转型,前者更容易辨别。 -
避免返回handles(包括reference、指针、迭代器)指向对象内部,帮助const成员函数的行为像一个const,避免发生空悬指针!
const Rectangle boundingBox(const GUIobject& obj); //该函数返回Rectangle对象 GUIobject pgo; //指向某个GUIObject对象const Point pUpperLeft = &(boundingBox(*pgo).pUpperLeft()); //boundingBox返回一个临时量,然后调用此临时量的成员函数,但语句结束后此对象被销毁! -
应该为异常安全而努力,当异常被抛出时,异常安全性的函数有以下特性
- 不泄漏任何资源
- 不允许数据败坏
异常安全函数提供以下三个保证之一: - 基本承诺:如果异常被抛出,程序内任何事物仍然保持在有效状态下
- 强烈保证:如果异常抛出,程序状态不会改变
- 不抛出保证:承诺不抛出异常
强烈保证往往能够以copy-and-swap实现出来,但强烈保证并非对所有函数都可实现或具备现实意义
函数提供的异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者。
-
将大多数inlining限制在小型、被频繁调用的函数身上,使潜在的代码膨胀更小,提升程序速度。
不要只因为function template出现在头文件,就将其声明为inline。
inline只是对编译器的申请,不是强制命令!
如果一个template所具现的所有函数都应该inlined,则将此template声明为inline。
大部分编译器拒绝太过复杂的函数inlining,而所有对virtual函数的调用也不会inline。
一个表面看似inline的函数具体的实现取决于建置环境以及编译器。
编译器通常不对”通过函数指针而进行调用”实施inline.
构造函数和析构函数往往最好不要inline。
大部分调试器对inline束手无策。 -
将文件间的编译依存关系降至最低,支持编译依存性最小化的一般构想是:相依于声明式,不要相依于定义式
基于此构想的两个手段是Handle classes、Interface classes。
程序库头文件应该以"完全且仅有声明式"的形式存在。
第七章 @
-
public继承意味着is-a的关系,适用于base class的每一件事情一定也适用于derived classes身上
-
避免遮掩继承而来的名称,derived classes内的名称会遮掩base classes内的名称!
为让被遮掩的名称再见天日,可使用using声明或转交函数
如果继承base class并加上重载函数,你又希望重新定义或覆写其中一部分,那么你必须为那些原本会被遮掩的每个名称引入using声明!否则会被遮掩!
如果你只想继承重载版本的其中一个版本,而using会令继承而来的所有同名函数都可见,因此可以用简单的转交函数来实现。
virtual void mf1() { Base::mf1(); } -
区分接口继承和实现继承!
- 成员函数的接口总是会被继承
- 声明一个pure virtual函数的目的是为了让derived classes只继承函数接口,可以为pure virtual提供实现代码,需要调用时指出class名。
- 声明impure virtual函数的目的是让derived classes继承该函数的接口和缺省实现。
- 声明non-virtual函数的目的是为了令dervied classes继承函数的接口及一份强制性实现!
non-virtual函数代表的意义是不变性(invariant)、凌驾特异性(specialization)
-
考虑virtual函数以外的其他选择
- Non-Virtual Interface手法实现Template Method模式
通过public non-virtual成员函数间接调用private virtual函数,称为NVI手法,non-virtual函数称为virtual函数的wrapper(外覆器)
wrapper可以确保在一个virtual函数被调用之前设定适当场景,调用结束后清理场景,允许derived classes重定义virtual函数 - 由Function Pointers实现Strategy模式,缺点是非成员函数无法访问class的non-public成员。
- 由tr1::function完成Strategy模式
tr1::function对象相当于一个指向函数的泛化指针,可以指向函数、函数对象、成员函数! - 古典的Strategy模式
- Non-Virtual Interface手法实现Template Method模式
-
绝不重新定义继承而来的non-virtual函数,基于条款32和条款34!
-
绝不重新定义继承而来的缺省参数值,继承一个带有缺省参数的virtual函数时,virtual函数动态绑定,而缺省参数值是静态绑定。
你可能会在调用一个定义于derived class内的virtual函数的同时,却使用base class为它所指定的缺省参数值。 -
通过复合塑模出"has-a"或"is-implemented-in-terms-of",复合的意义和public继承完全不同
-
Private继承意味is-implemented-in-terms-of,通常比复合的级别低。
当dervied class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
private继承可以造成empty base最优化,这就是EBO(空白基类最优化)的手法,但EBO一般只在单一继承才可行。 -
多继承比较复杂,程序有可能从一个以上的base classes继承相同名称,为了防止继承多份base类,你需要采用virtual继承!
使用virtual继承的那些class产生的对象往往比non-virtual的体积大,并且访问速度慢
对于virtual base classes使用的忠告:- 非必要不使用virtual bases
- 如果必须使用virtual base classes,则尽可能避免在其中放置数据。
多继承的正当用途:public继承某个Interface class和private继承某个协助实现的class的两相组合。
第八章 @
-
了解隐式接口和编译器多态!
以不同的template参数具现化function template会导致调用不同的函数,这就是编译器多态。
隐式接口由有效表达式组成! -
typename的双重意义
- 在template声明式中,class和typename没什么不同。
- 只能使用typename标识嵌套从属类型名称,但不得在base class lists或member initialization list内以它作为base class修饰符!
template内出现的名称如果相依于某个template参数,称之为从属名称
如果从属名称在class内呈嵌套状,则称之为嵌套从属名称
-
处理模板化基类内的名称可在derived class templates内通过"this->“指涉base class templates内的成员名称,或由一个明白写出的base class资格修饰符完成
-
将与参数无关的代码抽离templates
template生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数而造成的代码膨胀,往往可消除,以函数参数或class成员变量替换template参数。
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。 -
运用member function template(成员函数模板)生成"接受所有兼容类型"的函数。
member template并不改变语言规则,如果你声明member templates用于"泛化copy构造"或"泛化assignment"操作,还是需要声明正常的copy构造函数和copy assignment操作符。 -
当我们编写一个class template,而它所提供之"与此template相关的"函数支持"所有参数隐式类型转换"时,请将那些函数定义为"class template"内部的firend函数。
-
Traits classes使得"类型相关信息"在编译期可用,它们以templates和"templates特化"完成实现。
整合重载技术后,traits classes有可能在编译期对类型执行if…else测试。
设计并实现一个traits class:-
确认若干你希望将来可取得的类型相关信息。
-
为该信息选择一个名称。
-
提供一个template和一组特化版本,内含你希望支持的类型相关信息。
使用一个traits class:- 建立一组重载函数或函数模板,彼此间的差异只在于各自的traits参数,令每个函数实现码于其接受的traits信息相应和。
- 建立一个控制函数或函数模板,调用上述函数并传递traits class所提供的信息。
-
-
Template metaprogramming(TMP,模板元编程)是编写template-based c++程序并执行于编译期的过程。
TMP使用递归模板具现化来实现循环:// TMP可被用来生成"基于政策选择组合"的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。 template<unsigned n> struct Factorial { enum { value = n * Factorial<n-1>::value }; }; template<> struct Factorial<0> { enum { value = 1 }; };
第九章 @
-
了解new-handler的行为
当operator new抛出异常以反映一个未获满足的内存需求之前,它会先调用错误处理函数,为指定此函数,必须调用set_new_handler(位于)
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
Nothrow new是一个颇为局限的工具,它只适用于内存分配: 后继的构造函数调用还是可能抛出异常 -
了解new和delete的合理替换时机。
替换编译器提供的operator new或operator delete的理由:- 用来检测运用上的错误
- 为了收集动态内存分配的使用统计信息
- 为了增加分配和归还的速度
- 为了降低缺省内存管理器带来的空间额外开销
- 为了弥补缺省分配器中的非最佳齐位
- 为了将相关对象成簇集中
- 为了获得非传统的行为
-
编写new和delete时需固守常规。
operator new应该内含一个无穷循环,并在其尝试分配内存,如果无法满足需求,则调用new-handler,它应该有能力处理0bytes申请。
class专属版本还应该处理"比正确大小更大的(错误)申请”。
operator delete应该在收到null指针时不做任何事,class专属版本还应该处理"比正确大小更大的(错误)申请"。 -
当你写一个placement operator new,请确定也写出了对应的placement operator delete,否则可能发生内存泄漏。
声明placement operator new/delete时,注意不要遮掩正常的版本。
自定形式扩充标准形式,可利用继承机制及using声明式,base class内含所有正常new和delete,dervied使用using让base版本的可见。
结尾 @
- 不要轻忽编译器的警告。
- 熟悉TR1,TR1添加了智能指针、一般化函数指针,hash-based容器、正则表达式、另外10个组件的支持。
- 熟悉Boost