👉 欢迎阅读这篇文章 👇
目录
- 1、预定义符号
- 2、#define定义常量
- 3、#define定义宏
- 4、带有副作用的宏参数
- 5、宏替换规则
- 6、宏和函数对比
- 7、#和##
- 8、命名约定
- 9、#undef
- 10、命令行定义
- 12、头文件包含
1、预定义符号
C语言设置了一些预定义符号,可以直接使用,预定义符号在预处理期间处理。
c
__FILE__ //进行编译的源文件
__LINE__ //文件当前行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
例如:
c
#include <stdio.h>
int main()
{
printf("%s\n",__FILE__);//打印当前文件路径
printf("%d\n",__LINE__);//打印当前代码的行号
printf("%s\n",__DATE__);//打印该文件被编译的日期
printf("%s\n",__TIME__);//打印该文件被编译的时间
printf("%d\n",__STDC__);//部分编译器打印1,部分编译器报错
return 0;
}
2、#define定义常量
基本语法:
c
#define name stuff
举例:
c
#define MAX 1000
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;) //无限循环
#define CASE break;case //在写case语句的时候可以自动加break
//如果定义的stuff过长,可以分多行书写,除了最后一行外要加上续行符:\
#define DEBUG_PRINT printf("file:%s\\tline:%d\\ \
date:%s\\time:%s",\
__FILE__,__LINE__, \
__DATE__,__TIME__)
思考:在define定义标识符的时候,要不要在最后加上;?
建议不要加上 ; ,这样容易导致问题。
3、#define定义宏
#define机制包含了一个机制,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
宏的声明方式:
c
#define NAME(list) stuff
其中的list是一个由逗号隔开的符号表,他们可能出现在stuff中。
注意:
参数列表的左括号必须与name紧邻,如果两者之间有任何空白,参数列表就会被当作是stuff的部分。
举例:
定义一个求一个数的平方的宏
c
#include <stdio.h>
#define squart(x) x*x
int main(void)
{
int a =10;
int c = squart(a);
printf("%d",c);
return 0;
}
这个宏接受一个参数x。在主程序里,预处理的时候会将SQUART(a)替换为a*a。
但是仔细考虑会发现这个宏存在一个问题,假如这样使用:
c
int a =10;
int c = SQUART(a+1);
这时会将SQUART(a+1)替换为a+1*a+1,计算的时候就先算1*a再算其他部分,就会出现优先级的问题。
为了解决上述问题,我们应该在宏定义的时候,为x加上括号。
c
#define squart(x) (x)*(x)
再看一个宏定义
c
#define SQUART(x) (x)+(x)
int c = 10*SQUART(a+1);
这时会将10*SQUART(a+1)替换为10*(a+1)+(a+1),计算的时候就会出现先算10*(a+1),又出现了优先级的问题。
为了解决上方的问题,我们可以在宏定义的时候,在表达式两边加上括号
c
#define SQUART(x) ((x)+(x))
所以⽤于对数值表达式进⾏求值的宏定义都应该⽤这种⽅式加上括号,避免在使⽤宏时由于参数中的操作符和邻近操作符之间不可预料的相互作⽤。
4、带有副作用的宏参数
当宏参数在宏的定义中出现超过⼀次的时候,如果参数带有副作⽤,那么你在使⽤这个宏的时候就可能出现危险,导致不可预测的后果。
副作⽤就是表达式求值的时候出现的永久性效果。
例如:
c
x+1;//不带副作⽤
x++;//带有副作⽤
定义一个求两个数的最大值的宏
c
#include <stdio.h>
#define MAX(a,b) ((a>b)?(a):(b))
int main()
{
int x = 5;
int y = 8;
int z = MAX(x++,y++);
printf("%d %d %d",x,y,z);
return 0;
}
上方代码经过预处理后MAX部分会变成((x++>y++)?(x++):(y++))。
先把x=5,y=8传给MAX,后x和y自增,变成x=6,y=9。5<8,所以执行y++部分,先把y赋值给z,z=9。y再自增后y变成10。
最终x = 6,y = 10,z = 9。

5、宏替换规则
在程序中扩展#define定义符号和宏时,需要涉及几个步骤:
- 在调用宏时,首先对参数进行检查,看是否包含看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换⽂本随后被插⼊到程序中原来⽂本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果⽂件进⾏扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和
#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。 - 当预处理器搜索
#define定义的符号的时候,字符串常量的内容并不被搜索。
6、宏和函数对比
宏通常被用于执行简单的计算。
⽐如在两个数中找出较⼤的⼀个时,写成下⾯的宏,更有优势⼀些。
c
#define MAX(a, b) ((a)>(b)?(a):(b))
那为什么不⽤函数来完成这个任务?
- ⽤于调⽤函数和从函数返回的代码可能⽐实际执⾏这个⼩型计算⼯作所需要的时间更多。所以宏⽐函数在程序的规模 和速度⽅⾯更胜⼀筹。
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使⽤。反之宏可以适⽤于整形、⻓整型、浮点型等可以⽤ > 来⽐较的类型。宏的参数与类型⽆关。
和函数相⽐宏的劣势:
- 每次使⽤宏的时候,⼀份宏定义的代码将插⼊到程序中。除⾮宏⽐较短,否则可能⼤幅度增加程序的⻓度。
- 宏是没法调试的。
- 宏由于类型⽆关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。⽐如:宏的参数可以出现类型,但是函数做不到。
例如:
c
//利用宏定义一个malloc函数
#include <stdlib.h>
#define MALLOC(num,type) ((type*)malloc((num)*(sizeof(type))))
int main()
{
int* ptr=MALLOC(10,int);
return 0;
}
MALLOC(10,int)预处理后会变成((int*)malloc((10)*(sizeof(int))))。
利用函数无法做到上述代码。
宏和函数对比表 宏和函数对比表 宏和函数对比表
| 属性 | #define定义宏 | 函数 |
|---|---|---|
| 代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅增加 | 函数代码只出现在一个地方;每次使用函数的时候,都调用那一个地方的代码 |
| 执行速度 | 更快 | 存在函数的调用和返回额外开销,相对会慢一些 |
| 操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近的操作符优先级会产生不可预料的后果 | 函数参数只在函数调用的时候求值后把结果传给函数,表达式求值结果容易预测 |
| 带有副作用的参数 | 参数可能被替换到宏体中的多个位置,若宏的参数被多次计算,带有副作用的参数求值会产生不可预料的后果 | 函数参数只在传参的时候求值一次,结果容易控制 |
| 参数类型 | 宏的参数与类型无关,只要对参数的操作合法,他就可以适用于任何操作类型 | 函数的参数与类型有关,如果参数类型不同,就需要不同的函数,即使他们执行的任务相同 |
| 调试 | 宏不方便调试 | 函数可以逐语句调试 |
| 递归 | 宏不可以递归 | 函数可以递归 |
7、#和##
7.1#运算符
#运算符可以将宏的一个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。
#运算符的操作可以理解为"字符串化"
例:
假如想完成下列代码的任务
c
#include <stdio.h>
int main()
{
int a = 10;
printf("the value of a is %d\n",a);
float b = 3.5f;
printf("the value of b is %.2f\n",b);
return 0;
}
我们想定义一个宏来完成对printf函数的调用,并且能够根据参数名修改打印的信息。
如果这样定义宏
c
#define PRINT(x,format) printf("the value of x is format",x)
int main()
{
int a = 10;
PRINT(a,%d);
float b = 3.5f;
PRINT(b,%.2f);
return 0;
}
打开预处理后生成的.i文件观察

并没有进行任何的更改,因为当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。将PRINT里的a和%d传给宏后,x接收a,format接收%d,发现宏体里x和format都在字符串常量中,不会被搜索。只有参数x会被替换成a。
所以,我们要做的是把原本宏体里字符串常量的x和format变成参数,而且最后还可以再拼接成字符串。这时就需要用到#运算符了。
我们把字符串里的x抠出来,把x变成#x,这样就是先把x当作宏参数用,然后转化成字符串,方便后续拼接。
format需要接收的是一个占位符字符串,所以把format抠出来,让他变成一个宏参数,能够接收传来的字符串,后面再拼接即可。
c
#define PRINT(x,format) printf("the value of "#x" is "format"\n",x)
int main()
{
int a = 10;
PRINT(a,"%d");
float b = 3.5f;
PRINT(b,"%.2f");
return 0;
}
预处理后会被替换为

C 语言会自动把它们粘成一个完整字符串

7.2##运算符
##可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的⽂本⽚段创建标识符。##被称为记号粘合。
这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
我们想想,写⼀个函数求2个数的较⼤值的时候,不同的数据类型就得写不同的函数。
c
//定义不同类型数据的比大小函数
int int_max(int x,int y)
{
return x>y?x:y;
}
float float_max(float x,float y)
{
return x>y?x:y;
}
char char_max(char x,char y)
{
return x>y?x:y;
}
int main()
{
int m = 10;
int n = 20;
int m1 = int_max(m,n);
float m2 = float_max(3.5f,4.6f);
char m3 = char_max('a','d');
return 0;
}
但是这样写起来太繁琐了,现在我们这样写代码试试:
c
#define compare(type) type type##_max(type x,type y){return x>y?x:y;}
compare(int);
compare(float);
compare(char);
int main()
{
int m = 10;
int n = 20;
int m1 = int_max(m,n);
float m2 = float_max(3.5f,4.6f);
char m3 = char_max('a','d');
return 0;
}
经过预处理后就会变成
c
int int_max(int x,int y){return x>y?x:y;};
float float_max(float x,float y){return x>y?x:y;};
char char_max(char x,char y){return x>y?x:y;};
int main()
{
int m = 10;
int n = 20;
int m1 = int_max(m,n);
float m2 = float_max(3.5f,4.6f);
char m3 = char_max('a','d');
return 0;
}
和上面的代码一样的效果,这里就用到了##运算符来根据不同的类型拼接不同的函数名。
8、命名约定
⼀般来讲函数和宏的使⽤语法很相似。所以语⾔本⾝没法帮我们区分⼆者。
那我们平时的⼀个习惯是:
- 宏名全部⼤写
- 函数名不要全部⼤写
9、#undef
这条指令⽤于移除⼀个宏定义。
c
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
10、命令行定义
许多C 的编译器提供了⼀种能⼒,允许在命令⾏中定义符号。⽤于启动编译过程。
例如:当我们根据同⼀个源⽂件要编译出⼀个程序的不同版本的时候,这个特性有点⽤处。(假定某个程序中声明了⼀个某个⻓度的数组,如果机器内存有限,我们需要⼀个很⼩的数组,但是另外⼀个机器内存⼤些,我们需要⼀个数组能够⼤些。)
例如:
c
//命令行定义演示
#include <stdio.h>
int main()
{
int array [SZ];
int i = 0;
for(i = 0; i< SZ; i ++)
{
array[i] = i+1;
}
for(i = 0; i< SZ; i ++)
{
printf("%d " ,array[i]);
}
printf("\n");
return 0;
}
可以通过以下指令设置SZ的值并生成可执行文件
c
gcc test.c -DSZ=10 -o test
运行
c
./test

11、条件编译
在编译⼀个程序的时候我们可以决定是要将⼀条语句(⼀组语句)编译还是放弃。
我们可以使用条件编译指令
常⻅的条件编译指令:
1.
c
#if 常量表达式
//.....
#endif
//常量表达式由预处理器求值。
例如:
c
#define __DEBUG__ 1
int main()
{
#if __DEBUG__
printf("%d",10);
#endif
}
源文件与预处理后的.i文件对比


2.
c
//多个分支的条件编译
#if 常量表达式
//.......
#elif 常量表达式
//......
#else
//......
#endif
//......
例如:
c
// 例如:
#define M 50
int main()
{
#if M < 10
printf("%d",10);
#elif M < 45
printf("%d",15);
#else
printf("%d",20);
#endif
}
源文件与预处理后的.i文件对比



3.
c
//判断是否被定义
#if defined(symbol)
或
#ifdef symbol
#if !defined(symbol)
或
#ifndef symbol
例如:
c
// 例如:
#define M 50
#define N 30
int main()
{
#if defined(M)
printf("%d",10);
#endif
#ifdef M
printf("%d",15);
#endif
#if !defined(N)
printf("%d",20);
#endif
#ifndef N
printf("%d",25);
#endif
}
源文件与预处理后的.i文件对比


4.
c
//嵌套指令
#if defined(M)
#ifdef N
//.....
#endif
#ifdef O
//.....
#endif
#elif defined(Q)
#ifdef N1
//.....
#endif
#endif
12、头文件包含
12.1本地文件包含
c
#include "filename"
查找策略:
先在源⽂件所在⽬录下查找,如果该头⽂件未找到,编译器就像查找库函数头⽂件⼀样在标准位置查找头⽂件。如果找不到就提⽰编译错误。
Linux环境的标准头⽂件的路径:
c
/usr/include
12.2库文件包含
c
#include <filename.h>
查找头⽂件直接去标准路径下去查找,如果找不到就提⽰编译错误。
这样是不是可以说,对于库⽂件也可以使⽤ "" 的形式包含?
答案是肯定的,可以,但是这样做查找的效率就低些,当然这样也不容易区分是库⽂件还是本地⽂件
了。
12.3嵌套文件包含
我们已经知道, #include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include 指令的地⽅⼀样。
这种替换的⽅式很简单:预处理器先删除这条指令,并⽤包含⽂件的内容替换。
⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压⼒就⽐较⼤。
如果⼯程⽐较⼤,有公共使⽤的头⽂件,被⼤家都能使⽤,⼜不做任何的处理,那么后果将不堪设想。
我们可以使用条件编译解决头文件被重复引用的问题
每个头文件都这样写
c
#ifndef __TEST__
#define __TEST__
//头文件内容.........
#endif
这样在预处理的时候,先判断是否定义了__TEST__这个常量,第一次包含头文件的时候一定没有定义,那么就定义,第二次再包含,前面已经定义了__TEST__这个常量,这次就会直接跳出,不会再词拷贝头文件里的内容。
或者
c
#pragma once