1 什么是编译
在C/C++语言中,编译就是将源代码转换为机器代码的一个过程,以便计算机可以理解和执行。
编译过程通常包括以下几个步骤:
- 预处理(Pre-Processing):
- 所作工作:展开宏、头文件、替换条件编译、删除注释、空白,最终生成一个无预处理指令的代码文件
- 目标文件:若采用GCC编译,则其会通过预处理器(CPP)将 .c 源文件生成 .i 预处理文件
- 编译(Compilation):
- 所作工作:检查语法规范,将预处理后的代码翻译成汇编代码(汇编代码是与特定计算机架构相关的低级代码)
- 目标文件:若采用GCC编译,则其会通过编译器(CCL)将 .i 预处理文件生成 .s 汇编文件
- 汇编(Assembly):
- 所作工作:将汇编代码转换为机器代码
- 目标文件:若采用GCC编译,则其会通过汇编器(AS)将 .s 汇编文件生成 .o 或 .obj 目标文件,这种格式是计算机能直接执行的二进制格式
- 链接(Linking):
- 所作工作:处理不同模块间的引用,数据段合并,数据地址回填,使得所有代码可以协同工作
- 目标文件:若采用GCC编译,则其会通过链接器(LD)将多个目标文件和库文件链接生成一个可执行文件或库文件
2 编译过程的中间文件
- .c:源代码文件
- .i:预处理文件
- .s:汇编文件
- .o:目标文件
- .a:Linux下的静态库文件,由多个.o文件链接而成
- .so:Linux下的动态库文件,由多个.o文件链接而成
- .lib:Windows下的静态库文件,由多个.o文件链接而成
- .dll:Windows下的动态库文件,由多个.o文件链接而成
- .dll.a:.dll文件的导出声明文件
- .exe:Windows下的可执行文件,由多个目标文件和库文件链接而成
3 GCC
GCC(GNU Compiler Collection)是由GNU开发的编译器套件,也可以简单认为其就是编译器。它可以编译多种语言(包括C、C++、Objective-C、Fortran、Java等等)。当我们的程序只有一个源文件时,可以直接用gcc
命令编译它。
3.1 基本用法
gcc [options] [filenames] gcc 命令用于调用GCC编译器,options作为参数用于指示GCC的具体操作,filenames用于指定操作的具体文件
options的不同参数与功能
为空时:指示编译器对输入文件进行全链路操作,生成可执行文件
gcc main.c -o main // 调用GCC将输入文件生成可执行文件main.exe gcc main.c // 没有 -o, GCC 生成可执行文件名称默认 a.exe / a.out gcc main.i gcc main.s gcc main.o
-o:指示编译器为操作后的输出文件指定文件名称
gcc main.c -o main // -o 区分大小写 gcc main.c // 没有 -o, GCC 生成可执行文件名称默认 a.exe / a.out
-E:指示编译器仅对输入文件进行预处理
gcc -E main.c -o main.i // -E 区分大小写
-S:指示编译器仅对输入文件进行编译
gcc -S main.i -o main.s // -S 区分大小写 gcc -S main.c -o main.s
-c:指示编译器仅对输入文件进行汇编,通常用于编译不包含主程序的子程序文件
gcc -c main.s -o main.o // -c 不区分大小写 gcc -c main.i -o main.o gcc -c main.c -o main.o
-g:指示编译器在生成可执行文件时,同时产生能被GNU调试器GDB使用的调试信息,以调试程序
gcc -g main.c // -g 区分大小写 gcc -g main.i gcc -g main.s gcc -g main.0
-O:指示编译器对源代码进行优化。这些优化在大多数情况下都会使程序执行的更快。所谓优化,就是例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。
# -O0 不做优化 # -O1 默认优化,同时减小代码的长度和执行时间,效果等价于 -O # -O2 除了完成-O1的优化之外,还进行一些额外的调整工作,如指令调整 # -O3 除了完成-O2的优化之外,还进行一些包括循环展开和特性处理相关的优化工作 # -On 使得代码编译的速度变慢,但通常产生的代码执行速度会更快 gcc -g -O3 main.c -o main gcc main.c -o main -g -O0
-l:指示编译器用指定的库来链接程序。-l 参数后紧接的就是库名,在/lib和/usr/lib和/usr/local/lib里的库直接用-l参数就能链接。
# 链接glog库 gcc -lglog main.c
-L:指示编译器在指定的目录寻找库文件。若库文件没放在上面三个目录里,需要使用 -L 参数指定库文件所在目录,-L参数后紧接的就是库文件所在的目录名。
# 链接mytest库,libmytest.so在/home/bing/mytestlibfolder目录下 gcc -L/home/bing/mytestlibfolder -lmytest main.c
-I:指示编译器在指定的目录寻找头文件。/usr/include等系统目录一般是不用指定的,GCC知道去那里找,但是若头文件不在系统目录里,则需要 -I 参数指定。比如头文件放在/myinclude目录里,那编译命令行就要加上-I/myinclude 参数。如果不加则会得到一个”xxxx.h: No such file or directory”的错误。-I 参数可以指定相对路径,比如头文件在当前目录,可以用-I.来指定。
gcc -I./include main.c -o main gcc -I/d/1Document/3Code/test1/include main.c -o main
-Wall:指示编译器打印警告信息
gcc main.c -I./include -Wall
-w:指示编译器关闭警告信息
gcc main.c -w
-std:指示编译器使用指定的标准编译
# 使用 c11 标准编译 gcc main.c -std=c11
-v:指示编译器打印执行时的详细过程以及相关程序的版本号
gcc main.c -v
-D:指示编译器在预编译时定义指定的宏
gcc main.c -o main ./src/NXL_utils.c -I./include -DDEBUG // 编译器在预编译时会定义 DEBUG 宏(真是用处多多)
-M:指示编译器列出文件对其他文件的依赖关系
-MM:指示编译器列出文件对其他文件的依赖关系,但是不包含系统头文件
-MMD:指示编译器列出文件对其他文件的依赖关系,但是不包含系统头文件,并且将依赖关系导入到.d文件里面
gcc -M foo.c gcc -MM foo.c gcc -MMD foo.c
3.2 进阶技巧
3.2.1 直接编译
最初的目录结构:
. ├── include │ ├── NXL_Standard_Types.h │ └── NXL_Utils.h ├── main.c ├── readme.md └── src └── NXL_Utils.c 2 directories, 5 files
最简单的编译 main.c 为可执行文件 main.exe:
gcc main.c ./src/NXL_utils.c -I./include -o main
增加一大堆参数编译 main.c 为可执行文件 main.exe:
gcc main.c ./src/NXL_utils.c -I./include -o main -std=c11 -Wall -g -O2
执行 main.exe:
./main
3.2.2 编译静态库并链接生成可执行文件
在根目录下创建bin子目录用于存储可执行文件:
mkdir -p bin // -p: 确保目录名称存在,不存在就创建一个
在根目录下创建output子目录用于存储编译过程的中间文件(.o / .a):
mkdir -p output
现目录结构:
. ├── bin ├── include │ ├── NXL_Standard_Types.h │ └── NXL_Utils.h ├── main.c ├── output ├── readme.md └── src └── NXL_Utils.c 4 directories, 5 files
进入output子目录,汇编 NXL_Utils.c,生成对应的 NXL_Utils.o 文件:
cd output gcc -c ../src/NXL_Utils.c -I../include // 在output子目录下执行汇编操作
生成静态库libNXL_Utils.a:
ar -rcs libNXL_Utils.a NXL_Utils.o // 在output子目录下生成静态库文件 libNXL_Utils.a
回到上级根目录,链接,生成可执行文件: staticmain
gcc main.c -I./include -L./output -lNXL_Utils -o ./bin/staticmain // 链接生成可执行文件并且输出到bin目录
编译静态库并链接生成可执行文件后的目录结构:
. ├── bin │ └── staticmain.exe ├── include │ ├── NXL_Standard_Types.h │ └── NXL_Utils.h ├── main.c ├── output │ ├── libNXL_Utils.a │ └── NXL_Utils.o ├── readme.md └── src └── NXL_Utils.c 4 directories, 8 files
3.2.3 编译动态库并链接生成可执行文件
现目录结构:
. ├── bin ├── include │ ├── NXL_Standard_Types.h │ └── NXL_Utils.h ├── main.c ├── output ├── readme.md └── src └── NXL_Utils.c 4 directories, 5 files
先生成 NXL_Utils.o 文件:
gcc -c ./src/NXL_Utils.c -I./include -o ./output/NXL_Utils.o
再生成 libNXL_Utils.so 文件:
gcc -shared -fPIC ./output/NXL_Utils.o -o ./output/libNXL_Utils.so
也可以直接生成动态库 libNXL_Utils.so:
gcc -shared -fPIC ./src/NXL_Utils.c -I./include -o ./output/libNXL_Utils.so
链接,生成可执行文件:sharemain
gcc main.c -I./include -L./output -lNXL_Utils -o ./bin/sharemain
编译动态库并链接生成可执行文件后的目录结构:
. ├── bin │ └── sharemain.exe ├── include │ ├── NXL_Standard_Types.h │ └── NXL_Utils.h ├── main.c ├── output │ ├── libNXL_Utils.so │ └── NXL_Utils.o ├── readme.md └── src └── NXL_Utils.c 4 directories, 8 files
3.3 总结
虽然gcc编译指令使用起来非常方便,但也仅仅是针对源文件数量较少,类型较为单一的情况。如果我们要使用gcc来编译一个大型的项目,其中包括了很多的源文件,甚至还有不同类型的源文件时,用gcc命令逐个去编译,就很容易混乱而且工作量大,所以就出现了Make工具。关于Make工具的用法,博主会在该专栏的下一篇文章讲解。