纸上得来终觉浅,绝知此事要躬行。
- 陆游 《冬夜读书示子聿》

gcc使用教程

2025-08-30 22:41 · 31 minutes read

1. 常用编译选项

  1. Wall:启用大部分警告信息,帮助你发现潜在的错误。

    1gcc -Wall hello.c -o hello
    
  2. g:在编译时生成调试信息,适用于使用 gdb 进行调试时。

    1gcc -g hello.c -o hello
    
  3. O:优化选项。O 进行一般优化,O2O3 提供更高级别的优化。

    1gcc -O2 hello.c -o hello
    
  4. std=c99:指定 C 语言标准。例如,使用 C99 标准进行编译。

    1gcc -std=c99 hello.c -o hello
    
  5. I<dir>:指定头文件搜索路径。

    1gcc -I/usr/local/include hello.c -o hello
    
  6. L<dir>:指定库文件搜索路径。

    1gcc -L/usr/local/lib hello.c -o hello
    
  7. l<library>:链接库文件。例如,链接 math 库。

    1gcc hello.c -o hello -lm
    

2. 分步编译讲解

从 C 程序编写到生成最终可执行文件的过程,通常涉及以下几个步骤:预处理(Preprocessing)编译(Compilation)汇编(Assembly)链接(Linking)。每个步骤都有特定的作用和生成的中间文件。

1. 编写源代码(C 程序)

假设你编写了一个简单的 C 程序,保存为 hello.c

1#include <stdio.h>int main() {
2    printf("Hello, World!\n");
3    return 0;
4}

2. 预处理(Preprocessing)

预处理是编译的第一步。它主要进行宏展开、头文件包含、条件编译等操作。预处理的输出是一个扩展后的 C 代码文件,其中所有的 #include#define 和其他预处理指令已经被处理过了。

通过执行 gcc -E hello.c,你可以查看预处理后的代码:

1gcc -E hello.c

预处理操作包括:

例如,hello.c 中的 #include <stdio.h> 会被展开为 stdio.h 中的内容。

预处理完成后,输出文件通常是一个扩展后的 .i 文件。你也可以使用 -E 选项来输出此文件,默认不保存。

3. 编译(Compilation)

编译是将预处理后的 C 代码转化为汇编代码的过程。GCC 通过编译器将 C 代码转换为特定平台的汇编语言代码。

命令如下:

1gcc -S hello.c

这会生成一个 hello.s 文件,这个文件是汇编代码。汇编代码是由特定指令集(如 x86 或 ARM)组成的,依赖于目标平台的架构。

编译的具体操作:

汇编代码文件通常有 .s 后缀。

4. 汇编(Assembly)

在这个步骤中,汇编代码将被转化为机器代码(也称为目标代码)。机器代码是计算机能够理解并执行的二进制指令。这个过程由 汇编器(Assembler)完成。

你可以使用 gcc -c 进行这个步骤,命令如下:

1gcc -c hello.c

这会生成一个名为 hello.o 的目标文件(.o 是目标文件的常见后缀)。目标文件包含机器代码,但它并不具备独立执行的能力,必须经过链接(Linking)过程。

汇编过程:

5. 链接(Linking)

链接是将多个目标文件(.o 文件)和所需的库文件合并成一个最终的可执行文件的过程。链接器(Linker)负责将程序中引用的外部符号(如库函数)与其实际定义(库或其他目标文件中的实现)进行匹配。

假设你有多个目标文件,比如 file1.ofile2.o,你可以运行:

1gcc file1.o file2.o -o my_program

对于单一文件(例如 hello.o),你也可以直接运行:

1gcc hello.o -o hello

这会将 hello.o 与标准库(如 C 库)链接起来,生成一个名为 hello 的可执行文件。

链接过程包括:

链接后,最终生成一个可执行文件(如 hello)。这个文件可以直接运行。

6. 可执行文件

最终的可执行文件(在 Linux 中通常没有后缀,Windows 上可能是 .exe)已经准备好了。可以通过以下命令运行它:

1./hello

如果没有出错,它会输出:

1Hello, World!

总结流程

  1. 编写 C 代码hello.c)。
  2. 预处理gcc -E hello.c):处理宏定义、头文件和注释。
  3. 编译gcc -S hello.c):将 C 代码转换为汇编语言。
  4. 汇编gcc -c hello.c):将汇编代码转换为目标文件(.o)。
  5. 链接gcc hello.o -o hello):将目标文件和库文件链接成可执行文件。
  6. 执行./hello):运行生成的可执行文件。

3. 编译静态库

静态库(Static Library)是一个包含目标文件(.o 文件)集合的归档文件,它可以被多个程序共享使用,但在链接时将这些目标文件嵌入到最终的可执行文件中。静态库通常以 .a(在 Linux 或 macOS 上)或 .lib(在 Windows 上)为文件后缀。

静态库的构建过程

1. 编写源代码

首先,我们需要一些 C 源代码,假设你有一个简单的库代码 math.c 和它的头文件 math.h

math.c:

1#include "math.h"
2int add(int a, int b) {
3    return a + b;
4}
5
6int subtract(int a, int b) {
7    return a - b;
8}

math.h:

1#ifndef MATH_H
2#define MATH_H
3
4int add(int a, int b);
5int subtract(int a, int b);
6
7#endif

2. 编译源文件为目标文件(.o 文件)

在编译静态库之前,首先需要将源代码文件编译为目标文件。你可以使用以下命令将 math.c 编译为目标文件 math.o

1gcc -c math.c -o math.o

此时,math.o 是一个包含 addsubtract 函数实现的目标文件。

3. 创建静态库

接下来,你将这些目标文件打包成一个静态库。使用 ar 命令将目标文件 math.o 打包成一个静态库 libmath.a

1ar rcs libmath.a math.o

此时,libmath.a 文件就生成好了,它是一个包含 math.o 中目标文件内容的静态库。

4. 使用静态库

静态库文件可以在其他程序中使用。假设你有一个主程序 main.c,你希望使用 libmath.a 中的 addsubtract 函数。

main.c:

1#include <stdio.h>
2#include "math.h"
3int main() {
4    int a = 10, b = 5;
5    printf("Addition: %d\n", add(a, b));
6    printf("Subtraction: %d\n", subtract(a, b));
7    return 0;
8}

要使用静态库,首先需要编译 main.c 并将 libmath.a 链接到程序中。可以使用以下命令:

1gcc main.c -L. -lmath -o main

注意:静态库通常存放在 /usr/lib/usr/local/lib 等标准路径中,如果你的库文件存放在其他地方(例如当前目录),需要通过 -L 参数指定路径。

5. 运行程序

成功编译并链接后,你可以运行生成的可执行文件:

1./main

输出:

1Addition: 15
2Subtraction: 5

静态库的优缺点

优点:

  1. 独立性:静态库在编译时就与应用程序链接,生成的可执行文件包含了所有依赖的库代码,不需要运行时再去寻找库文件。
  2. 版本兼容性:静态库是已编译的目标文件,避免了在运行时出现动态库版本不匹配的问题。

缺点:

  1. 文件体积大:因为所有库代码都被嵌入到最终的可执行文件中,所以静态链接的程序比动态链接的程序要大。
  2. 更新不便:如果库文件有更新,必须重新编译和链接依赖这个库的所有程序。相对于动态库,静态库在更新时不够灵活。
  3. 内存占用:如果多个程序使用相同的静态库,每个程序都会包含库的副本,造成内存浪费。

6. 静态库与共享库的区别

总结

编译静态库的步骤:

  1. 编写源代码,并将源代码编译为目标文件(.o)。
  2. 使用 ar 命令将目标文件打包成静态库(.a)。
  3. 在应用程序中使用静态库,使用 L 指定库文件路径,使用 l 指定库名称。
  4. 编译并链接应用程序生成最终的可执行文件。

通过静态库,可以将多个目标文件进行组合,方便共享和重用代码,但同时也会增加最终程序的大小。

4. 编译共享库

编译共享库(Shared Library)是将代码编译成可以在多个程序之间共享的共享库文件,这种库在程序运行时被加载,而不是在编译时链接到程序中。共享库通常以 .so(在 Linux 上)或 .dll(在 Windows 上)为文件后缀。

共享库的构建过程

下面,我们将通过一个简单的例子来讲解如何编译共享库。

1. 编写源代码

首先,我们编写一个简单的库代码,假设库的名字是 math.c

math.c:

1#include "math.h"
2
3int add(int a, int b) {
4    return a + b;
5}
6
7int subtract(int a, int b) {
8    return a - b;
9}

math.h

1#ifndef MATH_H
2#define MATH_H
3
4int add(int a, int b);
5int subtract(int a, int b);
6
7#endif

2. 编译源代码为共享库

要将上述代码编译成共享库,使用 gcc-shared 选项。在 Linux 上,共享库通常以 .so 后缀结尾。

使用以下命令来编译 math.c 成共享库 libmath.so

1gcc -shared -fPIC math.c -o libmath.so

执行完该命令后,你将得到一个名为 libmath.so 的共享库文件。

3. 使用动态库

接下来,你可以编写一个程序来使用这个共享库。假设我们有一个主程序 main.c,它调用了 libmath.so 中的函数。

main.c

1#include <stdio.h>
2#include "math.h"
3
4int main() {
5    int a = 10, b = 5;
6    printf("Addition: %d\n", add(a, b));
7    printf("Subtraction: %d\n", subtract(a, b));
8    return 0;
9}

4. 编译并链接程序

编译并链接程序时,你需要告诉编译器如何找到共享库。可以通过 -L 参数指定动态库所在目录,通过 -l 参数指定链接的库名(省略 lib 前缀和 .so 后缀)。

例如,如果 libmath.somain.c 位于当前目录下,执行以下命令来编译和链接:

1gcc main.c -L. -lmath -o main

5. 设置共享库的路径

运行程序时,操作系统需要知道如何找到共享库。通常,动态库文件会安装在标准路径(如 /usr/lib/lib)下。如果动态库不在标准路径中,你需要通过设置环境变量来告诉操作系统在哪里查找它。

1export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

这里 . 表示当前目录。你可以在终端执行上述命令,或者将其写入 .bashrc 文件以便每次启动时自动设置。

6. 运行程序

现在,你可以运行编译后的程序:

1./main

输出将是:

1Addition: 15
2Subtraction: 5

动态库的优缺点

优点:

  1. 内存共享:多个程序可以共享同一个动态库副本,节省内存空间。
  2. 更新灵活性:如果动态库有更新,只需要更新库文件本身,所有依赖这个库的程序会自动使用新版本的库,而不需要重新编译。
  3. 减少可执行文件体积:动态库没有被编译到最终的可执行文件中,因此生成的可执行文件较小。

缺点:

  1. 依赖管理:程序运行时需要确保库的正确版本在系统中,并且动态库文件能够被找到。如果找不到库,程序会启动失败。
  2. 运行时开销:动态库在运行时加载,因此相较静态库,程序启动时可能稍慢一些。

动态库与静态库的对比

总结

编译动态库的步骤:

  1. 编写源代码,使用 fPIC 选项生成位置无关代码,使用 shared 选项生成动态库(.so)。
  2. 在应用程序中使用动态库,使用 L 指定库路径,使用 l 指定库名称。
  3. 通过设置环境变量 LD_LIBRARY_PATH 或编辑 /etc/ld.so.conf 来指定动态库的查找路径。
  4. 编译并运行程序,确保动态库能够在运行时被正确加载。

end.


🌙