目录
- 一、 程序的翻译环境和执行环境
- 1、翻译环境
- 预处理
- 编译
- 汇编
- 链接
- 2、执行环境
- 二、预处理详解
- 1、预定义符号
- 2、#define
- #define 语法
- #define 定义宏
- #define 替换规则
- 3、#和##
- 4、宏和函数对比
一、 程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境
- 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令
- 第2种是执行环境,它用于实际执行代码。
我们都是在.c源文件中编写代码,是怎么形成.exe文件,又是怎么输出结果的呢?
源文件经过翻译环境的处理,最后生成.exe文件,然后通过执行环境输出结果。
在翻译环境中源文件又是怎样被转换成可执行的机器指令呢?
1、翻译环境
我们都知道源程序通过编译和链接最终形成可执行程序,编译和连接便是翻译环境所做的事情。
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中
编译又可以分成:预编译(预处理)、编译、汇编
我们可以通过在Linux环境的gcc编译器下观看他的编译连接的过程。
预处理
我们先创建一个test.c文件,在test.c文件下编写代码,如下:
然后我们输入gcc -E test.c -o test.i
,这个命令就是预处理完成之后就停下来,预处理之后产生的结果放在test.i文件中。
在test.i文件中我们会发现有800多行代码,而且头文件也不在了,我们再通过>vim /usr/include/stdio.h
进入stdio.h
文件中去,查看stdio.h
头文件
通过test.i
文件与stdio.h
文件对比,会发现两个文件中的内容大致是一样的。
从这里,我们就可以知道:预处理阶段一定会做的一件事就是把要包含的文件给拷贝过来用,而使用的就是#include
这个指令。
但是,我们可以看到 stdio.h
文件中有900多行,而test.c
文件中有800多行,那预处理阶段还进行了什么操作呢?
如果我们在test.c
文件改成如下呢,再进入test.i
文件,查看呢?
我们会发现test.i
中并没有注释语句,而且也没有#define
语句,并且变量a的值变为100了
从这里,我们可以知道:预处理还做了注释的删除和#define符号的替换。
综上所述,预处理阶段会做的三件事有:
- 头文件包含
- 删除注释
- #define定义的宏进行替换
编译
我们输入gcc -S test.c
,这个命令就是编译完成之后停下来,结果保存到test.s中。
在test.s文件中,我们可以看到他是把C语言代码转换成汇编代码。
在这个过程中,他是经过语法分析、词法分析、语义分析、符号汇总转换成汇编代码的。
汇编
我们可以对test.c的代码进行修改
输入 gcc -c test.c
,这个命令汇编就是完成之后就停下来,结果保存在test.o中。
我们发现此文件是一个二进制文件,生成一个test.o文件,也就是一个目标文件。
在此,我们可以知道这个过程就是把汇编指令转换成二进制指令,这个过程中还会形成符号表。
test.o
文件是一个elf文件,我们可以通过readelf
指令输入readelf test.o -s
可以解读test.o
文件中的内容
链接
我们在创建一个add.c文件,在里面写一个加函数,使他生成一个add.o的目标文件,方便我们测试
test.c
文件中的代码:
我们在输入gcc test.o add.o
,会发现目录中出现了一个a.out文件,这个文件就是连接后生成的可执行文件。
由此,我们可以知道,这个阶段他会把我们的文件合并成一个可执行文件,也就是合并段表和符号表合并与重定位。
2、执行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
二、预处理详解
1、预定义符号
FILE :进行编译的源文件
LINE :文件当前的行号
DATE :文件被编译的日期
TIME :文件被编译的时间
STDC :如果编译器遵循ANSI C,其值为1,否则未定义
注意:这些预定义符号都是语言内置的。
举个例子:
#include <stdio.h>
int main()
{
printf("%s \nline:%d\n", __DATE__, __LINE__);
return 0;
}
运行结果:
2、#define
#define 语法
语法: #define name stuff
#define MAX 1000
#define reg register //为 register这个关键字,创建一个简短的名字
#define do_forever for(;;) //用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
// 如果定义的 stuff过长,可以分成几行写,
//除了最后一行外,每行的后面都加一个反斜杠(续行符)。
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
注意:在define定义标识符的时候,最好不要在最后加上;
例如:
上面这种情况就会出现语法错误。
#define 定义宏
#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏或定义宏。
宏的声明方式:#define name(parament-list) stuff
其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。
例如:
#include <stdio.h>
#define MAX(x,y) (x>y?x:y)
int main()
{
int a = 5;
int b = 6;
int m = MAX(a, b);
printf("%d\n", m);//结果:6
return 0;
}
注意:
参数列表的左括号必须与name紧邻。
如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。
我们再来看个例子:
#include <stdio.h>
#define SQU(X) X*X
int main()
{
printf("%d\n", SQU(6));//结果:36
printf("%d\n", SQU(6 + 1));//结果:13
return 0;
}
我们不看结果,会认为SQU(6+1)
的结果为36,但是,并不是,为什么呢?
替换文本时,参数X被替换成6 + 1,所以这条语句实际上变成了:
printf("%d\n", 6 + 1 * 6 + 1);
那么,如何解决这个问题呢?
我们可以在宏定义加上两个括号,便解决了,如下:
#include <stdio.h>
#define SQU(X) (X)*(X)
int main()
{
printf("%d\n", SQU(6));//结果:36
printf("%d\n", SQU(6 + 1));//结果:49
return 0;
}
我们在看一个例子:
#include <stdio.h>
#define DOU(x) (x) + (x)
int main()
{
printf("%d\n", 10 * DOU(6));//结果:66
printf("%d\n", DOU(6));//结果:12
return 0;
}
我们不看结果,会认为10 * DOU(6)
的结果为120,但是,并不是,为什么呢?
替换文本时,这条语句实际上变成了:
printf("%d\n", 10 * 6 + 6);
那么,如何解决这个问题呢?
我们可以在宏定义表达式两边加上一对括号,便解决了,如下:
#include <stdio.h>
#define DOU(x) ((x) + (x))
int main()
{
printf("%d\n", 10 * DOU(6));//结果:120
printf("%d\n", DOU(6));//结果:12
return 0;
}
注意:所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
#define 替换规则
在程序中扩展#define定义符号和宏时,需要涉及以下几个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
3、#和##
我们先来看看这串代码:
#include <stdio.h>
int main()
{
printf("hello"" world\n");
return 0;
}
运行结果:
可以看出,字符串是有自动连接的特点的。
我们再来看看这串代码:
#include <stdio.h>
int main()
{
int a = 10;
printf("the value of a is %d\n", a);
int b = 20;
printf("the value of b is %d\n", b);
float c = 3.5f;
printf("the value of c is %f\n", c);
return 0;
}
那么,我们可以用宏的方式进行求值呢?
这里我们就可以使用#
,把一个宏参数变成对应的字符串,代码如下:
#include <stdio.h>
#define PRINT(format, val) printf("the value of "#val" is "format"\n", val)
int main()
{
int a = 10;
PRINT("%d", a);
int b = 20;
PRINT("%d", b);
float c = 3.5f;
PRINT("%f", c);
return 0;
}
运行结果:
我们在看一个例子:
#include <stdio.h>
#define ADD_SUM(S,N) S##N
int main()
{
int sum_num = 1010;
printf("%d\n", ADD_SUM(sum_, num));
return 0;
}
运行结果:
综上所述:
#
:把一个宏参数变成对应的字符串。##
:可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。
注意:使用##
连接必须产生一个合法的标识符。否则其结果就是未定义的。
4、宏和函数对比
属性 | #define定义宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 |
执行速度 | 更快 | 存在函数的调用和返回的额外开销,所以相对慢一些 |
操作符优先级 | 宏参数的求值是在所有周围表达式的上下文环境里,除非加上括号,否则邻近操作符的优先级可能会产生不可预料的后果,所以建议宏在书写的时候多些括号。 | 函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测。 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数求值可能会产生不可预料的结果。 | 函数参数只在传参的时候求值一次,结果更容易控制。 |
参数类型 | 宏的参数与类型无关,只要对参数的操作是合法的,它就可以使用于任何参数类型。 | 函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使他们执行的任务是相同的。 |
调试 | 宏是不方便调试的 | 函数是可以逐语句调试的 |
递归 | 宏是不能递归的 | 函数是可以递归的 |