【C语言】预处理操作(详解)
慕雪年华

[TOC]

前言😜

上篇博客,我们提到了C语言程序运行的几个环节。

本篇博客中提到的预处理指令,就是在预处理阶段运行的一些代码。

本篇博客使用的编译器🎰

  • VS2019(win10)
  • 树莓派(linux-gcc)

1.预定义符号

1
2
3
4
5
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //测试编译器是否遵循ANSI C,遵循值为1,不遵循则该符号未定义

image

2.#define

2.1定义标识符

1
#define name stuff
1
2
3
4
5
6
7
8
9
10
#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__ )

加分号问题

define定义标识符的时候,最好不要在结尾加上;

1
2
#define MAX 1000;
#define MAX 1000

image

2.2定义宏

除了定义标识符以外,define还可以定义一个语句为标识符,允许把参数替换到文本中。这种机制叫做定义宏

下面是宏的申明方式:

1
#define name(parament-list)  stuff 

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

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

1
2
//如
#define DOUBLE(x) x+x

宏需要注意的问题

这个宏有一个问题👇

1
2
3
4
5
6
7
8
#define DOUBLE(x) x+x

int main()
{
printf("%d\n", 10*DOUBLE(3));

return 0;
}

这个式子输出的结果是什么呢?是60吗?

答案是否定的:#define宏在使用的时候执行的是直接替换

这个语句就相当于10*3+3,根据操作符优先级可知,结果为30+3=33

image

想解决这个问题,我们需要记住这个原则:

  • 给变量加上括号以确保优先级
  • 给整个宏再加上一个括号防止外部数据影响
1
#define DOUBLE(x) ((x)+(x))

再运行程序,发现答案变成了60

image

用于对数值表达式进行求值的宏定义,都应该用这种方式加上括号。避免在使用宏时,参数中的操作符或邻近操作符之间产生不可预料的相互作用

2.3define替换规则

image

2.4使用###

2.4.1#将字符串插入字符串

1
2
3
4
5
6
7
8
9
10
int main()
{
int a = 3;
int b = 5;
printf("the num of a is %d\n", a);
printf("the num of b is %d\n", b);
printf("\n");

return 0;
}

image

在这个代码里面,打印的前置内容a和b需要根据打印的变量进行更改

有没有一种办法,可以让他自己进行更改?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define PRINT(X) printf("the num of "#X" is %d\n", X)

int main()
{
int a = 3;
int b = 5;
printf("the num of a is %d\n", a);
printf("the num of b is %d\n", b);
printf("\n");

PRINT(a);
PRINT(b);

return 0;
}

可以看到,#X处的内容被替换成了我们需要打印的变量

image

实际上,这里#X旁边的双引号,是将整个字符串拆分成了3份进行打印

  • “the num of”
  • #X
  • “is %d\n”

字符串在打印的时候具有自动拼接的特性,所以我们可以通过这种方式将一个需要更改的字符插入到字符串中

2.4.2## 将两个符号合并

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

如图,这个宏将Class和10两个分离的符号合并成了Class10,printf识别出来并打印的了Class10的值

image

当我们同名变量有很多的时候,就可以利用这种宏来给不同的变量增加数据。

1
2
3
4
#define ADD_TO_SUM(num, value) sum##num += value;

int sum1,sum2,sum3,sum4;
ADD_TO_SUM(3, 10);//给sum3增加10.

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


2.5带副作用的宏参数

当宏在定义中出现超过一次时,如果参数带有副作用,使用这个宏的时候就有可能出现危险,导致无法预测的结果

副作用指表达式求值时出现的一些附带效果,如前置++和后置++

1
2
x+1//不带副作用
x++//有副作用

以下这个宏可以体现上面描述的问题

1
#define MAX(x,y) ((x)>(y)?(x):(y))

如果是一个函数封装,我们的理解是,a和b原来的值3和5被传入MAX,然后再各自++一次

但实际上并不是这样,可以看到a++了一次,但是b++了两次

image

这是为什么呢?

  • 宏执行的是直接替换
1
2
3
#define MAX(x,y) ((x)>(y)?(x):(y))
//上述宏被替换后的结果
int m=((a++)>(b++)?(a++):(b++))

这个表达式中,执行比较的时候,a和b各++一次,但是在最后返回b的时候,末尾b++被执行了一次

得到的结果就是a=4,b=7

2.6宏和函数对比

宏经常被用作执行简单的计算(如上面提到多次的MAX)

这时候宏对比函数有几个优点

  • 函数的调用需要压栈出栈,比实际执行这个小型代码需要的时间更长。宏比函数在程序的执行速度方面更胜一筹
  • 函数的参数必须声明位特定的类型,只适用于特定类型的表达式上。反之,宏可以用整型、长整型、浮点型等可以用>来比较的类型。宏的调用与类型无关

除了MAX这个简单宏语句外,宏的参数还可以出现数据类型

如下图中我们调用的这个宏,就可以做到用一种更简单的方式来调用malloc函数。此时的调用只需要写入待开辟数据个数和数据类型,不需要再写强制类型转换等语句,方便使用

image

有得就有失,宏当然也有缺点:

  • 每次使用宏,执行的是直接替换。如果此时宏比较长,则会增加程序的长度
  • 宏在预处理阶段执行了替换,无法进行调试
  • 宏与类型无关,不够严谨
  • 宏会出现操作符优先级问题,容易出错

2.7命名约定

一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。

那我们平时的一个习惯是:

  • 把宏名全部大写:MAX

  • 函数名不要全部大写:Max

1
2
3
4
5
6
#define MAX(x, y) ((x)>(y)?(x):(y))

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

2.7 #undef

这条指令用于移除一个宏定义

1
#undef NAME

如果不移除,就无法重复定义同名宏

image


3.命令行定义

一些C语言的编译器提供了一个功能,允许我们在命令行中定义一个符号,用于启动编译过程

当我们需要一个程序的不同版本时,可以使用该指令

如:在不同情况下需要不同长度的数组

image

如果我们直接编译这部分代码,编译器会报错,SZ未定义

image

但当我们写上这么一行命令

1
gcc test.c -D SZ=10

可以看到编译器没有报错,再次ls,发现多了一个a.out文件

image

执行该文件,可以看到SZ被定义成10并成功打印

4.条件编译

在编译一个程序的时候,我们可以通过条件编译指令来控制一组语句的使用与否

4.1if/endif

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define M 1
int main()
{
int i = 0;
int n = 10;
for (i = 0; i < 10; i++)
{
#if M
printf("%d\n", i);
#endif
//其他代码
}

return 0;
}

#if语句后为真即执行,为假不执行

  • 该语句之后只能跟随常量表达式,不能跟随定义的变量
1
2
3
4
5
6
7
8
#define M 1
#if M//正确

#if 3>5
#if M==1//正确

int a=1;
#if a//err

image

在linux环境下,使用编译语句执行预处理操作,可以看到生成的test.i文件中,printf代码是被包含进去的

image

如果把M更改为0,再次执行预处理操作。printf语句并没有包含在for循环中

image

  • 如果这里放入变量n,不起作用

image

你可能想问,如果这一行代码我不需要,直接注释掉不就ok了吗?

并不然。

有些时候我们为了验证之前写的程序是否正确,会编写一些测试代码,用于debug。这些代码在测试完成后可以删除,但是如果我们下次还需要测试同一个函数的时候,就有需要重新写一遍,很是不方便。

有了条件编译指令,我们就可以在程序的最上方#define定义一个常量,来控制是否进行测试。


4.2多个分支的调节编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define M 150

int main()
{
#if M<100
printf("less\n");
#elif M==100
printf("==\n");
#elif M>100&&M<200
printf("more\n");
#else
printf("hehe\n");
#endif

return 0;
}

和之前一样,不运行的代码,VS2019会显示为灰色

image

4.3判断符号是否已被定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define M 0

int main()
{
#if defined(M)
printf("hehe\n");
#endif

#ifdef M
printf("haha\n");
#endif
//这两条语句等价
return 0;
}

image

如果想把条件改为未定义的时候执行,可以使用下面这两种方式

  • !:逻辑反操作符

image

4.4嵌套指令

和其他语句一样,条件编译语句也可以嵌套使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define M 0
#define N 1

#if defined(M)
#ifdef OPTION1
M_option1();
#endif

#ifdef OPTION2
M_option2();
#endif
#elif defined(N)
#ifdef OPTION2
N_option2();
#endif
#endif

5.文件包含

我们知道,#include指令可以使另外一个文件被编译。它同时也是一个替换:

  • 编译器在预处理阶段删除这条指令,用包含文件的内容替换
  • 一个源文件被包含10次,就会被编译10次

5.1包含方式

1
2
#include "test.h"//本地文件
#include <stdio.h>//库文件

你可能会有这个疑问,这两种包含方式之间有什么区别呢?

双引号方式

  • 现在源文件(项目文件)所在目录下查找。
  • 如果该头文件未找到,编译器就会像查找库函数头文件一样在标准位置查找头文件。
  • 如果找不到就提示编译错误

VS2019标准位置(去VS的安装目录找)

1
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include

Linux环境标准位置

1
/usr/include

image

库文件方式

  • 只在标准路径下查找,如果找不到就提示编译错误

既然双引号方式也会去标准路径下查找,那是不是说我们可以用双引号方式包含库函数的头文件?

答案是肯定的

但是这么做,会让我们难以分辨头文件到底是库函数还是自定义头文件。两次查找也会影响程序编译效率


5.2嵌套文件包含问题

假如在项目合作中,出现了这种情况

image

  • comm.h和.c是公共文件
  • test1.h和test1.c使用了公共模块
  • test2.h和test2.c也使用了公共模块
  • test.h和test.c最终使用了test1和2模块

这种情况下,就相当于有两份comm.h的内容被拷贝到最终的程序中

假如comm.h中有define或者全局变量的定义,这就相当于一个定义语句写了两遍,出现了重复定义

如何解决?

我们可以在每个头文件的开头写

1
2
3
4
#ifndef __TEST_H__
#define __TEST_H__
//头文件的内容
#endif

这样,如果__TEST_H__符号已经被定义过,编译器就不会二次展开头文件中的代码,也就避免了这个问题

如果你使用的是VS编译器,在创建.h文件的时候,VS会自己包含一个语句

1
#pragma once

这个语句也有相同的作用

image

warning: #pragma once in main file

在我尝试在linux环境下使用#pragma once语句时,遇到了这个报错

image

解决这个问题的办法很简单,就是不要编译头文件

编译器会自动展开头文件,无需手动编译

image

image

网上查了查:出现这个问题的原因是编译器在编译头文件的时候,#pragma once本身是没有含义的语句,所以报错了。

  • 也有人说是因为linux不支持这个语句,我们来试试

右侧代码中包含了两个test.h的引用,在预处理中只包含了一次

image

去掉头文件中的#pragma once,再次编译,可以看到预处理文件中出现了两次头文件的内容

image

这说明linux-gcc编译器是支持该语句的,并非网上说的不支持!

还有更多……

其实预处理指令还远不止本博客中包含的这些

1
2
3
#error
#pragma
#line

这些预处理指令还等待我的学习~记录在小本本上了

image

如果这篇博客对你有帮助,还请点个👍吧!