de风——【从零开始学习Linux】(五):gcc编译器的基本使用

前言

在上一篇文章中,我们学会了使用 Vim 编辑器在服务器上编写代码。但是,写完代码之后呢?我们在 Windows 上用 VS Code、Dev-C++ 时,点一下"编译运行"按钮就能看到结果,但在 Linux 命令行下,谁来帮我们把 .c 源代码变成可执行程序呢?

答案就是------编译器 。而在 Linux 世界中,最经典、最常用的编译器就是 GCC(GNU Compiler Collection)。

很多新手可能会觉得"编译"是个黑盒:输入代码,输出程序,中间发生了什么完全不知道。其实,编译过程是非常清晰的,一旦理解了它的工作原理,你就能更好地排查代码错误、优化程序性能。本文将从零开始,带你一步步掌握 GCC 的基本使用。


思维导图

下面是本文内容的完整思维导图,建议先整体浏览一遍,建立全局认知后再逐节学习。


一、GCC 简介

1.1 什么是 GCC?

语法形式
bash 复制代码
gcc [选项] 要编译的文件 [选项] [目标文件]

GCC(GNU Compiler Collection,GNU 编译器套件)是 GNU 项目推出的编译器集合,最初名为 GNU C Compiler,只能编译 C 语言。后来不断发展,现在已经支持 C、C++、Objective-C、Fortran、Ada、Go 等多种语言。

在 Ubuntu 上,GCC 通常预装了 gcc 命令用于编译 C 程序,g++ 命令用于编译 C++ 程序。

新手提示:如果你的服务器上没有安装 GCC,可以使用以下命令安装:

bash

复制代码
sudo apt update
sudo apt install -y gcc g++

安装完成后,用 gcc --version 查看版本号,确认安装成功。

示例

------演示 GCC 版本查看与第一个编译程序

复制代码
# 查看 GCC 版本
gcc --version
# 输出示例:
# gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
# ...

# 用 Vim 创建第一个 C 程序
vim hello.c
cpp 复制代码
// hello.c 的内容
#include <stdio.h>

int main()
{
    printf("Hello, GCC! I am learning Linux.\n");
    return 0;
}
复制代码
# 编译 hello.c,生成可执行文件 hello
gcc hello.c -o hello

# 运行程序
./hello
# 输出:Hello, GCC! I am learning Linux.

新手理解gcc hello.c -o hello 这条命令做了两件事------把 hello.c(源代码)编译成机器能理解的二进制指令,并把结果输出(-o)到名为 hello 的文件中。./hello 中的 ./ 表示"当前目录下",Linux 不会自动在当前目录找可执行文件,所以必须加上。


1.2 GCC 与 g++ 的区别

语法形式
复制代码
gcc 源文件.c -o 可执行文件    # 编译 C 程序
g++ 源文件.cpp -o 可执行文件  # 编译 C++ 程序
功能

gccg++ 都是 GCC 编译器套件的前端命令,主要区别在于:

对比项 gcc g++
默认语言 C C++
编译 .c 文件 当作 C 代码 当作 C++ 代码
编译 .cpp 文件 需要加 -lstdc++ 链接 C++ 库 自动链接 C++ 标准库
链接阶段 不会自动链接 C++ 标准库 自动链接 C++ 标准库
示例

------演示 gcc 和 g++ 编译 C/C++ 程序的区别

复制代码
# C 程序用 gcc 编译(标准做法)
vim test.c
cpp 复制代码
// test.c
#include <stdio.h>
int main() {
    printf("This is C program.\n");
    return 0;
}
复制代码
gcc test.c -o test_c      # 正常编译
./test_c                   # 输出:This is C program.

# C++ 程序用 g++ 编译(标准做法)
vim test.cpp
cpp 复制代码
// test.cpp
#include <iostream>
int main() {
    std::cout << "This is C++ program." << std::endl;
    return 0;
}
cpp 复制代码
g++ test.cpp -o test_cpp  # 正常编译
./test_cpp                # 输出:This is C++ program.

# 用 gcc 编译 C++ 程序会报错(找不到 C++ 标准库)
gcc test.cpp -o test_cpp  # 报错!
# 需要手动指定链接 C++ 库:
gcc test.cpp -o test_cpp -lstdc++  # 这样才行

建议 :C 程序就用 gcc,C++ 程序就用 g++,各司其职最省心。


二、GCC 编译的四大步骤

这是本文最核心 的内容。很多人以为编译就是"一步到位",实际上 GCC 把编译过程分为了四个阶段预处理 → 编译 → 汇编 → 链接。每个阶段都可以单独执行,也可以一步完成。

复制代码
hello.c  ──预处理(-E)──>  hello.i  ──编译(-S)──>  hello.s
                                                      │
可执行文件  <──链接────  hello.o  <──汇编(-c)──── ─────┘

2.1 预处理(Preprocessing)

语法形式
复制代码
gcc -E 源文件.c -o 输出文件.i
功能

预处理阶段主要做以下几件事:

  1. 头文件展开 :把 #include <stdio.h> 引入的头文件内容插入到源文件中
  2. 宏替换 :把代码中用 #define 定义的宏全部替换为实际内容
  3. 去注释 :删除代码中的所有注释(///* */
  4. 条件编译 :根据 #ifdef#ifndef 等指令决定保留哪些代码

一句话理解 :预处理就是"文本替换"------把所有以 # 开头的预处理指令处理完毕,生成一个"纯净"的 C 代码文件。

示例

------演示 -E 预处理:查看头文件展开和宏替换的效果

复制代码
# 创建一个测试文件
vim macro.c
cpp 复制代码
// macro.c
#include <stdio.h>

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define PI 3.14159

// 这是一个注释,预处理后会被删除

int main()
{
    int x = 10, y = 20;
    printf("Max = %d\n", MAX(x, y));
    printf("PI = %f\n", PI);
    return 0;
}
cpp 复制代码
# 只执行预处理,生成 .i 文件
gcc -E macro.c -o macro.i

# 查看 .i 文件的前几行(头文件展开后内容非常多)
head -20 macro.i
# 你会看到 stdio.h 的内容被完整插入进来了

# 查看文件末尾(我们写的代码部分)
tail -15 macro.i
# 输出(注意:注释没了,宏被替换了):
# int main()
# {
#     int x = 10, y = 20;
#     printf("Max = %d\n", ((x) > (y) ? (x) : (y)));
#     printf("PI = %f\n", 3.14159);
#     return 0;
# }

# 对比源文件和预处理后文件的大小
wc -l macro.c macro.i
# macro.c 的代码只有十几行
# macro.i 可能有上千行(因为 stdio.h 被完整展开了)

实用场景 :当你的代码编译报错,提示某个函数"未定义"时,可以用 gcc -E 查看预处理后的文件,确认头文件是否被正确包含。


2.2 编译(Compilation)

语法形式
cpp 复制代码
gcc -S 预处理文件.i -o 输出文件.s
# 也可以直接从 .c 文件开始(自动先预处理)
gcc -S 源文件.c -o 输出文件.s
功能

编译阶段是整个编译过程的核心,主要做以下工作:

  1. 语法检查:检查代码是否符合 C 语言语法规范
  2. 语义分析:检查类型是否匹配、变量是否定义等
  3. 代码优化:根据优化级别对代码进行优化
  4. 生成汇编代码:将 C 代码翻译成汇编语言

一句话理解:编译就是"翻译"------把人类能看懂的 C 语言翻译成机器更接近的汇编语言。如果你的代码有语法错误,就会在这个阶段报错。

示例

------演示 -S 编译:查看 C 代码变成汇编代码

cpp 复制代码
# 从 .i 文件编译为汇编代码
gcc -S macro.i -o macro.s

# 也可以直接从 .c 文件开始(等价于先 -E 再 -S)
gcc -S macro.c -o macro.s

# 查看生成的汇编代码
cat macro.s
# 输出示例(汇编代码):
#     .file   "macro.c"
#     .text
#     .section .rodata
# .LC1:
#     .string "Max = %d\n"
# .LC2:
#     .string "PI = %f\n"
#     .text
#     .globl  main
#     .type   main, @function
# main:
#     pushq   %rbp
#     movq    %rsp, %rbp
#     ...

------演示编译阶段的语法检查功能

复制代码
# 故意写一个有语法错误的程序
vim error.c
cpp 复制代码
// error.c ------ 故意少了一个分号
#include <stdio.h>

int main()
{
    printf("Hello\n")   // 这里少了分号
    return 0;
}
复制代码
# 编译,会报语法错误
gcc -S error.c -o error.s
# 报错信息:
# error.c: In function 'main':
# error.c:6:5: error: expected ';' before 'return'
#     6 |     return 0;
#       |     ^~~~~~

新手必看 :编译器的报错信息非常有价值!它会告诉你哪个文件、第几行、什么错误 。学会阅读报错信息,是排查 Bug 的第一步。上面这个例子清楚地告诉我们:error.c 第 6 行,return 之前缺少分号。


2.3 汇编(Assembly)

语法形式
复制代码
gcc -c 汇编文件.s -o 输出文件.o
# 也可以直接从 .c 或 .i 文件开始
gcc -c 源文件.c -o 输出文件.o
功能

汇编阶段把汇编代码(.s 文件)翻译成机器可以识别的二进制指令 ,生成目标文件(.o 文件)。

这个阶段几乎不会出错,因为汇编语言和机器指令基本是一一对应的。

一句话理解 :汇编就是"翻译最后一公里"------把汇编语言翻译成 0 和 1 组成的机器码。生成的 .o 文件是二进制文件,用普通文本编辑器打开会看到乱码。

示例

------演示 -c 汇编:生成二进制目标文件

复制代码
# 从 .s 文件汇编为目标文件
gcc -c macro.s -o macro.o

# 也可以直接从 .c 文件一步到位(预处理+编译+汇编)
gcc -c macro.c -o macro.o

# 查看生成的 .o 文件
ls -l macro.o
# -rw-rw-r-- 1 user user 1544 ... macro.o

# .o 文件是二进制文件,直接 cat 会看到乱码
# 用 file 命令查看文件类型
file macro.o
# 输出:macro.o: ELF 64-bit LSB relocatable, x86-64, ...
# "relocatable" 表示这是可重定位的目标文件,还不能直接运行

# 尝试运行 .o 文件(会失败)
./macro.o
# 报错:bash: ./macro.o: cannot execute binary file
# 因为还没有链接,不是完整的可执行文件

理解 .o 文件.o 文件就像是一个"半成品"------机器码已经生成好了,但还没有和其他组件(如标准库函数)拼接在一起,所以不能运行。就像造车的零件已经做好了,但还没组装成整车。


2.4 链接(Linking)

语法形式
复制代码
gcc 目标文件.o -o 可执行文件
# 也可以直接从 .c 文件一步到位(完成全部四个步骤)
gcc 源文件.c -o 可执行文件
功能

链接阶段把一个或多个目标文件(.o)与库文件拼接在一起,生成最终可以执行的程序。

链接主要做两件事:

  1. 符号解析 :把代码中调用的外部函数(如 printf)与库文件中的实现"对接"起来
  2. 地址重定位:把各个目标文件的代码和数据安排到最终的内存地址中

一句话理解:链接就是"组装"------把各个零件(目标文件)和说明书(库文件)拼装成一辆可以开的"车"(可执行程序)。

示例

------演示链接:将 .o 文件链接为可执行程序

复制代码
# 把 macro.o 链接成可执行文件 macro
gcc macro.o -o macro

# 运行
./macro
# 输出:
# Max = 20
# PI = 3.141590

# 用 file 命令查看可执行文件类型
file macro
# 输出:macro: ELF 64-bit LSB shared object, x86-64, ...
# 注意这里变成了 "shared object"(共享对象),不再是 "relocatable"

------演示一步编译(最常用的方式)

复制代码
# 从 .c 文件直接编译为可执行文件(自动完成四个步骤)
gcc macro.c -o macro

# 运行
./macro
# 输出:
# Max = 20
# PI = 3.141590

日常使用 :平时写代码时,我们通常直接用 gcc hello.c -o hello 一步搞定。只有需要查看中间过程(调试、学习)时,才会分步执行。


四步编译总结

步骤 命令 输入 输出 做了什么
预处理 gcc -E .c .i 展开头文件、宏替换、去注释、条件编译
编译 gcc -S .i .s 语法检查、生成汇编代码
汇编 gcc -c .s .o 翻译成机器码(二进制目标文件)
链接 gcc .o 可执行文件 链接库文件、生成可执行程序

记忆口诀"ESC" → 预处理(-E )、编译(-S )、汇编(-c),最后链接不需要选项。正好对应键盘上的 Esc 键!


三、常用编译选项

除了 -E-S-c 这些控制编译阶段的选项外,GCC 还有很多实用的编译选项。下面介绍最常用的几个。

3.1 -o:指定输出文件名

语法形式
复制代码
gcc 源文件.c -o 输出文件名
功能

-o(output)选项用于指定编译后生成的可执行文件名。如果不加 -o,GCC 默认生成名为 a.out 的可执行文件。

示例

------演示 -o 指定输出文件名 vs 默认文件名

复制代码
# 不加 -o,默认生成 a.out
gcc hello.c
ls
# a.out  hello.c

./a.out
# 输出:Hello, GCC! I am learning Linux.

# 加 -o 指定输出文件名
gcc hello.c -o hello
ls
# a.out  hello  hello.c

./hello
# 输出:Hello, GCC! I am learning Linux.

# -o 也可以放在源文件前面
gcc -o myhello hello.c
./myhello
# 输出:Hello, GCC! I am learning Linux.

建议 :永远加上 -o 指定一个有意义的名字,不要让 a.out 满天飞。否则目录下全是 a.out,你根本分不清哪个是哪个程序。


3.2 -g:生成调试信息

语法形式
复制代码
gcc -g 源文件.c -o 可执行文件
功能

-g 选项在编译时生成调试信息,使得生成的可执行文件可以被 GDB 调试器 调试。如果没有 -g,GDB 将无法显示源代码、设置断点等。

重要概念 :程序发布有两种模式------Debug 模式Release 模式 。GCC 默认生成 Release 模式的程序(不含调试信息),加 -g 才是 Debug 模式。

示例

------演示 -g 对比:有调试信息 vs 无调试信息

复制代码
# 不加 -g(Release 模式)
gcc hello.c -o hello_release

# 加 -g(Debug 模式)
gcc -g hello.c -o hello_debug

# 对比两个文件的大小(带调试信息的文件更大)
ls -l hello_release hello_debug
# -rw-rw-r-- 1 user user 15960 ... hello_release
# -rw-rw-r-- 1 user user 26400 ... hello_debug   ← 更大

# 用 file 命令查看区别
file hello_release
# hello_release: ELF 64-bit LSB shared object, ... not stripped

file hello_debug
# hello_debug: ELF 64-bit LSB shared object, ... with debug_info, not stripped
#                                    ↑ 注意这里有 "with debug_info"

使用建议 :开发和调试阶段加上 -g,正式发布时去掉 -g(可以用 strip 命令去除调试信息,减小文件体积)。我们后续会专门讲 GDB 调试器的使用。


3.3 -Wall:显示所有警告信息

语法形式
复制代码
gcc -Wall 源文件.c -o 可执行文件
功能

-Wall(Warning All)选项让 GCC 在编译时显示所有常见的警告信息。这些警告虽然不会阻止编译,但往往暗示代码中存在潜在问题。

与之相对的是 -w 选项,用于关闭所有警告(不建议使用)。

示例

------演示 -Wall 发现潜在代码问题

复制代码
# 创建一个有潜在问题的程序
vim warning.c
cpp 复制代码
// warning.c ------ 有几个潜在问题
#include <stdio.h>

int main()
{
    int x = 10;
    int y;               // 未初始化的变量
    printf("x = %d\n", x);

    if (x = 20)          // 这里用了 = 而不是 ==(赋值而非比较)
    {
        printf("x is 20\n");
    }

    return 0;
}                        // y 声明了但没使用
复制代码
# 不加 -Wall 编译(静默通过)
gcc warning.c -o warning
# 没有任何提示,但代码其实有问题!

# 加 -Wall 编译
gcc -Wall warning.c -o warning
# 警告信息:
# warning.c: In function 'main':
# warning.c:8:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
#     8 |     if (x = 20)
#       |         ^
# warning.c:11:1: warning: unused variable 'y' [-Wunused-variable]
#    11 |     int y;
#       |     ^

# -w 选项关闭所有警告(不建议)
gcc -w warning.c -o warning   # 什么都不提示

强烈建议永远加上 -Wall 编译! 警告信息是编译器在帮你发现问题,忽略它们就是在给自己埋雷。很多严重的 Bug 都是从一个小警告开始的。


3.4 -O:编译优化级别

语法形式
复制代码
gcc -O级别 源文件.c -o 可执行文件
选项 含义
-O0 不优化(默认),编译速度快,适合调试
-O1 基本优化
-O2 中级优化,推荐的发布级别
-O3 最高优化,编译最慢,可执行文件可能更大
功能

-O 选项控制编译器的优化级别。优化可以让程序运行得更快,但代价是编译时间变长,且调试更困难(优化会打乱代码和源码的对应关系)。

示例

------演示不同优化级别对程序体积和编译时间的影响

复制代码
# 准备一个计算密集型程序
vim compute.c
cpp 复制代码
// compute.c
#include <stdio.h>

int main()
{
    long long sum = 0;
    for (int i = 1; i <= 100000000; i++)
    {
        sum += i;
    }
    printf("Sum = %lld\n", sum);
    return 0;
}
复制代码
# 不优化
gcc -O0 compute.c -o compute_O0

# 中级优化
gcc -O2 compute.c -o compute_O2

# 最高优化
gcc -O3 compute.c -o compute_O3

# 对比可执行文件大小
ls -l compute_O0 compute_O2 compute_O3
# compute_O0 可能稍大,优化后的文件可能更小或差不多

# 对比运行时间
time ./compute_O0   # 较慢
time ./compute_O2   # 较快
time ./compute_O3   # 最快

# 输出示例:
# Sum = 5000000050000000
#
# real    0m0.352s   ← O0
# real    0m0.121s   ← O2
# real    0m0.118s   ← O3

使用建议

  • 开发调试阶段 :用 -O0(不优化),方便调试
  • 正式发布阶段 :用 -O2,在性能和编译速度之间取得平衡
  • -O3 虽然最快,但有时反而会引入问题,普通项目用 -O2 就够了

3.5 -static:静态链接

语法形式
复制代码
gcc -static 源文件.c -o 可执行文件
功能

-static 选项让 GCC 在链接时使用静态链接,把所有依赖的库代码都复制到可执行文件中。生成的文件体积会大很多,但运行时不再依赖外部库文件。

示例

------演示 -static 静态链接与动态链接的对比

复制代码
# 动态链接(默认)
gcc hello.c -o hello_dynamic

# 静态链接
gcc -static hello.c -o hello_static

# 对比文件大小(静态链接的文件大得多)
ls -l hello_dynamic hello_static
# -rwxrwxr-x 1 user user   15960 ... hello_dynamic
# -rwxrwxr-x 1 user user  832416 ... hello_static   ← 大了 50 倍!

# 查看动态链接程序的依赖库
ldd hello_dynamic
# 输出:
#     linux-vdso.so.1 =>  (0x00007fffeb1ab000)
#     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
#     /lib64/ld-linux-x86-64.so.2 (0x00007f...)

# 查看静态链接程序的依赖库
ldd hello_static
# 输出:
#     not a dynamic executable
# (没有动态依赖,因为所有库代码都已经打包进去了)

注意 :Ubuntu 默认可能没有安装 C 静态库,使用 -static 时会报错。安装方法:

复制代码
sudo apt install -y libc6-dev
# 或者
sudo apt install -y glibc-source

四、多文件编译

在实际开发中,一个项目通常包含多个源文件,而不是把所有代码都写在一个 .c 文件里。GCC 支持多种多文件编译方式。

4.1 一次性编译多个文件

语法形式
复制代码
gcc 文件1.c 文件2.c 文件3.c -o 可执行文件
功能

将多个源文件一次性交给 GCC 编译,GCC 会分别预处理、编译、汇编每个文件,最后链接成一个可执行文件。

示例

------演示多文件一次性编译

复制代码
# 创建三个文件:main.c、add.c、add.h
vim add.h

// add.h ------ 头文件,声明函数
#ifndef __ADD_H__
#define __ADD_H__

int add(int a, int b);

#endif

vim add.c

// add.c ------ 实现加法函数
#include "add.h"

int add(int a, int b)
{
    return a + b;
}

vim main.c
cpp 复制代码
// main.c ------ 主程序
#include <stdio.h>
#include "add.h"    // 引入自定义头文件

int main()
{
    int result = add(3, 5);
    printf("3 + 5 = %d\n", result);
    return 0;
}
复制代码
# 一次性编译多个文件
gcc main.c add.c -o calc

# 运行
./calc
# 输出:3 + 5 = 8

优点 :简单直接,一条命令搞定。 缺点:如果只修改了一个文件,仍然要重新编译所有文件。对于大型项目,这会非常慢。


4.2 分步编译再链接

语法形式
复制代码
# 分别编译每个文件为 .o 目标文件
gcc -c 文件1.c -o 文件1.o
gcc -c 文件2.c -o 文件2.o

# 再将所有 .o 文件链接为可执行文件
gcc 文件1.o 文件2.o -o 可执行文件
功能

先分别把每个 .c 文件编译成 .o 目标文件,最后再链接在一起。这样做的好处是:如果只修改了某个文件,只需要重新编译那一个文件的 .o,再重新链接即可,大大节省编译时间。

示例

------演示分步编译:修改单个文件后只需重新编译该文件

复制代码
# 分别编译每个文件
gcc -c main.c -o main.o
gcc -c add.c -o add.o

# 链接
gcc main.o add.o -o calc

# 运行
./calc
# 输出:3 + 5 = 8

# ---- 现在假设我们修改了 add.c,但 main.c 没变 ----
# 只需要重新编译 add.c
gcc -c add.c -o add.o

# 重新链接
gcc main.o add.o -o calc

# main.o 不需要重新生成,节省了编译时间!
# 这就是大型项目使用 Makefile 的核心思想

延伸 :在大型项目中,源文件可能有几百上千个。如果每次都全部重新编译,可能要等好几分钟。而分步编译只重新编译修改过的文件,可能只需要几秒钟。这就是为什么大型项目要使用 Makefile 来管理编译流程------下一篇我们会详细讲解!


五、动态链接与静态链接

5.1 动态链接(默认方式)

语法形式
复制代码
gcc 源文件.c -o 可执行文件          # 默认动态链接
功能

动态链接是 GCC 的默认链接方式 。在链接时,不把库文件的代码复制到可执行文件中,而是在程序运行时才去加载库文件。

优点

  • 生成的可执行文件体积小
  • 多个程序可以共享同一个库,节省内存
  • 库更新后,程序不需要重新编译

缺点

  • 运行时依赖外部库文件,如果库不存在程序就无法运行
  • 运行时加载库有微小的性能开销
示例

------演示动态链接程序的依赖关系

复制代码
# 默认动态链接编译
gcc hello.c -o hello

# 用 ldd 命令查看程序依赖的动态库
ldd hello
# 输出:
#     linux-vdso.so.1 =>  (0x00007fffeb1ab000)
#     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
#     /lib64/ld-linux-x86-64.so.2 (0x00007f...)

# 解释:
# libc.so.6 就是 C 标准库(动态库)
# 我们代码中调用的 printf 函数就实现在这个库里
# 程序运行时才会去加载这个库

ldd 命令ldd(List Dynamic Dependencies)用于查看一个可执行程序依赖了哪些动态库。在排查"找不到库文件"错误时非常有用。


5.2 静态链接(-static)

语法形式
复制代码
gcc -static 源文件.c -o 可执行文件
功能

静态链接在编译时就把所有依赖的库代码复制到可执行文件中,程序运行时不再需要外部库。

优点

  • 可执行文件自带所有依赖,独立运行,不依赖环境
  • 运行速度稍快(不需要运行时加载库)

缺点

  • 生成的可执行文件体积大很多
  • 多个程序各自包含库的副本,浪费空间
  • 库更新后需要重新编译所有程序
示例

------演示动态链接 vs 静态链接的全面对比

复制代码
# 动态链接
gcc hello.c -o hello_dyn

# 静态链接
gcc -static hello.c -o hello_sta

# 对比文件大小
ls -l hello_dyn hello_sta
# -rwxrwxr-x 1 user user   15960 ... hello_dyn     ← 16KB
# -rwxrwxr-x 1 user user  832416 ... hello_sta     ← 832KB

# 对比依赖
ldd hello_dyn    # 有动态库依赖
ldd hello_sta    # "not a dynamic executable"(无动态依赖)

# 两个程序运行结果一样
./hello_dyn   # Hello, GCC! I am learning Linux.
./hello_sta   # Hello, GCC! I am learning Linux.

类比理解

  • 动态链接就像"点外卖"------需要吃饭的时候才叫,不用自己囤食材,但依赖外卖店开着门
  • 静态链接就像"自己囤食材"------一次性买齐所有材料放冰箱里,不依赖外部,但占地方

日常开发中绝大多数情况用动态链接(默认方式),只有在需要独立部署、不想依赖环境的场景下才用静态链接。


5.3 静态库与动态库

语法形式
类型 Linux 后缀 Windows 后缀 说明
静态库 .a .lib 编译时把库代码加入可执行文件
动态库 .so .dll 运行时才加载库代码
功能

库(Library) 是把一些常用的函数代码打包在一起形成的文件。比如 C 标准库 libc.so.6 就包含了 printfmallocstrlen 等常用函数的实现。

  • 静态库(.a:链接时把库代码完整复制到可执行文件中
  • 动态库(.so:链接时只记录依赖关系,运行时才加载库代码

为什么需要库? 比如你写了 printf 的调用,但 printf 的实现在哪里?答案就在 C 标准库里。编译器在链接阶段会自动去系统默认路径(如 /usr/lib)查找 libc.so.6,把 printf 的实现和你的代码"连接"起来。

示例

------演示查看系统中的动态库和静态库

复制代码
# 查看 C 标准动态库的位置
ldd hello | grep libc
#     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6

# 查看系统中 C 动态库文件
ls -l /lib/x86_64-linux-gnu/libc.so.6
# lrwxrwxrwx 1 root root 12 ... /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so

# 查看系统中的静态库文件
ls /usr/lib/x86_64-linux-gnu/libc.a 2>/dev/null
# 如果存在,说明安装了 C 静态库

# 查看可执行文件的链接方式
file hello
# hello: ELF 64-bit LSB shared object, ...
# "shared object" 表示动态链接

file hello_sta
# hello_sta: ELF 64-bit LSB executable, ...
# "executable" 表示静态链接

------演示库的后缀名对比

复制代码
# Linux 下常见的库文件后缀
# 动态库:.so(shared object)
# 静态库:.a(archive)

# Windows 下常见的库文件后缀
# 动态库:.dll(dynamic-link library)
# 静态库:.lib(library)

# GCC 默认使用动态库
# 如果要强制使用静态库,加 -static 选项

网吧的例子:理解动态链接和静态链接,可以用"网吧"来类比------

  • 动态链接:你去网吧上网,网吧的电脑上已经装好了各种软件(库),你直接用就行。但如果你带的程序网吧没装对应的软件,就跑不了。
  • 静态链接:你自带笔记本电脑,里面装好了所有需要的软件,到哪都能用,但背着电脑很重(文件大)。

六、常用编译选项速查表

为了方便大家复习,这里整理一张完整的速查表:

编译阶段控制选项

选项 功能 输出文件
-E 只预处理 .i
-S 预处理 + 编译 .s
-c 预处理 + 编译 + 汇编 .o
完成全部四步 可执行文件
-o 文件名 指定输出文件名 自定义

常用辅助选项

选项 功能 使用场景
-g 生成调试信息 GDB 调试时必须加
-Wall 显示所有警告 永远建议加上
-w 关闭所有警告 不建议使用
-O0 不优化(默认) 开发调试阶段
-O1 基本优化 一般用途
-O2 中级优化 发布推荐
-O3 最高优化 性能敏感场景
-static 静态链接 独立部署、不依赖环境
-l库名 链接指定库 -lm 链接数学库
-I目录 指定头文件搜索路径 头文件不在默认路径时
-L目录 指定库文件搜索路径 库文件不在默认路径时

常用组合

复制代码
# 日常开发最常用的组合
gcc -Wall -g hello.c -o hello

# 发布版本
gcc -Wall -O2 hello.c -o hello

# 链接数学库(使用 math.h 中的函数如 sin、sqrt)
gcc -Wall calc.c -o calc -lm

# 指定自定义头文件目录
gcc -Wall -I./include main.c -o main

# 静态链接
gcc -Wall -static hello.c -o hello

-lm 是什么? C 语言中 math.h 提供的数学函数(sincossqrt 等)不在默认的 C 标准库里,而在单独的数学库 libm.so 中。-lm 就是告诉链接器去链接这个数学库(-l 是链接选项,m 是库名 libm 的简写)。


七、总结

本篇我们学习了 GCC 编译器的基本使用,核心内容回顾:

  1. GCC 简介 :GCC 是 Linux 下最常用的编译器,gcc 编译 C 程序,g++ 编译 C++ 程序
  2. 编译四大步骤 :预处理(-E)→ 编译(-S)→ 汇编(-c)→ 链接,每步都可以单独执行
  3. 常用编译选项-o(指定输出名)、-g(调试信息)、-Wall(显示警告)、-O(优化级别)、-static(静态链接)
  4. 多文件编译:可以一次性编译,也可以分步编译再链接(大型项目推荐)
  5. 动态链接与静态链接:动态链接(默认)文件小但依赖环境,静态链接文件大但独立运行

学习建议

  • 理解编译的四个步骤是本文最核心的内容,建议手动操作一遍,观察每个阶段生成的文件
  • 养成-Wall 编译的习惯,让编译器帮你发现问题
  • 日常开发用 gcc -Wall -g hello.c -o hello 这个组合就够了
  • 理解动态链接和静态链接的区别,这在以后部署程序时很重要

下一篇我们将学习 Makefile 的编写,让多文件项目的编译变得自动化、高效化,敬请期待!


专栏目录

  • [第一篇:Xshell 连接腾讯云服务器](#第一篇:Xshell 连接腾讯云服务器)
  • [第二篇:Linux 基本指令入门](#第二篇:Linux 基本指令入门)
  • [第三篇:Linux 文件与目录管理](#第三篇:Linux 文件与目录管理)
  • [第四篇:Vim 编辑器的基本使用](#第四篇:Vim 编辑器的基本使用)
  • 第五篇:GCC 编译器的基本使用(本文)
  • 第六篇:Makefile 的自动化构建(敬请期待)
  • ...
相关推荐
数智工坊1 小时前
RustDesk 自建服务器教程
运维·服务器
一只旭宝1 小时前
面试预备:Linux指令专题
linux·笔记
xbzb1 小时前
Linux 进程调度与优先级调优避坑指南
linux·运维
金士顿1 小时前
ASP.NET Core Native AOT + systemd 实战:把 Linux ARM64 程序变成可靠的设备服务
linux·嵌入式·asp.net core·arm64
Rabitebla1 小时前
【Linux 系统编程】权限(一):身份、提权,和那 9 个权限位
linux·数据结构·c++·算法
Julien20041 小时前
Docker 容器技术的本质
运维·服务器·ssh·学习方法
byte轻骑兵1 小时前
【BlueZ 】Linux 内核蓝牙子系统入门:hci_core 模块与 BlueZ 的交互
linux·人工智能·bluez·电脑蓝牙·嵌入式蓝牙
江屿风1 小时前
【Linux系统】【Linux 进程程序替换机制解析及自定义 Shell 核心逻辑实现 】流食般投喂
linux·运维·服务器·开发语言·笔记
BJ_Bonree1 小时前
博睿数据加入ITSS分会,成为国家级信息技术服务标准化体系单位成员!
大数据·运维·数据库·人工智能·可观测性