前言😜
上篇博客,我们提到了C语言程序运行的几个环节。
本篇博客中提到的预处理指令,就是在预处理阶段运行的一些代码。
本篇博客使用的编译器🎰
- VS2019(win10)
- 树莓派(linux-gcc)
1.预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //测试编译器是否遵循ANSI C,遵循值为1,不遵循则该符号未定义
2.#define
2.1定义标识符
#define name stuff
#define MAX 1000
#define reg register
#define do_forever for(;;)
#define CASE break;case
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
加分号问题
define定义标识符的时候,最好不要在结尾加上;
#define MAX 1000;
#define MAX 1000
2.2定义宏
除了定义标识符以外,define还可以定义一个语句为标识符,允许把参数替换到文本中。这种机制叫做定义宏
下面是宏的申明方式:
#define name(parament-list) stuff
其中的parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中
参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
#define DOUBLE(x) x+x
宏需要注意的问题
这个宏有一个问题👇
#define DOUBLE(x) x+x
int main()
{
printf("%d\n", 10*DOUBLE(3));
return 0;
}
这个式子输出的结果是什么呢?是60吗?
答案是否定的:#define 宏在使用的时候执行的是直接替换
这个语句就相当于10*3+3 ,根据操作符优先级可知,结果为30+3=33
想解决这个问题,我们需要记住这个原则:
- 给变量加上括号以确保优先级
- 给整个宏再加上一个括号防止外部数据影响
#define DOUBLE(x) ((x)+(x))
再运行程序,发现答案变成了60
用于对数值表达式进行求值的宏定义,都应该用这种方式加上括号。避免在使用宏时,参数中的操作符或邻近操作符之间产生不可预料的相互作用
2.3define替换规则
2.4使用# 和##
2.4.1# 将字符串插入字符串
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;}
在这个代码里面,打印的前置内容a和b需要根据打印的变量进行更改
有没有一种办法,可以让他自己进行更改?
#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 处的内容被替换成了我们需要打印的变量
实际上,这里#X 旁边的双引号,是将整个字符串拆分成了3份进行打印
字符串在打印的时候具有自动拼接的特性,所以我们可以通过这种方式将一个需要更改的字符插入到字符串中
2.4.2## 将两个符号合并
##可以把位于它两边的符号合成一个符号。
它允许宏定义从分离的文本片段创建标识符。
如图,这个宏将Class和10两个分离的符号合并成了Class10,printf识别出来并打印的了Class10的值
当我们同名变量有很多的时候,就可以利用这种宏来给不同的变量增加数据。
#define ADD_TO_SUM(num, value) sum##num += value;int sum1,sum2,sum3,sum4;ADD_TO_SUM(3, 10);
这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
2.5带副作用的宏参数
当宏在定义中出现超过一次时,如果参数带有副作用,使用这个宏的时候就有可能出现危险,导致无法预测的结果
副作用指表达式求值时出现的一些附带效果,如前置++和后置++
x+1
以下这个宏可以体现上面描述的问题
#define MAX(x,y) ((x)>(y)?(x):(y))
如果是一个函数封装,我们的理解是,a和b原来的值3和5被传入MAX,然后再各自++一次
但实际上并不是这样,可以看到a++了一次,但是b++了两次
这是为什么呢?
#define MAX(x,y) ((x)>(y)?(x):(y))
这个表达式中,执行比较的时候,a和b各++一次,但是在最后返回b的时候,末尾b++ 被执行了一次
得到的结果就是a=4,b=7
2.6宏和函数对比
宏经常被用作执行简单的计算(如上面提到多次的MAX)
这时候宏对比函数有几个优点
- 函数的调用需要压栈出栈,比实际执行这个小型代码需要的时间更长。宏比函数在程序的执行速度方面更胜一筹
- 函数的参数必须声明位特定的类型,只适用于特定类型的表达式上。反之,宏可以用整型、长整型、浮点型等可以用>来比较的类型。宏的调用与类型无关
除了MAX这个简单宏语句外,宏的参数还可以出现数据类型
如下图中我们调用的这个宏,就可以做到用一种更简单的方式来调用malloc函数。此时的调用只需要写入待开辟数据个数和数据类型,不需要再写强制类型转换等语句,方便使用
有得就有失,宏当然也有缺点:
- 每次使用宏,执行的是直接替换。如果此时宏比较长,则会增加程序的长度
- 宏在预处理阶段执行了替换,无法进行调试
- 宏与类型无关,不够严谨
- 宏会出现操作符优先级问题,容易出错
2.7命名约定
一般来讲函数的宏的使用语法很相似,所以语言本身没法帮我们区分二者。
那我们平时的一个习惯是:
-
把宏名全部大写:MAX -
函数名不要全部大写:Max
#define MAX(x, y) ((x)>(y)?(x):(y))int Max(int x, int y){ return x > y ? x : y;}
2.7 #undef
这条指令用于移除一个宏定义
#undef NAME
如果不移除,就无法重复定义同名宏
3.命令行定义
一些C语言的编译器提供了一个功能,允许我们在命令行中定义一个符号,用于启动编译过程
当我们需要一个程序的不同版本时,可以使用该指令
如:在不同情况下需要不同长度的数组
如果我们直接编译这部分代码,编译器会报错,SZ未定义
但当我们写上这么一行命令
gcc test.c -D SZ=10
可以看到编译器没有报错,再次ls,发现多了一个a.out 文件
执行该文件,可以看到SZ被定义成10并成功打印
4.条件编译
在编译一个程序的时候,我们可以通过条件编译指令来控制一组语句的使用与否
4.1if/endif
#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 语句后为真即执行,为假不执行
#define M 1
#if M
#if 3>5
#if M==1
int a=1;
#if a
在linux环境下,使用编译语句执行预处理操作,可以看到生成的test.i文件中,printf代码是被包含进去的
如果把M更改为0,再次执行预处理操作。printf语句并没有包含在for循环中
你可能想问,如果这一行代码我不需要,直接注释掉不就ok了吗?
并不然。
有些时候我们为了验证之前写的程序是否正确,会编写一些测试代码,用于debug。这些代码在测试完成后可以删除,但是如果我们下次还需要测试同一个函数的时候,就有需要重新写一遍,很是不方便。
有了条件编译指令,我们就可以在程序的最上方#define 定义一个常量,来控制是否进行测试。
4.2多个分支的调节编译
#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会显示为灰色
4.3判断符号是否已被定义
#define M 0int main(){#if defined(M) printf("hehe\n");#endif#ifdef M printf("haha\n");#endif
如果想把条件改为未定义的时候执行,可以使用下面这两种方式
4.4嵌套指令
和其他语句一样,条件编译语句也可以嵌套使用
#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包含方式
#include "test.h"
#include <stdio.h>
你可能会有这个疑问,这两种包含方式之间有什么区别呢?
双引号方式
- 现在源文件(项目文件)所在目录下查找。
- 如果该头文件未找到,编译器就会像查找库函数头文件一样在标准位置查找头文件。
- 如果找不到就提示编译错误
VS2019标准位置(去VS的安装目录找)
C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\VC\Tools\MSVC\14.29.30133\include
Linux环境标准位置
/usr/include
库文件方式
既然双引号方式也会去标准路径下查找,那是不是说我们可以用双引号方式包含库函数的头文件?
答案是肯定的
但是这么做,会让我们难以分辨头文件到底是库函数还是自定义头文件。两次查找也会影响程序编译效率
5.2嵌套文件包含问题
假如在项目合作中,出现了这种情况
- comm.h和.c是公共文件
- test1.h和test1.c使用了公共模块
- test2.h和test2.c也使用了公共模块
- test.h和test.c最终使用了test1和2模块
这种情况下,就相当于有两份comm.h的内容被拷贝到最终的程序中
假如comm.h 中有define或者全局变量的定义,这就相当于一个定义语句写了两遍,出现了重复定义
如何解决?
我们可以在每个头文件的开头写
#ifndef __TEST_H__
#define __TEST_H__
#endif
这样,如果__TEST_H__ 符号已经被定义过,编译器就不会二次展开头文件中的代码,也就避免了这个问题
如果你使用的是VS编译器,在创建.h文件的时候,VS会自己包含一个语句
#pragma once
这个语句也有相同的作用
warning: #pragma once in main file
在我尝试在linux环境下使用#pragma once 语句时,遇到了这个报错
解决这个问题的办法很简单,就是不要编译头文件
编译器会自动展开头文件,无需手动编译
网上查了查:出现这个问题的原因是编译器在编译头文件的时候,#pragma once 本身是没有含义的语句,所以报错了。
- 也有人说是因为linux不支持这个语句,我们来试试
右侧代码中包含了两个test.h的引用,在预处理中只包含了一次
去掉头文件中的#pragma once ,再次编译,可以看到预处理文件中出现了两次头文件的内容
这说明linux-gcc编译器是支持该语句的,并非网上说的不支持!
此内容我会单独再发一篇博客,网上几乎没有 有关这个报错的资料
还有更多……
其实预处理指令还远不止本博客中包含的这些
#error
#pragma
#line
这些预处理指令还等待我的学习~记录在小本本上了
如果这篇博客对你有帮助,还请点个👍吧!
|