前言:哈喽小伙伴们,从我们开始学习C语言到实现如今的成果,可以说我们对C语言的掌握已经算是精通级别了,但是我们只学习了怎么写代码,却没怎么了解过代码的背后是怎么工作的。
那么今天这篇文章我们一起来学习C语言的最后一部分知识——编译和连接。
跟上节奏不要掉队哦!
一.翻译环境和执行环境
我们每次在写代码前,都要先创建一个形如test.c的源文件,这个文件是一个文本信息文件,在它里边我们只能看到我们所写的代码,比如说博主我写的第一个C语言代码:
那么我们如何才能实现代码的功能呢???
这就要通过翻译环境来实现了,也就是我们常用的各种编译器,这些编译器又叫做集成开发环境,它们能够对代码进行翻译,生成一个可执行的程序(后缀.exe)。
最后可执行程序再通过执行环境来实现代码的运行,执行环境就是我们电脑的操作系统等。
下面我们来详解这两个环境。
1.翻译环境
翻译环境又分为两个部分:编译和链接。
那么对于博主所使用的VS2019编译器来说,它们分别代表着cl.exe和link.exe两个可执行程序。
- 组成一个程序的每个源文件通过编译过程分别转换成目标文件(后缀.obj)
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,同时它也可以搜索程序员所创建的个人程序库,引入其中所用到的函数。
那么对于编译而言,它又分成三个阶段来完成:
- 预编译(预处理)
- 编译
- 汇编
预处理阶段包括:
- 注释被替换成一个空格(删除)
- 头文件的包含 #include<>
- 预处理指令的执行 #define的替换
编译过程就是将C语言代码翻译成汇编代码,包括:
- 词法分析
- 语法分析
- 语义分析
- 符号汇总
那么汇编的作用就是:
将汇编代码翻译成二进制的指令,并生成后缀为.obj的目标文件。
对于链接来说,主要作用是:
链接目标文件和链接库生成可执行的二进制程序。
大家只需要记住这些步骤以及每一步要做的事即可,不必深究。
接下来我们来看运行环境。
2. 运行环境
运行环境也可以说就是程序的执行过程:
- 程序必须载入内存才能执行,这个过程一般由操作系统来完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序开始执行,调用main函数。
- 开始执行程序代码。这时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能意外终止。
这些过程不做展开讲解,小伙伴们也只需要了解即可。
二.预处理
1.预定义符号
__FILE__ 进行编译的源文件
__LINE__ 文件的当前行号
__DATE__ 文件被编译的日期
__TIME__ 文件被编译的时间
__STDC__ 如果编译器遵循ANSI C,其值为1,否则未定义
注意上边都是双杠。
这些预定义符号都是C语言所内置的,可以直接使用:
#include<stdio.h>
int main()
{
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
return 0;
}
得到结果:
但是当我们使用__STDC__时,编译器却报错了:
这说明博主当前使用的VS2019没有定义该预处理指令。
2.#define
(1)定义标识符
#define我们前边已经了解过了,它是一个宏定义指令,可以将任意类型的数据用一个新的名字代替,例如:
#define N 100
#define INT int
#define STR "abcdefg"
前者为新名字,后者为数据。
同时#define也可以重命名一个表达式,例如:
#define M 4+5
之后就可以把M当做4+5来用。
但是值得注意的是,M的值并不是4+5的结果9,而是4+5这个表达式:
#include<stdio.h>
#define M 4+5
int main()
{
int n = 5 * M;
printf("%d", n);
return 0;
}
来看这个代码,n的值会是多少呢,45吗???
结果却是25。
实际的运算式为:
int n = 5 * 4 + 5;
如果想得到45,还得给M加个()。
(2)定义宏
事实上#define还有类似于函数的用法:
它存在一个机制,允许把参数替换到文本中,这种实现就被称为宏。
怎么理解这句话呢???下面我们来看宏的声明:
#define name(parament-list) stuff
parament-list是一个由逗号隔开的符号表,也就是我们所要使用的参数表。
值得注意的是,参数列表的左括号必须和name紧连,如果之间有空格,参数列表就也会被认为是stuff的一部分。
还是不够理解?没问题,下面我们就通过具体例子来讲解宏到底怎么用:
#include<stdio.h>
#define ADD(x,y) x+y
int main()
{
int a = 20;
int b = 30;
int c = Add(a, b);
printf("%d", c);
return 0;
}
来看,我们定义了ADD(x,y)这样一个宏,它的内容是x+y,也就是说当我们以后去调用这个宏时,他都会被替换成x+y。来看结果:
是不是感觉这个宏和函数非常的相似?事实确实如此。
但是宏也有一些弊端,比如说如果我将上边的运算式改为:
int c = 4 * ADD(a, b);
c的值会是什么呢? 50吗??
并不是,上述式子的实际形式为:
int c = 4 * a + b;
因为就算是宏,他也是要执行#define的规则,它只是会替换表达式,并不是替换成表达式的运算结果。
因此关于#define的应用,该加括号的时候一定要记得加。
(3)宏和函数的对比
那么既然宏的功能与函数这么相似那么我们该如何在它们两个之间进行选择呢???
一般情况下,宏更适用于一些简单的运算,就比如我们上边所写的ADD加法宏。
那么对于简单的运算,为什么宏就一定优于函数呢???
有以下两个原因:
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 更为重要的是,函数的参数必须声明为特定的类型。而宏参数不需要定义类型就可以使用。
函数相较于宏,不仅仅要执行运算,还要执行函数的调用和返回,这些都需要花费时间,所以对于简单的运算,宏是优于函数的。
那什么时候该用函数呢???
而对于一些相对复杂的功能,用宏可能写都写不出来的,函数就更占优势。
此外,宏是不方便调试的,如果我们的代码出错了,想要调试找错误,这时候就非常麻烦。
虽然宏参数不需要类型,但是在特定情况下我们是必须确定参数类型的,这时候就必须用函数。
所以说,宏和函数各有千秋,小伙伴们一定要对症下药。
3.#undef
该预处理指令的作用是移除一个宏定义。
#include<stdio.h>
#define X 1000
int main()
{
int x = X;
printf("%d\n", x);
#undef X
int X = 1;
printf("%d\n", X);
return 0;
}
先将X定义为1000,然后赋值给x并输出,随后我取消X的定义,又重新给X赋值,结果如下:
4.条件编译
何为条件编译???其实是针对一段代码的编译与否。
有时候我们写代码时可能会出现这种情况:某些代码可能只需要在特定的情况下运行,也就是说这段代码时用时不用,这时候就产生一个问题:
不用时,它在那里占着空间,但是要是把它删了,用的时候还得再重新写,这就很麻烦,所以,我们引出条件编译:只有当满足条件时,这段代码才编译,否则这段代码将会是注释的效果。
条件编译和if-else条件判断语句很相似,一样分为好几种写法,下面我们就来一一讲解。
(1)单分支
单分支,也就是只有一次判断,其语法为:
#if 常量判断表达式
//执行语句
#endif
下面我们来看一个实际例子:
对于图1,因为m==5,所以能够执行输出语句,而图2不满足,所以输出语句变得暗淡,可以理解为被注释掉了。
(2)多分支
多分支,也就是会进行多次判断,语法为:
#if 常量判断表达式
//执行语句
#elif 常量判断表达式
//执行语句
#else
//执行语句
#endif
这里的运算和if-else语句完全相同,博主就不在图示了,只点出重要的信息:
哪个常量判断表达式满足,就执行哪里的语句,都不满足,则执行else后的语句。
三.结语
到此为止呢,关于C语言所有知识的大纲博主已经统统分享完啦。
现在回想起来,一开始博主我还是非常抵触写文章的,但是慢慢写下来就会发现写文章也是一种乐趣,因为能够把自己对知识的理解写出来真的很有成就感。
OK,第一阶段到此结束,后续有空博主也还会分享过于C语言里边的一些细小琐碎的知识。
第二阶段,数据结构,敬请期待!!!
最后还是希望大家能给博主点点关注,一键三连!!!
我们下期再见啦!!!