C语言之预处理那点事
在C语言中,曾出现各种各样新的标准,有的昙花一现,有的则源远流传。我们这篇来看流传下来的,简化开发者编程和提升性能的一种精粹“预处理”。
一、程序的翻译和执行环境
2.构建我们的main函数
在ANSI C的任何一种实现中,存在两个不同的环境。
第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第二种为运行环境,他用于实际执行代码。
翻译环境有有一下几步操作:
经过汇编过程生成目标文件,在经过链接完成翻译环境的工作,生成一个可执行程序文件。
当有多个源文件时:
链接器的目的是把多个源文件生成的目标文件进行整合,从而形成一个单一而完整的可执行程序。
链接器也会引入标准C函数库中任何被改程序所用到的函数,而且他还可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
我们用Linux系统来具体的演示一下每个阶段:
下面一串代码为我们测试使用的代码:
#include<stdio.h>
#define MAX 10
int main()
{
int a = MAX;//给a赋值
printf("%d", a);
return 0;
}
gcc -E test.c > test.i//将test.c进行预编译,并把编译的内容重定向到test.i中
我们发现里面的MAX变为我们所定义的10,且没有我们的注释,代码量也从8行变为800多行,这是对include头文件的包含造成的。
编译阶段:
gcc -S test.i > test.s//将test.i进行编译,并把编译的内容重定向到test.s中
这是在Linux下进行汇编的结果。
我们也可以直接在VS上查看汇编代码:
汇编:
gcc -c test.s > test.o//将test.s进行汇编,并把编译的内容重定向到test.o中
进行汇编是将汇编语言转化为.o(Windows下为.obj)目标二进制文件,且文件不可执行。
最后进行:
gcc test.o -o test//生成test的可执行程序,-o代表对生成的可执行程序进行命名
ls显示当前目录的文件,./test为执行该程序。
运行环境:
程序的执行过程:
1.程序必须载入内存中。
2.程序执行开始,进行main函数调用。
3.开始执行程序代码。这个时间程序将使用一个运行时堆栈(stack),用来存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,用来存储他们的值
4.终止程序。正常终止main函数,或者程序异常终止。
二、预定义符号的介绍
1.预定义符号
__FILE__ //进行编译的源文件
__LINE__ //文件当前行号
__DATE__ //文件被编译日期
__TIME__ //文件被编译时间
这些预定义符号都是语言内置的。
例如:
int main()
{
printf("file:%snline:%dndata:%sntime:%s", __FILE__, __LINE__, __DATE__, __TIME__);
return 0;
}
2.#define
语法:
#define name stuff
例如:
#define MAX 10
#define un unsigned //为unsigned创建一个更简短的名字
替换规则:
在程序中拓展#define定义符号和宏时,需要一下几个步骤:
1.调用宏时,先对参数进行检查,看是否包含任何由#define定义的符号,如果有,它们首先被替换。
2.替换的文本随后被插入到程序中原来的位置
3.最后再次对文本进行扫描,重复1,2过程。
注意:
1.宏参数和#define定义中可以出现其他#define定义的变量。对于宏来说,不能出现递归。
2.当预处理器搜索#define定义的符号时,字符串常量的内容并不被搜索。
下面我们看几个宏的错误示例:
#define MAX = 10 //多了一个等号
#define MIN 1;//多了一个分号
#define ADD(x,y) x*y//x*y没有加括号,正确应为(x)*(y)
int main()
{
int a = MAX;
int b = MIN;
ADD(5 + 1, 6 + 1);
return 0;
}
让我们看看进行预编译后的结果吧:
我们可看出对a进行赋值时多了一个等号,对b赋值时末尾多一个分号,进行宏替换的x和y的运算顺序也和我们所想的不一样。
宏是替换,使用宏要注意宏所编写的是否正确。
带副作用的宏参数:
当宏参数在宏的定义中出现超过一次的时间,如果参数带有副作用,那么使用这个宏可能会出现危险,导致不可预测的后果。副作用就是表达式求值的时间出现的永久性效果。例如:
x+1;//不带副作用
x++;//带副作用
下面我们用一个具体例子来看下结果:
#define MAX(X,Y) ((X)>(Y)?(X):(Y))
int main()
{
int a = 10;
int b = 11;
int max = MAX(a++, b++);
printf("%dn", max);
printf("%dn", a);
printf("%dn", b);
return 0;
}
我们看到a和b的值发生了变化。这就是副作用宏。
#undef:用来移除一个宏定义。。
3.宏和函数的比较
上面宏定义的运算我们为什么不用函数来执行呢?原因有一下两个方面:
1.用于调用函数和函数返回的代码的时间可能比执行这个代码所需更多时间。所以宏比函数在程序的规模和速度上更胜一筹。
2.函数的参数必须要指明特定的类型。而宏可以适用于任何类型。宏是和类型无关的。
宏和函数相比劣势也很明显:
1.每次使用宏时,需要进行宏替换,如果宏比较长,则可能大幅增加程序的长度。
2.宏是无法调试的
3.宏是和类型无关的,也就不够严谨
4.宏可能带来运算符优先级的问题,导致程序出错。
命名约定:
一般来说函数和宏使用语法很相似。所以语言本身无法帮我们区分。所以出现了命名约定。宏名全部为大写,函数名不要全部大写
4.条件编译
在编译一个程序的时间如果我们要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。比如:用来调试的代码,头文件是否包含等。
1:
#if 常量表达式
//。。。。
#endif
//常量表达式由预处理器求值
#define __DEBUG__ 1
int main()
{
int a = 0;
#if __DEBUG__
a = 10;
#endif
printf("%d", a);
return 0;
}
2:多个分支的条件编译条件
#if 常量表达式
//。。。。
#elif 常量表达式
//。。。。
#endif
用法和上面相同。
3.判断是否被定义
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifdef symbol
判断是否被定义通常用于判断是否重复包含头文件,#pragma once和判断是否被定义类似。
总结
预处理可以改变程序设计环境,提高编程效率。所以学好预处理可以对我们起到锦上添花的作用。