高质量C++/C编程
从小白开始成长,仍需要不断总结知识内容,充实自己!!!
- 为了防止头文件被重复引用,应当用ifndef/define/endif 结构产生预处理块。
- #include <filename.h> 格式来引用标准库的头文件(编译器将从标准库目录开始搜索)。
- #include “filename.h” 格式来引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
- 头文件中只存放“声明”而不存放“定义”。
- 不提倡使用全局变量,尽量不要在头文件中出现象extern int value 这类声明。
空行:
- 在每个类声明之后、每个函数定义结束之后都要加空行。
- 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
代码行:
- 一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
- if、for、while、do 等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
- 尽可能在定义变量的同时初始化该变量(就近原则)
- 关键字之后要留空格。像const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。像if、for、while 等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
- 函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
- (’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。
- ‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。
- 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=” “>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
- 一元操作符如“!”、“~”、“++”、“–”、“&”(地址运算符)等前后不加空格。
- 像“[]”、“.”、“->”这类操作符前后不加空格。
- 对于表达式比较长的for 语句和if 语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++)和if ((a<=b) && (c<=d))。
对齐:
- 程序的分界符‘{’和‘}’应独占一行并且位于同一列,同时与引用它们的语句左对齐。
- { }之内的代码块在‘{’右边数格处左对齐。
- 代码行最大长度宜控制在70 至80 个字符以内。代码行不要过长,否则眼睛看不过来,也不便于打印。
- 长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
- 应当将修饰符 * 和& 紧靠变量名
注释:
- C 语言的注释符为“/…/”。C++语言中,程序块的注释常采用“/…/”,行注释一般采用“//…”
类:
- 将private 类型的数据写在前面,而将public 类型的函数写在后面,采用这种版式的程序员主张类的设计“以数据为中心”,重点关注类的内部结构。
- 将public 类型的函数写在前面,而将private 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口或服务)。
共性规则:
-
标识符应当直观且可以拼读,可望文知意,不必进行“解码”。标识符最好采用英文单词或其组合,便于记忆和阅读
-
标识符的长度应当符合“min-length && max-information”原则。
-
命名规则尽量与所采用的操作系统或开发工具的风格保持一致。Windows 应用程序的标识符通常采用“大小写”混排的方式,Unix 应用程序的标识符通常采用“小写加下划线”的方式。
-
程序中不要出现仅靠大小写区分的相似的标识符。
-
程序中不要出现标识符完全相同的局部变量和全局变量,尽管两者的作用域不同而不会发生语法错误,但会使人误解。
-
变量的名字应当使用“名词”或者“形容词+名词”。
-
全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
-
用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。
-
尽量避免名字中出现数字编号,如Value1,Value2 等,除非逻辑上的
确需要编号。这是为了防止程序员偷懒,不肯为命名动脑筋而导致产生无意义的名
字(因为用数字编号最省事)。
Windows应用程序命名规则:
- 类名和函数名用大写字母开头的单词组合而成。
- 变量和参数用小写字母开头的单词组合而成。
- 常量全用大写的字母,用下划线分割单词。
- 静态变量加前缀s_(表示static)。
- 如果不得已需要全局变量,则使全局变量加前缀g_(表示global)。
- 类的数据成员加前缀m_(表示member),这样可以避免数据成员与成员函数的参数同名。
运算符的优先级:
- 如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
复合表达式:
- 不要编写太复杂的复合表达式。
- 不要有多用途的复合表达式。
- 不要把程序中的复合表达式与“真正的数学表达式”混淆。
if:
-
不可将布尔变量直接与TRUE、FALSE 或者1、0 进行比较。
-
应当将整型变量用“==”或“!=”直接与0 比较。
-
不可将浮点变量用“==”或“!=”与任何数字比较。
千万要留意,无论是float 还是double 类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
-
应当将指针变量用“==”或“!=”与NULL 比较。
循环:
- 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。
- 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外。
- 不可在for 循环体内修改循环变量,防止for 循环失去控制。
- 建议for语句的循环控制变量的取值采用“半开半闭区间”写法。
- 每个case 语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
- 不要忘记最后那个default 分支。即使程序真的不需要default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default 处理。
常量:
-
尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
-
const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
-
有些集成化的调试工具可以对 const 常量进行调试,但是不能对宏常量进行调试。
-
在C++ 程序中只使用const 常量而不使用宏常量,即const 常量完全取代宏常量。
-
需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
-
如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
-
类中的常量:
有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用const 修饰数据成员来实现。const 数据成的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const 数据成员的值可以不同。
-
不能在类声明中初始化const 数据成员,const 数据成员的初始化只能在类构造函数的初始化表中进行。
-
怎样才能建立在整个类中都恒定的常量呢?别指望const 数据成员了,应该用类中的枚举常量来实现。枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数。
函数设计:
-
参数的书写要完整,不要贪图省事只写参数的类型而省略参数名字。如果函数没有参数,则用void 填充。
-
参数命名要恰当,顺序要合理。应将目的参数放在前面,源参数放在后面。
-
如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改。
-
如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
-
避免函数有太多的参数,参数个数尽量控制在5 个以内。如果参数太多,在使用时容易将参数类型或顺序搞错。
-
尽量不要使用类型和数目不确定的参数。
-
不要省略返回值的类型。
-
函数名字与返回值类型在语义上不可冲突。
-
不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return 语句返回。
-
有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
-
如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
-
在函数体的“出口处”,对return 语句的正确性和效率进行检查。
-
return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁
-
要搞清楚返回的究竟是“值”、“指针”还是“引用”。
-
如果函数返回值是一个对象,要考虑return 语句的效率。
“创建一个临时对象并返回它”的过程是不同的,编译器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了效率。
-
-
函数的功能要单一,不要设计多用途的函数。
-
函数体的规模要小,尽量控制在50 行代码之内。
-
尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。(static函数)
-
不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
-
用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
断言:
- 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
- 在函数的入口处,使用断言检查参数的有效性(合法性)。
- 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查。
- 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要使用断言进行报警。
引用与指针的比较:
- 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
- 不能有NULL 引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
- 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
内存管理:
-
内存分配未成功,却使用了它。如果是用malloc 或new 来申请内存,应该用if(p==NULL)或if(p!=NULL)进行防错处理。
-
内存分配虽然成功,但是尚未初始化就引用它。一是没有初始化的观念;二是误以为内存的缺省初值全为零,导致引用初值错误(例如数组)。
-
内存分配成功并且已经初始化,但操作越过了内存的边界。
-
忘记了释放内存,造成内存泄露。
-
释放了内存却继续使用它。
(1)程序中的对象调用关系过于复杂,实在难以搞清楚某个对象究竟是否已经释放了内存,此时应该重新设计数据结构,从根本上解决对象管理的混乱局面。
(2)函数的return 语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,
因为该内存在函数体结束时被自动销毁。
(3)使用free 或delete 释放了内存后,没有将指针设置为NULL。导致产生“野指针”。 -
用malloc 或new 申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL 的内存。
-
不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
-
避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作。
-
动态内存的申请与释放必须配对,防止内存泄漏。
-
用free 或delete 释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
-
若想把数组a 的内容复制给数组b,不能用语句 b = a ,否则将产生编译错误。应该用标准库函数strcpy 进行复制。同理,比较b 和a 的内容是否相同,不能用if(b==a) 来判断,应该用标准库函数strcmp进行比较。
-
指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。
-
对于非内部数据类型的对象而言,光用maloc/free 无法满足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
参数的缺省值
- 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
- 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
足动态对象的要求。对象在创建的同时要自动执行构造函数, 对象在消亡之前要自动执行析构函数。由于malloc/free 是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
参数的缺省值
- 参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
- 如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。