Abel'Blog

我干了什么?究竟拿了时间换了什么?

0%

C++Effective-C++

简介

c++基础书,回顾。做一做笔记。这本书被侯杰推荐,第三版版编写于2005年。类似的还有More Effective C++,Scott Meyers,1996年编写,Exceptional C++,Herb Sutter,1999年编写。

1.让自己习惯C++

条款1 视C++为一个语言联邦

  1. c++先是继承了c语言;
  2. 增加了面向对象的内容;
  3. Template C++,模板元编程;
  4. STL,包含容器、迭代器、算法、函数对象;

条款2 尽量以const,enum,inline代替define

防止出现什么问题。

条款3 尽可能使用const

const最首先适用于定义常量变量。比define优势在于有namespace。在编译的时候,会有多份存在。

使用模板函数、inline函数替换掉define的函数,避免传值时如果有累加操作造成问题。

Const用于变量

1
2
3
4
5
Char greeting[]=”hello”;
Char*p = greeting;
Const char*p = greeting;// 修饰内存块不可修改
Char*const p = greeting;// 修饰指针不可修改
Const char* const p = greeting;// 两者都不能修改

在class里面的this指针就是个T*const this;

Const 成员函数,为了确定该成员函数可当作const T* 指针调用。1.表面此函数不会修改任何成员;2.提供const 对象调用函数。我们在编写operator函数的时候,实现是否含有const分别实现操作函数。

const成员函数行为流派有两种。

Bitwise constness:const成员函数不会修改对象内任何一个bit。

Logic constness:const成员函数里面,需要修改部分成员变量。需要使用mutable修饰释放掉non-static成员变量的bitwise constness束缚。

编写const函数的实现,使用const_cast包装成非const,使用static_cast将自己修改成const,这样就能避免代码重新编写。避免代码重复。

条款4 去顶对象被使用前已先被初始化

对象的变量在使用的时候,一定做过一次赋值,防止半随机造成不可知的问题。

2.构造/析构/赋值运算

条款5 了解C++默默编写并调用哪些函数

需要搞清楚构造函数、复制构造函数的运行逻辑。

条款6 若不想使用编译期自动生成的函数,就该明确拒绝

如果不需要使用默认的构造、析构、赋值函数,那就需要明确的将其设置成private,或者是私有成员函数。在新版本里面可以直接使用delete来将函数置空。

条款7 为多态基类声明virtual析构函数

std::string作为基类是错误的,由于std::string的析构函数不是个virtual方式的。

需要学会如何计算一个class的size。

如果存在继承的情况,在构造、析构函数里面不要调用虚方法。还未生成其调用关系,这样就会造成问题。

条款8 别让异常逃离析构函数

析构函数不要出现异常,否则会存在内存泄漏。

条款9 绝不再构造和析构过程中调用virtual函数

因为这样调用的时候,从不会下降至derived class。

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

这种主要是为了在赋值连锁形式的时候,可以直接获取一个引用。此规则约定俗成。为了减少连锁操作的时候,重复分配。

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

Opertator=操作符,一定要判断是否在自己赋值自己,而且需要返回*this,因为有可能赋值给你的那个值在做完操作之后马上就销毁了。

复制对象的时候,一定要将每个成员变量都复制了,包括它的基类的数据结构也需要考虑。

条款12 复制对象时勿忘其每个成分

复制对象相关函数一共两个:复制构造函数,赋值操作符函数。

当子类在实现复制工作都时候,可能父类的成员无法访问,这样也得驱动父类的复制流程。

3.资源管理

条款13 用对象管理资源

可以考虑使用std::auto_ptr只能指针来控制内存释放。利用RAII管理内存。

条款14 在资源管理类中小心coping行为

RAII(Resource Acquisition Is Initialization),资源取得时机便是初始化时机。

使用了Lock类构造,析构函数来控制临界区。如果定义了两个并且将已经生成的Lock赋值给另外一个Lock,当第二个Lock构造的时候,将会重新进入临界区,造成问题。

条款15 在资源管理类中提供访问原始数据结构的接口

在new/delete应该要匹配方式来调用。New [],delete []。

将new语句和产生std::auto_ptr写到一块,这样可以防止异常造成内存泄漏。老的写法,新的写法是直接调用std::make_shared_ptr,其实意图也一样。

条款16 成对使用new和delete时要采取相同形式

new对应deletenew[]对应delete[]

条款17 以独立语句将newed对象置入智能指针

1
2
3
// 用非常干脆的方式将内存分配和构造智能指针操作绑定一块
// 防止在这期间出现了任何的异常,造成内存丢失;
std::shared_ptr<Widget> pw(new Widget);

4.设计与声明

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

设计接口的时候应该要定义明确,防止调用者造成一些误会。

条款19 设计class犹如设计type

设计类的时候,需要考虑的事情:

  • 新type的对象应该如何被创建和销毁?
  • 对象的初始化和对象的赋值有什么样的差别?
  • 新的对象如果被passed by value,意味着什么?
  • 什么是新type的“合法值”?
  • 你的新type需要配合某个继承图系(inheritance graph)吗?
  • 你的新type需要什么样的转换?
  • 什么样的操作符和函数对此新type而言是合理的?
  • 什么样的标准函数应该驳回?条款6-若不想使用编译期自动生成的函数,就该明确拒绝
  • 谁该取用新type的成员?
  • 什么是新type的“未申明接口”(undeclared interface)?条款29
  • 你的新type有多么一般化?有可能可以使用template来实现一类的type。
  • 你真的需要一个新type吗?

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

这个特性是来自于c++继承了c的环境,都是传值。容易造成复制构造函数函数的调用,并且在传递完成之后将会调用析构。

如果使用pass-by-value,可能造成了切割问题(slicing problem),继承类传入基类的变量将会抹去全部的多态。

使用pass-by-value的情况是在使用迭代器、函数对象的时候,可能需要。

条款21 必须返回对象时,别妄想返回reference

如果直接使用栈上内存,其实离开作用域就会无效。如果使用new方式,最后无人释放。如果使用static变量模式,在多线程,或者同一个语句里面会存在冲突的问题。所以最好的方式还是通过构造一个将亡值返回回去就好了,也不要使用引用方式。

条款22 将成员变量定义成private模式

使用接口控制访问权限。

Public和protected两种类型都不算太好的封装。 保不齐继承、外部会乱用这些东西。

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

在类里面提供力度很细的函数来提供操作,而用no-member、no-friend方式替换成员函数组装成套的调用。最终我们使用一个namespace将这些no-member函数封装起来。

条款24 若所有参数都需要转换,就需要写一个non-member函数

举例子就是A类,实现了operator *的操作函数,但是无法实现a实例乘上一个自然数。解决的方法就是去实现一个全局函数,const A& operator(const A& r,const A& l);而且不需要设置其为A的friend函数。

条款25 考虑写出一个不抛异常的swap函数

提供一份swap函数的实现,不能让这个过程中出现任何的异常造成各种问题。C++11里面提供了std::move方法,加上了右值引用将会很好的优化交换的速度,减少大量的内存分配。swap也需要避免其中抛出异常造成内存问题。

5.实现

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

如果代码段没有用到这个变量,可以不事先定义。如果这个函数在执行过程中,一些函数没有使用就出异常了,这样还是支付了此类的构造成本。

两个方案比较:

  1. for循环外面定义一个对象,在循环里面只对这个变量修改然后用;
  2. 在内部使用构造、析构来写临时变量;

第二种方式清晰,当for循环很小的时候推荐使用。

条款27 尽量减少调用cast语句

Cast方式老风格,c语言风格。

(T)val;c语言风格

T(val);函数风格

二者基本上无区别。

现在新的方式有下列种类:

1
2
3
4
Const_cast<T>(v)消除const特性;
Dynamic_cast<T>(v)从基类转向派生类的转换,唯一有消耗的一种cast。
Reinterpret_cast<T>(v)尝试做一次低级转换,和编译器有关系。
Static_cast<T>(v)强制隐形转换,能做上面除了const_cast之外的全部操作。

好处:

  1. 使用新的写法,更加能让代码使用grep找出来。
  2. 专用的cast方式,能让编译器帮忙矫正一些问题。

优良的代码很少使用cast语句的,这个功能又是不可或缺的。

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

类里面的handler返回尽量使用const T&方法返回出去,这样能维护其封装性。

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

Strive for exception-safe code

讲述了一个例子,使用c风格的lock,unlock,中间有一次delete一次计数器累加,还有一次内存分配。这种代码存在的一些不好的点是可能在new的时候发生异常,如何防范呢?直接使用shared_ptr来管理内存分配,使用RAII方式来控制lock,unlock操作,将delete/new操作直接使用shared_ptr的reset方式来制作,将累加代码移到最后一行,这样能减少大部分问题。

强烈安全级别的时候,使用copy-and-swap,将需要修改的对象拷贝一份出来,当新的一份内存的操作完全执行成功了,再去做一次swap。

在新的标准里面存在noexcept的语法。但是在std里面会存在std::bad_alloc的异常放出来。

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

inline函数其实就是将这些代码直接嵌入到调用者部分,“不含函数调用”的代码。

免除了调用函数的成本,为每个调用者将这块的机器码复制过去,在内存比较小的机器上,将会吃掉更多内存。将会让代码膨胀,导致换页行为。

不要将析构、构造函数使用inline方式。

不要在template函数里面使用inline方式。

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

使用接口来依赖。

能用object references,object pointers就不要使用object。

多使用class声明、而非定义。为声明和定义分离两个文件出来。

6.继承与面向对象设计

c++继承有三种:public/protected/private。

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

public继承是is-a方式的,每个特性都需要在子类里面使用。

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

如果遮挡了,那就要使用forward方式取驱动调用。

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

Pure virtual:一定要子类实现;
virtual:基类提供缺省实现;
No-virtual:直接使用基类的实现,最好不要自己实现,可以声明成protected方式。

条款35 考虑使用virtual以外的其他选择

这种模式其实就是将处理流程在基类里面想清楚,然后固化流程,提供中间处理特例。使用函数指针来提供灵活性。

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

这样的函数是静态绑定,当调用的时候,很容易就变得混乱。

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

在基类pure virtual函数中定义过了函数的默认参数,子类函数里面不能再去定义默认参数,可能会存在混乱的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Shape {
public:
enum ShapeColor {Red, Green,Blue};
virtual void draw(ShapeColor color=Red) const = 0;
}

class Rectangle : public Shape {
public:
virtual void draw(ShapeColor color=Green) const; // * 这个就是问题
}

Shape* pr = new Rectangle();
pr->draw(); // 将会是Red方式。

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

Is-implemented-in-terms-of。

条款39 明智的使用private继承方式

private继承了之后,基类的protected/public的接口都会成为private。

条款40 明智的谨慎的使用多重继承

多重继承空间会消耗的大,但是也有存在的必要,最常见的就是iostream的实现,由于读写的fd需要独立。

7.模板

通用性代码编写需要使用到模板,看完这章也不会让你成为模板专家。

条款41 了解隐式接口和编译期多态

面向对象世界里面,显式接口-explicit interface,运行期多态-runtime polymorphism 解决问题。

在模板世界里面,上述的显式接口,运行时多态都还存在,隐式接口-implicit interface,编译器多态-compile-time polymorphism 也将会被重视起来。

条款42 了解typename的双重意义

Template里面有两种class/typename。其实没有不同。

当我们在template内部使用一个类型的时候需要增加一个typename C::const_iterator。这样能避免有个静态成员变量时这个名字。Nested dependent name。

条款43 学习处理模板化基类内的名称

模板函数里面直接调用导入来的类的一个方法,如果出现了某个类需要特例化的时候,就需要自己去指定实现一份。从template继承下来的类,调用的时候需要this->方式,或者明白写上base class资格修饰符来完成。

条款44 将于参数无关的代码抽离templates

模板将会造成代码膨胀code bloat。二进制带着重复的代码、数据。

条款45 运用成员函数模板接受所有兼容类型

其实就是在class上定义过一个template,在构造/成员函数上也可以增加template来做修饰。这样的好处是成员函数能自由的生成更多的适配的函数出来给目标代码调用。

条款46 需要类型转换时请为模板定义非成员函数

这个思路其实类似条款24,当一个class需要兼容其他类型对其做操作符运算,最好从类里面分离出来。可以直接在template里面写一个friend const Rational operator*(const Rational& lhs, const Rational& rhs)函数出来。条款30里面提到的在头文件里面的函数都会成为inline。为了让这个函数消耗更加小,其实应该将这个函数做出一个doMultiply的函数,让inline函数调用。

条款47 请使用traits classes表现类型信息

这一章节需要理解了条款42,里面将会使用到typedef typename IterT::iterator_category。Z最终达成使用了if (typeid(typename std::iterator_traits::iterator_category) == xxxx)的语句的编写,其实是在编译期间去确定代码的流程。
如果我们需要对这个固定式的函数做一些特例化,就需要使用重载了,其实就是将模板中定制好类型这样来调用。Traits广泛用于标准程序库中,可以在阅读stl源码相关代码的时候获取知识。
总结traits classes使得 typeid 在编译期可用。可以使用template,特例化完成实现。
重载技术,能在编译期对类型执行if…else测试。

条款48 认识template元编程

Template metaprogram其实就是c++写成,执行于C++编译器内的程序。它的输出就是若干的c++源码。TMP源于1990s。而且这个功能是非常有用的。如果没有啥映像,说明你一定没有足够认真地思考它。

两个伟大的效力:

  1. 让某些事情更更容易。
  2. 可以将工作从运行期移植到编译期。

编译期就能检查出问题,编译时间变长,执行更高效,更小空间,运行期更小内存。

递归函数演示,将可以使用TMP在编译期就计算出结果。

用三个例子来说明TMP的用途。

  1. 确保量度单位正确。早期错误侦测。
  2. 优化矩阵运算。条款21提到operator*必须返回新对象,而条款44导入了SquareMatrix class。如果有4个矩阵×操作。会产生4次临时性矩阵保存调用结果。使用TMP里面的expression templates,就能合并这个操作。减少内存,执行效率会提高很多。
  3. 可以生成客户定制的设计模式实现品。Strategy(条款35),observer,Visitor都可以使用TMP来做实现。

TMP不是主流,程序库开发员会是它的重要用户。

可能在其他的书籍里面能看到更多的信息。

8.定制new与delete

条款49 了解new-handler的行为

new操作的时候,如果内存不足将会抛异常。之前是返回null。条款51编写new和delete时需固守常规

设计良好的new-handler函数必须做到下面的事情:

  • 让更多内存可被使用。启动分配大块内存,new-handler的时将释还给程序使用。
  • 安装另一个new-handler。当本身的new-handler无法处理,我们又知道又另一个可以替换的,能替换掉new-handler。
  • 卸载new-handler。将null指针传入,当分配时,会抛出异常。
  • 抛出bad_allo。不要再operator new里面捕获异常,将问题抛到内存索求处接着处理。
  • 不返回,

1993年,C++要求operator new必须在无法分配内存时返回null,新一代operator new应该抛出bad_alloc异常,为了兼容提供了“分配失败便返回null”行为。这个形式成为“nothrow”形式。

1
Widget* pw2 = new (std::nothrow) Widget;

条款50 了解new和delete的合理替换时机

  • 用来检查运用上的错误。
  • 为了强化效能。
  • 为了收集使用上的统计数据。

  • 为了检测运用错误。

  • 为了收集动态内存值使用统计信息。
  • 为了增加分配和归还的速度。
  • 为了降低缺省管理器带来的空间额外开销。
  • 为了弥补缺省分配其中的非最佳齐位
  • 为了将相关对象成簇集中。
  • 为了获取非传统的行为。
  • 有许多里有需要写一个自定义的new和delete,包括改善效能、对heap运行错误进行调试、收集heap使用信息。

条款51 编写new和delete时需固守常规

  • 需要检查是否在分配0byte的内存。
  • 应该包含一个无限循环,并且尝试分配内存,如果无法满足就调用new-handler。class专属版本则还应该处理“比正确大小更大的申请”。子类size可能比基类大。
  • delete的时候,收到null的指针啥时候都不做。

条款52 写了placement new也要写placement delete

  • 如果写了一个替换掉new,要写一个delete。否则会有可能内存泄漏。
  • 写了替换的new,delete,不要无意识地掩盖了正常版本。

9.杂项

条款53 不要轻忽编译器的警告

  • 努力让自己的编译期最严苛警告级别。
  • 不要过度依赖于编译期的报警,可能不同编译器警告不一样,所以有可能换了个编译器警告就没有了。

条款54 让自己熟悉包括TR1在内的标准程序库

TR1表示Technical Report 1。这里面的一些东西都已经加入到了c++11标准了。

  • stack,priority_queue
  • 国际化wchar_t
  • 复数模板 complex valarray
  • shared_ptr weak_ptr。
  • function
  • bind语法
  • Hash tables
  • 正则表达式
  • tuples 变量数组
  • array 固定长度的数组STL化
  • mem_fn 绑定成员函数
  • reference_wrapper 让references更像对象。
  • random_number 随机数
  • 数学特殊函数。
  • C99兼容扩展
  • traits classes
  • result_of 推到函数调用的返回值。

条款55 让自己熟悉Boost库

  • 字符串与文本处理
  • 容器:bitsets,多维数组
  • 函数对象和高级编程,lambda表达式。
  • 泛型编程,可见 条款47
  • 模板元编程 条款48
  • 数学和数值计算。有理数、八元数、四元数,公约数,多从运算、随机数。
  • 正确性和测试,条款41.
  • 数据结构,覆盖了TR1的类型。
  • 支持C++和python无缝操作
  • 内存,scoped_array,智能指针。
  • 杂项 CRC检测、时间日期处理,文件系统。