C语言:预处理详解
语法形式:#define name(标识符)stuff(常量值)//register是C语言中的一个关键字,是寄存器的意思int main()//建议将num中的值放到寄存器register中//为啥说是建议?因为寄存器数量有限,会有编译器决定是否将变量放到寄存器中//这句代码的效果等价于上面那一句return 0;switch (i)case 1:CASE 2:CASE 3://效果上等价于:s
预定义符号
C语言中定义了一些符号,在预处理期间进行处理,这些符号也叫做预定义符号。
__FILE__ //进行编译的源文件的名称
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编辑器遵循ANSI C,它的值就为1,否则未定义
代码示例:
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
#define定义常量
语法形式:
#define name(标识符) stuff(常量值)
代码示例:
#define M 100
#define STR "hehe"
#define reg register
//register是C语言中的一个关键字,是寄存器的意思
#include<stdio.h>
int main()
{
register int num;//建议将num中的值放到寄存器register中
//为啥说是建议?因为寄存器数量有限,会有编译器决定是否将变量放到寄存器中
reg int num1;//这句代码的效果等价于上面那一句
printf("%d\n", M);
printf(STR);
return 0;
}
#define CASE break;case
#include<stdio.h>
int main()
{
int i;
scanf("%d", &i);
switch (i)
{
case 1:
CASE 2:
CASE 3:
}
//效果上等价于:
switch (i)
{
case 1:
break;
case 2:
break;
case 3:
break;
}
return 0;
}
#define定义常量实现死循环:
当for循环中的判断条件什么都没写时,就相当于判断条件恒为真,就会进入死循环,如何用define来定义呢?
#define do_forever for(;;)
#include<stdio.h>
int main()
{
int i = 1;
do_forever
{
i++;
printf("%d ", i);
}
return 0;
}
如果用define来定义常量时,后面的常量值太长应该怎么办,直接换行吗?就像这个代码:
#define print printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",__FILE__,__LINE__,__DATE__,__TIME__)
如果要换行,就需要在每一行的末尾(包括空格的末尾)加上"\":
#define print printf("file:%s\tline:%d\tdate:%s\ttime:%s\n",\
__FILE__,\
__LINE__,\
__DATE__,\
__TIME__)
#include<stdio.h>
int main()
{
print;
return 0;
}
#define定义带参宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或者定义宏。#define
定义的带参宏(如 #define MAX(a,b) ((a)>(b)?(a):(b))
)形式上类似函数,因此常被称为“宏函数”(Macro Function)。
需要注意的是,宏函数是预处理阶段的文本替换,不是真正的函数,没有函数调用的开销(如栈帧分配、参数传递等)
#define 宏名(参数列表) 替换表达式
-
参数列表:类似函数参数,但无需指定类型。
-
替换表达式:需用括号包裹参数和整体,避免优先级问题。
代码示例:
//定义一个宏,计算一个数的平方
#define SQ(x) x*x
#include<stdio.h>
int main()
{
int x = 10;
int r = SQ(x);
printf("%d\n", r);
return 0;
}
但是上面那个代码一定能够实现所有情况吗?下面这个代码就不行:
//定义一个宏,计算一个数的平方
#define SQ(x) x*x
#include<stdio.h>
int main()
{
int x = 10;
int r = SQ(x+1);
printf("%d\n", r);
return 0;
}
#define实现的是一种替换机制,对传入的表达式不会进行任何处理,而是直接进行替换,上面那个代码进行替换以后,就变成了:x+1*x+1,由于运算符的优先级,最后答案会变成21,而不是我们预想的121,为了使代码更完善,我们应该进行如下修改:
//定义一个宏,计算一个数的平方
#define SQ(x) ((x)*(x))
#include<stdio.h>
int main()
{
int x = 10;
int r = SQ(x+1);
printf("%d\n", r);
return 0;
}
宏参数的副作用问题
宏的参数在预处理阶段是直接文本替换,而不是像函数那样先求值再传递。这会导致参数表达式被多次展开,如果参数包含自增、函数调用、IO操作等副作用,可能会引发非预期的行为。
如以下代码:
//写一个宏,求两个数之间的较大值
#include<stdio.h>
#define max(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 10;
int b = 20;
int m = max(a, b);
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("m=%d\n", m);
return 0;
}
但如果改成下面的代码,运行结果是啥?
//写一个宏,求两个数之间的较大值
#include<stdio.h>
#define max(a,b) ((a)>(b)?(a):(b))
int main()
{
int a = 10;
int b = 20;
int m = max(a++, b++);
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("m=%d\n", m);
return 0;
}
在执行int m = max(a++,b++)的时候,会将宏展开:((a++)>(b++)?(a++):(b++))
后置加加是先使用再加加,也就是说会先比较a和b的大小,比较结果是a<b,然后再对a和b各自自增1,之后a和b的值分别是11,21,由于a<b,所以m的结果就是表达式b++,由于是后置加加,是先使用再加加,所以m的值会先变成21,然后再对b自增1,最终结果就是
a=11,b=22,m=21
代码定义了一个求较大值的宏 max(a,b)
,并在 main()
函数中调用它,参数使用了自增操作 a++
和 b++
。由于宏是直接文本替换,这会导致自增操作被执行多次,从而产生副作用。
宏替换的规则
在程序中扩展#define
定义符号和宏时,需要涉及几个步骤。
-
在调用宏时,首先对参数进行检查,看看是否包含任何由
#define
定义的符号。如果是,它们首先被替换。 -
替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
-
最后,再次对结果文件进行扫描,看看它是否包含任何由
#define
定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和
#define
定义中可以出现其他#define
定义的符号。但是对于宏,不能出现递归。 - 当预处理器搜索
#define
定义的符号的时候,字符串常量的内容并不被搜索。
根据代码理解:预处理器搜索#define
定义的符号的时候,字符串常量的内容并不被搜索
//宏替换时,字符串常量中的内容不会被搜索
#include<stdio.h>
#define M 100
int main()
{
printf("M=%d", M);//也就是说,这个字符串中的M就是M,不会被替换成100
return 0;
}
宏函数与函数对比
宏函数与函数的用法比较相似,比如,我们分别用宏函数和函数实现比较两个整数的大小的代码:
#define max(a,b) ((a)>(b)?(a):(b))
int Max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int a = 10;
int b = 20;
int m = max(a,b);
printf("a=%d\n", a);
printf("b=%d\n", b);
printf("m=%d\n", m);
return 0;
}
以上两个代码都能实现基本的两个数的大小比较,但哪种实现方式更好呢?
上面例子来看,将代码写成宏更加有优势。
宏通常被应用于执行简单的运算。
那为什么不用函数来完成这个任务?
原因:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。(宏函数直接实现替换机制,当需要替换代码量较小的时候,替换机制的效率是比较高的,而调用函数涉及函数传参、函数体内部计算、函数返回值等操作,效率相对较低)
- 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用于
>
来比较的类型。宏的参数是类型无关的。
和函数相比宏的劣势:
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏是没法调试的。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程容易出现错。
宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。
属性 | #define 定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除非非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多写括号。 | 函数参数只在函数调用的时候求值一次,的结果值传递给函数。表达式的求值结果容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的型不同,就需要不同的函数,即使他们执的任务是不同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |
一个命名约定:习惯上,我们将宏名全部大写,函数名则不会全部大写
#和##
#运算符将宏的一个参数转换为字符串字面量,它仅允许出现在带参数的宏的替换列表中。
#运算符所执行的操作可以理解为“字符串化”。(在宏定义中,#
可以将宏参数直接转换为双引号包裹的字符串)
在讲这之前,我们先来看一下下面这个代码:
#include<stdio.h>
int main()
{
//以下两个代码的效果是完全相同的
printf("hello world\n");
printf("hello ""world\n");//两个字符串会合并成同一个字符串
return 0;
}
再来看以下代码:
#include<stdio.h>
int main()
{
int a = 10;
float b = 2.3f;
printf("the val of a is %d\n", a);
printf("the val of b is %f\n", b);
return 0;
}
以上的代码我们能否将其封装成一个宏函数,当我们对这个函数传递参数时能达到打印任何类型的效果,比如我们传递整形类型的参数时,他会按照以上形式将参数的值打印出来,传递浮点数类型的参数时,他又会按照以上形式将参数的值打印出来:
#include<stdio.h>
#define PRINT_VAL(n,format) printf("the val of n is "format"\n",n)
int main()
{
int a = 10;
float b = 2.3f;
printf("the val of a is %d\n", a);
printf("the val of b is %f\n", b);
//使用宏函数打印:
PRINT_VAL(a, "%d");
PRINT_VAL(b, "%f");
return 0;
}
这个代码确实可以打印任意类型的参数的值,但是还有一个小缺陷,看运行结果:
当使用宏函数打印时,我们无法将字符串中的n替换成对应的变量a或b,这个问题应该如何解决呢?这就需要使用#对n进行字符串化了:
#include<stdio.h>
#define PRINT_VAL(n,format) printf("the val of "#n" is "format"\n",n)
//#n会将n转换成一个字符串,假设传入的参数是a,就会把n替换成"a"
//假设传入的参数是b,就会把n替换成"b"
int main()
{
int a = 10;
float b = 2.3f;
printf("the val of a is %d\n", a);
printf("the val of b is %f\n", b);
//使用宏函数打印:
PRINT_VAL(a, "%d");
PRINT_VAL(b, "%f");
return 0;
}
现在再看运行结果,就已经得到我们想要的效果了。
##:在C/C++的预处理阶段,##
是标记粘贴操作符(Token Pasting Operator),用于将两个独立的标识符(或宏参数)拼接成一个新的标识符。它的核心作用是在编译前生成新的符号名称,常用于代码生成、泛型编程或避免重复代码。
在讲##的作用之前,我们先来看下面的代码
//写函数:比较两个数的大小
//注意:对于不同类型的数的比较,需要写成不同的函数:
int int_max(int x, int y)
{
return x > y ? x : y;
}float float_max(float x, float y)
{
return x > y ? x : y;
}double double_max(double x, double y)
{
return x > y ? x : y;
}
可以看到,上面三个函数除了类型不同,函数名不同,逻辑都一样,就好像有一个模版,我们直接把类型往里面套就好了,那么,这个模板咋写?
// "\"相当于续行符:
#define GENERATE_MAX(type) \
type type##_max(type x, type y)\
{\
return x > y ? x : y; \
}
上面这个代码就相当于生成了一个函数模版,我们可以利用这个模板生成函数:
// "\"相当于续行符:
#define GENERATE_MAX(type) \
type type##_max(type x, type y)\
{\
return x > y ? x : y; \
}
GENERATE_MAX(int)
////就相当于产生了一个函数:
//int int_max(int x, int y)
//{
// return x > y ? x : y;
//}
GENERATE_MAX(float)
//就相当于产生了一个函数:
//float float_max(float x, float y)
//{
// return x > y ? x : y;
//}
#include<stdio.h>
int main()
{
//直接调用函数:
int x= int_max(1, 2);
float y = float_max(3.14f, 2.31f);
printf("%d\n%f\n", x, y);
return 0;
}
#undef
语法形式:
#undef name
这条指令用于移除一个宏定义,如果一个现存的名字需要被重新定义,那么它的旧名字首先要被移除。
#define M 100
#include<stdio.h>
int main()
{
printf("%d\n", M);
#undef M//取消M原来的宏定义,取消后M相当于未定义
#define M 2000
printf("%d\n", M);
return 0;
}
条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。
因为我们有条件编译指令。
常见的条件编译指令:
1 1.
2 #if 常量表达式
3 //...
4 #endif
5 //常量表达式由预处理器求值。
6 如:
7 #define __DEBUG__ 1
8 #if __DEBUG__
9 //...
10 #endif
11
12 2.多个分支的条件编译
13 #if 常量表达式
14 //...
15 #elif 常量表达式
16 //...
17 #else
18 //...
19 #endif
20
21 3.判断是否被定义
22 #if defined(symbol)
23 #ifdef symbol
24
25 #if !defined(symbol)
26 #ifndef symbol
27
28 4.嵌套指令
29 #if defined(OS_UNIX)
30 #ifdef OPTION1
31 unix_version_option1();
32 #endif
33 #ifdef OPTION2
34 unix_version_option2();
35 #endif
36 #elif defined(OS_MSDOS)
37 #ifdef OPTION2
38 msdos_version_option2();
39 #endif
40 #endif
比如:
#include<stdio.h>
int main()
{
#if 0//if后面的表达式为假,printf("hehe")就不参与编译
printf("hehe");
#endif
return 0;
}
注意哦,上面的#if后面的表达式一定要是常量表达式,否则,像下面的代码就会发生错误:
#include<stdio.h>
int main()
{
int c = 100;
#if c>10
printf("hehe");
#endif
return 0;
}
运行结果:
我们可以看到虽然代码没有报错,但是判断条件c>10明显是真,而屏幕上依然没有打印hehe,这就是因为c是一个变量, #if
是预处理指令,它在编译前由预处理器处理,而预处理阶段无法访问变量的值。
多条件编译指令:
#define M 100
int main()
{
#if M==0
printf("hehe\n");
#elif M<10
printf("haha\n");
#elif M==100
printf("hihi\n");
#else
printf("xixi\n");
#endif
return 0;
}
#include<stdio.h>
#define M 100
int main()
{
//判断M是否已经被定义过了,如果被定义过了,就打印hehe
#if defined(M)
printf("hehe\n");
#endif
return 0;
}
//这种写法与下面那种写法等价
#include<stdio.h>
#define M 100
int main()
{
//判断M是否已经被定义过了
#ifdef M
printf("hehe\n");
#endif
return 0;
}
#include<stdio.h>
#define M 100
int main()
{
//判断M是否已经被定义过了,如果没有被定义过,就打印hehe
#if !defined(M)
printf("hehe\n");
#endif
return 0;
}
//两种写法等价
#include<stdio.h>
#define M 100
int main()
{
//判断M是否已经被定义过了
#ifndef M
printf("hehe\n");
#endif
return 0;
}
其实我们这一小节还只是讲了一部分预处理指令,如果有想要深入了解的小伙伴,可以参考《C语言深度解剖》这本书哦。
更多推荐
所有评论(0)