13 关于编译预处理
- 文件包含有两种语法形式
#include <头文件名>
#include "头文件名"
第一种一般用来包含开发环境提供的库头文件,编译预处理器会在开发环境设定的搜索路径中查找所需文件。第二种形式一般用来包含自己编写的头文件,编译预处理器首先在当前工作目录搜索头文件,找不到才会到开发环境设定的路径中查找。
- 为避免同一个编译单元包含同一个头文件的内容超过多次,需要在头文件中使用内包含卫哨。例如:
#ifndef _STDDEF_H_INCLUDED_
#define _STDDEF_H_INCLUDED_
......
#endif
- 避免同一个编译单元包含同一个头文件的内容超过多次,还可以使用外部包含卫哨。在其他文件开头部分这么写:
#if !define(_INCLUDED_STDDEF_H_)
#include <stddef.h>
#define _INCLUDED_STDDEF_H_
#endif
外部包含卫哨可以显著提高编译速度,因为可以避免多次查找和打开头文件。
- 如果同时使用内部包含卫哨和外部包含卫哨,建议使用同一个标志宏,这样可以少定义一个宏。
- 一般只在头文件中包含其他头文件时使用外部包含卫哨,源文件没必要,也基本不影响编译速度。
- 头文件中包含其他头文件建议顺序为:
(1) 包含当前工程中个所需要的自定义头文件; (2) 包含第三方程序库的头文件; (3) 包含标准头文件。 - 源文件中包含次序推荐为:
(1) 包含源文件对应的头文件(如果存在) (2) 包含当前工程中个所需要的自定义头文件; (3) 包含第三方程序库的头文件; (4) 包含标准头文件。 - #define伪指令用来定义一个宏。可以带参数也可以不带参数。#define后面的第一个连续字符序列为宏名,剩下的部分统统为宏体。
- 宏定义具有文件作用域,无论宏定义出现在文件中的哪个位置(函数体内,类型定义内,名字空间内部等),在后面的任何地方都可以引用这个宏。
- 宏定义不是C++/C语句,因此不需要使用分号“;”作为结束,否则这个分号也会被看做宏体的一部分。
- 任何宏在编译预处理阶段都只是进行简单的文本替换,不做类型检查和语法检查。参数替换发生在宏扩展之前。
- 宏定义可以嵌套
- 宏不可以调试,即使宏替换后出现了语法错误,编译器只会将错误定义到源程序中而不是宏定义中。
- 宏替换不会替换程序中双引号括起来的字符串及字符串子串。
- 定义带参数的宏时,宏名和左括号之间不能出现空格,否则使用时会出现问题,预处理会把括号当成宏体的一部分。
- 带参数的宏体和各个形参应该分别用括号括起来,可以避免造成意想不到的错误。
- 不要在引用宏定义的参数列表中使用增量和减量运算符,否则可能导致变量多次求值,且结果可能与预期不符。
- 带参数的宏定义不是函数,因此没有函数调用的开销,但每一次扩展都会生成重复的代码,结果会使可执行代码的体积增大。
- liline函数不可能完全取代宏,很多时候宏都具有一定的优势。
- 当不在需要某一个宏时,可以使用#undef来取消定义。
- 不要定义很复杂的宏,宏定义应该简单而清晰;
- 宏名应采用大些字符组成的单词或缩写序列,并在各单词之间使用“_”分隔。
- 如果需要公布某个宏,该宏的定义应该放置在头文件中,或者放置在实现文件(.cpp)的顶部。
- 不要使用宏来定义新类型名,应该使用typedef,否则容易造成错误。
- 给宏添加注释时,请使用块注释(/* */),而不要使用行注释(//)。因为有些编译器可能会把宏后面的行注释理解为宏体的一部分。
- 尽量使用const取代宏来定义符号常量。
- 对于较长的使用频率较高的重复代码片段,建议使用函数或模板而不要使用带参数的宏定义;而对于较短的重复代码片段,则可以使用带参数的宏定义。
- 尽量避免在局部范围内(函数内、类型定义内)定义宏,除非它只在该局部范围内使用,否则会损害程序的清晰性。
- 条件编译#if、#ifdef、#ifndef、#elif、#else、#endif、defined,这些可以控制预处理器选择不同代码段作为编译器的输入,必须以#endif为结束。这些结构类似程序控制结构的选择结构,只是它们是在编译预处理阶段发挥作用的。
- 利用#if可以屏蔽一段代码,这种屏蔽方式比注释好。
#if 0
...
...
#endif
- #if的条件只能使用常值或常值表达式,不能计算有变量参与其中的表达式。如果值非0,则为真,否则为假。
- #ifdef XYZ和#if defined(XYZ)是等价的。XYZ被称为调试宏。如果前面曾用#define定义过宏XYZ,则条件为真,否则为假。同样,#ifndef XYZ等价于#if defined !(XYZ)。
- #error用于输出与平台、环境等有关的信息。会把它后面的字符串序列输出,程序不会进入编译阶段。
- #pragma用于执行语言实现所定义的动作,具体应该参考你所用的编译器的帮助文档。
- 宏体中的“#”叫做构串操作符,只能用来修饰带参数的宏的形参,它的作用是将实参的字符序列转换成字符串。比如:
#define STRING(x) #x #x
#define TEXT(x) "class" #x "Info"
那么,STRING(abc)会被展开成字符串“abcabc”,TEXT(abc)会被展开成字符串“classabcInfo”。
- 宏体中的“##”叫做合并操作符,它会将其左右的字符序列合并成一个新的标识符,注意不是字符串。比如:
#define CLASS_NAME(name) class##name
#define MERGE(x,y) x##y##z
那么CLASS_NAME(SysTimer)会展成标识符classSysTimer,MERGE(me,To)会展成标识符meTome。
- 用合并标识符##产生的标识符必须预先有定义,否则会报错:标识符未定义。
- C++继承了ANSI C的预定义的符号常量,预处理器在处理代码是会将它们转换为确定的字面常量。这些符号不能再用#define重新定义,也不能用#undef取消。见下表:
符号常量 | 解释 |
---|
__LINE __ | 引用该符号的语句的代码行号 | __FILE __ | 引用该符号的语句的源文件名称 | __DATE __ | 引用该符号的语句所在源文件的编译日期(字符串) | __TIME __ | 引用该符号的语句所在源文件的编译时间(字符串) | __TIMESTAMP __ | 引用该符号的语句所在源文件的编译日期和时间(字符串) | __STDC __ | 标准C语言环境都会定义该宏以标识当前环境 |
某些编译器会在此基础上定义一些自己的宏,具体需要参考编译器的文档。
14.关于文件结构和程序版式
- C++/C的文件结构和程序版式并不影响功能,也没多少技术含量,但是能够反映出开发者的职业化程度。
- 一个C++/C程序工程中,所有文件可以按下面描述来组织:
程序工程 | Include\ |
---|
| Source\ | | Shared\ | | Resource\ | | Debug\ | | Release\ | | Bin\ | | IDE生成的文件 | | Readme | | 临时文件 | | 配置文件 | | 数据 | | DLL文件 |
-
Include目录存放头文件,可以在细分子目录;Source目录存放源文件,可以细分目录;Shared目录存放共享文件;Resource目录存放资源文件,如图片,视频、图标等;Debug目录存放调试版本生成的中间文件;Release目录存放发行版本生成的中间文件;Bin目录存放自己创建的lib文件和dll文件。 -
编译时的相对路径和运行时的相对路径是不同的。#include包含目录是相对于工程所在路径,或者是相对于当前文件的路径,而程序中类似OpenFile语句中的相对路径是运行时可执行文件所在的路径。 -
头文件的所有内容会被合并到某一个或几个源文件中。一个编译单元是指一个源文件将每一个包含的头文件递归地展开后形成的源文件。 -
通过头文件调用库功能。用户只需按照头文件中的接口声明来调用库函数,而不必关心接口是怎么实现的。 -
头文件能加强类型安全检查,使用头文件也可以提高程序的可读性。 -
头文件内部编排顺序: (1)头文件注释(包括文件说明、功能描述、版权说明),这个必须有。 (2)内部卫哨开始。这个必须有。 (3)#include其他头文件。 (4)外部变量和全局函数声明。 (5)常量和宏定义。 (6)类型前置声明和定义。 (7)全局函数原型和内联函数的定义 (8)内部卫哨结束。 (9)文件版本修订说明。 -
版权、版本信息的内容部分应该包括: (1)版权信息。 (2)文件名称、简要描述、创建日期和作者 (3)当前版本信息和说明。 (4)历史版本信息和修订说明。 -
源文件里结构顺序一般建议如下: (1)源文件注释(包括文件说明、功能描述、版权声明等),这个必须有。 (2)预处理指令 (3)常量和宏定义 (4)外部变量声明和全局变量定义及初始化 (5)成员函数和全局函数的定义。 (6)文件修改记录。 -
关于空行的使用,建议:在ADT/UDT定义之间留空行,ADT内部的各个访问段(public,private等)之间留空行;每一段内按照相关性分组的,各组之间留空行。函数定义之间留空行。函数体内完整的控制结构即单独的语句块之间留空行。return语句之前留空行。 -
一行代码只做一件事,如只定义一个变量或只写一条语句; -
if、elseif、for、do、while等语句独占一行,其他语句不得紧跟其后。不论该语句块有多少行语句都用“{}”括起来。 -
局部变量在定义的同时初始化。 -
关键字之后要留空格。如const、virtual、inline、case后留一个空格。if、elseif、for、while、switch后也留一个空格在写括号。 -
函数名之后不留空格。 -
“(”、“[”,向后紧跟,“]”、“)”、“,”、“;”向前紧跟,紧跟处不留空格。如果“;”不是一行的结束,在它后面留空格。 -
预编译指令#和保留字之间不留空格;文件包含伪指令中文件名与两端的“<”、“>”、““”、“””之间不留空格。 -
二元运算符前后加空格。 -
一元运算符与操作数之间不留空格。 -
“.”、“->”、“.”、“->”、“::”这类运算符前后不加空格。“?”、“:”前后要加空格。 -
代码行尽量控制在70~80字符以内。 -
长表达式在低优先级运算符处拆分多行,运算符放在新行行首,新拆分的行要适当缩进。 -
“{”和“}”应独占一行,同级的处于同一列,不同级的按4字节右缩进。 -
建议修饰符“*”和“&”紧靠变量名,或者使用typedef做一个类型映射。 -
写注释的时候不可过多。不要用注释拼图案。 -
应该边写代码边写注释,修改代码同时也修改注释,保持一致性。 -
注释的位置应与被描述的代码相邻,可以放在上方或右方,不要放在下方。 -
代码有多重嵌套时,应在一些段落结束的时候加注释。 -
编辑复合类型时,pirvate限定的成员要在后面,public成员在前面,毕竟对于用户来说,接口比私有数据更值得关注。
15.关于命名
- 程序中不要出现仅依靠大小写来区分的相似的标识符。
- 不要出现局部变量和全局变量同名的现象。
- 变量名应当使用“名词”或者“形容词+名词”。
- 全局函数的名字硬干使用“动词”或者“动词+名词”。类成员函数应当只使用“动词”,因为省略掉的名字就是对象本身。
- 反义字组命名应当具有反意义的变量或相反动作的函数。
- 尽量避免名字中出现数字,除非逻辑上需要如此。
- 类型名和函数名以大写字母开头的单子组合而成
- 变量名和参数名采用第一个单词首字母小写,而后面单词首字母大写的单词组合。
- 符号常量和宏名用全大写的单词组成,单词之间用单下划线_分割。首尾最好不要用下划线。
- 给静态变量前面加上前缀s_。
- 给全局变量前面加上前缀g_。
- 给类数据成员前加上前缀m_。
- 做第三方库,里面的标识符会具有统一的前缀标识。
|