前言
本章内容简单介绍了编译器,并讲述了编译器的过程。
C语言 | 快速了解C的发展史🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 指针、数组 一文透彻~~~🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 结构体、联合、枚举🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 声明🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 作用域 在也不用担心分不清变量的作用域拉!!!🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 编译步骤 会用C还不知道C如何编译???🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 数据类型总结🧡💛💚💙
C语言 | 【耗费一夜总结三本C语言系列】之 位及进制的用法🧡💛💚💙
文章目录
一、为何需要编译
我们现在所学的编程语言(除汇编语言)都属
高级编程语言
,以多种方式简化了我们的编程工作。如使用c语言,代码不想汇编那样繁琐。接触了python,会觉得编程变得更加简便。高级语言使用高级的指令让程序员更使用更加贴切
、更抽象
得去描述问题
,或者你的想法
。
1.1 编译器的作用到底是什么呢?
编译器简介
编译器不是一个单一的程序,它分别由预处理器、语法、语义检查器、代码生成器、汇编程序、优化器、链接器、等组成,由编译器驱动器来调控;
int a = 10;
int b = a;
当我们写出代码时,这些代码仅仅是我们能够看懂的指令。而非计算机所能识别的指令,对于计算机而言,它只认识机器语言。要如何才能够让他们转换成相应的语言呢?此时编译器就充当中间者的身份。将程序员编写的
高级语言
翻译成计算机能理解的机器语言
,从而让代码能够在计算机上运行。
且编译器在不同的CPU产商中使用的
指令系统
和编码格式
不同,需要合适的编译器便可将高级语言转换给不同类型的cpu识别
。
- 编译器由
编译器设计者所决定
,在不同的编译器所采取的行动可能并不相同。- 编译器只有在违反
语法规则
和约束条件
下才会产生错误。例如:你的代码出现问题,它可能会中止你从程序、或者发出一条警告、或者啥也不干。
1.2 编译器限制来保证了代码的可移植性
ANSI C规定每个ANSI C编译器必须要支持以下几点【举例,若要详细需查看文档】:
- 函数定义
形参
数量不能少于31个;- 函数调用时
实参
数量不能少于31个;- 一个条
源代码行
至少可以由509个字符;- 表达式中至少可以支持
32层嵌套括号
;
1.3 不同系统使用的编译器不同
- Windows 下常用的是微软开发的
Visual C++
,它被集成在Visual Studio
中,一般不单独使用;- Linux 下常用的是 GUN 组织开发的 GCC,很多 Linux 发行版都自带
GCC
;- Mac 下常用的是
LLVM/Clang
,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。
二、编译步骤
典型的C语言实质上通过编译和链接俩个步骤来完成。其中编译器将
.c
文件装成中间代码,在通过链接将代码合并,从而生成可执行文件。
可知,C在编译大型项目时,速度会很慢,甚至需要几个小时。如果编译好后,需要调整,则需要再次编译。针对于此,C采用多个模块化,将头文件写在
.h
中,源代码写在.c
中,且通过不同的功能在对文件进行细分。可对其进行单独编译,在使用链接器合并。如若其中一个文件有过修改,则只需单独将更改过的模块重新编译即可,节省了大量的时间成本,提高文件修改调试效率。
2.1 编译
步骤:
- 预处理;
- 编译;
- 目标文件。
2.1.1 预处理【宏定义、文件包含、条件编译】
头文件展开
,并将头文件的源代码插入;
- 各种声明;
- inline函数的定义;
- 宏定义;
- 全局变量定义;
- 外部变量定义;
宏展开
也就是将宏定义与代码中使用的宏替换
,并且删除
#define;- 处理
条件编译
指令;删除
源代码中的注释
,并且添加行号
以及文件名标识
;- 执行
#pragma
定义的相关指令;
使用预处理命令
g++ -o test.i -E test.cpp
即可生成一个预处理后的文件,多达2万多行。
2.1.2 编译
此过程编译器将会检查代码的规范性,以及语法的正确性,以及确定了代码要做的实际工作。若无误,则生成汇编代码。
使用命令
g++ -S test.i -o test.s
即可生成汇编代码。
2.1.3 目标文件
将
汇编
文件转换成二进制
文件,虽然此时包含机器语言但依旧不能直接运行。因为该代码只是存储编译器翻译的源代码
,并不是一个完整的程序;缺失启动代码
以及库代码
;
启动代码
启动代码根据不同的
系统
或者硬件
系统而不同,充当着程序与操作系统
之间的接口
。
库代码
存储正在的函数代码。
使用
g++ –c test.s –o test.o
文件
2.2 链接
目标文件不能直接执行,还需要载入连接器。链接器会确认main函数的进入点,再将
目标文件
与库代码
、系统的标准启动代码
进行链接,对于库代码只需将程序中所用的函数提取即可。且将源代码中的库代码与目标形成计算机可识别的二进制机器代码
即可执行程序。
使用
g++ test.o –o test.exe
即可生成test.exe的可执行文件;
2.2.1 静态链接
即函数库(仅所需要的函数)的一份拷贝是可执行文件的物理组成部分;
.a
为后缀;- 易出现与升级后的系统不兼容的情况;
2.2.2 动态链接
即可执行文件包含文件名,并且在载入器运行时(将函数库映射到进程中)可找到所需的函数库;
.so
为后缀;
优点:
- 可执行文件的
体积小
;- 运行速度稍慢,但节省
磁盘空间
及虚拟内存
;- 能让程序与使用的特定的函数库
版本分离
,供应用程序二进制接口(介于程序与函数库二进制间服务),便于函数库版本升级,无需重新链接;- 允许运行时选择需要执行的函数库;
2.3 一步到位
g++ test.cpp -o test
即可生成test可执行文件。
2.4 运行
程序运行时,载入器会找到
main的入口
,并且在此之前,载入器会把共享的数据对象载入到进程
的地址空间。外部函数不调用,则载入器不会对其进行解析,即不会造成额外的开销;当程序要运行时,会被分为三个阶段分别为:
- 链接-编辑;
- 载入;
运行时链接;
2.4.1 链接编辑
- 在静态链接下,该模块被链接编辑后即运行;
- 在动态链接下,该模块被链接编辑后,又在运行时链接即运行;
3. 静态与动态库的创建及使用
3.1 静态库
命名规则:
- 【lib
库的名称
.a】
制作:
将
.c
文件生成.o
文件打包
生成命令:ar rcs 静态库名称 生成的.o文件
使用:
打包头文件以及静态库即可;
- 使用命令:
gcc main.c lib/静态库 -o a.out -I头文件路径
- 或者:
gcc main.c -Iinclude -L 静态库的目录 -l 库的名称 -o a.out
优点:
- 生成程序的时候,不需要提供对应的库,只需要相应的
.o
文件;- 加载库的速度快;
缺点:
- 库被打包到应用程序,导致库的体积很大;
- 库发生了改变,需要重新编译环境;
3.2 共享库
命名规则:
- 【lib
名称
.so】
制作步骤:
- 生成与位置无关的代码;
- 将
.o
打包成共享库(动态库);
制作:
- 创建与位置无关的
.o
文件gcc -fPIC -c *.c -I../include
;gcc -shared lib命名.so
;
使用:
当生成的可执行文件出现找不到该共享库时:
- 可使用
ldd 可执行文件
查看动态链接器;
- 动态链接器是根据环境变量进行查找;
- 若找不到可将
so
文件放到lib
中(不推荐);- 或者加入
export LD_LIBRARY_PATH=...
;- 或者修改
/etc/ld.so.conf
添加动态库的路径,在使用ldconfig
执行(推荐);
优点:
- 执行程序体积小;
- 动态库更新不需要重新编译程序;
缺点:
- 需要提供动态库;
- 速度相对慢,没有打包到应用程序中;