一、常用编译选项
-
基本编译
gcc [input].c -o [output]
示例:gcc hello.c -o hello # 将 hello.c 编译为可执行文件 hello ./hello # 运行程序
-
分步编译
- 预处理:
-E
(生成.i
文件)gcc -E hello.c -o hello.i
- 编译为汇编:
-S
(生成.s
文件)gcc -S hello.i -o hello.s
- 汇编为目标文件:
-c
(生成.o
文件)gcc -c hello.s -o hello.o
- 链接为可执行文件:
gcc hello.o -o hello
- 预处理:
-
调试信息
-g
选项生成带调试信息的文件(供 GDB 使用):gcc -g test.c -o test_debug
-
优化选项
-O1
,-O2
,-O3
表示优化级别(-O2
最常用):gcc -O2 main.c -o optimized_program
-
警告选项
-Wall
启用所有常见警告-Werror
将警告视为错误
gcc -Wall -Werror strict_code.c -o strict_program
-
链接库文件
-l
指定库名(如数学库-lm
)-L
指定库文件路径
gcc calc.c -lm -L/usr/local/lib -o calc
二、示例场景
1. 编译多文件项目
# 编译两个文件并链接
gcc main.c utils.c -o app
2. 生成静态库
# 1. 编译为 .o 文件
gcc -c mylib.c -o mylib.o
# 2. 打包为静态库
ar rcs libmylib.a mylib.o
# 3. 使用静态库
gcc main.c -L. -lmylib -o static_app
3. 生成动态库(共享库)
# 1. 编译为位置无关代码(-fPIC)
gcc -fPIC -c mylib.c -o mylib.o
# 2. 生成动态库(-shared)
gcc -shared mylib.o -o libmylib.so
# 3. 使用动态库(需设置库路径)
gcc main.c -L. -lmylib -o dynamic_app
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH # 临时添加库路径
三、C语言是如何编译的
1. 预处理
类比:准备食材
想象你在做一道复杂的菜,首先你需要把所有的食材准备好,比如切菜、洗菜、调味等。预处理阶段就像是这个准备食材的过程。
具体过程
预处理器(cpp
)会处理源代码文件中的预处理指令,比如 #include
、#define
和 #ifdef
等。它会将这些指令替换成实际的代码或数据。
-
#include
:将指定的头文件内容插入到当前文件中。 -
#define
:定义宏,替换代码中的宏定义。 -
#ifdef
/#ifndef
:条件编译,根据条件决定是否包含某段代码。
示例
#include <stdio.h>
#define PI 3.14159int main() {printf("The value of PI is %f", PI);return 0;
}
预处理后,#include <stdio.h>
会被替换成 stdio.h
文件的内容,PI
会被替换成 3.14159
。
2. 编译
类比:编写食谱
现在你已经准备好了食材,接下来你需要编写一个详细的食谱,告诉厨师每一步该怎么做。编译阶段就像是这个编写食谱的过程。
具体过程
编译器(gcc
)会将预处理后的源代码转换成汇编代码。这个过程会检查语法错误、类型检查等。
- 语法检查:确保代码符合C语言的语法规则。
- 类型检查:确保变量和函数的类型使用正确。
示例
预处理后的代码会被编译成类似以下的汇编代码(具体内容取决于编译器和平台):
main:push rbpmov rbp, rspsub rsp, 16mov edi, OFFSET FLAT:.LC0call puts@PLTmov eax, 0leaveret
3. 汇编
类比:将食谱翻译成操作步骤
现在你已经有了详细的食谱,接下来需要将这些食谱翻译成具体的操作步骤,让厨师可以一步步执行。汇编阶段就像是这个翻译过程。
具体过程
汇编器(as
)会将汇编代码转换成机器码,生成目标文件(.o
文件)。这个过程不会进行任何优化,只是简单地将汇编指令转换成机器码。
示例
上面的汇编代码会被转换成机器码,生成一个目标文件 main.o
。
4. 链接
类比:将多个菜谱合并成一本完整的烹饪书
现在你已经有了每道菜的具体操作步骤,接下来需要将这些步骤合并成一本完整的烹饪书,确保所有的步骤和食材都能协调工作。链接阶段就像是这个合并过程。
具体过程
链接器(ld
)会将多个目标文件和库文件合并成一个可执行文件。这个过程会解析所有的符号引用,确保每个函数和变量的调用都能找到正确的定义。
- 符号解析:确保每个函数和变量的调用都能找到正确的定义。
- 重定位:将符号的地址修正为最终的内存地址。
假设你有两个源文件 main.c
和 utils.c
,编译后会生成 main.o
和 utils.o
。链接器会将这两个目标文件合并成一个可执行文件 program
。
总结
- 预处理:准备食材,处理
#include
、#define
等指令。 - 编译:编写食谱,将预处理后的代码转换成汇编代码。
- 汇编:将食谱翻译成操作步骤,生成目标文件。
- 链接:合并多个菜谱,生成最终的可执行文件。
5.函数库
静态库
定义:静态库是将一系列的目标文件(.o
文件)打包成一个单独的文件,在编译链接时,链接器会把静态库中被程序使用的代码直接复制到最终的可执行文件中。静态库的文件扩展名通常是 .a
静态连接:
在静态连接中,编译器和链接器在编译阶段就把程序所需要使用的库代码(静态库)直接合并到最终生成的可执行文件中。
- 创建静态库:首先要有多个目标文件。假设我们有两个源文件
add.c
和sub.c
,分别实现加法和减法功能。
// add.c
int add(int a, int b) {return a + b;
}// sub.c
int sub(int a, int b) {return a - b;
}
使用 `gcc` 编译这两个源文件生成目标文件:
gcc -c add.c sub.c
然后使用 `ar` 工具创建静态库:
ar rcs libmath.a add.o sub.o
这里 ar
是归档工具,r
表示将目标文件插入到归档文件中,c
表示如果归档文件不存在则创建它,s
表示写入一个目标文件索引。
这样来使用静态库
gcc main.c -L. -lmath -o main
-L.
表示在当前目录下查找库文件,-lmath
表示链接名为 libmath.a
的静态库(注意,-l
后面跟的是库名去掉 lib
前缀和 .a
后缀)。
优点
- 独立性:可执行文件包含了静态库的所有代码,不依赖外部的库文件,移植方便。
- 性能:由于代码已经直接嵌入到可执行文件中,运行时不需要额外的加载时间,执行效率较高。
缺点
- 文件体积大:每个使用静态库的可执行文件都会包含一份库代码的副本,会导致可执行文件体积变大。
- 更新困难:如果静态库有更新,所有使用该库的可执行文件都需要重新编译链接。
动态库
定义:动态库是在程序运行时才被加载到内存中供程序使用的库。多个程序可以共享同一个动态库的内存副本。是C/C++或者其他第三方软件提供的所有方法的集合,被所有程序以链接的方式关联起来。动态库的文件扩展名通常是 .so
动态链接:
在动态连接中,编译阶段只是记录程序需要使用哪些动态库(.so
文件),然后再需要时加载,并不会把库代码复制到可执行文件中。
- 创建动态库:同样使用之前的
add.c
和sub.c
源文件,先编译成目标文件:
gcc -c -fPIC add.c sub.c
-fPIC
表示生成位置无关代码,这是创建动态库所必需的。
使用 gcc
创建动态库:
gcc -shared -o libmath.so add.o sub.o
-shared
表示生成共享库。
使用动态库:
gcc main.c -L. -lmath -o main
编译命令和静态库类似,但在运行可执行文件时,需要确保系统能找到动态库。可以通过设置 LD_LIBRARY_PATH
环境变量来指定动态库的搜索路径:
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
优点
- 节省内存:多个程序可以共享同一个动态库的内存副本,减少了内存的使用。
- 更新方便:如果动态库有更新,只需要替换动态库文件,不需要重新编译使用该库的所有程序。
缺点
- 依赖问题:可执行文件依赖于外部的动态库文件,如果动态库文件丢失或版本不兼容,程序可能无法正常运行。
- 加载时间:程序运行时需要加载动态库,会有一定的加载时间开销。
注意:linux系统中认为后缀无意义,但不是代表gcc认为后缀无意义,也就是说我们后缀不对的话gcc编译会出错的。