【C语言】预处理详解

目    录

1 预定义符号

2 #define

2.1 #define定义标识符

2.2 #define定义宏

2.3 #define替换规则

2.4 #和##

2.5 带副作用的宏参数

2.6 宏和函数的对比

2.7 宏的命名

3 #undef

4 条件编译

5 文件包含

5.1 头文件被包含的方式

5.2 嵌套文件包含


1 预定义符号

__FILE__      //进行编译的源文件(文件名)   打印格式:%s
__LINE__     //文件当前的行号                         打印格式:%d
__DATE__    //文件被编译的日期                     打印格式:%s
__TIME__     //文件被编译的时间                     打印格式:%s
__STDC__   //如果编译器遵循ANSIC,其值为1,否则未定义   打印格式:%d
其中:
以上预定义符号都是C语言中内置的,默认可直接使用;符号中的"__"是两条下划线

预定义符号测试使用:

测试代码:

printf("当前编译原文件名:\n%s\n", __FILE__);
printf("文件当前行号:\n%d\n", __LINE__);
printf("文件被编译的日期:\n%s\n", __DATE__);
printf("文件被编译的时间:\n%s\n", __TIME__);

测试结果:

 注意:这里在VS环境下是没有定义__STDC__的,所以无法直接使用该符号。

2 #define

2.1 #define定义标识符

语法:

#define name stuff
其中:
当我们使用#define定义的标识符编写代码时,在经过预处理后,代码中的标识符name会被全部替换成stuff。

使用举例:

#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\t \
                         date:%s\ttime:%s\n" ,\
                         __FILE__,__LINE__ ,\
                         __DATE__,__TIME__ )  

在VS2022环境下测试:

//测试代码

#define MAX 100
#define STR "We have a bright future!"

int main() {

    printf("%d\n", MAX);
    printf(STR);

    return 0;
}

输出结果:

注意:在使用#define定义标识符的时候,最好不要在后面添加 ; 号 

例如:在以上代码的基础上进行如下修改:

#define MAX 100   ——>   #define MAX 100;

此时经过预处理替换后的代码实际上变成了:

int main() {

    printf("%d\n", 100;);  //将 ; 号也进行了替换,不符合语法要求,更无法通过编译
    printf("We have a bright future!");

    return 0;
}

2.2 #define定义宏

#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。

宏的声明方式:

#define name( parament - list ) stuff
其中:
①parament - list是一个由逗号隔开的符号表,而符号表里的符号可能会出现在stuff中。
②参数列表的左括号必须与name紧邻,如果两者之间存在任何空白,参数列表就会被解释为stuff的一部分,此时的定义宏,就变成了不合法或非预期的#define定义的标识符。

宏定义举例:

//宏定义

#define SQUARE(x) x*x   //宏接收一个参数x

int main() {

    printf("%d\n", SQUARE(5));  //宏使用,预处理器在预处理时会用 5*5 替换SQUARE(5)

    return 0;
}

程序运行输出结果:

 那此例子中的宏定义就没有问题了吗?

值得注意的是:

当我们将原代码 printf("%d\n", SQUARE(5));  修改为 printf("%d\n", SQUARE(1 + 5));  

或许我们预期的输出是36,但运行程序后会发现,实际输出的是11,这又是为什么呢?

分析:

不同于函数传参(将1+5运算后得到的值6作为参数传入),在预处理进行文本替换时,宏参数x被直接替换成了 1+5 ,所以在经过预处理后,这条语句实际上变成了:

printf("%d\n", 1+5*1+5); //并没有按照预想的次序进行求值,最终输出11 

如果想要输出36,正常来说的代码应该是:printf("%d\n", (1+5)*(1+5)); 倒推回去,即表示在宏定义时我们期望能通过括号来保证运算顺序,即修改宏定义为:

#define SQUARE(x) (x)*(x) 如此即可得到预期输出

那这里我们再看一个例子:

//宏定义

//这里使用了括号,避免发生上个例子中的问题,那这样是不是就万无一失了呢?

#define DOUBLE(x) (x) + (x)  

int main() {

    printf("%d\n", 10 * DOUBLE(5)); 

    return 0;
}

或许这里我们预期输出的值是100,但运行程序发现实际输出的值为55,这又该如何解释呢?

我们发现:

上述代码在进行宏替换后,代码 printf("%d\n", 10 * DOUBLE(5));  会变成 

printf ("%d\n",10 * (5) + (5)); 这里乘法运算会优先于宏定义中的加法运算所以输出了55。而若想使宏定义中的加法先进行运算,显然文本替换后的代码应当是 

printf ("%d\n",10 * ((5) + (5))); 即表示在宏定义时,代码应当为:

#define DOUBLE( x)   ( ( x ) + ( x ) ) 如此即可输出预期值100。

总结:用于对数值表达式进行求值的宏定义都应该用上述两个例子中的修正方式加上括号,避免在使用宏时发生参数中的操作符或邻近操作符之间不可预料的相互作用。

2.3 #define替换规则

在程序中扩展#define定义的标识符和宏时,包括以下几个步骤:

①在调用宏时,首先对参数进行检查,查看是否包含任何由#define定义的标识符,如果是,则它们首先被替换。

②替换文本随后被插入到程序中原来文本的位置上。对于宏,参数名被它们的值所替换。

③最后,再次对结果文件进行扫描,查看是否还包含任何由#define定义的符号。如果是,则重复上述处理过程。

注意:

①宏参数和#define定义的标识符中可以出现其它#define定义的标识符。但是对于宏,不能出现递归。例如:

                        #define MAX 5
                        //#define SQUARE(x) ((x)*(MAX))  //可行
                        #define SQUARE(x) ((x)*(SQUARE(x)))  //不可行

②当预处理器搜索#define定义的标识符时,字符串常量的内容并不被搜索。例如:

#define MAX 5

printf("MAX"); //此时字符串中的MAX不会被搜索,更不会被替换,实际打印的还是 MAX

2.4 #和##

如何将参数插入到字符串中?

方式一

这里有一段代码如下:

char* p = "We have ""a bright future!\n";
printf("We have ""a bright future!\n");
printf("%s", p);

运行结果:

 可以发现:字符串具有自动连接的特点。

那如果我们想将参数插入到字符串中,即可以这样编写代码:

#define PRINT(FORMAT, VALUE) printf("The value is "FORMAT"\n", VALUE)

int main() {

    PRINT("%d", 10);  //将字符串"%d"作为宏参数

    return 0;
}

输出结果:

 注意:这种方式只有当字符串作为宏参数的时候才可以将(字符串)参数插入到字符串中。

方式二

除方式一外,我们还可使用#,把一个宏参数变成对应的字符串。

例如:

//代码中的#FORMAT和#VALUE经过预处理器处理后会变为"FORMAT"和"VALUE"

#define PRINT(FORMAT, VALUE) printf("The value of "#VALUE" is "#FORMAT"\n", VALUE)

int main() {

    int i = 10;
    PRINT(%d, i+3);

    return 0;
}

输出结果:

##的作用 

把位于##两边的符号合成一个符号,允许宏定义从分离的文本片段创建标识符

例如:

#define CAT(A,B) A##B

int main() {

    int a = 2;
    int b = 3;
    int twoSum = a + b;

    //将two和Sum连接成为twoSum(已定义的合法标识符),并输出其值
    printf("%d\n", CAT(two, Sum)); 

    return 0;
}

注意:使用##连接必须产生一个合法的标识符。否则其结果是未定义的,可能会报标识符未定义的错误。

2.5 带副作用的宏参数

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用(即在表达式求值的时候出现永久性效果),那么在使用这个宏的时候就可能出现危险,导致非预期的结果。

例如:

a+1;//不带副作用
a++;//带有副作用

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

int main() {

    int a = 3;
    int b = 2;
    //经预处理后代码为:max = ((a+1) > (b) ? (a+1) : (b));
    //int max = MAX(a + 1, b); //使用不带副作用的宏参数
    //经预处理后代码为:max = ((a++) > (b) ? (a++) : (b));
    int max = MAX(a++, b); //使用带副作用的宏参数
    printf("a=%d b=%d max=%d\n", a, b, max);

    return 0;
}

如上代码所示,我们预期比较a+1的值与b的值的大小并输出被比较的两个值与比较后得到的较大值。当使用不带副作用的宏参数时,代码执行输出的结果为:a=3 b=2 max=4,符合预期;而当使用带副作用的宏参数时,代码执行输出的结果为:a=5 b=2 max=4,可以看到输出的较大值并不是由a+1后得到的结果,原数a的值在比较中被修改了两次,输出的a值已不是原来用于比较的值了,这就是使用带副作用的宏参数后产生的非预期结果。因此在调用宏时,我们应当尽量避免使用带副作用的宏参数。

2.6 宏和函数的对比

宏通常被应用于执行简单的运算,比如找两个数之间的较大数:

#define MAX(a, b) ( (a) > (b) ? (a) : (b) )

那为什么不用函数来实现呢?

原因如下:

宏相比函数的优势:

①用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。

②函数的参数必须声明为特定类型。所以函数只能在类型合适的表达式上使用。反之,宏适用于整型、长整型、浮点型等可以用>来比较的类型,即宏是类型无关的,或者说,宏参数无类型。

当然,宏并不是万能的,相比函数它也有自己的劣势:

宏相比函数的劣势:

①每次使用宏的时候都会将一份宏定义的代码插入到程序中。除非宏比较短,否则可能大幅增加程序的长度。

②宏是没法调试的。

③宏由于类型无关,相对也就不够严谨。

④宏可能会带来运算符优先级的问题,导致程序容易出错。

基于宏参数无类型的特点,宏有时可以做到函数做不到的事情,例如:宏参数可以是某个类型,但函数参数不可以

//定义宏用于动态开辟内存空间

#define MALLOC(NUM, TYPE) (TYPE*)malloc(NUM*sizeof(TYPE))

//宏使用

MALLOC(10, int); //将int类型作为宏参数

//经预处理替换后

(int*)malloc(num*sizeof(int));

以下是关于宏和函数的一个总结对比: 

属性#define定义宏函数
代码长度每次使用都会将宏代码插入到程序中,除了非常小的宏之外,程序的长度会大幅增长函数代码只出现于一个地方,每次使用函数时都调用固定地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,相对较慢
操作符优先级宏参数的求值是在替换后进行的,可能受到邻近操作符的影响,需要在宏定义时对参数以及宏体整体添加括号以确保运算顺序函数参数只在函数调用的时候求值一次,传递给函数的是其求值后的结果值,函数实现的结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,可能会产生未预期的结果函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏参数与类型无关,只要对参数的操作是合法的,宏参数可以是任何类型的数据,甚至是类型本身函数参数是与类型相关的,如果参数的类型不同,就需要不同的函数,即使执行的任务性质是一样的
调试宏不能调试函数可以逐语句调试
递归宏不能递归函数可以递归

2.7 宏的命名

通常函数和宏的使用语法相似,有时只靠语言无法区分一个表达式是宏还是函数,基于此,我们习惯:

将宏名全部大写

函数名不要全部大写,通常只有首字母大写

3 #undef

//用于移除一个宏定义

//如果现存的某个#define定义的宏或者标识符需要被重新定义,则需要先移除旧的定义

#undef NAME
例如:
#define N 10 //定义标识符
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )  //定义宏
#undef MAX //移除定义的宏MAX
#undef N //移除定义的标识符N

4 条件编译

通过条件编译指令,在编译程序时,我们可以选择性的编译某一条或某一组语句,例如:

#include <stdio.h>

#define __DEBUG__  //定义符号__DEBUG__
int main()
{
    int i = 0;
    int arr[10] = { 0 };
    for (i = 0; i < 10; i++)
    {
        arr[i] = i;
        #ifdef __DEBUG__  //如果定义了符号__DEBUG__,就编译如下语句
        printf("%d ", arr[i]);  //观察数组是否赋值成功 
        #endif  //结束条件编译
    }
    return 0;
}

如上代码所示,我们定义了符号__DEBUG__,所以在条件编译指令中的语句将会被编译执行,最终输出数组内容如下:

 以下列举一些常见的条件编译指令(类似于我们之前常用的if else 命令):

①单分支条件编译指令

#if   常量表达式(真假)//由预处理器求值

//.....

#endif

如:

#define __DEBUG__ 1

#if __DEBUG__  //表达式值为真,所以编译下面语句

//...

#endif

②多分支条件编译指令

#if 常量表达式

//...

#else if 常量表达式

//...

#else

//...

#endif

③判断是否被定义

#if defined(symbol)

#ifdef symbol

#if !defined(symbol)

#ifndef symbol

④嵌套指令

#if defined(MODE1)

        #ifdef OPTION1

                //...

        #endif

        #ifdef OPTION2

                //...

        #endif

#elif defined(MODE2)

        #ifdef OPTION1

                //...

        #endif

#endif

5 文件包含

我们通过#include指令来包含当前文件所需要用到的头文件。在编译过程中,预处理器会先删除包含文件这条指令,接着用所包含的文件内容进行替换,如果我们#include指令包含同一个头文件10次,那这个头文件的内容就会被10次替换到当前文件中,实际被编译10次。

5.1 头文件被包含的方式

◾ 库文件包含

#include <filename.h>

查找方式:直接到头文件所在的标准路径下查找,如果找不到就提示编译错误。

VS环境下标准头文件的路径(根据安装情况有所差异):

//VS2013的默认路径

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//本人使用的编译环境下的头文件路径

D:\Windows Kits\10\Include\10.0.19041.0\ucrt

如果想查询自己所使用的头文件的路径,可通过如下方式打开:双击源文件中包含的头文件,右击后选择转到相应文档,接着在打开文档处右击选择复制文件完整路径或直接打开其所在文件夹。

 Linux环境下标准头文件的路径:

/ usr / include

◾ 本地文件包含

#include "filename.h"

查找方式:先在源文件所在目录下查找,如果在该目录下未找到被包含文件,编译器就会像查找库函数头文件一样在标准位置查找头文件,如果找不到则提示编译错误。

那是否对库文件也可以使用#include " " 的方式进行文件包含呢?答案是肯定的,但是这种方式查找头文件的效率要相对#include <> 包含的方式低一些,而且这样也不方便区分包含的是本地文件还是库文件了。

5.2 嵌套文件包含

如本节开头所说,同一个头文件如果被包含多次,在预处理时就会被替换多次,并被多次编译,不仅造成文件内容重复,还影响程序编译效率。因此,在进行文件包含时,应当注意避免文件的重复包含,但有时在项目文件多的情况下,某些文件的重复包含是不易被察觉的,且要自己一点点检查是否存在文件包含难免影响工作效率。如下嵌套文件包含的场景就会存在文件重复包含的问题:

test01.h和test02.h都中包含了test03.h,而test.h中又包含了test01.h和test02.h,test.c中包含了test.h,如此当编译源文件test.c时,文件test03.h就会被编译两次,造成文件内容重复编译。

那如何解决这个问题呢?以下有两种方式:

◾ 方式一

使用条件编译,在每个头文件开头编写以下代码:

#ifndef __TEST_H__

#define __TEST_H__

//头文件的内容

#endif

如图所示,在文件test01.c中用#include多次包含了文件test.h,但因为在头文件test.h中添加了条件编译指令表示:在第一次包含头文件时,头文件中的标识符__TEST_H__还未被定义,此时定义__TEST_H__,并预处理时将头文件内容替换到源文件中头文件所在的位置;在接下来头文件包含时,由于第一次编译已经定义了__TEST_H__,所以直接结束了条件编译,不会编译头文件中的内容,也就避免了重复编译。

◾ 方式二

在每个头文件开头添加以下代码:

#pragma once

采用这两种方式就可以避免头文件的重复引入。

以上是我对C语言部分预处理指令的一些学习记录总结,如有错误,希望大家帮忙指正,也欢迎大家给予建议和讨论,谢谢!

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大米饭_Mirai

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值