欢迎来到本期频道!
你是否好奇.c源文件是如何转变成.exe可执行程序的。
一:源文件到可执行程序的大致过程
在ANSI C的任何一种实现中,一定存在翻译环境和运行环境。
二:编译与链接
1.编译
Ⅰ.预处理
① 命令行定义
许多c的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。 |
② #define 定义常量
定义标识符时,一般不在最后加; 因为容易出现问题。 |
#define Symbol 常量表达式/函数实现/关键字 //定义常量
//如果替换的内容较长,可以用续航符分段写。例如
#define test int a=1;\
printf("%d",a)
预处理阶段,语句中的(不包含字符串)Symbol被替换成该符号后面的内容。
③ #define 定义宏
#define机制允许把参数替换到文本中,这种实现被称为宏或定义宏。 |
#define name(parament-list) stuff
//参数列表左括号必须紧邻name,否则就是定义常量。
宏的替换规则
1.调用宏时,首先对参数进行检查,查看是否包含由#define定义的符号。如果是,它们首先被替换。
2.替换文本随后被插入到程序中原来文本的位置。参数名被它们的值替换。
3.再次对结果文件扫描,查看是否由#define定义的符号,如果是,重复以上过程。
使用宏
#include<stdio.h>
#define WAY(x,y) x*y+x/y //定义宏
int main()
{
printf("%d ", WAY(2, 1)); //1号
printf("%d ", WAY(4-2, 2-1)); //2号
int a = 2;
int b = 0;
printf("%d\n", WAY(a++, ++b)); //3号 带副作用的参数
return 0;
}
代码结果:
4 1 5 |
观察:
我们使用宏时,觉得这和函数很相似,但是如果是函数的话,这三个值应该是一样的,这说明宏有区别于函数的特点。 |
1号结果是我们想要的。 |
2号不是,那该如何解决?上面规则中介绍到宏的本质是替换,所以推断出此处产生了优先级的问题,那加上括号便可以解决问题--> ((x)*(y)+(x)/(y)) |
3号是因为带副作用的参数所导致的难以预测的结果。 |
宏的运算符
#运算符—>字符串化
#define OUT(n,formate) printf(#n"=" formate ,n) //将n替换成a后,再把a转换成字符串
int main() //即printf("a" "=" "%d" ,a)
{
int a=1;
OUT(a,"%d");
return 0;
}
输出结果:
a=1 |
##运算符—>记号粘合
#include<stdio.h>
#define GENERIC_MAX(type) \
type type##_max(type x, type y)\
{\
return x>y ? x : y; \
}
GENERIC_MAX(int); //替换后是 int int_max(int x,int y) { return x>y?x:y } ;
GENERIC_MAX(float); //如果没有##,这里的int_max就是type_max 所以##起到了记号粘合的作用
int main()
{
int ret1 = int_max(3, 5);
float ret2 = float_max(1.02f, 3.08f);
printf("%d %.2f", ret1, ret2);
return 0;
}
运行结果:
5 3.08 |
宏和函数对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都要被替换,可能会大幅度增加代码长度 | 每次使用时,只需调用同一份函数代码 |
操作符优先级 | 由于宏是文本替换,所以临近操作符的优先级可能会造成难以预测的后果,所以加上括号以达预期效果 | 函数调用时,只将参数值传递,结果容易预测 |
参数类型 | 宏的参数与类型无关,操作合法,即可使用任意类型 | 函数传参与类型有关,对于一些相同的任务,也要定义不同的函数。 |
带副作用的参数 | 副作用参数可能会被替换到宏体多次,将造成不可预料的后果 | 函数传参只传参数值,结果易控制 |
执行速度 | 更快 | 有函数调用和返回的开销,慢些 |
递归 | 不可以 | 可以 |
调试 | 不方便 | 可逐语句 |
强调一下
宏:参数可以是类型
函数:可递归
④ 命名约定
由于函数和宏的使用语法很相似,所以为了区别,我们通常把宏名全大写,函数名不全大写。 |
⑤ 预定义符号
c语言设置了一些预定义符号,可直接使用。 |
预定义符号 | 含义 |
---|---|
__FILE__ | 进行编译的源文件名 |
__LINE__ | 文件当前所在行号 |
__DATE__ | 文件编译时的日期 |
__TIME__ | 文件编译时的时间 |
__STDC__ | 如果遵循ANSI C,值为1,否则未定义 |
⑥ 条件编译
条件编译使得我们可以对一条语句(一组语句)选择是否编译。 |
常见的条件编译指令:
#if 常量表达式
//表达式由预处理器求值,表达式为真,包含的语句参与编译
#elif 常量表达式
//
#else
//
#endif
#if defined(符号)
//符号定义了,语句参与编译,与该符号的值无关
#endif
//或者
#ifdef 符号
//
#endif
#if !defined(符号)
//符号没有定义,语句参与编译,与符号值无关
#endif
//或者
#ifndef 符号
//
#endif
⑦ 头文件包含
本地文件包含
#include"filename.h"
查找策略:先在源文件所在目录下查找,如果未找到,编译器就像查找库函数头文件一样在标准位置查找。
找不到提示编译错误。
库文件包含
#include<filename.h>
查找策略:直接标准路径下查找,找不到提示编译错误。
所以为了提高查找效率,本地头文件用" ",库函数头文件用< >. |
文件多次包含怎么办?
----条件编译。
#ifndef FILENAME_H
#define FILENAME_H
//...头文件内容
#endif
Ⅱ. 编译
编译的目的是把c语言转换成汇编语言 |
编译的主要过程 |
---|
词法分析 |
语法分析 |
语义分析及优化,该阶段会报告错误的语法信息 |
Ⅲ.汇编
汇编目的是将汇编语言文件转化成二进制文件 |
2.链接
链接是一个复杂的过程,链接时需要把一堆文件链接在一起才生成可执行程序。 |
链接解决的是一个项目中多文件,多模块互相调用的问题。 |
链接主要过程 |
---|
地址和空间分配 |
符号决议 |
重定位 (地址的修正过程) |
… … |
三:运行环境
1.程序必须载入内存中。
在有操作系统的环境中:一般这个由操作系统完成。 |
在独立的环境中: 程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。 |
2.程序的执行便开始,接着调用main函数。
3.开始执行程序代码。
这时将使用一个运行时堆栈,存储函数的局部变量和返回地址. 同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程一直保留它们的值. |
4.终止程序。
正常终止main函数;意外终止. |