023-C语言预处理详解

发布于:2025-05-15 ⋅ 阅读:(10) ⋅ 点赞:(0)

C语言预处理详解

1. 预定义符号

C语言设置了一些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。

__FILE__ // 进行编译的源文件
__LINE__ // 文件当前的行号
__DATE__ // 文件被编译的日期
__TIME__ // 文件被编译的时间
__STDC__ // 如果编译器遵循ANSI C,其值为1,否则未定义。

2. #define定义常量

基本语法:

#define name stuff

如果使用#define,在预处理阶段时,代码中所有的name将会被替换为stuff。

注意,这一句的最后不需要加;,如果加上;;也会被替换进代码。

举例:

#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case

// 如果定义的stuff过长,可以分成几行写,除了最后一行外,每行的最后丢加上一个续行符(反斜杠\)
#define DEBUG_DRINT printf("file:%s\tline:%d\t\
							date:%s\ttime:%s\n,\
							__FILE__,__LINE__\
							__DATE__,__TIME__")

3. #define定义宏

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

下面是宏的声明方式:

#define name( parament-list ) stuff

其中的parament-list是一个有逗号隔开的符号表,它们可能出现在stuff中。

注意:参数列表发左括号必须于name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举例:

#define SQUARE(x) x * x

这个宏接收一个参数x,在上述声明后,如果将SQUARE(5);置于程序中,预处理器就会用下面这个表达式替换上面的表达式:5 * 5

使用宏时,最好多使用括号,否则会出现一些问题:

int a = 5;
printf("%d\n", SQUARE(a + 1));

这里我们想要的结果应该是36,但实际上它将打印为11。

在使用宏时,替换过程中,它并不会将传入的表达式先进行运算再替换,而是直接替换,所以实际上上面的代码被替换成了:

printf("%d\n", a + 1 * a + 1);

想要解决这个问题,我们只需要在宏定义时多加上一些括号就行了。

#define SQUARE(x) ((x) * (x))

4. 带有副作用的宏参数

上面说过,当宏函数在进行替换时是直接替换,那么如果传入的参数为x,而宏函数中出现了x++,将会导致传入的参数x在执行完宏函数后发生变化。

同样的,如果传入的参数出现类似x++的表达式,并且宏函数中不止一次使用该参数,且我们不知道该宏内部情况,那么将导致不可预测的后果。这就是带有副作用的宏参数。

所以我们在定义和使用宏函数时,应该尽量避免使用类似上述的表达。

5. 宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤。

  1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
  2. 替换文本随后被插入到程序中原来的文本的位置。对于宏,参数名被他们的值所替换。
  3. 最后再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。

注意:

  1. 宏参数和#define定义中可以出现其他#define定义的符号。但是对应宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。

6. 宏函数的对比

宏通常被应用于执行简单的运算。

比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。

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

那么为什么不使用函数完成这个任务:

  1. 由于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多。所以宏函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏可以让整型、长整型、浮点型等直接使用>等来直接比较,宏是类型无关的

和函数相比宏的劣势:

1. 每次使用宏时,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅增加程序的长度。
1. 宏没法进行调试。
1. 宏类型无关,不够严谨。
1. 宏可能会带来运算符优先级的问题,导致程序容易出错。

宏有时候可以做函数做不到的事。比如:宏的参数可以出现类型,但函数做不到。

#define MALLOC(num, type) (type*)malloc((num) * sizeof(type))

宏和函数的对比:

属性 #define定义宏 函数
代码长度 每次使用都会将宏代码插入到程序中。除了使用很小的宏之外,程序的长度会大幅增长。 函数代码只出现在一个地方,每次调用函数时,都调用同一份代码。
执行速度 更快。 存在函数调用和返回的额外开销,会慢一些。
操作符优先级 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果。 函数参数在函数调用时求值,将结果值传递给函数。
带有副作用的参数 参数可能被替换到宏体中的多个内置,如果宏的参数被多次计算,带有副作用的参数求值可能会产生不可预料的后果。 函数参数只在传参的时候求值一次,易控制。
参数类型 宏的蚕食与类型无关,只要对参数的操作是合法的, 函数的参数是与类型有关的,如参数类型不同,就需要使用不同的函数。
调试 宏是不方便调试的 函数是可以逐语句调试的
递归 宏是不能递归的 函数是可以递归的

7. #和##

7.1 #运算符

#运算符将宏的⼀个参数转换为字符串字面量。它仅允许出现在带参数的宏的替换列表中。

#运算符所执行的操作可以理解为“字符串化”。

当我们有一个变量 int a = 10; 的时候,我们想打印出: the value of a is 10

#define PRINT(n) printf("the value of "#n" is %d", n);

7.2 ##运算符

##可以把它两边的符号变成一个符号,##被称为记号粘合剂。

这样的连接必须产生合法的标识符,否则其结果就是未定义的。

当我们想要写一个函数求两个数的最大值时,不同的数据类型就要写不同的函数。

int int_max(int x, int y)
{
	return x > y ? x : y;
}

float float_max(float x, float y)
{
	return x > y ? x : y ;
}

但如果使用##,就只需要写一份。

#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}

使用宏定义不同函数。

GENERIC_MAX(int) //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float) //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名

int main()
{
    //调⽤函数
    int m = int_max(2, 3);
    printf("%d\n", m);
    float fm = float_max(3.5f, 4.5f);
    printf("%f\n", fm);
    
    return 0;
}

8. 命名约定

  1. 宏名全部大写
  2. 函数名不要全部大写

9. #undef

用于移除一个宏定义。

#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。

10. 命令行定义

许多编译器提供了一种能力,允许在命令行中定义符号。如果我们想要一个程序在不同情况下定义不同大小的数组:

#include <stdio.h>

int main()
{
    int array [ARRAY_SIZE];
    int i = 0;
    for(i = 0; i< ARRAY_SIZE; i ++)
    {
    	array[i] = i;
    }
    for(i = 0; i< ARRAY_SIZE; i ++)
    {
        printf("%d " ,array[i]);
    }
    printf("\n" );

    return 0;
}

编译指令:

//linux 环境演⽰
gcc -D ARRAY_SIZE=10 programe.c

11. 条件编译

在编译一个程序的过程中,我们可能需要制作不同的版本,有些版本可能要舍弃一些功能,这是我们就可以使用条件编译指令。

比如调试性的代码,删除了,以后万一要调整代码的功能可能还得重新写,保留了又影响程序的运行效率,这时我们就可以选择性的编译。

1. 一个分支语句的条件编译

语法结构:

#if 常量表达式
...(需要条件编译的代码)
#endif

在#if后写常量表达式,如果苍凉表达式为真,则编译#if和#endif之间的内容,否则不编译该内容。使用如下:

#include <stdio.h>

#define __DEBUG__ 1

int main()
{
#if __DEBUG__ == 1
	printf("1");
#endif
	printf("2");

	return 0;
}

在上面我们定义了一个__BEDUG__宏,我们可以修改这个宏的内容来实现条件编译:

  • 当把__BEDUG__定义为1时,条件满足,将会编译printf("1");
  • 当__BEDUG__为其他值时,条件不满足,将不会编译printf("1");
  • printf("2");在#if和#endif之外,它的编译将不会受条件编译的影响。

2. 多个分支语句的条件编译

条件编译类似于if…else…语句,也可以又多个分支的条件编译,语法格式如下:

#if 常量表达式
	...
#elif 常量表达式
	...
#elif 常量表达式
	...
#else
	...
#endif

其中,#elif可以有任意多个,#else可以没有,具体使用如下:

#include <stdio.h>

#define __DEBUG__ 6

int main()
{
#if __DEBUG__ == 1
	printf("1");
#elif __DEBUG__ == 2
	printf("2");
#elif __DEBUG__ == 3
	printf("3");
#else
	printf("4");
#endif
	printf("5");

	return 0;
}
  • 当__BEDUG__为1时printf("1");将会被编译被定义为对应内容时,相应的语句才会被编译,如__BEDUG__为1时printf("1");将会被编译,__BEDUG__为2时printf("2");将会被编译。
  • 当__BEDUG__的值不满足任何#if或#elif的条件时,将会编译#else后的内容。

3. 判断某个宏是否被定义

我们也可以通过判断某个宏是否被定义来决定是否进行条件编译。

语法结构:

#if defined(宏名)
	...
#endif

#ifdef 宏名
	...
#endif

-----------------------------------------------
        
#if !defined(宏名)
	...
#endif

#ifndef 宏名
	...
#endif

上面的结构中,上两种结构在宏被定义时,将会编译相应的代码,下两种结构在宏没被定义时,将会编译相应的代码。

#include <stdio.h>

#define __DEBUG__

int main()
{
#if defined(__DEBUG__)
	printf("1");
#endif

#ifdef __DEBUG__
	printf("2");
#endif

#if !defined(__DEBUG__)
	printf("3");
#endif

#ifndef __DEBUG__
	printf("4");
#endif
	printf("5");

	return 0;
}

4. 嵌套编译

条件编译也支持嵌套使用。

#include <stdio.h>

#define TEST1
#define TEST2 1

int main()
{
#if defined(TEST1)
	#if TEST2 == 1
		printf("1");
	#endif
	printf("2");
#endif

	return 0;
}

12. 头文件包含

12.1 头文件被包含方式

12.1.1 本地文件被包含方式
#include "filename"

本地文件我们使用双引号引起来,此时,编译时先会在源文件的目录下进行查找,如果没有找到,就到标准库的头文件目录里面去查找,如果还找不到就会编译错误。

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

/usr/include

VS2022环境下标准头文件路径

D:\Software\VS2022\MicrosoftVisualStudio\2022\Community\VC\Tools\MSVC\14.41.34120\include
// 按照自己安装的路径去找
12.1.2 库文件包含
#include <filename>

库文件使用尖括号括起来即可,被尖括号括起来的文件,编译时会直接在标准库的头文件目录中查找。

根据上面的查找规则,库文件也可以使用双引号,但是查找的效率会低些。

12.2 嵌套文件包含

当我们包含头文件时,可能会包含多个重复的头文件,并不一定是在同一个文件中一起包含的,有可能是我们在文件1中包含了stdio.h,在文件2中也包含了stdio.h,同时文件1又包含了文件2,此时就会造成文件的重复包含,重复包含文件会导致一些错误。

这时我们可以使用条件编译来解决:

#ifndef __TEST_H__
#define __TEST_H__
// 包含头文件
#endif

我们只需要在包含每个头文件前都加上这个内容就可以解决。

但是这用起来太繁琐了,我们还有更简单的办法可以解决这个问题:

#pragma once

我们只需要在包含头文件前加上这一条指令,就可以避免头文件的重复引入。

13. 其他预处理指令

13.1 #error

作用:强制中断编译并输出错误信息

#error "错误信息"

13.2 #line

作用:从该行开始,修改编译器报告的行号和文件名。

#line 100 "newfile.c"

件,并不一定是在同一个文件中一起包含的,有可能是我们在文件1中包含了stdio.h,在文件2中也包含了stdio.h,同时文件1又包含了文件2,此时就会造成文件的重复包含,重复包含文件会导致一些错误。

这时我们可以使用条件编译来解决:

#ifndef __TEST_H__
#define __TEST_H__
// 包含头文件
#endif

我们只需要在包含每个头文件前都加上这个内容就可以解决。

但是这用起来太繁琐了,我们还有更简单的办法可以解决这个问题:

#pragma once

我们只需要在包含头文件前加上这一条指令,就可以避免头文件的重复引入。

13. 其他预处理指令

13.1 #error

作用:强制中断编译并输出错误信息

#error "错误信息"

13.2 #line

作用:从该行开始,修改编译器报告的行号和文件名。

#line 100 "newfile.c"

网站公告

今日签到

点亮在社区的每一天
去签到