程序的环境和预处理
文章目录
程序的环境一共分为两种:
- 翻译环境:在这个环境中源代码被转换成可执行的机器指令。
- 执行环境:用于实际执行代码。
下图是各个的包含关系:
程序的翻译环境
这里我们重点解释一下翻译环境:
编译
- 以c语言为例,编译是将源文件(后缀为.c)通过编译器转换成目标代码
- 目标文件在通过链接器捆绑在一起,形成一个单一而完整的可执行程序
- 连接器同时也会引入标准c函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,并将其需要的函数也链接入库
预处理(预编译)
- 完成头文件的包含,将库函数里面包含的内容拷贝过来
- 将#define定义的符号和宏进行替换
- 将注释删除
编译
将c语言代码转换成汇编语言
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
汇编
- 生成符号表
- 把汇编代码转化成机器识别的二进制指令
链接
- 合并段表
- 符号表的合并与重定义(跨文件链接函数)
以这个程序为例
这张图简述了符号表在翻译环境中是如何链接不同文件的,链接环节实际上是将有效地址进行保留。
程序的执行环境
- 程序的执行首先调用main函数
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在整个程序执行过程中一直保留他们的值
预处理详解
预定义符号
__FILE__ 进行编译的源文件 %s
__LINE__ 文件当前的行号 %d
__DATE__ 文件被编译的日期 %s
__TIME__ 文件被编译的时间 %s
实例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
FILE* p = fopen("kksk.txt", "w");
fprintf(p, "%s\t %s", __DATE__, __TIME__);
return 0;
}
文件写入,文件中就出现了编译的日期和时间
#define
#define定义标识符
语法:
#define NAME staff
举例
#define MAX 100
#define reg register //为register这个关键字,创建一个简短的名字
#define do_foever for(;;) //用更形象的符号来替换循环
#define CASE break;case //再写case语句的时候自动把后面的break补上
但是我以前在书写的时候经常在后面加上;
#define N 100;
int main()
{
int b = N //等价于
int b = 100;
int c = N + 1;// 这就是错误的了
//等价于 int c = 100; +1;
}
最主要的是要意识到#define的内容是发生替换,而不是赋值。
#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏
声明方式
#define name(parament-list) stuff
其中parament-list是一个由逗号隔开的符号表,他们可能出现在stuff中,注意:参数列表的左括号必须与name相邻否则就会被认为是stuff的一部分
易错点:
#define fun(a,b) a*b
int main()
{
printf("%d", fun(1 + 3,2));
return 0;
}
这里很多人认为答案是8,但实际上答案是7。我们不能以函数的常量表达式来看待参数,而是要以替换的思想去看待,上式其实被转换成了 1+3*2。
#define fun(a,b) (a)*(b)+1
int main()
{
printf("%d", fun(1 + 3,2)*fun(1,1));
return 0;
}
这个实际上的计算过程是(1+3)*2+1*(1)*(1)+1
,所以结果为
#define替换规则
- 在调用宏时,首先对参数检查,看是否有宏定义的符号如果有,就进行替换
- 替换文本被插入程序中原来文本的位置。对于宏,参数名被他们替换
带副作用的宏参数
当宏参数在宏的定义中出现超过一次的时候,如果参数的值出现改变(自增自减),那么宏在使用起来会出现不可预料的结果。
例如:
#define fun(a,b) ((a)>(b)?(a):(b))
int main()
{
int x = 5;
int y = 8;
int z = fun(x++, y++);
printf("%d %d %d",x,y, z);
return 0;
}
由宏的替换原理我们可以知道:z = ((x++)>(y++)?(x++):(y++));
由于是一个三木操作符所以先判断(x++)>(y++)
;是真是假,由于是后置++所以为假
所以表达式变成 z=(y++)
此时由于自增缘故,x=6,y=9
最后又是后置++所以z=9,x=6,y=10
运行结果:
宏和函数的对比
宏和函数的功能十分相似,为什么不用宏替换函数?
宏相对于函数的优点:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 宏的参数是没有类型的,所以可以计算所有类型。而函数的形参是有特定类型的,如果什么的时候是int类型那么传double,float就不行了,而宏就可以很好的完成
宏相对于函数的缺点:
- 每次使用宏的时候,由于宏的方式是替换,所以一大段代码要插入程序中。除非宏比较短,否则可能大幅度增加程序的长度
- 宏是无法调试的
- 宏由于与类型无关,也就不够严谨
- 宏可能会带来运算符优先级的问题,容易出错
- 宏不能递归
宏还有一个函数做不到的就是:宏的参数可以出现类型
#include<stdio.h>
#pragma once
#define Malloc(p,type) malloc(2*sizeof(type))
int main()
{
int* pf = (int*)Malloc(pf,int);//传入int类型
if (pf == NULL)
{
perror("malloc");
return 0;
}
for (int i = 0; i < 2; i++)
{
pf[i] = i;
}
free(pf);
pf = NULL;
return 0;
}
属性 | #define定义的宏 | 函数 |
---|---|---|
代码长度 | 每次使用都会向程序里面插入宏定义的代码段,所以会造成程序大幅度增长 | 函数只出现于一个地方,只有在调用的时候才开辟空间 |
执行速度 | 更快 | 由于函数存在调用和返回,所以相对慢一点 |
操作符优先级 | 宏的操作符要结合上下文,将代码替换到宏出现的位置在进行判断 | 函数传递表达式的时候是先计算出结果在传参 |
参数类型 | 宏没有参数类型,只要是操作合法就可以 | 函数参数类型要严格符合函数定义时的参数类型 |
调试 | 宏无法调试 | 函数可以调试 |
递归 | 宏无法递归 | 函数可以递归 |
命名习惯:
宏:全大写
函数:不要全大写
undef
用于移除一个宏定义。
#undef NAME
如果现存的一个名字需要被重新定义,那么它的旧名字需要首先被移除
条件编译
在编译的过程中我们可以人为的选择性编译,因为我们有条件编译指令
常见的编译指令:
#if 条件
//....
#endif//满足条件编译,不满足条件不编译
#if 条件(常量表达式)
//....
#elif 条件
//......
#endif
不满足编译条件的代码就和注释一样不会参与在预处理阶段被删除
文件包含
- 本地文件包含
#include"filename"
查找策略:现在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置找头文件
- 库文件包含
#inclue<stdio.h>
直接在标准路径下去查找,如果找不到就提示编译错误
嵌套文件包含
如图test.c里面就包含了两个头文件,这样就造成了文件内容重复
解决方法:
- 方法一:
#ifndef __TEST_H_
#define __TEST_H_
#endif
如果头文件是第一次调用,_TEST_H_是没有被定义的,所以进入if里面,进去之后立刻定义__TEST_H,这样下次再调头文件的时候就不会进入头文件了。
- 关键字
#pragma once
解决所有问题