《高质量的C_C++编程指南》读书笔记

第一章 文件结构

  1. 头文件由三部分构成:

    1. 头文件开头处的版权和版本声明。
    2. 预处理块。
    3. 函数和类结构声明等。
  2. 为了防止头文件被重复引用,应当用 ifndef/define/endif 结构产生预处理块。

  3. 头文件中只存放「声明」而不存放「定义」。

  4. 定义文件由三部分组成:

    1. 定义文件开头处的版权和版本声明。
    2. 对一些头文件的引用。
    3. 程序的实现体(包括数据和代码)。

第二章 程序的版式

  1. 在每个类声明之后、每个函数定义结束之后都要加空行。在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
  2. 尽可能在定义变量的同时初始化该变量(就近原则)。
  3. 应当将修饰符 * 紧靠变量名。
  4. 注释的位置应与被描述的代码相邻,可以放在代码的上方或右方,不可放在下方。
  5. 定义类时应主张将 public 类型的函数写在前面,而将 private 类型的数据写在后面,即「以行为为中心」。

第三章 命名规则

  1. 标识符应当直观且可以拼读,可望文知意,不必进行「解码」。
  2. 标识符的长度应当符合 min-length 和 max-information 原则。
  3. 程序中不要出现仅靠大小写区分的相似的标识符。
  4. 程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
  5. 简单 Windows 程序命名规则:

    1. 类名和函数名用大写字母开头的单词组合而成。
    2. 变量和参数用小写字母开头的单词组合而成。
    3. 常量全用大写的字母,用下划线分割单词。
    4. 静态变量加前缀 s_(表示 static)。如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global)。
    5. 类的数据成员加前缀 m_(表示 member),这样可以避免数据成员与成员函数的参数同名。

第四章 表达式和基本语句

  1. 一元运算符 + - * 的优先级高于对应的二元运算符。
  2. 如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
  3. if 语句中各种类型变量与零值比较:

    1. bool 变量:不可将布尔变量直接与 TRUEFALSE 或者 10 进行比较。(应为if(flag)if(!flag))。
    2. 整型变量:应当将整型变量用 ==!= 直接与 0 比较。
    3. 浮点变量:不可将浮点变量用 ==!= 与任何数字比较。(浮点数有精度限制,应为 if((x>=-EPSINON) && (x<=EPSINON)),其中 EPSINON 是允许的误差(即精度))。
    4. 指针变量:应当将指针变量用 ==!=NULL 比较。
    5. 为防止将 if(p==NULL) 写成 if(p=NULL),可以将其写成 if(NULL==p)
  4. 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。

  5. 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。(循环「流水线」作业)。

  6. 不可在 for 循环体内修改循环变量,防止 for 循环失去控制。

  7. 建议 for 语句的循环控制变量的取值采用「半开半闭区间」写法。

  8. 每个 case 语句的结尾不要忘了加 break,否则将导致多个分支重叠(除非有意使多个分支重叠)。

  9. 不要忘记最后那个 default 分支。即使程序真的不需要 default 处理,也应该保留语句 default: break; 这样做并非多此一举,而是为了防止别人误以为你忘了 default 处理。

第五章 常量

  1. 常量是一种标识符,它的值在运行期间恒定不变。C 语言用 #define 来定义常量(称为宏常量)。C++ 语言除了 #define 外还可以用 const 来定义常量(称为 const 常量)。

  2. const 与 #define 的比较:

    1. const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
    2. 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。

    3. 在 C++ 程序中只使用 const 常量而不使用宏常量,即 const 常量完全取代宏常量。

  3. 不能在类声明中初始化 const 数据成员。const 数据成员的初始化只能在类构造函数的初始化表中进行。

  4. 怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中的枚举常量来实现(是不是只能定义为整型常量?!见下一条)

   class A{
       enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
       int array1[SIZE1];
       int array2[SIZE2];
   };
  1. 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。

第六章 函数设计

  1. 如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该指针在函数体内被意外修改。
  2. 如果输入参数以值传递的方式传递对象,则宜改用 const & 方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
  3. 尽量不要使用类型和数目不确定的参数。这种风格的函数在编译时丧失了严格的类型安全检查。C 标准库函数 printf 是采用不确定参数的典型代表,其原型为:int printf(const chat *format[, argument]…);
  4. 不要省略返回值的类型:
    1. C 语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为 void 类型。
    2. C++ 语言有很严格的类型安全检查,不允许上述情况发生。
  5. 函数名字与返回值类型在语义上不可冲突。违反这条规则的典型代表是 C 标准库函数 getchar
  6. 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。
  7. 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。例如字符串拷贝函数 strcpy 的原型:char *strcpy(char *strDest, const char *strSrc);
  8. 如果函数的返回值是一个对象,有些场合用「引用传递」替换「值传递」可以提高效率。而有些场合只能用「值传递」而不能用「引用传递」,否则会出错。
  9. 在函数体的入口处,对参数的有效性进行检查。
  10. 在函数体的出口处,对 return 语句的正确性和效率进行检查。注意事项如下:

    1. return 语句不可返回指向「栈内存」的「指针」或者「引用」,因为该内存在函数体结束时被自动销毁。
    2. 要搞清楚返回的究竟是「值」、「指针」还是「引用」。
    3. 如果函数返回值是一个对象,要考虑 return 语句的效率。eg:return String(s1 + s2);这是临时对象的语法,表示「创建一个临时对象并返回它」。不要以为它与「先创建一个局部对象 temp 并返回它的结果」是等价的,如 String temp(s1 + s2); return temp;实质不然,上述代码将发生三件事。首先, temp 对象被创建,同时完成初始化;然后拷贝构造函数把 temp 拷贝到保存返回值的外部存储单元中;最后, temp 在函数结束时被销毁(调用析构函数)。然而「创建一个临时对象并返回它」的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。
  11. 【建议 6-4-4】不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。

  12. 断言 assert 是仅在 Debug 版本起作用的宏,它用于检查「不应该」发生的情况。为了不在程序的 Debug 版本和 Release 版本引起差别, assert 不应该产生任何副作用。所以 assert 不是函数,而是宏。

  13. 【规则 6-5-1】使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。

  14. 指针与引用:

    1. 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
    2. 不能有 NULL 引用,引用必须与合法的存储单元关联(指针则可以是 NULL)。
    3. 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。

    4. 引用的主要功能是传递函数的参数和返回值。 C++ 语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。

  15. 「引用传递」的性质象「指针传递」,而书写方式象「值传递」。

第七章 内存管理

  1. 内存分配方式有三种:

    1. 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量, static 变量。
    2. 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    3. 从堆上分配,亦称动态内存分配。程序在运行的时候用 mallocnew 申请任意多少的内存,程序员自己负责在何时用 freedelete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
  2. 常见的内存错误及其对策如下:

    1. 内存分配未成功,却使用了它。
    2. 内存分配虽然成功,但是尚未初始化就引用它。
    3. 内存分配成功并且已经初始化,但操作越过了内存的边界。
    4. 忘记了释放内存,造成内存泄露。
    5. 释放了内存却继续使用它。有三种情况:

      1. 程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
      2. 函数的 return 语句写错了,注意不要返回指向「栈内存」的「指针」或者「引用」,因为该内存在函数体结束时被自动销毁。
      3. 使用 free 或 delete 释放了内存后,没有将指针设置为 NULL。导致产生「野指针」(空悬指针)。
  3. 【规则 7-2-1】用 mallocnew 申请内存之后,应该立即检查指针值是否为 NULL。防止使用指针值为 NULL 的内存。

  4. 【规则 7-2-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。

  5. 【规则 7-2-3】避免数组或指针的下标越界,特别要当心发生「多 1」或者「少 1」操作。

  6. 【规则 7-2-4】动态内存的申请与释放必须配对,防止内存泄漏。

  7. 【规则 7-2-5】用 freedelete 释放了内存之后,立即将指针设置为 NULL,防止产生「野指针」。

  8. 指针与数组:

    1. 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
    2. 指针可以随时指向任意类型的内存块,它的特征是「可变」,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险。

    3. 不能对数组名进行直接复制与比较。应该用标准库函数 strcpy 进行复制,用标准库函数 strcmp 进行比较。

    4. 用运算符 sizeof 可以计算出数组的容量(字节数)。但是 sizeof(指针)的值却是 4。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。

    5. 当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

  9. 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。如果非得要用指针参数去申请内存, 那么应该改用 「指向指针的指针」,或者用函数返回值来传递动态内存。

  10. freedelete 只是把指针所指的内存给释放掉,但并没有把指针本身干掉。指针的地址仍然不变(非 NULL),只是该地址对应的内存是垃圾。此时如果不将指针设为 NULL,那么 if(NULL==p) 语句将不起作用。

  11. 我们发现指针有一些「似是而非」的特征:

    1. 指针消亡了,并不表示它所指的内存会被自动释放。
    2. 内存被释放了,并不表示指针会消亡或者成了 NULL 指针。

    3. 「野指针」不是 NULL 指针,是指向「垃圾」内存的指针。成因主要有几种:

      1. 指针变量没有被初始化。
      2. 指针 p 被 free 或者 delete 之后,没有置为 NULL
      3. 指针操作超越了变量的作用范围。
  12. malloc/freenew/delete:前者是库函数,对于自定义的对象,不能自动调用其构造和析构函数;后者是关键字,可以调用构造函数和析构函数。对于内部数据类型两者是等价的。

  13. 如果用 free 释放「 new 创建的动态对象」,那么该对象因无法执行析构函数而可能导致程序出错。 如果用 delete 释放「 malloc 申请的动态内存」,理论上讲程序不会出错,但是该程序的可读性很差。

  14. malloc、free、new、delete:

    1. malloc 返回值的类型是 void *,所以在调用 malloc 时要显式地进行类型转换,将 void * 转换成所需要的指针类型。
    2. malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
    3. 为什么 free 函数不像 malloc 函数那样复杂呢?这是因为指针 p 的类型以及它所指的内存的容量事先都是知道的,语句 free(p) 能正确地释放内存。如果 pNULL 指针,那么 freep 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 freep 连续操作两次就会导致程序运行错误。
    4. new 内置了 sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言, new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么 new 的语句也可以有多种形式。
    5. 如果用 new 创建对象数组,那么只能使用对象的无参数构造函数。eg:Obj *objects = new Obj[100]; // 创建 100 个动态对象 不能写成 Obj *objects = new Obj[100](1);// 创建 100 个动态对象的同时赋初值 1
    6. 在用 delete 释放对象数组时,留意不要丢了符号 ' []'。

第八章 C++函数的高级特性

  1. 对比于 C 语言的函数, C++ 增加了重载 (overloaded)、内联( inline)、const 和 virtual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数, const 与 virtual 机制仅用于类的成员函数。
  2. 如果 C++程序要调用已经被编译后的 C 函数,C++ 提供了一个 C 连接交换指定符号 extern C 来解决这个问题。
  3. 注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。全局函数被调用时应加 :: 标志。
  4. 成员函数的重载、覆盖与隐藏:

    1. 成员函数被重载的特征:

      1. 相同的范围(在同一个类中);
      2. 函数名字相同;
      3. 参数不同;
      4. virtual 关键字可有可无。
    2. 覆盖是指派生类函数覆盖基类函数,特征是:

      1. 不同的范围(分别位于派生类与基类);
      2. 函数名字相同;
      3. 参数相同;
      4. 基类函数必须有 virtual 关键字。
    3. 隐藏是指派生类的函数屏蔽了与其同名的基类函数,规则如下:

      1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
      2. 如果派生类的函数与基类的函数同名, 并且参数也相同, 但是基类函数没有 virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
    4. 【规则 8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中。

  5. 【规则 8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。

  6. 运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧。

  7. 如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。 如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。

9. 在 C++ 运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。(sizeof::..* 、 ?:typeid 等等)

  1. 不能改变 C++ 内部数据类型(如 intfloat 等)的运算符。
  2. 不能重载 .,因为 . 在类中对任何成员都有意义,已经成为标准用法。
  3. 不能重载目前 C++ 运算符集合中没有的符号,如 #@$ 等。原因有两点,一是难以理解,二是难以确定优先级。
  4. 对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。

  5. 在 C 程序中,可以用宏代码提高执行效率。宏代码本身不是函数,但使用起来象函数。预处理器用复制宏代码的方式代替函数调用, 省去了参数压栈、生成汇编语言的 CALL 调用、返回参数、执行 return 等过程,从而提高了速度。使用宏代码最大的缺点是容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。使用宏代码还有另一种缺点:无法操作类的私有数据成员。

  6. 对于任何内联函数,编译器在符号表里放入函数的声明(包括名字、参数类型、返回值类型)。如果编译器没有发现内联函数存在错误,那么该函数的代码也被放入符号表里。在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。

  7. 所以在 C++ 程序中,应该用内联函数取代所有宏代码,assert 恐怕是唯一的例外。 assert 是仅在 Debug 版本起作用的宏,它用于检查「不应该」发生的情况。为了不在程序的 Debug 版本和 Release 版本引起差别, assert 不应该产生任何副作用。 如果 assert 是函数, 由于函数调用会引起内存、 代码的变动, 那么将导致 Debug 版本与 Release 版本存在差异。 所以 assert 不是函数, 而是宏。

  8. 关键字 inline 必须与函数定义体放在一起才能使函数成为内联,仅将 inline 放在函数声明前面不起任何作用。所以说, inline 是一种「用于实现的关键字」,而不是一种「用于声明的关键字」。

  9. 定义在类声明之中的成员函数将自动地成为内联函数。

  10. 以下情况不宜使用内联:

    1. 如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
    2. 如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
    3. 类的构造函数和析构函数容易让人误解成使用内联更有效。要当心构造函数和析构函数可能会隐藏一些行为,如「偷偷地」执行了基类或成员对象的构造函数和析构函数。所以不要随便地将构造函数和析构函数的定义体放在类声明中。

第九章 类的构造函数、析构函数与赋值函数

  1. 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。
  2. 既然能自动生成函数,为什么还要程序员编写?原因如下:

    1. 如果使用「缺省的无参数构造函数」和「缺省的析构函数」,等于放弃了自主「初始化」和「清除」的机会, C++ 发明人 Stroustrup 的好心好意白费了。
    2. 「缺省的拷贝构造函数」和「缺省的赋值函数」均采用「位拷贝」而非「值拷贝」的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
  3. 构造函数有个特殊的初始化方式叫「初始化表达式表」(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。

  4. 构造函数初始化表的使用规则:

    1. 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
    2. 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化。
    3. 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。

    4. 非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。(第二种方式会先创建其对象,调用无参构造函数,再调用其赋值函数)。

    5. 对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。

  5. 构造从类层次的最根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。

  6. 缺省的拷贝构造函数以「位拷贝」方式执行,以 string 类为例,将造成三个错误:

    1. 一是 b.m_data 原有的内存没被释放,造成内存泄露;
    2. 二是 b.m_dataa.m_data 指向同一块内存, ab任何一方变动都会影响另一方;
    3. 三是在对象被析构时, m_data 被释放了两次。
  7. 拷贝构造函数和赋值函数非常容易混淆,常导致错写、错用。拷贝构造函数是在对象被创建时调用的,而赋值函数只能被已经存在了的对象调用。

  8. 类  string 拷贝构造函数与普通构造函数的区别是:在函数入口处无需与 NULL 进行比较,这是因为「引用」不可能是 NULL,而「指针」可以为 NULL

  9. string 类拷贝构造函数四部曲:

    1. 检查自赋值;
    2. 释放原有空间;
    3. 重新分配空间并拷贝;函数 strlen 返回的是有效字符串长度,不包含结束符 \0。函数 strcpy 则连 \0 一起复制。
    4. 返回本对象的引用。目的是为了实现象 a = b = c 这样的链式表达。注意不要将 return *this; 错写成 return this;
  10. 偷懒的办法处理拷贝构造函数与赋值函数:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。

  11. 基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:

    1. 派生类的构造函数应在其初始化表里调用基类的构造函数。
    2. 基类与派生类的析构函数应该为虚(即加 virtual 关键字)。如果析构函数不为虚,将不会调用派生类的析构函数。
    3. 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。即调用基类的赋值函数。(因为不能直接操作基类的私有成员)

第十章 类的继承与组合

  1. 继承:若在逻辑上 B 是 A 的「一种」,并且 A 的所有功能和属性对 B 而言都有意义,则允许 B 继承 A 的功能和属性。
  2. 组合:若在逻辑上 A 是 B 的「一部分」( a part of),则不允许 B 从 A 派生,而是要用 A 和其它东西组合出 B。

第十一章 其他编程经验

  1. 使用 const 提高函数的健壮性:const 不止能定义 const 常量,更大的魅力是它可以修饰函数的参数、返回值,甚至函数的定义体。

    1. const 修饰函数参数:

      1. 如果参数作输出用,不论它是什么数据类型,也不论它采用「指针传递」还是「引用传递」,都不能加 const 修饰,否则该参数将失去输出功能。
      2. const 只能修饰输入参数:

        1. 如果输入参数采用「指针传递」,那么加 const 修饰可以防止意外地改动该指针,起到保护作用。
        2. 如果输入参数采用「值传递」,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加 const 修饰。
    2. 对于非内部数据类型的参数而言,像 void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生 A 类型的临时对象用于复制参数 a,而临时对象的构造、复制、析构过程都将消耗时间。

    3. const 修饰函数的返回值:

      1. 如果给以「指针传递」方式的函数返回值加 const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加 const 修饰的同类型指针。eg:const char * GetString(void);
      2. 如果函数返回值采用「值传递方式」,由于函数会把返回值复制到外部临时的存储单元中,加 const 修饰没有任何价值。
      3. 如果返回值不是内部数据类型,将函数 A GetA(void) 改写为 const A& GetA(void) 的确能提高效率。但此时千万千万要小心,一定要搞清楚函数究竟是想返回一个对象的「拷贝」还是仅返回「别名」就可以了,否则程序会出错。
    4. 函数返回值采用「引用传递」的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。

    5. const 成员函数:任何不会修改数据成员的函数都应该声明为 const 类型。const 成员函数的声明看起来怪怪的: const 关键字只能放在函数声明的尾部,大概是因为其它地方都已经被占用了。