编译链接
1.程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
1.第一种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
2.第二种是执行环境,它用于实际执行代码
2.详解编译+链接
2.1翻译环境
*组成一个程序的每个源文件通过编译过程分别转换成目标代码。
*每个目标文件由连接器捆绑在一起i,形成一个单一而完整的可执行程序。
*链接器同时也会引入标准c函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
2.2翻译本身也分为几个阶段
预处理只会处理#开头的语句,编译阶段只校验语法,链接时才会去找实体,所以是链接时出错的,故选C。这里附上每个步骤的具体操作方式:
预处理:相当于根据预处理指令组装新的C/C++程序。经过预处理,会产生一个没有头文件(都已经被展开了)、宏定义(都已经替换了),没有条件编译指令(该屏蔽的都屏蔽掉了),没有特殊符号的输出文件,这个文件的含义同原本的文件无异,只是内容上有所不同。
编译:将预处理完的文件逐一进行一系列词法分析、语法分析、语义分析及优化后,产生相应的汇编代码文件。编译是针对单个文件编译的,只校验本文件的语法是否有问题,不负责寻找实体。
链接:通过链接器将一个个目标文件(或许还会有库文件)链接在一起生成一个完整的可执行程序。
链接程序的主要工作就是将有关的目标文件彼此相连接,也就是将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。在此过程中会发现被调用的函数未被定义。需要注意的是,链接阶段只会链接调用了的函数/全局变量,如果存在一个不存在实体的声明(函数声明、全局变量的外部声明),但没有被调用,依然是可以正常编译执行的。
2.3运行环境
程序执行的过程:
1.程序必须载入内存中,在有操作系统的环境中:一般这个由操作系统来完成。在独立的环境中,程序的载入必须由手工安排,也有可能是通过可执行代码置入只读内存来完成。
2.程序的执行便开始。接着便调用main函数。
3.开始执行程序代码,这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址,程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
4.终止程序,正常终止main函数;也有可能是意外终止。
3.预处理详解
3.1预定义符号
__ FILE __ 进行编译的源文件
__ LINE __ 文件当前的行号
__ DATE __ 文件被编译的日期
__ TIME __ 文件被编译的时间
__ STDC __ 如果编译器遵循ANSI C,其值为1,否则未定义
这些预定义符号都是语言内置的。
我们在vs2019中尝试将这些符号打印出来
从图上可以看出vs2019并不满足遵循ANSI 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\nline:%d\n \
date:%s\ntime:%s\n", \
__FILE__, __LINE__, \
__DATE__, __TIME__);
我们拿最后一个来举例
#include<stdio.h>
# define DEBUG_PRINT printf("file:%s\nline:%d\n \
date:%s\ntime:%s\n", \
__FILE__, __LINE__, \
__DATE__, __TIME__);
int main()
{
/*printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);*/
//printf("%hS", __STDC__);
DEBUG_PRINT
return 0;
}
得出结果
这里给大家提个小小的建议:
#define 定义标识符的最后不要加上‘;’,避免出错。
3.2.2#define定义宏
#define机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏(define macro)。
下面是宏的申明方式:
#define name(parament_list) stuff |
---|
其中的parament_list是一个由逗号隔开的符号表,它们可能出现在stuff中
注意: 参数列表的左括号必须与name紧邻。 如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分
如:
#define SQUARE(x) x*x |
---|
例如:
可以得到
但是需要注意的是,宏参数是直接替换,因此我们需要加括号
这里假如我们传参数为(5+1),乍一看,以为会得到36,但其实不是。
替换为
那么结果得到的就是11而不是36
然而我们将定义宏处的内容部分加上括号,就不会再出现这种问题了
替换为
结果是
除了stuff里面每个小单元的括号以外,最外层的括号也很重要
比如:
我们要求一个数的二倍
这样可以得到6的倍数12,但是如果我们给DOUBLE(6)乘以一个5,我们想得到一个60,但如果没有外层的大括号,结果就会大不相同
替换后
因此,得到的是36而不是60
但是我们加上括号过后
替换后得到
这样我们就可以得到正确的结果
宏的参数也可以是多个
比如定义一个加法
结果为10
3.2.3#define替换规则
在程序中扩展#define定义符号和宏时,需要设计几个步骤。
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果是,它们首先被替换。
2.替换文本随后被插入到程序中原本文本的位置。对于宏,参数名被他们的值所替换。
3.最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述操作过程。
注意:
1.宏参数和#define定义中可以出现其他#define定义的符号,但是对于宏,不能出现递归。
2.当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
比如:
字符串中的M并不会被替换为100
打印结果为
3.2.4#和##
字符串具有自动连接的特点
例如:
结果为
如果我们想将宏参数插入字符串中,我们可以利用#
例如:
#会将字符x替换为“x”,因此就具有与其他字符串连接的能力
结果为
##的作用
##可以把位于它两边的符号合并为一个符号,它允许宏定义从分离的文本片段创建标识符。
比如:
##将两边的符号连接为一个标识符,这里nu##m等价于num,因此
结果为
注意:这样的连接必须产生一个合法的标识符,否则其结果就是未定义的。
3.2.5副作用
什么是副作用呢:
1.m=n+1;//n的值不变
2.m=++n;//n的值也加一了
这两种赋值方式,第二种具有副作用
在#define定义的宏在传参的时候不要使用带有副作用的参数
比如:
这里的b进行了两次++,a进行了一次++。因此,最后的结果为
3.3#undef
这条指令用于移除一个宏定义
# undef NAME |
---|
比如:
#undef过后,MAX就不能再使用
3.4命令行定义
3.5条件编译
在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。 比如说:
调试性的代码,删除可惜,保留又碍事,所以我们可以选择性的编译。
条件编译指令 | 相关操作 |
---|---|
#if | 如果条件为真则执行相应操作 |
#elif | 如果前面条件为假,而该条件为真,则执行相应操作 |
#else | 如果前面条件均为假,则执行相应操作 |
#ifdef(#if defined) | 如果该宏已定义,则执行相应操作 |
#ifndef(#if !defined) | 如果该宏没有定义,则执行相应操作 |
#endif | 结束相应的条件编译指令 |
举例:
打印结果为
打印结果也为
当然,条件编译指令也是支持嵌套使用的,这里我就不再为大家举例了
3.6文件包含
我们已经知道,#include指令可以使另外一个文件被编译,就像它实际出现于#include指令的地方一样。
这种替换的方式很简单:
预处理器先删除这条指令,并用包含文件的内容替换。
这样一个源文件被包含10次,那就实际被编译10次。
3.6.1头文件被包含的方式
1.本地文件包含
#include “filename” |
---|
查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数一样在标准位置查找头文件。
如果找不到就提示编译错误。
2.库文件包含
#include < filename > |
---|
查找头文件是直接去标准路径下去查找,如果找不到就提示编译错误。
这样是不是可以说,对于库文件也可以使用“”的形式呢?
答案是可以的,但是这样一来查找库文件的效率就低了,而且不容易区分库文件和本地文件。
3.6.2嵌套文件包含
由上图可得,test.c文件使用了两次add.h,因此,也会编译两次,最后程序中就会出现两份add.h的内容,这样就造成了文件内容的重复。
为了解决这种重复使用的情况,我们可以这样做:
在每个头文件的开头写:
这样就可以解决重复使用的问题,编译的时候只编译一次,其中_ONE_可以自己随意定制
除此之外,也可以直接使用
#pragma once |
---|
这样的效果也是一样的 。