这篇文章上次修改于 765 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

第一卷

第二章 对象的创建与使用

  • 解释器将源代码转化成一些动作(它可由多组机器指令组成)并立即执行这些动作。例如,BASIC就是一个流行的解释性语言。传统的BASIC解释器一次翻译和执行一行,然后将这一行的解释丢掉。因为解释器必须重新翻译任何重复的代码,程序执行就变慢了。
  • 使用解释器有许多好处。从写代码到执行代码的转换几乎能立即完成,并且源代码总是现存的,所以一旦出现错误,解释器能很容易地指出。对于解释器,较好的交互性和适于快速程序开发(不必要求可执行程序)也是常被提到的两个优点。
  • 编译器直接把源代码转化成汇编语言或机器指令。最终的结果是一个或多个机器代码的文件。这是一个复杂的过程,通常分几步完成。使用编译器时,从写源代码转到执行代码,是一个较长的过程。 编译器生成的程序往往只需较少的运行空间,并且执行速度更快。
  • 声明是向编译器介绍名字—标识符。它告诉编译器“这个函数或这个变量在某处可找到,它的模样像什么”。而定义是说:“在这里建立变量”或“在这里建立函数”。它为名字分配存储空间。
  • extern:这只是一个声明,它的定义在别的地方。
  • 编译过程:预处理,转换成汇编或机器代码,连接。

第三章 C++ 中的 C

  • static 初始化只在函数第一次调用时执行,函数调用之间变量的值保持不变。用这种方式,函数可以“记住”函数调用之间的一些信息片断。
  • static 具有文件作用域(file scope)。

第四章 数据抽象

  • 一个struct的大小是它的所有成员大小的和。有时,当一个struct被编译器处理时,会增加额外的字节以使得边界整齐,这主要是为了提高执行效率。

第五章 隐藏实现

  • 继承的结构可以访问protected成员,但不能访问private成员。
  • 程序开始运行之前,所有的访问说明信息都消失了。访问说明信息通常是在编译期间消失的。

第六章 初始化与清除。

  • 一旦有构造函数而没有默认构造函数,上面的对象定义就会产生一个编译错误。
  • 如果想把内存初始化为零,那就得显式地编写默认的构造函数。

第七章 函数重载与默认函数

  • 不能把默认参数作为一个标志去决定执行函数的哪一块,这是基本原则。在这种情况下,只要能够,就应该把函数分解成两个或多个重载的函数。一个默认的参数应该是一个在一般情况下放在这个位置的值。

第八章 常量

  • 通常C++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里。当然,想绝对不为任何const分配存储是不可能的,尤其对于复杂的结构。在这种情况下,编译器建立存储。
  • C++中的const默认为内部连接(internal linkage),也就是说,const仅在const被定义过的文件里才是可见的,否则,由于众多的const在多个cpp文件内分配存储,容易引起连接错误,连接程序在多个对象文件里看到同样的定义就会“抱怨”。
  • 使正指向的元素不发生改变:const int u; 从标识符开始,是这样读的:“u是一个指针,它指向一个const int。”这里不需要初始化,因为u可以指向任何标识符(也就是说,它不是一个const),但它所指的值是不能被改变的。同样的效果:int const u;
  • 使指针本身成为一个const指针,必须把const标明的部分放在的右边:int const w = &d;
  • 如果一个函数按值返回一个类对象为const时,那么这个函数的返回值不能是一个左值(即它不能被赋值,也不能被修改)。
  • 当按值返回一个内建类型时,const没有意义的原因是:编译器已经不让它成为一个左值(因为它总是一个值而不是一个变量)。
  • 带const指针参数的函数比不带const指针参数的函数更具一般性。
  • 一个const成员函数调用const和非const对象是安全的,因此,可以把它看做成员函数的最一般形式
  • 应当在类声明里使用关键字mutable,以指定一个特定的数据成员可以在一个const对象里被改变。

第九章 内联函数

  • 内联函数在适当的地方像宏一样展开,所以不需要函数调用的开销。因此,应该(几乎)永远不使用宏,只使用内联函数。
  • 使用内联函数的目的是减少函数调用的开销。但是,假如函数较大,由于需要在调用函数的每一处重复复制代码,这样将使代码膨胀,在速度方面获得的好处就会减少。
  • 当编译器看到内联函数和对内联函数体的进行分析没有发现错误时,就将对应于函数体的代码也放入符号表。代码是以源程序形式存放还是以编译过的汇编指令形式存放取决于编译器。
  • 一般地,任何种类的循环都被认为太复杂而不扩展为内联函数。

第十章 名字控制

  • 当退出 main() 函数时,所有被创建的对象的析构函数按创建时相反的顺序被调用。
  • 在文件作用域内,一个被明确声明为static的对象或函数的名字对翻译单元(用本书的术语来说也就是出现声明的.cpp文件)来说是局部于该单元的。内部连接的一个好处是这个名字可以放在一个头文件中而不用担心连接时发生冲突。
  • auto 告诉编译器这是一个局部变量。
  • 要确保每个翻译单元只有一个未命名的名字空间。
  • 不要把一个全局的using指令引入到一个头文件中,因为那将意味着包含这个头文件的任何其他头文件也会打开这个名字空间(头文件可以被另一个头文件包含)。
  • 静态成员函数不能访问一般的数据成员,而只能访问静态数据成员,也只能调用其他的静态成员函数。

第十一章 引用和拷贝构造函数

  • 有一个简单的技术防止通过按值传递方式传递:声明一个私有拷贝构造函数。
  • 当传递一个可被修改的参数地址时,从代码维护的观点看,使用指针可能更安全些。
  • 编译器构造的默认拷贝构造函数不会执行构造函数中自定义的某些初始化步骤,所以最好是创建自己的拷贝构造函数而不让编译器创建。这样就能保证程序在我们的控制之下。

第十二章 运算符重载

  • 只有在能使涉及类的代码更易写,尤其是更易读时(请记住,读代码的机会比写代码多多了)才有理由重载运算符。
  • 返回值优化

    return Interger(left.i + right.i);
    与
    Interger tmp(left.i + right.i);
    return tmp;
    不一样

    后者将发生三件事。首先,创建tmp对象,其中包括构造函数的调用。然后,拷贝构造函数把tmp拷贝到外部返回值的存储单元里。最后,当tmp在作用域的结尾时调用析构函数。
    相反,“返回临时对象”的方式是完全不同的。当编译器看到我们这样做时,它明白对创建的对象没有其他需求,只是返回它,所以编译器直接地把这个对象创建在外部返回值的内存单元。因为不是真正创建一个局部对象,所以仅需要一个普通构造函数调用(不需要拷贝构造函数),且不会调用析构函数。

  • 当准备给两个相同类型的对象赋值时,应该首先检查一下自赋值(self-assignment):这个对象是否对自身赋值了?在一些情况下,执行这些赋值运算都是无害的,但如果对类的实现进行了修改,那么将会出现差异。
  • 加关键字explicit(只能用于构造函数)可避免构造函数转换。

    class One {
     public:
         One();
    };
    
    class Two {
     public:
         explict Two(const One&) {}
    };
    
    void f(Two)
    
    int main() {
      One one;
      // f(one); // no auto conversion allowed
        f(Two(one));
    }

第十三章 动态对象创建

  • 由于malloc()只是分配了一块内存而不是生成一个对象,所以它返回了一个void类型指针。而C++不允许将一个void类型指针赋予任何其他指针,所以必须做类型转换。
  • 从堆里搜索一块足够大的内存来满足请求。这可以通过检查按某种方式排列的映射或目录来实现,这样的映射或目录用以显示内存的使用情况。这个过程很快但可能要试探几次,所以它可能是不确定的—即每次运行malloc()并不是花费了完全相同的时间。
  • 当operator new()找不到足够大的连续内存块来安排对象时,将会发生什么事情呢?一个称为new-handler的特殊函数将会被调用。
  • 当我们创建一个new表达式时,会发生两件事。首先,使用operator new()分配内存,然后调用构造函数。在delete表达式里,调用了析构函数,然后使用operator delete()释放内存。我们无法控制构造函数和析构函数的调用(否则可能会意外地搅乱它们),但可以改变内存分配函数operator new()和operator delete()。

第十四章 继承和组合

  • 当希望基类中的所有公有成员在派生类中仍是公有的时,可以在继承时通过使用关键字public。
  • 如果子对象没有默认构造函数或如果想改变构造函数的某个默认参数,情况怎么样呢?这会出现问题的,因为这个新类的构造函数没有权利访问这个子对象的私有数据成员,所以不能直接地对它们初始化。解决的方法很简单:构造函数的初始化表达式表。
  • 任何时候重新定义了基类中的一个重载函数,在新类之中所有其他的版本则被自动地隐藏了。
  • 构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。operator=也不能被继承,因为它完成类似于构造函数的活动
  • 向上类型转换总是安全的。因为是从更专门的类型到更一般的类型—对于这个类接口可能出现的惟一的事情是它失去成员函数,而不是获得它们。
  • 无论何时我们在创建了自己的拷贝构造函数时,都要正确地调用基类拷贝构造函数(正如编译器所作的)。
  • 确定应当用组合还是用继承,最清楚的方法之一是询问是否需要从新类向上类型转换。

第十五章 多态性和虚函数

  • 对于在构造函数中调用一个虚函数的情况,被调用的只是这个函数的本地版本。也就是说,虚机制在构造函数中不工作。
  • 构造函数是不能为虚函数的。但析构函数能够且常常必须是虚的。
  • 在析构函数和构造函数中,只有成员函数的“本地”版本被调用;虚机制被忽略。在构造函数的情况下,这样做是因为类型信息还不可用,然而在析构函数中,这样做是因为信息(也就是VPTR)虽存在,但不可靠。
  • 有时,在进行向下类型转换时,我们可以知道正在处理的是何种类型,这时使用dynamic_cast产生的额外开销就没有必要,可以通过使用static_cast来代替它。但是静态地浏览类层次总是有风险的,所以除非特殊情况,我们一般使用dynamic_cast。

第十六章 模版介绍

  • 即使是在创建非内联函数定义时,我们还是通常想把模板的所有声明和定义都放入一个头文件中。
  • 迭代器的设计更安全,所以数组越界的可能性更小(或者说,如果有数组越界,就会更早被发现)。

第二卷 实用编程技术

第一部分 建立稳定的系统

第一章 异常处理

  • setjmp()/longjmp() 并不调用析构函数,所以对象不会被正确地清理。
  • 如果没有任何一个层次的异常处理器能够捕获某种异常,一个特殊的库函数 terminate()(在头文件<exception>中定义)会被自动调用。默认情况下, terminate() 调用标准 C 库函数 abort() 使程序执行异常终止而退出。在Unix系统中, abort() 还会导致主存储器信息转储(core dump)。当 abort() 被调用时,程序不会调 用正常的终止函数,也就是说,全局对象和静态对象的析构函数不会执行。
  • 析构函数中不要抛出异常。程序在处理一个异常的时候会释放在栈上分配的对象,这时,对象的析构函数被调用,从而产生了第二个异常,这个异常迫使程序调用 terminate()。
  • 构造函数中不要抛出异常。当构造函数没有正常结束时不会调用相关联的析构函数。
  • 在构造函数中捕获异常,用于释放资源。在对象的构造函数中分配资源,并且在对象的析构函数中释放资源。
  • 当资源分配成为局部对象生命周期的一部分,如果某次分配失败了,那么在栈反解的时候,其他已经获得所需资源的对象能够被恰当地清理。这种技术称为资源获得式初始化(Resource Acquisition Is Initialization, RAII),因为它使得对象对资源控制的时间与对象的生命周期相等。
  • 由于异常规格说明在逻辑上也是函数声明的一部分,所以在继承层次结构中也必须保持一致。例如,如果基类的一个成员 函数声明它只抛出一种类型的异常A,那么派生类中覆盖这个函数的函数不能在异常规格说明列表中添加其他异常。因为如果添加其他异常,就会造成依赖于基类接口的任何程序崩溃。
  • 当无法知道会触发什么异常时,不要使用异常规格说明。这就是为什么模板类,也就是标准C++库的主要组成部分,不使用异常规格说明的原因。
  • 惟一必须用到多重继承的情况是:当需要将一个对象指针向上类型转换成两个不同的基类类型时。
  • 避免悬挂指针。请使用 auto_ptr 或其他智能指针(smart pointer)类型来处理指向堆内存的指针。

第二章 防御性编程

  • 运行时检测所有断言所耗费的机器周期可能会大大降低程序的执行效率,以至严重影响这个系统在该领域的应用。断言在软件产品中失灵所造成的问题远比效率降低要严重得多,因此要做出明智的选择。
  • 先编写单元测案例试然后编写代码或许能够导致更快地完成工作,比直接编写代码更快。

第二部分 标准 c++ 库

第三章 深入理解字符串

  • 只有当字符串被修改的时候才创建各自的拷贝,这种实现方式称为写时复制(copy-on-write)策略。

第五章 深入理解模版

  • 若一个模板代码内部的某个类型被模板类型参数所限定,则必须使用关键字typename作为前缀进行声明,除非它已经出现在基类的规格说明中,或者它出现在同一作用域范围内的初始化列表中(这种情况下一定不要使用typename关键字)。
  • 前缀“template<>”告诉编译器接下来的是一个模板的特化。模板特化的类型必须出现在函数名后紧跟的尖括号中,就像通常在一个明确指定参数类型的函数调用中一样。

第七章 通用容器

  • 注意,使用 reserve() 预分配存储区与通过给出一个整数作为 vector 构造函数的第 1 个参数是有区别的;后者将使用元素类型的默认构造函数来初始化被规定的元素个数。
  • deque 允许在序列的两端快速地插入元素,并且在其扩展存储区的时候不需要拷贝和销毁对象的操作。deque 也允许使用操作符 operator[] 进行随机访问,但是没有 vector 的操作符 operator[] 执行得那么快。
  • deque 的典型实现是利用多个连续的存储块(同时在一个映射结构中保持对这些块及其顺序的跟踪)。
  • 在向序列两端添加未知数量的对象时,deque 远比 vector 更有效率。这意味着,只有在确切知道到底需要多少个对象的时候,vector 才是最优的选择。
  • STL set 用一棵平衡树数据结构来存储其元素以提供快速的查找,因此在遍历 set 的时候就产生了排序的结果。

第八章 运行时类型识别

  • 实现RTTI典型的方法是,通过在类的虚函数表中放置一个附加的指针。这个指针指向那个特别类型的type_info结构。
  • 如果有菱形继承结构出现,就需要通过引入虚基类来消除重复子对象。这不仅增加了混乱,而且使接下来的表达方式变得更加复杂和低效。