# 编译
# 编译器
- C 语言是编译型语言。源代码不能直接作为程序运行,需要用 gcc 等工具,编译成可执行文件(内含二进制机器代码),才能运行。
- 负责编译工作的工具有多种,统称为编译器。
常见的几种 C 语言编译器:
- gcc(GNU C Compiler)
- 最初只支持编译 C 语言的源代码,后来还支持 C++、Objective-C、Fortran、Java 等语言,并且被移植到多个平台。
- 是 GNU 编译器集合(GNU Compiler Collection)的一部分,后者是类 Unix 系统上的标准编译器。
- g++
- gcc 提供的另一种编译命令,本质上也是 gcc 编译器。
- 支持编译 C、C++ 语言,但默认将源代码当作 C++ 语言处理。
- MSVC(Microsoft Visual C++)
- 支持编译 C、C++ 语言。
- Clang
- 支持编译 C、C++、Objective-C 等语言。
- 基于 LLVM 编译器运行。
# gcc
gcc 基本的编译流程如下:
预处理(Pre-Processing):主要是处理 C 语言源文件中的预处理命令,生成一个中间文件
*.i
。gcc -E test.c -o test.i
-o
选项是控制生成的文件名。
汇编(Assembling):由中间文件
*.i
生成汇编语言文件*.s
。gcc -S test.i -o test.s
编译(Compiling):由汇编语言文件
*.s
生成目标文件。gcc -c test.s -o test.o
- 目标文件又称为对象文件,它是二进制文件,与最终的可执行文件很像。
链接(Linking):由目标文件生成最终的可执行文件。
gcc test.o -o 1
上述四个步骤可以简化为一个步骤,称为 构建(Build):
gcc test.c -o test -I /root/test/include # 添加头文件的检索目录 -L /root/test/lib # 添加库文件的检索目录 -l lib1 # 链接一个名为 lib1 的库文件 -static # 只使用静态链接库
- 使用了头文件或库文件时,gcc 会先检索-I、-L 指定的目录,再检索环境变量 PATH 路径。
编译时,可能遇到几种报错:
- error :致命错误,会导致编译失败。
- warning :警告,此时程序可以编译、链接,但运行时可能出错。
- 比如用户定义了一个变量,却没有使用它,则编译时会警告:unused variable
- 比如 ANSI C 标准没有规定,访问数组越界的结果是什么。这种操作称为 undefined behavior ,具体结果由各个编译器决定,可能报错,也可能不报错。
# 构建
- 当项目中源文件较多、引用关系较复杂时,用编译器逐个编译比较麻烦,建议使用构建工具。
- 构建工具能调用编译器,自动化地编译大量源文件,并进行链接、打包等操作。
- 构建工具举例:
- make :通常调用 gcc 作为编译器,根据 Makefile 文件,构建项目。
- cmake :可以根据 CMakeList.txt 文件自动生成 Makefile 文件。在 Windows 上则是生成 visual studio 的 project 文件。
- qmake :用于生成 Qt 项目的 Makefile 文件。
- 有的 IDE 本身提供了构建项目的功能,或者调用某种构建工具。
# make
:一个命令行工具,用于构建 C 语言项目。
- 命令:
make # 开始编译(默认使用当前目录下的 Makefile) clean # 删除所有被 make 创建的文件 install # 编译并安装到系统中 uninstall # 卸载
- make 在执行时,会读取 Makefile 文件中的指令,获取模块间的依赖关系,判断哪些文件过时了需要重新编译,然后生成中间代码或最终的可执行程序。
# 跨平台
假设你在主机 A 上编写了一个 C 语言程序,如何分享给主机 B 使用?
- 可以将主机 A 上编译的二进制文件,拷贝到主机 B 。
- 优点:
- 主机 B 拿到文件,可以直接运行。
- 缺点:
- 主机 B ,可能与主机 A 差异大(比如操作系统不同、CPU 架构不同),不能运行主机 A 编译的二进制文件,这称为不兼容。
- 优点:
- 可以将主机 A 上的源文件,拷贝到主机 B 。
- 优点:
- 即使主机 B ,与主机 A 差异大,也可以重新编译生成二进制文件,运行效果与主机 A 相同。(除非两个主机的编译器,差异大)
- 缺点:
- 主机 B 拿到源文件,需要编译,才能运行。这增加了耗时,还需要安装编译工具。
- 优点:
- 可以将主机 A 上编译的二进制文件,拷贝到主机 B 。
假设将主机 A 上编译的二进制文件,拷贝到主机 B ,如何保证兼容性(又称为跨平台)?
- 有的编译工具,支持交叉编译。在主机 A 上,刻意编译出兼容主机 B 的二进制文件。
- 比如主机 A 是 64bit CPU ,可以编译出兼容 32bit CPU 的二进制文件,能在主机 B 上运行,不能在主机 A 上运行。
C 语言的跨平台挺麻烦,但其它高级编程语言擅长跨平台。比如 Java 通过 JVM 的机制与底层平台解耦,只需编译一次,就可以拷贝到多个平台上直接运行。
# 文件格式
在类 Unix 系统中:
- 可执行文件、库文件的内容格式,都遵循 ELF(Executable and Linkable Format,可执行和可链接格式)。
- 可执行文件通常没有扩展名,或者扩展名为
.bin
。 - 目标文件的扩展名为
.o
。 - 动态链接库的扩展名为
.a
。 - 静态链接库的扩展名为
.so
。
在 Windows 系统中:
- 可执行文件的扩展名为
.exe
。 - 目标文件的扩展名为
.obj
。 - 动态链接库的扩展名为
.lib
。 - 静态链接库的扩展名为
.dll
。
- 可执行文件的扩展名为
# 库文件
:包含了一些二进制代码,可以被程序导入、调用。
文件名的格式为
lib + 库名 + 扩展名
。用途:
- 可以将程序的代码分离成多个模块,方便调整。
- 可以将一个程序的部分代码解耦出来,保存在库文件中,给另一个程序调用,提高代码的复用性。
- 可以将某种语言的程序保存在库文件中,给另一种语言的程序调用,提高代码的通用性。
分类:
- 动态链接库
- :又称为共享库,在程序运行时才被动态导入。
- 这会减小程序文件的体积,但在运行时需要花时间加载。
- 静态链接库
- :在程序编译时的 "链接" 阶段被导入,保留在可执行文件中。
- 这会增加程序文件的体积,但运行时不必花时间加载。
- 动态链接库
例:从目标文件生成动态链接库
gcc -shared test.o... -o test.so # 可以调用多个目标文件,还可以从.c 文件生成 -fPIC # 使生成的代码全部采用相对地址,这样就可以被加载到内存的任意位置 -shared # 生成共享动态链接库
- gcc 在编译时会根据库名去寻找相应的库文件,且优先使用动态链接库,当动态链接库文件不存在时才寻找相同库名的静态链接库。
例:从目标文件生成静态链接库
ar -rc lib1.a 1.o ... # 可以加上多个目标文件
# 相关概念
- MinGW(Minimalist GNU For Windows)
- 该项目将 GNU 编译器集合移植到了 Windows 上,让用户可以在 Windows 上使用 GNU 编译器(一般是在 DOS 窗口中使用),生成扩展名为 .exe 的可执行文件。
- 在编译时,会将源代码中调用的 Linux 库文件、API 转换成 Windows 库文件、API 。
- Msys
- :MinGW 的子项目,提供了一个在 Windows 上运行的简单 Linux 系统。
- Cygwin
- :一个可以在 Windows 上运行的简单 Linux 系统。
- Cygwin 将一些 Linux 特有的库文件、API 移植到了 Windows 上,模拟出 Linux 的运行环境。被 Cygwin 编译出的程序在运行时会调用这些库文件。
- 在 2020 年代以前,很多人会使用 Cygwin ,从而在 Windows 系统上运行 Linux 软件。但目前 Cygwin 很少被使用,因为 Windows 提供了 Windows 的 Linux 子系统(Windows Subsystem for Linux ,WSL),功能更强。
- Visual Studio
- :微软公司提供的一个重量级、功能丰富的 IDE ,支持 C、C++、C# 等语言的开发、调试、构建。
- 可用于在 Windows 上编译 C、C++ 代码,而且比 MinGW 的功能更强。
- 提供了 MSVC 编译器,以及 msbuild、cl、link、nmake 等命令行工具。
- Qt
- :一个跨平台的 C++ 开发框架。
- 提供了一些与平台解耦的 C++ 库文件,因此在该框架中开发的程序可以直接编译成 Linux、Windows、MacOS 等多种平台的可执行文件。