《Effective C++》读书笔记: 习惯C++/构造析构赋值/资源管理

本文主要是记录自己在看书过程中认为重要的东西,也为想快速浏览本书中的条款的人提供一些帮助

让自己熟悉C++

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

作者认为C++可以看做主要由以下四个"次语言"组成的语言联邦:

  • C语言: 区块、语句、预处理、内置数据类型、数组、指针
  • 面向对象的C++: 类、封装、继承、多态、动态绑定(virtual函数)
  • Template C++: 泛型编程
  • STL

总结: C++高效编程视状况而改变,取决于使用C++的那一部分
(所以这就是C++语法如此复杂的原因?)

条款02: 尽量以const, enum, inline替换 #define

  • 原因: define宏定义在预处理阶段展开,没有进入编译器处理,也没有进入记号表(指的就是记录变量的值的表),导致难以追踪。应该使用const或enum替换单纯变量,用inline函数替换形似函数的宏
  • 当然真要用宏定义也未尝不可,比如调试的时候可能会用到(这个是我加的,不确定原书有没有这种说法)

条款03: 尽可能使用const

  • const出现在*号左边表示被指物是常量:底层const
  • const出现在*号右边表示指针本身是常量:顶层const

类中的const函数:

  • mutable 关键字,被修饰的变量可以在const函数中被改变
  • 当const成员函数和non-const成员函数功能等价时,可以让non-const函数调用const函数,以减少代码重复,但是反过来是不行的

条款04: 确定对象被使用前已经初始化

  • 构造函数本体内部的行为不是初始化,而是赋值;使用成员初始列替换赋值动作,可以提高效率
  • 总是使用成员初始列,可以避免不必要的麻烦;并且顺序最好与声明顺序相同
  • 跨编译单元初始化问题: 比较复杂,建议看书

构造/析构/赋值运算

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

  • 默认构造函数和析构函数主要是放置一些幕后的代码,比如基类的构造函数、非静态成员变量的构造/析构函数
  • 默认拷贝构造函数和拷贝赋值运算符, 由编译器创建的版本只是单纯的将非成员变量拷贝
  • 编译器可能拒绝为含有引用(C++中的引用不能更改所指对象)或者const成员的类生成默认拷贝赋值运算符

条款06: 若不想使用编译器生成的函数,就应该明确拒绝

  • 将拷贝构造函数和拷贝赋值运算符声明为private的(只声明,不实现),以阻止编译器暗自创建,同时可以起到阻止外部调用的作用;如果此时友元函数或者成员函数调用,会在链接时报错(因为没有具体实现)
  • 将基类的拷贝构造函数和拷贝赋值运算符声明为private的,继承的子类即使是友元函数或成员函数调用这两个方法会在编译期报错
  • 可以考虑使用Boost提供的nocopyable类

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

  • 当一个子类对象经由一个基类指针被删除,而该基类带着一个non-virtual的析构函数,其结果是未定义的
  • 当一个类并没有被设计为一个基类,令其析构函数为virtual的反而是一个坏主意,因为会造成对象体积的增加(因为需要维护一个虚函数表)
  • 继承一个带有non-virtual析构函数的基类要注意和上述一样的未定义问题,比如继承STL容器或string时; 书中甚至说要"拒绝继承标准容器的诱惑"(但是后面又提到标准容器的设计不是为了多态用途,所以其实继承他们是没有问题的??)
  • 当一个抽象类没有其他函数可以用来构建一个纯虚函数,但是程序员又想使其成为抽象类,可以将这个类的析构函数声明为纯虚的,并且需要提供一个定义(这点不是很明白,难道不会默认合成一个吗)
  • 该条款只针对多态性质的类,如果不是为了多态用途,那么不应该声明virtual析构函数

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

  • 不要在析构函数中吐出异常,如果可能出现异常,应该捕获并吞下(什么叫吞下??我不确定是否指abort),或者结束程序

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

这条没太看懂…为什么要在构造函数里调用虚函数???

条款10 令operator=返回*this的引用

  • 原因是为了实现 x=y=z 之类的连续赋值
  • 这是一件约定俗成的事情,string, vector都是这样处理的

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

  • 通常赋值时先把自己的内部指针delete掉,然后拷贝rhs的对应指针,但是如果rhs是自己本身,delete时可能把自己析构掉,就出现了问题
Widget&
Widget::operator=(const Widget & rhs){
	delete pb;  // 如果rhs=this, 就把rhs的pb删掉了
	pb = new Bitmap(*rhs.pb);  // 那么这一步就不成立
	return *this;
}
  • 感觉这是一个很经典的问题,教材中给出的解决方案是"证同测试",
if(this == &rhs) return *this;
  • 上面的解法存在的问题是,如果new时出现了问题(比如内存不足),那么最后Widget的pb指针指向了一块被删除的区域
  • 更好的解法, 既可以处理异常,也能处理自赋值;要点在于在复制pb所指之前不应该删除pb
Widget&
Widget::operator=(const Widget& rhs){
	Bitmap* pOrig = pb;  // 保存副本
	pb = new Bitmap(*rhs.pb);
	delete pOrig;
	return *this;
}
  • 还有一个技术是copy-and-swap技术,即先拷贝一个rhs副本,再与自身交换

条款12 复制对象的每一个成分

  • 拷贝构造函数要确保复制对象的每一个成员,因为一旦自定义了拷贝构造函数,编译器不会帮助程序员复制忽略的成员
  • 不要以某个拷贝函数实现另一个拷贝函数,如果是为了避免代码重复,应该声明单独的私有函数完成重复的拷贝功能

资源管理

条款13: 以对象管理资源

  • 原因: 手动设定的delete语句可能因为种种原因(提前return, continue) 造成无法执行,造成资源泄露
  • 关键想法
    • 获得资源后立刻放进管理对象(如shared_ptr, auto_ptr)
    • 管理函数运行析构函数确保资源被释放

条款14: 在资源管理类中小心拷贝行为

  • 原因: 存在拷贝行为时,可能出现问题(比如对于一个锁)
  • 拷贝行为处理方式:
    • 禁止复制(private的拷贝函数)
    • 引用计数(shared_ptr思想)
    • 深拷贝
    • 转移底层资源所有权

条款15: 在资源管理类中提供对原始资源的访问

这条看的有点迷糊,就把最后的总结记下来吧

  • APIs往往需要访问原始资源,所以每一个RAII类应该提供对应的方法
  • 对原始资源的访问可能经过显式转换(较安全)或隐式转换(较方便)

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

这条简单来说就是new [] 对应 delete [], 比较基础的问题

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

  • 原因: 在一条语句中(比如函数传参)编译器执行子语句的顺序是不确定的
  • 比如书中的例子
    void process(std::tr1::shared_ptr(new Widget), priority())
    编译器可能先执行new,再执行priority函数, 最后执行智能指针的构造,那么可能出现的问题在于执行priority时出错,那么原先new出来的资源就泄露了
  • 解决方法: 不要那么花里胡哨,老老实实用独立语句构造指针
    std::tr1::shared_ptr pw(new Widget)
    process(pw, priority())
    (感觉这样代码可读性也好一点,即使不考虑资源泄露的问题)

你可能感兴趣的:(读书笔记,编程语言)