目录
一、预定义符号
二、#define 定义常量
三、#define 定义宏
(1)宏定义的使用
(2)带副作用的宏参数
(3)宏替换的规则
(4)宏与函数对比
(5)#和##
① #运算符
② ##运算符
(6)宏的命名规则
(7)#undef
四、命令行定义
五、条件编译
(1)条件编译的使用
(2)常见的条件编译
① 基础的条件编译
② 多个分支的条件编译
③ 判断是否被定义
④ 嵌套指令
六、头文件的包含
(1)头文件被包含的方式
① 本地文件包含
② 库文件包含
(2)嵌套文件包含
① 嵌套文件包含的概念
② 嵌套文件包含的解决方法
七、其它预处理指令
一、预定义符号
预定义符号,会在预处理阶段,被直接替换为它的内容。
预定义符号有:
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
在 VS 环境中演示,预定义符号__STDC__不可使用:
用 gcc 编译器演示,gcc 遵循标准C:
执行命令 gcc -E test.c -o test.i(进行预处理),打开 test.i:
二、#define 定义常量
#define 定义常量,会在预处理阶段,将代码中的名字直接替换为内容。
语法形式:
#define name stuff
// name: 名字
// stuff: 内容
// 举例:
#define M 100
在其它场景下的用法:
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续⾏
符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \date:%s\ttime:%s\n" ,\__FILE__,__LINE__ , \__DATE__,__TIME__ )
示例代码:
预处理后的 test.i 文件:
有些语言的 switch 语句没有 break,使用这些语言的程序员再使用 C 语言就非常不习惯,老忘记加 break,像示例代码一样使用 #define 定义,编码时就不用写 break 了。
注意:#define 定义标识符,最后不加 ; 。
如下情况,#define 定义常量加了 ; ,发生错误:
预处理阶段,第 52 行被替换成 max = 100;;,表示两条语句。因为 if 语句没加{},if 只跟 一条语句,所以发生了错误。
三、#define 定义宏
(1)宏定义的使用
#define 定义宏,在预处理阶段,将代码中的 名字(参数),替换为宏的内容,并把参数带入内容中。
语法形式:
#define name( parament-list ) stuff
// name: 名字
// parament-list: 参数列表,由逗号隔开
// stuff: 内容
注意:name后应紧跟(,如果之间加了空格,会被认为 ( parament-list ) stuff 是 stuff, 属于#define 定义标识符。
示例代码1:
预处理后的结果:
将参数改为 x+1(宏的参数是直接替换,而不计算):
预处理后的结果:
改进代码(为了防止替换后,因操作符优先级等,导致运算顺序不是预料的结果,应尽量加小括号):
预处理后的结果:
示例代码2:
改进代码:
(2)带副作用的宏参数
若宏参数带有副作用,并且在宏定义中同一个宏参数不止出现一次,那么这个宏可能会出现不可预料的结果。
赋值符号的右边是示例宏参数:
y = x+1;//执行后,对x不改变,不带副作用
y = x++;//执行后,对x改变,带有副作用
示例代码,期望获得 X、Y 两者最大值,但出现问题:
而定义函数,传入带副作用的参数,却不会出现问题:
结论:应避免使用带副作用的宏参数。
(3)宏替换的规则
- 调用宏时,首先对宏参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
例如:
- 再次对结果文件进行扫描,看它是否包含任何由#define定义的符号。如果是,重复上述处理过程。
例如:
注意:
- 宏定义不能出现递归。
例如下面的错误示范:
预处理后的代码:
因为宏只进行一次替换,如果宏定义存在递归,那么替换不完全,会把剩下的宏认为是函数,但这个函数又没被定义,所以出现链接错误。
- 预处理器搜索#define定义的符号的时,字符串常量的内容并不被搜索。
例如:
(4)宏与函数对比
执行简单的计算时,宏比函数更有优势。
相比函数,宏的优势:
- 函数,需要函数调用、执行计算、函数返回;宏只需要直接执行计算;因此,宏比函数在程序规模和计算速度方面,更优。
- 函数的参数,必须声明特定的类型;宏的参数,类型无关。因此,宏比函数更灵活。
例如:计算两个数中的较大值。
函数的实现:
int Max(int x, int y)
{return x > y ? x : y;
}int main()
{int a = 7, b = 5;int m = Max(a, b);printf("m = %d\n", m);return 0;
}
宏定义实现:
#define MAX(X, Y) ((X) > (Y) ? (X) : (Y))int main()
{int a = 7, b = 5;float m = MAX(a, b);//int m = ((a)>(b)?(a):(b));printf("m = %f\n", m);return 0;
}
调试,查看函数实现的汇编代码:
① 调用函数,执行了19条指令。
② 计算,执行了 9 条指令。
③ 返回函数,执行了10条指令。
调试,查看宏定义实现的汇编代码:
计算,执行了9条指令。
结论:因为函数的实现方法,需要创建函数栈帧,所以多了调用函数、返回函数的指令,在代码规模和计算速度上,明显比宏的实现方法差。
宏参数没有类型的限制,也可以传入浮点数:
相比函数,宏的劣势:
- 每使用一次宏,都会插入一段宏定义的代码,除非宏很短,否则会大幅增加程序的长度(宏在预处理阶段,会直接被替换成一大段宏的内容;而每次调用的函数,代码只需定义一次)。
- 宏无法调试(在预处理阶段,宏就已经被替换掉,而调试是在 .exe 文件生成后执行的操作)。
- 因为宏类型无关,所以不够严谨。
- 宏可能产生操作符优先级的问题,导致程序运行结果出乎意料(宏参数为表达式、宏参数带有副作用的情况)。
宏有时可以做函数做不到的事,比如宏参数可以是类型,但函数做不到。如下面的代码,只需要给宏传入元素个数、元素类型,就能实现动态开辟空间:
#define MALLOC(N, Type) (Type*)malloc(N * sizeof(Type))int main()
{//int* p = (int*)malloc(10 * sizeof(int));int* p = MALLOC(10, int);return 0;
}
总结宏和函数的对比:
属性 | #define定义宏 | 函数 |
代码长度 | 每次使用宏,宏内容被替换到程序中,程序长度会大幅增加。 | 每次使用函数,都调用同一份函数定义的代码。(胜) |
执行速度 | 更快。(胜) | 会有调用函数、返回函数的额外开销。 |
操作符优先级 | 宏参数求值,在宏内容表达式的上下文环境里,容易产生邻近操作符优先级的问题,导致计算结果不可预料。要多使用圆括号。 | 函数参数求值,只在函数调用时求一次,传给函数,计算结果容易预测。(胜) |
带有副作用的参数 | 带有副作用的参数,可能会被替换到宏内容表达式的多个位置,多次求值,产生不可预料的结果。 | 带有副作用的参数,只在函数调用时求值一次,结果容易预测。(胜) |
参数类型 | 宏的参数类型无关,更灵活。(胜) | 函数的参数定义了特定类型。 |
调试 | 不能调试。 | 可调试。(胜) |
递归 | 不能递归。 | 可递归。(胜) |
执行简单计算,使用宏;执行复杂计算,使用函数。当计算复杂时,计算的花销远大于调用函数、返回函数的花销,可以忽略不计。复杂的计算,使用函数,更不易出错。
在C++中引入了内联函数(inline),它既具备了函数的优势,又具备了宏的优势。
(5)#和##
① #运算符
作用:在宏定义的内容表达式中使用,可将宏参数转化为字符串。
先了解一个没见过的知识:
示例,我们想打印3句话,但是代码很重复:
红框是3句打印不同的部分,将它们作为宏参数,使用宏(由于预处理器不搜索程序中的字符串常量,所以红框中的v并没有被替换):
此时,在宏定义的表达式中,使用#操作符,将传入的参数转为字符串:
② ##运算符
作用:在宏定义的内容表达式中使用,可将两个参数合成一个标识符(应是合法的标识符)。这被称为记号粘合。
示例,使用函数实现计算两个参数的较大值,但参数类型不同,函数的实现也不同,这样的代码很重复(红框中是不同的部分):
使用宏定义和##,减少编程的繁琐:
预处理后的结果:
(6)宏的命名规则
使用宏和函数的语法非常相似,可以从命名规则角度区分它们。
- 宏名全部大写(如:MAX)。
- 函数名不要全部大写(如:Max)。
但这只是一个习惯,并不是定死的,C标准中也有宏定义是小写命名的:
(7)#undef
作用:移除一个#define定义。
示例:
四、命令行定义
一些C编译器,允许命令行定义符号。比如,我们有时想要用一个源文件,编译出不同版本程序。
示例,在程序中声明了 SIZE 长度的数组。在内存有限的机器上,我们想要很小的数组;在内存较大的机器上,我们想要较大的数组。这可以通过命令行定义(VS不支持,gcc 支持)实现,源代码如下:
#include <stdio.h>
int main()
{int array[SIZE];int i = 0;for (i = 0; i < SIZE; i++){array[i] = i;}for (i = 0; i < SIZE; i++){printf("%d ", array[i]);}printf("\n");return 0;
}
使用如下命令,定义 SIZE:
五、条件编译
(1)条件编译的使用
作用:选择一组语句编译或者不编译。
示例,调试性的代码,不想执行调试,但想保留代码,使用条件编译:
因为定义了 __DEBUG__,所以会编译 printf 语句,在预处理阶段,将 printf 语句保留了下来:
如果不想编译 printf 语句,就注释掉 __DEBUG__ 的定义:
预处理阶段,去掉了 printf 语句:
(2)常见的条件编译
① 基础的条件编译
语法形式:
#if 常量表达式//...
#endif
示例(预处理阶段,M被替换成5,常量表达式 5==1 为假,不编译 printf 语句):
#define M 5int main()
{#if M==1printf("hehe\n");
#endifreturn 0;
}
② 多个分支的条件编译
语法形式:
#if 常量表达式//...
#elif 常量表达式//...
#else//...
#endif
示例(最终编译 printf("哈哈\n");):
#define M 5int main()
{#if M==1printf("hehe\n");
#elif M==2printf("haha\n");
#elif M==3printf("heihei\n");
#else printf("哈哈\n");
#endifreturn 0;
}
③ 判断是否被定义
语法形式1:
// 判断 symbol 是否被定义
#if defined(symbol)
// ...
#ifdef symbol// 判断 symbol 是否没被定义
#if !defined(symbol)
// ...
#ifndef symbol
语法形式2:
// 判断 symbol 是否被定义
#ifdef symbol
// ...
//#endif// 判断 symbol 是否没被定义
#ifndef symbol
// ...
#endif
示例代码(最终编译 printf("1hehe\n"); 和 printf("3hehe\n");):
#define Mint main()
{
//判断M是否被定义过,关于值是多少,不关心
// 语法形式1
#if defined(M)printf("1hehe\n");
#endif#if !defined(M)printf("2hehe\n");
#endif// 语法形式2
#ifdef Mprintf("3hehe\n");
#endif#ifndef Mprintf("4hehe\n");
#endifreturn 0;
}
④ 嵌套指令
if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif
#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif
#endif
很少用到,只有在很大的项目中常用,比如打开 stdio.h 头文件看,就是用了很多条件编译指令(因为代码是跨平台的,根据不同的平台,有不同的代码):
六、头文件的包含
(1)头文件被包含的方式
① 本地文件包含
语法形式:
#include "filename"
查找策略:先在源文件所在目录下找,如果没有,再在标准位置找,如果找不到就提示编译错误。
比如我的项目下的文件目录:
test.c 中包含了本地头文件 add.h,故先在 test.c 所在目录下找:
Linux 环境的标准头文件路径:
VS2013 环境默认的标准头文件路径:
// 根据自己实际的安装路径找
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
② 库文件包含
语法形式:
#include <filename.h>
查找策略:直接到标准路径下找,找不到就提示编译错误。
库文件包含也可以用 " ",但是这种方式会先查找源文件目录,再查找标准路径,会降低查找效率,也不易区分是本地文件还是库文件。
(2)嵌套文件包含
① 嵌套文件包含的概念
在预处理阶段,会把头文件包含的所有内容,替换到使用 #include 的文件中。如果同一个头文件被重复包含多次,在预处理后的文件中就会有许多重复的代码,使编译效率大大降低。
比如,A、B、C 3个文件都包含了头文件 add.h,文件 D 又对A、B、C 进行整合,相当于 D 重复包含了 3 次 add.h 头文件。对于大型工程,会包含3~5万个文件,如果重复包含头文件,而不作处理,其预处理后的文件中重复代码之多,后果不堪设想。
示例,test.c 重复包含头文件 add.h:
// test.c 中的内容
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"
#include "add.h"int main()
{return 0;
}// add.h 中的内容
int Add(int x, int y);
test.c 预处理后的内容:
② 嵌套文件包含的解决方法
方法1:在头文件 add.h 中,使用条件编译
#ifndef __ADD_H__
#define __ADD_H__
//头⽂件的内容
#endif
在源代码 test.c 中,第一次包含头文件 add.h,符号 __ADD_H__ 未被定义,编译 #define __ADD_H__ 和头文件的内容。test.c 后面再包含头文件 add.h,因为已经定义过 __ADD_H__,不再编译 #define __ADD_H__ 和头文件的内容。
示范:
// 更改后,add.h 中的内容
#ifndef __ADD_H__
#define __ADD_H__
int Add(int x, int y);
#endif
test.c 预处理后的内容:
方法2:在头文件中加以下内容(在 VS 中创建新的头文件,会自动包含这条语句)
#pragma once
注:在《高质量C/C++编程指南》中附录的考试试卷,就包含头文件包含相关笔试题目。
七、其它预处理指令
#error
#pragma
#line
...
更多参考《C语言深度解剖》
- #pragma pack() 参考:【C语言】自定义类型——结构体-CSDN博客 修改默认对齐数部分。
- #pragma comment() 参考:【C语言】函数-CSDN博客 多个文件,导入静态库部分。