目录
一、编译与链接
1.翻译环境
-预处理
-编译
-汇编
-链接
2.执行环境
二、预定义符号
三、#define定义常量
四、#define定义宏
五、带有副作用的宏参数
六、宏替换的规则
七、 宏函数的对比
八、#和##
1.#运算符
2.##运算符
九、命名约定
十、#undef
十一、 命令行定义
十二、 条件编译
十三、 头文件的包含
1.本地头文件包含
2.库文件包含
十四、 其他预处理指令
一、编译与链接
在ANSI C的任何⼀种实现中,存在两个不同的环境
第一种呢就是翻译环境,顾名思义就是将源代码被转换为可执行的二进制指令
第二种呢就是执行环境,可用于实际执行代码,并且输出结果
然后我们再来说翻译环境,是如何将一段代码转换为可执行的二进制指令的呢
其实编译这一部分又分为了预处理(预编译),编译,汇编
在一个程序中可能会有多个.c文件,这些文件会单独的经过编译处理生成对应的目标文件
Windows环境下生成的目标文件后缀为.obj,Linux环境下生成的目标文件为.o
多个目标文件跟链接库一起经过链接器的处理最终生成可执行程序
链接库呢,它是指运行时库(支持程序运行的基本函数集合)第三方库
知道了上面的操作,我们就可以展开,成为了以下这个过程
1.翻译环境
-预处理
在预处理阶段,源文件和头文件会被处理为.i为后缀的文件,处理规则如下
- 将所有的#define删除,并且展开所有宏定义
- 处理所有的条件编译指令
处理#include 预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置 删除所有的注释 添加行号文件名标识, ⽅便后续编译器生成调试信息等 或保留所有的#pragma的编译器指令,编译器后续会使用
-编译
- 将源代码程序被输⼊扫描器进行词法分析,把代码中的字符分割成⼀系列 的记号(关键字、标识符、字⾯量、特殊字符等)
- 接下来语法分析器,将对扫描产⽣的记号进⾏语法分析,从而产生语法树。这些语法树是以表达式为节点的树
- 由语义分析器来完成语义分析,即对表达式的语法层⾯分析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息。
-汇编
汇编器是将汇编代码转转变成机器可执行的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令就是根据汇编指令和机器指令的对照表⼀⼀的进行翻译,也不做指令优化
-链接
链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。链接解决的是⼀个项⽬中多个文件、多模块之间互相调⽤的问题
2.执行环境
- 程序必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。
- 在独⽴的环境中,程序的载⼊必须由手工安排,也可能是通过可执⾏代码置⼊只读内存成。
- 程序的执⾏便开始。接着便调⽤main函数。
- 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执行过程⼀直保留他们的值。
- 终⽌程序。正常终⽌main函数;也有可能是意外终止
二、预定义符号
__FILE__ //进⾏编译的源⽂件
__LINE__ //⽂件当前的⾏号
__DATE__ //⽂件被编译的⽇期
__TIME__ //⽂件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
三、#define定义常量
基本语法
# define name stuff
当然所定义的类型没有限制,可以为了定义值,可以为了替换复杂名字,也可以为了省事,下面这几种都是正确的定义方法:
#define MAX 1000
#define float f //为 float这个关键字,创建⼀个简短的名字
#define forever for(;;) //⽤更形象的符号来替换⼀种实现(死循环)
#define CASE break;case //在写case语句的时候⾃动把 break写上
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ );
/如果定义的过长,可以分成几行写,除了最后一行外,后⾯都加⼀个反斜杠 \ (续行符)
所以宏的定义可以各式各样,给了我们很大的自由度,使我们能尽情去发挥自己想象
那么还有一个问题在定义定义的标识符的时候需不需要加;呢,答案是否定的,就比如说
#define PR printf("hehe");
int main()
{
PR; //加了分号就相当于 printf("hehe");; 容易发生错误
return 0;
}
为了避免上述的这个错误,我们定义的标识符的时候不需要加;
四、#define定义宏
#define name( list ) stuff
举个例子来说明
#define SUPP( x ) x * x
int main()
{
SUPP(2,3);
return 0;
}
SUPP就是我们定义的一个宏,将宏置于函数内部(等预处理的时候,会自动替换成表达式 x * x)
但同时会存在一些问题
#define SUPP( x ) x * x
int main()
{
SUPP(2); // 2 * 2 == 4
SUPP(2+1); // 2 + 1 * 2 + 1 == 5
10 * SUPP(5+2); //10 * 5 + 2 * 5 + 2 == 62
return 0;
}
因为被定义的宏是预处理阶段所进行的,在预处理的时候直接替换函数中的表达式,所以难免会有许多操作符,优先级之类的问题,这个解决问题的方法就是在表达式加上对括号就解决了
#define SUPP( x) ( ( x ) + ( x ) )
五、带有副作用的宏参数
a = 1;
//a赋值的同时自己的值也改变了
b = a++; // a = 2,b = 1;
a+1;//不带副作⽤
a++;//带有副作⽤
拿下面这个例子来举例子
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
...
x = 5;
y = 8;
z = MAX(x++, y++);
printf("x=%d y=%d z=%d\n", x, y, z);//输出的结果是什么?
还记得我说了,宏是在预处理阶段替换宏为函数中的表达式:替换完了为
x = 5,y = 8;
MAX(x++,y++) 替换为 ((x++)>(y++)?(x++)(y++))
x=6 y=9 y=10
5 > 8假,执行y++
六、宏替换的规则
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
七、 宏函数的对比
#define MAX(a, b) ((a)>(b)?(a):(b))
//定义宏和函数的两种方式
int Max(a,b)
{
return ((a)>(b)?(a):(b));
}
我们从以下几个方面来分析宏 和 函数 的优缺点:
- 代码长度:#define所定义的宏,每次使用的时候都会被插入到程序中,除了特别小的宏以外,程序的长度会大幅度增长;而函数的代码只出现一个地方,调用都用同一份
- 执行速度:#define所定义的宏更快;二函数存在着调用和返回等额外的步骤速度会慢一些
- 操作符优先级:#define所定义的宏求值是在上下文的环境中,结果不可预测 会存在着很多的问题;而函数的参数只在函数调用时候将结果传给函数,表达式课预测
- 带有副作用的参数:#define所定义的宏参数可能会被替换多个位置,多次被计算,对值有着不可预测的结果;函数只在传参的时候求值易控制
- 参数类型:#define所定义的宏与类型无关,只要操作是合法的,可以适用于任何参数类型;函数的参数是与类型有关,如果类型不同,所需的函数也不同
- 调试:宏是不能调试的;函数是可以逐句逐条调试
- 递归:宏是不能递归的;函数是可以递归的
八、#和##
1.#运算符
先给大家补充一个知识点,字符串中包含的字符串两个会合成一个字符串
printf("haha""hehe");
//两个输出的结果相同
printf("hahahehe");
#define PRI(n) printf("the value of "#n " is %d", n);
int main()
{
PrT(6); //printf("the value of "#n " is %d", n);
return 0;
}
结果为the value of n is 6
不难发现#n,将转换成了一个字符串
2.##运算符
int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>yx:y;
}
那如果使用##,一切都会变的很简单
#define GENERIC_MAX(type) \
type type##_max(type x,typey) \
{ \
return (x>y?x:y); \
} \
GENERIC_MAX(int); //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名
GENERIC_MAX(float); //替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名
在预处理阶段,预处理中的所有type全部被替换
九、命名约定
把宏名全部⼤写 ,函数名不要全部⼤写
十、#undef
#undef NAME
//如果现存的⼀个名字需要被重新定义,那么它的旧名字⾸先要被移除。
可以看到 定义了一个MAX,正常打印完毕以后,#undef 移除这个宏定义,再次打印MAX就会报错
十一、 命令行定义
十二、 条件编译
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
#if defined(symbol)
#ifdef symbol
#if !defined(symbol)
#ifndef symbol
十三、 头文件的包含
1.本地头文件包含
#include "filename"
拿双引号引用
2.库文件包含
#include <filename.h>
拿单尖括号引用
#ifndef __TEST_H__ \\当未定义这个头文件时才会执行下面
#define __TEST_H__
.....
#endif
#pragma once
这两种都可以避免头文件重复引进
十四、 其他预处理指令
希望对你有帮助