目录
1.什么是预处理
预处理的作用
2.预定义符号
3.宏定义 #define
3.1 #define 定义标识符
3.2 #define 定义宏
3.2.1用法
3.2.2警告:
3.3 # 和 ##
3.3.1 # 把宏参数替换成对应的字符串
3.3.2 ## 合并两个片段
3.4 避免将带有副作用的参数传递给宏
3.5 宏和函数的对比
3.6 undef
4.条件编译
例一
例二
例三
头文件包含问题
方法1
方法2
C语言的程序中可包括各种以符号#开头的编译指令,这些指令称为预处理命令。预处理命令属于C语言编译器,而不是C语言的组成部分。通过预处理命令可扩展C语言程序设计的环境。
在集成开发环境中,编译,链接是同时完成的。其实,C语言编译器在对源代码编译之前,还需要进一步的处理:预编译。预编译的主要作用如下:
●将源文件中以”include”格式包含的文件复制到编译的源文件中。
●用实际值替换用“#define”定义的字符串。
●根据“#if”后面的条件决定需要编译的代码。
具体可见这篇文章1.2预编译部分:【C语言】你知道.c文件是如何变成.exe文件的吗_
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
这些预定于符号都是语言内置的,如下代码和运行结果:
#include
int main()
{
printf("line:%d\n", __LINE__);
printf("date:%s\n", __DATE__);
printf("time:%s\n", __TIME__);
return 0;
}
语法: #define name stuff
例如: #define N 100
如果有了这个宏定义,那么此程序中,所有的 N 都会 在预编译阶段 被替换成100。
又例如:
如下代码和运行结果:
#include
#define N 10
int main()
{
int m = N;
printf("%d\n", m);
}
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name( parament-list ) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
如下,#define MAX(x,y) (x>y?x:y) 这就相当于把MAX(x,y) 直接替换成了 (x>y?x:y) 这个三目运算符,相当于 printf("%d\n", (a>b?a:b)); SQUARE同理,得到的结果也和预期一样:
#include
#define MAX(x,y) (x>y?x:y)
#define SQUARE(x) x*x
int main()
{
int a = 10;
int b = 20;
printf("%d\n", MAX(a,b));
printf("%d\n", SQUARE(5));
return 0;
}
宏定义不一定总是能够得到完美的结果,如下,目的是输出6*6=36,但是实际运行结果却不是:
#include
#define SQUARE(x) x*x
int main()
{
printf("%d\n", SQUARE(5+1));
return 0;
}
这是因为宏定义是直接替换,所以这里替换成了 5+1*5 +1 ,结果是11,如下:
在宏定义里加上两个括号就可以解决这个问题:
#include
#define SQUARE(x) (x)*(x)
int main()
{
printf("%d\n", SQUARE(5 + 1)); //给宏里面的x加上括号就会避免这个问题
return 0;
}
但是,这样就完美了吗?我们来看下面的代码,本意是10*12=120,但是实际输出结果如下图:
#include
#define ADD(x) (x)+(x)
int main()
{
printf("%d\n", 10*ADD(5 + 1));
return 0;
}
这又是为什么呢?我们看到下面的扩展,再仔细联想,就可得知,宏将ADD(5+1) 替换成了 (5+1)+(5+1) 那么10*ADD(5+1) 就变成了 10*(5+1)+(5+1),所以出错:
所以,最终还要在宏的外层添加一个括号才可,这样才是最好的写法:
#include
#define ADD(x) ((x)+(x))
int main()
{
printf("%d\n", 10*ADD(5 + 1));
return 0;
}
最后总结,用#define定义宏,不仅要将每个参数单独括号,还要将最外层加一个括号,逐层括号。
如下,想要打印不同数据类型的数据,但是其输出格式大体上是一样的,如下代码写起来就很麻烦,冗余较多。
#include
int main()
{
int a = 10, b = 20;
float c = 23.44;
printf("The value of a is:%d \n", a);
printf("The value of b is:%d \n", b);
printf("The value of c is:%f \n", c);
return 0;
}
此时就想要写一个宏,实现打印各种数据,且格式保持不变,如果如下的代码这样写,就会导致"The value of val is" 这段字符串里面,val并不会对应地变成 a 或者b 或者c这些变量名,而会一直是"val" 这个字符串,不符合格式要求。那么就一定要把val 放到双引号外面,此时#的作用就来了:
#define PRINT(val,format) printf("The value of val is:"format"\n",val)
如下,这样子写,#val 是把 val 当作一个字符串来的,会自动加上引号,相当于 #val== "val" 这样子无论val是什么变量名,都可以完美替换。
#define PRINT(val,format) printf("The value of "#val" is:"format"\n",val)
如下代码和运行结果:
#include
#define PRINT(val,format) printf("The value of "#val" is:"format"\n",val)
int main()
{
int a = 10, b = 20;
float c = 23.44;
PRINT(a, "%d");
PRINT(b, "%d");
PRINT(c, "%f");
return 0;
}
作用如下,目前而言基本上没有用到,就做简单介绍吧。
#include
#define CAT(a,b) a##b //把a,b合并起来了
int main()
{
int Class1 = 10;
int Class2 = 20;
printf("%d\n", CAT(Class, 1));
printf("%d\n", CAT(Class, 2));
return 0;
}
如下代码和运行结果, int m = MAX(++a, ++b); 被宏替换成了: int m = ((++a) > (++b) ? (++a) : (++b)); 实际上变成了上面一行这样运算, 4>5 ,是错的,所以冒号后面运算,冒号前面不运算,返回的值是++5,为6,所以m=6,b=6,a=4。传入这样的参数,会导致结果不可控,产生一些难以预测的后果。
#include
#define MAX(x,y) ((x)>(y)?(x):(y))
int main()
{
int a = 3;
int b = 4;
int m = MAX(++a, ++b);
//m = ((++a) > (++b) ? (++a) : (++b));
printf("%d %d %d\n", m, a, b);
return 0;
}
宏通常被应用于执行简单的运算,比如在两个数中找出较大的一个。那为什么不用函数来完成这个任务?
原因有二:
1. 用于 调用函数和从函数返回的代码 可能比 实际执行这个小型计算工作 所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
2. 更为重要的是函数的参数必须声明为特定的类型。(所以函数只能在类型合适的表达式上使用。反之这个宏可以适用于整形、长整型、浮点型等。宏是类型无关的。)
宏的缺点:
1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
2. 宏是没法调试的。
3. 宏由于类型无关,也就不够严谨。
4. 宏可能会带来运算符优先级的问题,导致程容易出现错。
这条指令用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。比如说调试性质的代码,删除可惜,不删碍事,这时用条件编译就可以解决问题。如下,#ifdef 和 #endif 是配套的,它的意思是,如果#define 了 _DEBUG_ 那么就执行在#ifdef 和 #endif 之间的代码,否则在这之间的代码,在预编译阶段就不会被保留。(具体可见这篇文章的1.2.4 部分(174条消息) 【C语言】你知道.c文件是如何变成.exe文件的吗_Austerlitzl的博客-CSDN博客_c语言怎么导出exe)
#include
//#define _DEBUG_
int main()
{
int arr[10] = { 0 };
for (int i = 0;i < 10;i++)
{
arr[i] = i;
#ifdef _DEBUG_
printf("%d\n", arr[i]);
#endif // DEBUG
}
return 0;
}
我将#define _DEBUG_ 注释起来了,如下运行结果,并没有打印什么内容。
条件编译不仅仅是有 #ifdef 和 #endif 语句,也有 #if 语句,如下。从代码颜色深浅也可以看出,printf("hehe\n"); 是要被编译的代码,颜色更明亮一些。实际上也是如此。
条件编译也可以有嵌套指令,如下是由 #if #elif #else #endif 嵌套而成的指令。如图,#define CASE2 和 #define CHOICE2 那么顺着来看,应该被编译的语句是printf("choice2\n"); 。由颜色深浅也可以一眼看出,printf("choice2\n"); 是被编译的。
头文件包含有两种写法 #include "" 和 #include <> ,前者是先在次工程项目的文件里面寻找,有没有 "" 里面的头文件,如果有则包含,没有则在库里面寻找; 后者是直接在库里面寻找头文件。
在写项目时,经常要一个头文件被其他多个文件包含,那么在预编译阶段,就会造成头文件的内容多次出现在整个代码中,冗余且重复,很不方便,这时用条件编译语句就可以很方便地解决问题。
第一种方法,在头文件里面,使用条件编译语句,如下。#ifndef 地意思是 if not define ,下面条件编译的意思是 : 如果没有 #define _TEST_h_ ,那么就 #define _TEST_h_ ,并且将头文件的内容包含进去。 如果已经 #define _TEST_h_ 那么自然就直接到#endif 头文件不会被包含。(那么第一次之后 再有包含该头文件的代码时,由于第一次已经#define _TEST_h_ ,后面就不会再包含)
//方法1,如下,这种方法比较老
//如果没有define _TEST_h ,那么define一个,并且把头文件内容包含进去
//下一次引用该头文件时,已经定义了_TEST_h_ 所以不会包含头文件进去了
#ifndef _TEST_h_
#define _TEST_h_
//这中间是头文件原本的内容,这里简单写一下就好
int Add(int x, int y);
#endif // !_TEST_h_
在头文件顶部写一行 #pragma once 即可。这是比较新的写法。(VS2019环境下,如果建立一个.h 的头文件,会自动在第一行生成这个。其他版本没试过,2019之后的应该也会自动生成,之前就不知道了)
关于C语言的预处理知识,本文就介绍到这里,欢迎在评论区指正错误!!!