(1).为何要“以编译器替换预处理器”?
(2).何为“预处理器“? 作用是什么?
(3).何为符号表(symbol table)?
(4).#define相对const的缺陷和优势?
(5).const定义常量的好处?
(6).const定义常量需要注意的几个问题?
(7).inline相对#define的优势?
(8).inline相对普通函数的优缺点?
(9).enum hack是什么东东?
(10).enum、const、#define对比?
(11).结论
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------->
有这样一个预定义宏:
#define ASPECT_PATIO 1.653
记号名称ASPECT_PATIO也许从未被编译器看见(一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 汇编程序 (assembler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)),也许在编译器开始处理源码之前它就被预处理器移走了。于是记号名称ASPECT_PATIO有可能没进入记号表(symbol table),所以如果出现了错误编译器不会显示ASPECT_PATIO,只会显示1.653错误。解决之道是以一个常量替换上述的宏(#define):
const double AspectRatio = 1.653;
编译器的主要工作流程为:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 汇编程序 (assembler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)
预处理器的主要工作是:读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。再详细一些就是:
删除注释、插入被#include指令包含的内容、定义和替换由#define指令定义的符号以及确定代码的部分内容是否应该根据一些条件编译指令经行编译。
伪指令主要包括以下四个方面:
(1)宏定义指令,如#define Name TokenString,#undef等。对于前一个伪指令,预编译所要做的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。
(3)头文件包含指令,如#include "FileName"或者#include <FileName>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。
什么是符号表:
符号表是用来记录编译过程中的各种信息的表格。在编译程序工作的过程中,需要不断收集、记录、查证和使用源程序中的一些语法符号(简称为符号)的类型和特征等相关信息。为方便起见,一般的做法是让编译程序在其工作过程中建立并保存一批表格,如常数表、变量名表、数组内情向量表、过程或子程序名表及标号表等,将它们统称为符号表或名字表。
符号表中的每一项包括两个部分:一部分填入名字(标识符);另一部分是与此名字有关的信息,这些信息将全面地反映各个语法符号的属性以及它们在编译过程中的特征,诸如名字的种属(常数、变量、数组、标号等)、名字的类型(整型、实型、逻辑型、字符型等)、特征(当前是定义性出现还是使用性出现等)、给此名字分配的存储单元地址及与此名语义有关的其它信息等。
符号表的作用:
◦登记编译过程输入和输出信息
◦在语义分析过程中用于语义检查和中间代码生成
◦作为目标代码生成阶段地址分配的依据
因此,由于宏定义是在预处理阶段进行宏展开的,所以在符号表中并不存在宏名,因而编译阶段如果出现了宏相关的错误根本就无法定位到该宏。
优势:
a.#define不占用内存,因为是预编译指令,编译之前都已经进行了宏替换,不需要改变值,没有内存分配的必要。
const常量是占有内存被“冻结”了的变量,需要分配内存
b.定义局部变量时,#define的作用域为定义点到整个程序结束,也可以用#undef取消定义从而限制作用域
const常量如果在函数体内定义则作用域只限于该函数体
劣势:
a.const定义的常量是有数据类型的,这样编译器可以对const常量进行数据类型安全检查
#define定义的常量只是进行简单的字符替换,没有类型安全检查
b.在调试程序的时候可以跟踪const定义的常量
#define定义的常量在符号表中不存在,无法跟踪,其常量出错之后无法进行定位
c.const可以做为class的成员
#define不可以做为class成员
const可以修饰函数参数、函数返回值、变量、类成员,方式意外的修改,提高程序的健壮性
* 静态常量数据成员可以在类内初始化(即类内声明的同时初始化),也可以在类外,即类的实现文件中初始化,不能在构造函数中初始化,也不能在构造函数的初始化列表中初始化;
* 静态非常量数据成员只能在类外,即类的实现文件中初始化,也不能在构造函数中初始化,不能在构造函数的初始化列表中初始化;
* 非静态的常量数据成员不能在类内初始化,也不能在构造函数中初始化,而只能且必须在构造函数的初始化列表中初始化;
* 非静态的非常量数据成员不能在类内初始化,可以在构造函数中初始化,也可以在构造函数的初始化列表中初始化;
类型 初始化方式 |
类内(声明) |
类外(类实现文件) |
构造函数中 |
构造函数的初始化列表 |
非静态非常量数据成员 |
N |
N |
Y |
Y |
非静态常量数据成员 |
N |
N |
N |
Y (must) |
静态非常量数据成员 |
N |
Y (must) |
N |
N |
静态常量数据成员 |
Y |
Y |
N |
N |
宏的定义很容易产生二意性:
#define ABS(x) ((x)>0? (x):-(x))
如果有ABS(++i),则i会被自加2次
a.inline 定义的类的内联函数,函数的代码被放入符号表中,在使用时直接进行替换,(像宏一样展开),没有了调用的开销,效率也很高。
b.类的内联函数也是一个真正的函数,编译器在调用一个内联函数时,会首先检查它的参数的类型,保证调用正确。然后进行一系列的相关检查,就像对待任何一个真正的函数一样。这样就消除了它的隐患和局限性。
c.inline 可以作为某个类的成员函数,当然就可以在其中使用所在类的保护成员及私有成员。
我们可以把它作为一般的函数一样调用,但是由于内联函数在需要的时候,会像宏一样展开,所以执行速度确比一般函数的执行速度要快。当然,内联函数也有一定的局限性。就是函数中的执行代码不能太多了,如果,内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。
在类中初始化数组成员的大小时,可以采用静态常量数据成员,例如:
class MyTest
{
private:
static const int MaxNumber = 5;
int score[MaxNumber];
};
除此之外,还可以使用enum类型的数据,即改用所谓的"the enum hack"补偿做法,其理论基础是“一个属于枚举类型(enumerated type)的数值可权充int被使用”。例如:class MyTest
{
private:
enum {MaxNumber = 5}; //"the enum hack"使MaxNumber成为5的一个记号名称
int score[MaxNumber];
};
a.宏和枚举之间的差别主要在作用的时期和存储的形式不同,宏是在预处理的阶段进行替换工作的,它替换代码段的文本,程序运行的过程中宏已不存在了。而枚举是在程序运行之后才起作用的,枚举常量存储在数据段的静态存储区里。宏占用代码段的空间,而枚举除了占用空间,还消耗CPU资源。
b.可以取一个const的地址,而不能取一个enum的地址,也不能取一个#define的地址。
c.enum和#define一样绝不会导致非必要的内存分配。
对于单纯常量,最好以const对象或enum替换#define;
对于形似函数的宏(macros),最好改用inline函数替换#define