前言:
由于c语言的程序编译链接的这块知识点不清楚,回来复习一遍,以便于好理解c++知识,我会尽快更新下一篇文章。
目录
1.程序的翻译环境和执行环境
2.翻译环境(编译+链接)
编译(编译器)
预编译(预处理)
1.头文件的包含
2.注释的测试
编译过程
汇编过程
链接
1.合并段表
2.符号表的合并和重定位
计算机语言的发展
运行环境(翻译之后)
3.预处理详解
3.1 预定义符号
3.2 #define
3.2.1 #define 定义标识符
3.2.2 #define 定义宏
3.2.3#define 替换规则
3.2.4 #和##
3.2.5 带副作用的宏参数
1.程序的翻译环境和执行环境
总体过程:
在ANSI C的任何一种实现中,存在两个不同的环境
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
2.翻译环境(编译+链接)
我们常说一个test.c文件要经过以下步骤才能产生可执行的程序,那么具体是怎么做到的呢?
在vs编译器上是要经过以下过程的:
源文件和目标文件的关系:
链接库?
VS是一个集成开发环境,集成很多的功能ctr1+F5,不方便观察每个细节的功能接下来,我使用gcc这个编译器给大家演示
由于上图的操作把这个编译运行的步骤一次走完了,我们一步步来,咱们用指令让程序停在预编译的阶段:
编译(编译器)
预编译(预处理)
将控制台的内容放到test.i之后,预处理阶段所做的事情:
1.头文件的包含
2.注释的测试
编译过程
功能:把C语言代码翻译成汇编代码
1.语法分析
2.词法分析
3.语义分析
4.符号汇总函数名,全局变量, 不会汇总 局部变量(函数运行起来才行)
链接:https://pan.baidu.com/s/1cVcZRTj772hJXm0mO0Q_cw?pwd=1234
提取码:1234
汇编过程
1.把汇编代码转换成二进制的指令
2.形成符号表
链接
1.合并段表
2符号表的合并和重定位
1、2的作用其实是在链接期间为这种跨文件的代码进行协作的时候起作用的。
在vscode中操作:
过程:
1.合并段表
2.符号表的合并和重定位
把无效的地址给替换掉,符号表和段表的关系是:符号表是段表的内容
那该如何体现这个作用呢?
假设我们在test.c文件底下extern了一个函数Add,但是在add.c底下没有实现这个Add函数,就会输出以下错误(链接型错误)
如果说这个函数写错的话,在main函数内调用大写的,但是在add.c内定义成小写的,是依然找不到
计算机语言的发展
运行环境(翻译之后)
1. 程序必须载入 内存 中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。2. 程序的执行便开始。接着便调用 main 函数(main函数第一行开始)3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈( stack )-- 函数栈帧 ,存储函数的局部变量和返回地址。 程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。4. 终止程序。正常终止 main 函数;也有可能是意外终止。
3.预处理详解
3.1 预定义符号
__FILE__ //进行编译的源文件__LINE__ //文件当前的行号__DATE__ //文件被编译的日期__TIME__ //文件被编译的时间
执行:
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
而放在gcc编译器上面是可以执行通过的:
编译器在代码编译的时候,会对函数和变量名重命名的,C++ 中会更加复杂
在C语言中重命名的规则基本就是:加_
3.2 #define
3.2.1 #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 MAX 1000
#define reg register//为 register这个关键字,创建一个简短的名字
#define do_forever for(;;)//用更形象的符号来替换一种实现
#define CASE break;case //在写case语句的时候自动把 break写上。
长行如何拆分,\后面只能是换行,不能是其他
如果定义的 stuff过长,可以分成几行写,除了最后一行外,每行的后面都加一个反斜杠(续行符)。
在define定义标识符的时候,要不要在最后加上 ; ?
#define MAX 1000;#define MAX 1000
if ( condition )max = MAX ;elsemax = 0 ;
这里会出现语法错误.
3.2.2 #define 定义宏
#define name( parament-list ) stuff其中的 parament - list 是一个由逗号隔开的符号表,它们可能出现在 stuff 中
#define SQUARE( x ) x * x
SQUARE ( 5 );
5 * 5
int a = 5 ;printf ( "%d\n" , SQUARE ( a + 1 ) );
替换文本时,参数x被替换成a + 1,所以这条语句实际上变成了:printf ("%d\n",a + 1 * a + 1 );
#define SQUARE(x) (x) * (x)
printf ( "%d\n" ,( a + 1 ) * ( a + 1 ) );
#define DOUBLE(x) (x) + (x)
定义中我们使用了括号,想避免之前的问题,但是这个宏可能会出现新的错误。
int a = 5 ;printf ( "%d\n" , 10 * DOUBLE ( a ));
printf ("%d\n",10 * (5) + (5));
#define DOUBLE( x) ( ( x ) + ( x ) )
所以用于对数值表达式进行求值的宏定义都应该用这种方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。
所以这就涉及到#define的替换规则了
3.2.3#define 替换规则
在程序中扩展 #define 定义符号和宏时,需要涉及几个步骤。1. 在调用宏时,首先对参数进行检查,看看是否包含任何由 #define 定义的符号。如果是,它们首先被替换。2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。3. 最后,再次对结果文件进行扫描,看看它是否包含任何由 #define 定义的符号。如果是,就重复上述处理过程。
3.2.4 #和##
如何把参数(变量名)插入到字符串中?
首先我们看看这样的代码:
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
这里输出的是不是hello bit ?
请看下图,有没有那么一条语句,将这三个变量传参传过去,然后实现它们各自的输出呢?
现在定义一个宏
#define print_format(num, format) \
printf("the value of #num is "format)
并且让它输出:
这时候发现需要正确输入它们的数值:
## 可以把位于它两边的符号合成一个符号。它允许宏定义从分离的文本片段创建标识符。
3.2.5 带副作用的宏参数
x + 1 ; //不带副作用x ++ ; //带有副作用
在vscode2019内调试一下:
想知道宏和函数的区别吗,欲知后事如何,请听下回分解。