学习笔记之高质量C++/C编程指南

高质量C++/C编程指南

  • http://man.lupaworld.com/content/develop/c&c++/c/c.htm

高质量C++/C编程指南(附录 C :C++/C 试题的答案与评分标准)

  • http://www.warting.com/program/201111/38402.html
  • http://www.360doc.com/content/10/0911/15/2507295_52863476.shtml

目 录

前 言

第1章 文件结构

1.1 版权和版本的声明

1.2 头文件的结构

1.3 定义文件的结构

1.4 头文件的作用

1.5 目录结构

第2章 程序的版式

2.1 空行

2.2 代码行

2.3 代码行内的空格

2.4 对齐

2.5 长行拆分

2.6 修饰符的位置

2.7 注释

2.8 类的版式

第3章 命名规则

3.1 共性规则

3.2 简单的WINDOWS应用程序命名规则

3.3 简单的UNIX应用程序命名规则

第4章 表达式和基本语句

4.1 运算符的优先级

4.2 复合表达式

4.3 IF 语句

4.4 循环语句的效率

4.5 FOR 语句的循环控制变量

4.6 SWITCH语句

4.7 GOTO语句

第5章 常量

5.1 为什么需要常量

5.2 CONST 与 #DEFINE的比较

5.3 常量定义规则

5.4 类中的常量

第6章 函数设计

6.1 参数的规则

6.2 返回值的规则

6.3 函数内部实现的规则

6.4 其它建议

6.5 使用断言

6.6 引用与指针的比较

第7章 内存管理

7.1内存分配方式

7.2常见的内存错误及其对策

7.3指针与数组的对比

7.4指针参数是如何传递内存的?

7.5 FREE和DELETE把指针怎么啦?

7.6 动态内存会被自动释放吗?

7.7 杜绝“野指针”

7.8 有了MALLOC/FREE为什么还要NEW/DELETE ?

7.9 内存耗尽怎么办?

7.10 MALLOC/FREE 的使用要点

7.11 NEW/DELETE 的使用要点

7.12 一些心得体会

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

8.1 函数重载的概念

8.2 成员函数的重载、覆盖与隐藏

8.3 参数的缺省值

8.4 运算符重载

8.5 函数内联

8.6 一些心得体会

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

9.1 构造函数与析构函数的起源

9.2 构造函数的初始化表

9.3 构造和析构的次序

9.4 示例:类STRING的构造函数与析构函数

9.5 不要轻视拷贝构造函数与赋值函数

9.6 示例:类STRING的拷贝构造函数与赋值函数

9.7 偷懒的办法处理拷贝构造函数与赋值函数

9.8 如何在派生类中实现类的基本函数

9.9 一些心得体会

第10章 类的继承与组合

10.1 继承

10.2 组合

第11章 其它编程经验

11.1 使用CONST提高函数的健壮性

11.2 提高程序的效率

11.3 一些有益的建议

参考文献

附录A :C++/C代码审查表

附录B :C++/C试题

附录C :C++/C试题的答案与评分标准

 

言简意赅。第6、7章还有CONST介绍很清楚。试题很经典。

 

第 1 章  文件结构

  • 类的成员函数可以在声明的同时被定义,并且自动成为内联函数
  • 头文件能加强类型安全检查。如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
  • 如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。

第 2 章  程序的版式

  • 尽可能在定义变量的同时初始化该变量(就近原则)
  • 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
  • 应当将修饰符 * 和& 紧靠变量名
  • 当代码比较长,特别是有多重嵌套时,应当在一些段落的结束处加注释,便于阅读。
  • 类的版式主要有两种方式:
    • (1)将private 类型的数据写在前面,而将public 类型的函数写在后面,如示例8-3(a)。采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。
    • (2)将public 类型的函数写在前面,而将private 类型的数据写在后面,如示例8.3(b)。采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。

第 3 章  命名规则

  • “匈牙利”法,该命名规则的主要思想是“在变量和函数名中加入前缀以增进人们对程序的理解”
  • Windows 应用程序的标识符通常采用“大小写”混排的方式,如AddChild。而Unix 应用程序的标识符通常采用“小写加下划线”的方式,如add_child。
  • 程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
  • 类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
  • 尽量避免名字中出现数字编号
  • 静态变量加前缀s_(表示static)。
  • 如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。
  • 类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。
  • 为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。

第 4 章  表达式和基本语句

  • 如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
  • 如 a = b = c = 0 这样的表达式称为复合表达式。允许复合表达式存在的理由是:(1)书写简洁;(2)可以提高编译效率。但要防止滥用复合表达式。
  • 应当将指针变量用“==”或“!=”与NULL 比较。
  • 程序中有时会遇到if/else/return 的组合,应该将如下不良风格的程序改写成更加简练的 return (condition ? x : y);
  • 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。
  • 建议for 语句的循环控制变量的取值采用“半开半闭区间”写法。示例 4-5(a)中的x 值属于半开半闭区间“0 =< x < N”,起点到终点的间隔为N,循环次数为N。

第 5 章  常量

  • C++ 语言可以用const 来定义常量,也可以用 #define 来定义常量。但是前者比后者有更多的优点:
  • (1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
  • (2) 有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
  • 在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。
  • 需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。
  • 如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。例如:
    • const float RADIUS = 100;
    • const float DIAMETER = RADIUS * 2;
  • const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const 数据成员的值可以不同。
  • const 数据成员的初始化只能在类构造函数的初始化表中进行,例如
    •   class A
    •   {⋯
    •   A(int size); // 构造函数
    •   const int SIZE ;
    •   };
    •   A::A(int size) : SIZE(size) // 构造函数的初始化表
    •   {
    •   ⋯
    •   }
  • 怎样才能建立在整个类中都恒定的常量呢?别指望const 数据成员了,应该用类中的枚举常量来实现。例如
    •   class A
    •   {⋯
    •   enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
    •   int array1[SIZE1];
    •   int array2[SIZE2];
    •   };
  • 枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。

第 6 章  函数设计

  • C 语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。C++ 语言中多了引用传递(pass by reference)。由于引用传递的性质象指针传递,而使用方式却象值传递
  • 如果函数没有参数,则用void 填充。
  • 一般地,应将目的参数放在前面,源参数放在后面。
  • 如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
  • 如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
  • 尽量不要使用类型和数目不确定的参数。这种风格的函数在编译时丧失了严格的类型安全检查。
  • 不要省略返回值的类型。C 语言中,凡不加类型说明的函数,一律自动按整型处理。这样做不会有什么好处,却容易被误解为void 类型。C++语言有很严格的类型安全检查,不允许上述情况发生。由于C++程序可以调用C 函数,为了避免混乱,规定任何C++/ C 函数都必须有类型。如果函数没有返回值,那么应声明为void 类型。
  • 函数名字与返回值类型在语义上不可冲突。
  • 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。
  • 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。例如字符串拷贝函数strcpy 的原型:char *strcpy(char *strDest,const char *strSrc);
  • 如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
  • 对于赋值函数,应当用“引用传递”的方式返回String 对象。如果用“值传递”的方式,虽然功能仍然正确,但由于return 语句要把 *this 拷贝到保存返回值的外部存储单元之中,增加了不必要的开销,降低了赋值函数的效率。
  • 对于相加函数,应当用“值传递”的方式返回String 对象。如果改用“引用传递”,那么函数返回值是一个指向局部对象temp 的“引用”。由于temp 在函数结束时被自动销毁,将导致返回的“引用”无效。
  • 在函数体的“入口处”,对参数的有效性进行检查。很多程序错误是由非法参数引起的,我们应该充分理解并正确使用“断言”(assert)来防止此类错误。
  • 在函数体的“出口处”,对return 语句的正确性和效率进行检查。
  • return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
  • 如果函数返回值是一个对象,要考虑return 语句的效率。例如 return String(s1 + s2); 这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建一个局部对象temp 并返回它的结果”是等价的。“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。
  • 内部数据类型如int,float,double 的变量不存在构造函数与析构函数,虽然该“临时变量的语法”不会提高多少效率,但是程序更加简洁易读。
  • 函数的功能要单一,不要设计多用途的函数。
  • 函数体的规模要小,尽量控制在50行代码之内。
  • 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在C/C++语言中,函数的static 局部变量是函数的“记忆”存储器。建议尽量少用static 局部变量,除非必需。
  • 程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用。断言 assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
  • 程序员可以把assert 看成一个在任何系统状态下都可以安全使用的无害测试手段。
  • 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
  • 在函数的入口处,使用断言检查参数的有效性(合法性)。
  • 引用的一些规则如下:
  •   (1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
  •   (2)不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
  •   (3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
  • C++语言中,函数的参数和返回值的传递方式有三种:值传递、指针传递和引用传递。
  • 指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。
  • 如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。

第 7 章  内存管理

  • 内存分配方式有三种:
    •   (1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
    •   (2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    •   (3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。
  • 常见的内存错误及其对策如下:
    •   内存分配未成功,却使用了它。
      •     编程新手常犯这种错误,因为他们没有意识到内存分配会不成功。常用解决办法是,在使用内存之前检查指针是否为NULL。如果指针p 是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
    •   内存分配虽然成功,但是尚未初始化就引用它。
      •     犯这种错误主要有两个起因:一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。内存的缺省初值究竟是什么并没有统一的标准,尽管有些时候为零值,我们宁可信其无不可信其有。所以无论用何种方式创建数组,都别忘了赋初值,即便是赋零值也不可省略,不要嫌麻烦。
    •   内存分配成功并且已经初始化,但操作越过了内存的边界。
      •     例如在使用数组时经常发生下标“多1”或者“少1”的操作。特别是在for 循环语句中,循环次数很容易搞错,导致数组操作越界。
    •   忘记了释放内存,造成内存泄露。
      •     含有这种错误的函数每被调用一次就丢失一块内存。刚开始时系统的内存充足,你看不到错误。终有一次程序突然死掉,系统出现提示:内存耗尽。动态内存的申请与释放必须配对,程序中malloc 与free 的使用次数一定要相同,否则肯定有错误(new/delete 同理)。
    •   释放了内存却继续使用它。有三种情况:
      •     (1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
      •     (2)函数的return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁。
      •     (3)使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。
  • 【规则7-2-1】用malloc 或new 申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL 的内存。
  • 【规则7-2-2】不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
  • 【规则7-2-3】避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
  • 【规则7-2-4】动态内存的申请与释放必须配对,防止内存泄漏。
  • 【规则7-2-5】用free 或delete 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
  • 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。
  • 指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。
  • 指针p 指向常量字符串“world”(位于静态存储区,内容为world\0),常量字符串的内容是不可以被修改的。从语法上看,编译器并不觉得语句p[0]= ‘X’有什么不妥,但是该语句企图修改常量字符串的内容而导致运行错误。
  • 不能对数组名进行直接复制与比较。
  • 语句 p = a 并不能把a 的内容复制指针p,而是把a 的地址赋给了p。
  • sizeof(a)的值是12(注意别忘了’\0’)。指针p 指向a,但是sizeof(p)的值却是4。这是因为sizeof(p)得到的是一个指针变量的字节数,相当于sizeof(char*),而不是p 所指的内存容量。
  • 注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。
  • 如果函数的参数是一个指针,不要指望用该指针去申请动态内存。编译器总是要为函数的每个参数制作临时副本,指针参数p 的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p 的内容,就导致参数p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请了新的内存,只是把_p 所指的内存地址改变了,但是p 丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory 就会泄露一块内存,因为没有用free 释放内存。如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”
  • 由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态内存。这种方法更加简单
  • 用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把return 语句用错了。这里强调不要用return 语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡
  • 函数 Test5 运行虽然不会出错,但是函数GetString2 的设计概念却是错误的。因为GetString2 内的“hello world”是常量字符串,位于静态存储区,它在程序生命期内恒定不变。无论什么时候调用GetString2,它返回的始终是同一个“只读”的内存块。
  • 别看 free 和delete 的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。
  • 指针p 被free 以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p 成了“野指针”。如果此时不把p 设置为NULL,会让人误以为p 是个合法的指针。
  • 我们发现指针有一些“似是而非”的特征:
    •   (1)指针消亡了,并不表示它所指的内存会被自动释放。
    •   (2)内存被释放了,并不表示指针会消亡或者成了NULL 指针。
  • “野指针”不是NULL 指针,是指向“垃圾”内存的指针。
  • “野指针”的成因主要有两种:
    •   (1)指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL 指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
    •   (2)指针p 被free 或者delete 之后,没有置为NULL,让人误以为p 是个合法的指针。
    •   (3)指针操作超越了变量的作用范围。
  • malloc 与free 是C++/C 语言的标准库函数,new/delete 是C++的运算符。它们都可用于申请动态内存和释放内存。
  • 对于非内部数据类型的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
  • 因此 C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。注意new/delete 不是库函数。
  • 我们不要企图用malloc/free 来完成动态对象的内存管理,应该用new/delete。由于内部数据类型的“ 对象”没有构造与析构的过程,对它们而言malloc/free 和new/delete 是等价的。
  • 既然 new/delete 的功能完全覆盖了malloc/free,为什么C++不把malloc/free 淘汰出局呢?这是因为C++程序经常要调用C 函数,而C 程序只能用malloc/free 管理动态内存。
  • 如果在申请动态内存时找不到足够大的内存块,malloc 和new 将返回NULL 指针,宣告内存申请失败。通常有三种方式处理“内存耗尽”问题。
    •   (1)判断指针是否为NULL,如果是则马上用return 语句终止本函数。
    •   (2)判断指针是否为NULL,如果是则马上用exit(1)终止整个程序的运行。
    •   (3)为new 和malloc 设置异常处理函数。
  • 函数 malloc 的原型如下:void * malloc(size_t size);
  • 我们应当把注意力集中在两个要素上:“类型转换”和“sizeof”。
    •   malloc 返回值的类型是void *,所以在调用malloc 时要显式地进行类型转换,将void * 转换成所需要的指针类型。
    •   malloc 函数本身并不识别要申请的内存是什么类型,它只关心内存的总字节数。
    •   在 malloc 的“()”中使用sizeof 运算符是良好的风格,但要当心有时我们会昏了头,写出 p = malloc(sizeof(p))这样的程序来。
  • 函数 free 的原型如下:void free( void * memblock );
  • 运算符new 使用起来要比函数malloc 简单得多,这是因为new 内置了sizeof、类型转换和类型安全检查功能。对于非内部数据类型的对象而言,new 在创建动态对象的同时完成了初始化工作。如果对象有多个构造函数,那么new 的语句也可以有多种形式。
  • 如果用new 创建对象数组,那么只能使用对象的无参数构造函数。
  • 在用delete 释放对象数组时,留意不要丢了符号‘[]’。

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

  • 对比于C 语言的函数,C++增加了重载(overloaded)、内联(inline)、const 和virtual四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const 与virtual 机制仅用于类的成员函数。
  • 在 C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,即函数重载。这样便于记忆,提高了函数的易用性,这是C++语言采用重载机制的一个理由。
  • C++语言采用重载机制的另一个理由是:类的构造函数需要重载机制。
  • 所以只能靠参数而不能靠返回值类型的不同来区分重载函数。编译器根据参数为每个重载函数产生不同的内部标识符。
  • 该函数被C 编译器编译后在库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用C 函数。C++提供了一个C 连接交换指定符号extern“C”来解决这个问题。
  • 注意并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。
  • 如果类的某个成员函数要调用全局函数Print,为了与成员函数Print 区别,全局函数被调用时应加‘::’标志。
  • 当心隐式类型转换导致重载函数产生二义性
  • 成员函数被重载的特征:
    •   (1)相同的范围(在同一个类中);
    •   (2)函数名字相同;
    •   (3)参数不同;
    •   (4)virtual 关键字可有可无。
  • 覆盖是指派生类函数覆盖基类函数,特征是:
    •   (1)不同的范围(分别位于派生类与基类);
    •   (2)函数名字相同;
    •   (3)参数相同;
    •   (4)基类函数必须有virtual 关键字。
  • 本来仅仅区别重载与覆盖并不算困难,但是C++的隐藏规则使问题复杂性陡然增加。这里“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
    •   (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    •   (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
  • 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
  • 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
  • 不合理地使用参数的缺省值将导致重载函数output 产生二义性。
  • 在 C++语言中,可以用关键字operator 加上运算符来表示函数,叫做运算符重载。
  • 在 C++运算符集合中,有一些运算符是不允许被重载的。这种限制是出于安全方面的考虑,可防止错误和混乱。
    •   (1)不能改变C++内部数据类型(如int,float 等)的运算符。
    •   (2)不能重载‘.’,因为‘.’在类中对任何成员都有意义,已经成为标准用法。
    •   (3)不能重载目前C++运算符集合中没有的符号,如#,@,$等。原因有两点,一是难以理解,二是难以确定优先级。
    •   (4)对已经存在的运算符进行重载时,不能改变优先级规则,否则将引起混乱。
  • C++ 语言支持函数内联,其目的是为了提高函数的执行效率(速度)。
  • 对于 C++ 而言,使用宏代码还有另一种缺点:无法操作类的私有数据成员。
  • 在调用一个内联函数时,编译器首先检查调用是否正确(进行类型安全检查,或者进行自动类型转换,当然对所有的函数都一样)。如果正确,内联函数的代码就会直接替换函数调用,于是省去了函数调用的开销。
  • C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操作类的数据成员。所以在C++ 程序中,应该用内联函数取代所有宏代码,“断言assert”恐怕是唯一的例外。assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况。
  • inline 是一种“用于实现的关键字”,而不是一种“用于声明的关键字”。
  • 高质量C++/C 程序设计风格的一个基本原则:声明与定义不可混为一谈,用户没有必要、也不应该知道函数是否需要内联。
  • 定义在类声明之中的成员函数将自动地成为内联函数
  • 以下情况不宜使用内联:
    •   (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
    •   (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。

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

  • 对于任意一个类A,如果不想编写上述函数,C++编译器将自动为A 产生四个缺省的函数,如
    • A(void); // 缺省的无参数构造函数
    • A(const A &a); // 缺省的拷贝构造函数
    • ~A(void); // 缺省的析构函数
    • A & operate =(const A &a); // 缺省的赋值函数
  • 既然能自动生成函数,为什么还要程序员编写?原因如下:
    • (1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人Stroustrup 的好心好意白费了。
    • (2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
  • 除了名字外,构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void 的函数不同。
  • 构造函数有个特殊的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。
  • 构造函数初始化表的使用规则:
    • 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
    • 类的 const 常量只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化
    • 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。
  • 类B 的构造函数在函数体内用赋值的方式将成员对象m_a 初始化。我们看到的只是一条赋值语句,但实际上B 的构造函数干了两件事:先暗地里创建m_a对象(调用了A 的无参数构造函数),再调用类A 的赋值函数,将参数a 赋给m_a。
  • 对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程序版式似乎更清晰些。
  • 成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。
  • String c = a; // 调用了拷贝构造函数,最好写成 c(a);
  • 类 String 拷贝构造函数与普通构造函数(参见9.4 节)的区别是:在函数入口处无需与NULL 进行比较,这是因为“引用”不可能是NULL,而“指针”可以为NULL。
  • 类 String 的赋值函数比构造函数复杂得多,分四步实现:
    • (1)第一步,检查自赋值。
    • (2)第二步,用delete 释放原有的内存资源。如果现在不释放,以后就没机会了,将造成内存泄露。
    • (3)第三步,分配新的内存资源,并复制字符串。注意函数strlen 返回的是有效字符串长度,不包含结束符‘\0’。函数strcpy 则连‘\0’一起复制。
    • (4)第四步,返回本对象的引用,目的是为了实现象 a = b = c 这样的链式表达。
  • 如果我们实在不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,怎么办?偷懒的办法是:只需将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。
  • 基类的构造函数、析构函数、赋值函数都不能被派生类继承。如果类之间存在继承关系,在编写上述基本函数时应注意以下事项:
    • 派生类的构造函数应在其初始化表里调用基类的构造函数。
    • 基类与派生类的析构函数应该为虚(即加virtual 关键字)。
    • 在编写派生类的赋值函数时,注意不要忘记对基类的数据成员重新赋值。

第 10 章  类的继承与组合

  • C++的“继承”特性可以提高程序的可复用性。
  • 如果类A 和类B 毫不相关,不可以为了使B 的功能更多些而让B继承A 的功能和属性。
  • 若在逻辑上B 是A 的“一种”(a kind of ),则允许B 继承A 的功能和属性。
  • 看起来很简单,但是实际应用时可能会有意外,继承的概念在程序世界与现实世界并不完全相同。
  • 所以更加严格的继承规则应当是:若在逻辑上B 是A 的“一种”,并且A 的所有功能和属性对B 而言都有意义,则允许B 继承A 的功能和属性。
  • 若在逻辑上A 是B 的“一部分”(a part of),则不允许B 从A 派生,而是要用A 和其它东西组合出B。

第 11 章  其它编程经验

  • 被const 修饰的东西都受到强制保护,可以预防意外的变动,能提高程序的健壮性。所以很多C++程序设计书籍建议:“Use const whenever you need”。
  • const 只能修饰输入参数:
    •   如果输入参数采用“指针传递”,那么加const 修饰可以防止意外地改动该指针,起到保护作用。
    •   如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。
  • 对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较底。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
  • 为了提高效率,可以将函数声明改为void Func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临时对象。但是函数void Func(A &a) 存在一个缺点:“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为void Func(const A &a)。
  • 内部数据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
  • “const &”修饰输入参数的规则
    •   对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void Func(A a) 改为void Func(const A &a)。
    •   对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void Func(int x) 不应该改为void Func(const int &x)。
  • 用const 修饰函数的返回值
    •   如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
    •   如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值。
    •   函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。
  • 任何不会修改数据成员的函数都应该声明为const 类型。
  • const 成员函数的声明看起来怪怪的:const 关键字只能放在函数声明的尾部
  • 程序的时间效率是指运行速度,空间效率是指程序占用内存或者外存的状况。
  • 全局效率是指站在整个系统的角度上考虑的效率,局部效率是指站在模块或函数角度上考虑的效率。
  • 不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
  • 以提高程序的全局效率为主,提高局部效率为辅。
  • 在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
  • 先优化数据结构和算法,再优化执行代码。
  • 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
  • 不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
  • 当心那些视觉上不易分辨的操作符发生书写错误。我们经常会把“==”误写成“=”,象“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢1”失误。然而编译器却不一定能自动指出这类错误。
  • 变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
  • 当心变量的初值、缺省值错误,或者精度不够。
  • 当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
  • 当心变量发生上溢或下溢,数组的下标越界。
  • 当心忘记编写错误处理程序,当心错误处理程序本身有误。
  • 当心文件I/O 有错误。
  • 避免编写技巧性很高代码。
  • 不要设计面面俱到、非常灵活的数据结构。
  • 如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
  • 尽量使用标准库函数,不要“发明”已经存在的库函数。
  • 尽量不要使用与具体硬件或软件环境关系密切的变量。
  • 把编译器的选择项设置为最严格状态。
  • 如果可能的话,使用PC-Lint、LogiScope 等工具进行代码审查。

附录 C :C++/C 试题的答案与评分标准

  • extern "C"_百度百科
    •   http://baike.baidu.com/view/2814224.htm
  • 指针本身在大多数系统上都是一个无符号整数(在32bit机上是4byte,在64bit机上是8byte)
posted on 2016-06-12 16:15  浩然119  阅读(393)  评论(0编辑  收藏  举报