源码变成可执行程序的流程
大概流程
当我们在vs下编译了一个工程的时候,我们会发现,在我们的debug中每个.c文件都会对应的生成一个.obj文件(目标文件),如下图
那么这是为什么呢?
我们就要大概来说一说关于程序编译的流程了,如下图,每个源码文件通过编译器生成对应的目标文件,然后再通过链接器与链接库连接,最终生成我们的可执行程序。大概了解了之后就请继续往下看以更深入的了解它们。
具体流程
预处理
预处理有四大作用:
1.头文件的复制
2.注释的清除
3.#define定义的替换
4.条件编译
因为我们的vs是IDE(集成开发环境),会将源码到程序的步骤一步走完,不便理解,我们可以在Linux环境下进行一步一步的详解。
头文件的复制
话不多说,我们打开Linux虚拟机,执行预处理后我们先打开test.c,再打开test.i进行对比
这是test.c 一段很简单的代码
然后我们再打开test.i,我们发现原来只有九行的代码现在已经变成了八百多行,相对应的#include包含的头文件这句代码消失了。
这就是头文件的复制,在预处理阶段,会将你所包含的头文件的内容拷贝一份进你的代码中,就会让你的代码变得很大
注释的清除
同样的我们在源代码中增加注释,如下图
我们会发现对应的注释也没了,所以预处理还有清除注释的功能
#define定义的替换
同上,我们在再来做个小实验,定义一个宏
我们会发现宏所在的地方被替换了
条件编译
编译
话不多说,我们依然是从实验中得出我们的结论
我们会发现,编译这个步骤的作用是将源代码转换成汇编代码
汇编
汇编这个流程也是同样的,我们先执行完汇编这个步骤,再打开。
当我们打开了test.o这个文件的时候,我们发现里面是一堆我们看不懂的符号,但是呢这是机器看得懂的符号,也就是二进制代码。
所以说,我们可以知道,汇编这个阶段的作用就是将会汇编代码转换成我们的二进制指令,也就是计算机能识别的代码
此外,我们也可以使用一个叫readelf这个指令来读这个二进制代码(因为在Linux环境下,.o文件是以elf这个的形式来组织的)
-s的话是一个选项,表示显示symple,即汇总的符号
到这,我们发现了一个叫做符号汇总的东西,那么具体有啥用呢这个符号汇总,我们接着往下看。
先注意一点:符号汇总 是编译的时候产生的,但是在汇编的时候有一个形成符号表的功能,所以我们需要在.o文件中查看形成的符号表
还有一点,上文我们说过,每个源文件都会对应的生成自己的目标文件,当然,每个目标文件里面也有自己对应的符号表,至于这个有啥用呢,我们就要继续谈到链接了。
链接
至于链接的操作,就接着上面,如下图,会之间生成可执行程序
在这里,链接主要有两个作用
第一:合并段表
第二:符号表的合并和符号表的重定位
之前我们说过,源码文件在编译的时候进行符号合并,在汇编的时候生成符号表,以及每个源码文件都会生成对应的目标文件。
因此,如果我们有多个文件,比如在一个.c文件里面有函数的实现,在另一个.c文件里面又有函数的调用,它们就会有符号的重合,因此,在最后一步链接的时候进行各个目标文件里面的符号表的合并和重定位,最终让我们的各个文件里面的函数和符号相互连接起来。
预处理详解
define
#define定义标识符
说到define,我们最常见的就是用define定义一个标识符,如下面代码
#define MAX 999
#define MIN -999
#include<stdio.h>
int main()
{
printf("%d %d\n",MAX,MIN);
return 0;
}
只要我们定义了标识符,在后面的代码中我们就可以使用这些标识符来代表我们的预定的值,
除此之外,使用deifne定义的标识符的一个优点就是便于维护代码,在我之前的关于一些小项目的博客中就穿插了使用define定义标识符的方式来创建一些数组之类,这样当你后期想要修改或者维护代码的时候你就只用修改定义的标识符即可。
#define定义标识符的注意点
如下图代码
#define MAX 999
#define MIN -999;
注意点就是在define定义标识符的时候我们需不需要在后面加一个分号
答案是不需要加分号,这样容易和你自己在语句中加入的分号造成语法错误
#define定义宏
除了定义标识符之外,define还能定义宏。
宏
简单来说,宏就是允许带参数的标识符。举个栗子,看如下代码
#define ADD(N1,N2) N1+N2
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2));
//相当于代码printf("%d\n",1+2);
return 0;
}
这里程序运行的结果是3,如上图注释,所谓宏,就是允许参数的替换
宏的易错点
看下图代码
#define ADD(N1+N2) N1+N2
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2)*5);
return 0
}
这段代码输出的答案是什么呢?是不是15呢?然而并不是,答案是11.这是为什么呢?
因为define替换的一个重要规则就是**“只替换,不计算”**
上图中的printf代码等价于下面代码
printf("%d\n",ADD(1,2)*5);
//等价于printf("%d\n",1+2*5);
因此对于宏,首先的第一步就是替换,不要做计算!替换之后才按正常的做法去计算。
所以我们对于宏,我们可以在最外面加上括号,防止出现一些不必要的错误,如下图代码
#define ADD(N1+N2) (N1+N2)
#include<stdio.h>
int main()
{
printf("%d\n",ADD(1,2)*5);
return 0
}
这样代码就不容易出错了
宏与函数对比
那么为什么要有宏呢?
同样具有参数替换的东西我们自然而然会想到函数,那么它两有啥区别呢?
1.宏比函数更快,因为宏是替换代码,直接就可以运行,但函数是需要调用,同时开辟栈帧的,因此,宏的速度是快于函数的。
2.宏容易使代码变得过长,因为宏的替换相当于是copy,对于一些较大的宏就容易让代码变得冗长。
3.宏是无法进行调试的,因为它直接替换。
4.宏是无法进行递归的,而函数是能进行递归的
4.宏与类型无关,而函数是固定类型的,怎么理解呢?请看下图代码
#define MAX(a,b) a>b?a:b
这样一个求两个数之间最大值的代码如果用宏来实现,无论a,b是何类型,都能求出二者的最大值
但是如果是函数呢?
我们就可能要写很多主体代码大致相同,但参数类型不相同的函数才能实现上面的功能了。
define替换规则
1.在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号,如果有,那么进行替换
2.替换文本替换了之前宏之前原本的文本位置
3最后,在对结果进行扫描,看看它们是否有任何由#define定义的符号,如有,则进行上述步骤
4.注意,字符串中并不会搜索#define定义的符号。
#和##的作用
#的作用
#的作用就是向字符串中插入参数,也叫将参数字符串化
因为之前我们说过,字符串中的字符是不被检查的。这样的方式就让我们能向一个字符串中加入参数,如下图代码
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE)
//向字符串中插入参数
int main()
{
int i = 10;
PRINT("%d", i + 3);
//相当于PRINT("the value of i+3 is %d \n",i+3);
system("pause");
}
//输出的结果是the value of i + 3 is 13
##的作用
##的作用就是将操作符的两边字符合成一个新的标识符,如下图代码
#include<stdio.h>
#include<stdlib.h>
#define PRINT(FORMAT, VALUE) printf("the value of " #VALUE " is " FORMAT "\n", VALUE)
#define PRINTF(FORMAT,i) PRINT(FORMAT,NUM##i)
int main()
{
int i = 1;
int NUM1 = 10;
int NUM2 = 11;
int NUM3 = 13;
PRINTF("%d", 1);//NUM和1合成了新的标识符NUM1
PRINTF("%d", 2);//NUM和2合成了新的标识符NUM2
PRINTF("%d", 3);//NUM和3合成了新的标识符NUM3
system("pause");
}
//输出的结果是the value of NUM1 is 10
// the value of NUM2 is 11
// the value of NUM3 is 13
条件编译
下面是常用条件编译指令
1.
#if 常量表达式
//...
#endif
//常量表达式由预处理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__ //如果__DEBUG__为1,那么就执行之后的代码,否则就不执行
//..
#endif
2.多个分支的条件编译
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif //这是一组条件编译的结束语
3.判断是否被定义
#if defined(symbol)
#ifdef symbol //if defined(...)和ifdef ... 是等价的,都是如果定义了...就执行接下来的代码
#if !defined(symbol)
#ifndef symbol
4.嵌套指令
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
条件编译的一个作用就是我们在测试代码的时候我们可以写上一些测试代码,但是一加一删又很麻烦,因此我们就可以采用条件编译来选择性的运行我们需要的代码
头文件包含
两中头文件包含的区别
在平常我们写C语言的过程中,我们会使用以下两种包含头文件的方式
#include<stdio.h>
#include"find.h"
那么<>和""这两种方法有什么区别呢?
<>:此种包含方式,编译器会直接从库里面查找对应的头文件
“” :此种包含方式,编译器会先在本地目录下查找,即你自己定义的头文件,如果找不到,再从库文件中查找。
如何避免头文件被重复包含
为什么要避免头文件被重复包含?
因为对于在头文件中的全局变量,或者函数声明,如果重复包含头文件,会造成重复定义或者重复声明的错误。
方法一:使用条件编译,在你的头文件中加入以下代码
#ifdef __TEST__
#define __TEST__
//..头文件的内容
#endif
这段代码的意思是,看是否定义了给定的标识符,如果没有定义,那么就是第一次引用头文件,就正常引用头文件,如果之前引用定义过,那么将跳过头文件内容,从而避免了头文件被重复包含的问题。
方法二:在头文件中加入以下代码
#pragma once
//..头文件的内容
到这,学习了这些知识以后,我相信你已经对C语言如何变成一个程序,以及预处理的操作有一些了解了,我们从一个整体的角度来重新了认识了C语言,也为我们C语言的最终话画个句点。
希望这篇文章对你有所帮助,有错误的地方欢迎指正。感谢观看!
转载:https://blog.csdn.net/weixin_51306225/article/details/115456082