前言
编译与链接指的是文本文件“.c“变成可执行程序“.exe”的中间过程。其中编译指的是讲文本文件”.c“通过汇编翻译成计算机能懂的目标文件“.obj”,而链接是指将这些目标文件“.obj”组合起来,加上标准的库文件组成一个可执行程序“.exe”。接下来将会仔细和大家讲讲编译与链接的关联。
一. 翻译环境与运行环境
在电脑中存储的代码信息都是文本信息,当然C语言代码也是文本信息。电脑在翻译这些代码信息并输出内容的时候就需要代码经过翻译环境翻译和运行环境运行,用来发挥代码的功能。
1. 翻译环境
翻译环境指的是编译和链接的过程,这个在前言里也提到了,编译指的是讲文本文件”.c“通过汇编翻译成计算机能懂的目标文件“.obj”,而链接是指将这些目标文件“.obj”组合起来,加上标准的库文件组成一个可执行程序“.exe”。具体的情况如下图所示:
除了编译和链接之外,电脑在将文件转化为可执行程序的时候还有其他的过程。这些过程分别为预处理和汇编。他们的整体顺序和对应的文件关系如下:
从图中可以看出“.c”文件通过预处理和编译过程变成“.i”文件,然后通过汇编和链接变成“.o”。最后成为“.exe”文件。当然这也只是一个概括,编译中还包括一些其他的小过程,例如词法分析、语义分析等。
2. 运行环境
运行环境指的就是可执行程序在电脑上运行、输出结果的环境。具体的情况如下图所示:
这个过程就简单的多了,只有一步。但是具体的实现有多复杂就不得而知了。
3. 整个过程
对于翻译和运行的总过程,可以如下图所示:
二. 编译
对于编译过程来说,会有宏和预定义符号需要讲一讲。他们具体是怎么样被编译器识别,翻译后的内容怎么样,这些都是程序员需要了解的过程。
1. 预定义符号
预定义符号是电脑编译器定义的宏,这里简单给大家介绍5种。例如“__FILE__”,“__DATE__”,“__TIME__”,“__LINE__”,“__STDC__”。
#include <stdio.h>
int main ()
{
printf("%s\n", __FILE__);//文件路径
printf("%s\n", __DATE__);//文件编译日期
printf("%s\n", __TIME__);//文件时间
printf("%d\n", __LINE__);//行号
printf("%d\n", __STDC__);//是否完全支持C标准,是则为1
return 0;
}
当然这些预定义符号是编译器已经为程序员编译好了的内容,最后一个“__STDC__”并不是所有编译器都有这个功能,例如“VS”。所以并不是所有编译器都能支持C语言全部标准的。代码在“vscode”上的运行结果如下:
2. 宏
关于宏的内容就比较的多了,宏的作用就是在预编译的时候会被替换为预先定义的符号。
2.1 普通的宏
这里普通的宏指的是将预定义的符号转化为之后的内容,例如:
#include <stdio.h>
#define MAX 100
int main ()
{
int a = 0;
a = MAX;
printf("a = %d\n", a);
printf("MAX = %d\n", MAX);
return 0;
}
在预编译的时候"MAX"会被替换为“100”,像这样将文本替换为数字就是一种简单的宏,这种宏可以方便编程员修改数据,只需要在宏的里面修改一次“100”的这个部分,所有的"MAX“都会被替换。
2.2 长一点的代码宏
#include <stdio.h>
#define M printf("%d\n", 100)
int main ()
{
M;
return 0;
}
如图所示,也可以将“M”直接替换为一个语句“printf("%d\n", 100)”。执行结果如下:
2.3 宏函数
宏函数和函数的作用类似,可以将定义符号中的内容更换。
#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
int a = 5;
printf("%d", SQUARE(a));
return 0;
}
在编译的时候“SQUARE(a)”根据预定义的宏“#define SQUARE(x) x*x”会被修改为“a*a”,所以最后的结果为25。结果如下:
3. 宏的优点与缺陷
3.1 宏的优点
可以更为方便的修改代码中的数据,比如创建一个最大值,那么就可以用宏定义这个最大值,如果以后想修改这个最大值只需要找到这个宏就好了。
对于宏函数来说,宏函数比普通函数具有以下有点:
1)宏函数是直接替换,而调用函数需要经过三个过程:调用函数、运算、返回值。这样的话宏函数的运行速度快过普通函数。
2)宏函数可以用于各种类型,例如“int”、“float”等,而函数一旦定义就只能用一种数据类型。这样宏函数更为方便。
3.2 宏的缺陷
宏虽然有许多优点,可以更加方便程序员写程序,但是缺点也不能忽视:
1)会增加代码的长度。
2)宏无法调试,会给程序员调试带来不便。
3)宏与类型无关,不够严谨。
4)宏函数的计算会有副作用,而且容易出错。
3.3 宏的缺陷举例
接下来通过举例说明第4)点的不足。例如:
#include <stdio.h>
#define SQUARE(x) x*x
int main()
{
printf("%d", SQUARE(5 + 1));
return 0;
}
如果是调用普通函数,结果应该是25,但是实际上真的是如此计算的吗?它的实际结果如下:
那么为什么会这样呢?这是因为他的定义如此“#define SQUARE(x) x*x”,那么“SQUARE(5 + 1)”会被替换为“5 + 1 * 5 + 1”,这样结果就是11了。
为了避免这种情况,需要为宏函数的计算明确计算级,也就是说需要增加括号。
#include <stdio.h>
#define SQUARE(x) ((x)*(x))
int main()
{
printf("%d", SQUARE(5 + 1));
return 0;
}
如果把代码修改为以上的形式,那么计算结果就不会出错了。结果如下:
虽然加括号可以避免计算出错,但是宏函数还有难以想象的副作用。例如调用比较大小的函数:
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
int a = 3;
int b = 5;
int n = MAX(a, b);
printf("a = %d\n,b = %d\n,n = %d\n", a, b, n);
return 0;
}
这样的话结果为:
那么函数的副作用来自哪里呢?将代码改为:
#include <stdio.h>
#define MAX(a, b) ((a)>(b)?(a):(b))
int main()
{
int a = 3;
int b = 5;
int n = MAX(a++, b++);
printf("a = %d\nb = %d\nn = %d\n", a, b, n);
return 0;
}
重点是“MAX(a++, b++)”,如果是正常函数结果应该是“ a = 4, b = 6, n = 6"。但是宏函数有所不同,实际结果如下:
那么为什么会这样呢?原因是“MAX(a++, b++)”被替换为了“((a++)>(b++)?(a++):(b++))”。在执行的时候,先判断“(a++)>(b++)?”->“(3)>(5)?”,然后a变成了4,b变成了6。接着条件为假执行“(b++)”,那么n拿到的值为6,b变成了7,所以结果为“ a = 4, b = 7, n = 6"。这就是带有副作用的宏函数。
另外再定义宏函数的时候不能在“MAX”“(a++, b++)”之间增加空格,不然那就会变成:
“MAX”->“(a++, b++)”
而且在定义宏函数的时候最好不要再末尾增加“;”在编译的时候可能会出现以下错误:
#include <stdio.h>
#define PRINT(n) printf("the value of n is %d \n", n);
int main()
{
if(1)
PRINT(6);
else
PRINT(11);
return 0;
}
这个时候else和if会因为多个“;”而无法匹配。因为代码实际会被替换为:
int main()
{
if(1)
printf("the value of n is %d \n", 6);;
else
printf("the value of n is %d \n", 11);;
return 0;
}
而“if”语句在没有“{}”的时候只能有一条语句,所以导致“else”找不到它对应的“if”。
3.4 宏里面的符号作用
3.4.1 “\”
因为宏的定义只在一行写的会很长,不方便查看。而宏又不能像函数那样换行,于是就有了“\”来帮助换行,使用的方式如下:
#include <stdio.h>
#define PRINT(n) printf("the value of n is \
%d \n", n)
int main()
{
PRINT(6);
return 0;
}
那么它的打印结果如下:
这个符号的缺陷就是不能再前后增加“ ”,请读者注意。
3.4.2 “#”和“##”
“#”在宏里面的作用是链接两部分的内容,如果想替换转义字符那么怎么用呢?请看以下代码:
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
PRINT(n, %d);
PRINT(m, %.2f);
return 0;
}
在这段代码中“PRINT(n, %d)”会被替换为“printf("the value of n is %d \n", n)”,同理“PRINT(m, %.2f)”会被替换为“printf("the value of m is %.2f \n", m)”。这样“#”的作用就能体现出来了。代码执行结果如下:
“##”的作用我还不算特别会用,这里就不献丑了。
3.5 宏定义
在宏里面有能修改电脑是否编译一下代码的代码,如果满足条件在编译的时候会移除该代码。
3.5.1 “#if”与“#endif”
对于一下代码:
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
PRINT(n, %d);
#if m > 4
PRINT(m, %.2f);
#endif
return 0;
}
在预编译的时候,计算机会判断“#if m > 4”如果满足条件,到“#endif”之间的“PRINT(m, %.2f);”才会参与编译。结果如下:
需要注意的是“#if”要有“#endif”做结尾,否则的话以下的代码都会在“#if”的判断范围之内,例如:
#if 0
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
PRINT(n, %d);
PRINT(m, %.2f);
return 0;
}
这样“#if 0”以下的代码都不会编译,大佬屏蔽代码喜欢炫技的可以用。
3.5.2 “#elif”
可以看出来“#if”其实和if语句的判断方式一样,那么““#elif”呢?这也和else if作用相当,例如:
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
#if m > 4
PRINT(n, %d);
#elif m < 4
PRINT(m, %.2f);
#endif
return 0;
}
这段代码会执行后面的“PRINT(m, %.2f);”。
3.5.3 “#ifdef”与“#ifndef”
如果该宏被定义就会执行一下内容,反之不执行。例如:
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
#ifdef PRINT
PRINT(n, %d);
PRINT(m, %.2f);
#endif
return 0;
}
函数被定义了“PRINT(n, %d);”和“PRINT(m, %.2f);”才会参与编译,结果为:
“#ifndef”和“#ifdef”作用正好相反,如果宏没被定义采执行以下语句:
#include <stdio.h>
#define PRINT(n, format) printf("the value of "#n" is "#format" \n", n)
int main()
{
int n = 6;
float m = 3.14f;
#ifndef PRINT
PRINT(n, %d);
PRINT(m, %.2f);
#endif
return 0;
}
这样的执行,函数不会输出任何东西。
三. 库函数和头文件的调用
库函数和头文件的调用使用"#include"包含文件调用,例如:
#include <stdio.h>
#include "add.h"
调用标准库里的函数可以使用“<>”和“""”,而调用自己的头文件只能使用“""”。会在链接的时候链接到一起成为一个可执行文件。
作者结语
以上就是电脑编译和链接,比较浅显,只是我自己理解的东西。对于抛砖引玉来说,绝对只能算是抛转而已。和电脑相关的知识还有许多,推荐一本书《程序员的自我修养》。里面会把计算机相关得到知识讲的很清楚,包括编译和链接。
虽然这篇博客很简单,但还是希望读者们能够喜欢这种简单的风格,如果有什么建议可以直接私信我,我不一定会回复但是一定会看的。