前言
在上一篇文章中,我们学会了使用 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++ 程序
功能
gcc 和 g++ 都是 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
功能
预处理阶段主要做以下几件事:
- 头文件展开 :把
#include <stdio.h>引入的头文件内容插入到源文件中 - 宏替换 :把代码中用
#define定义的宏全部替换为实际内容 - 去注释 :删除代码中的所有注释(
//和/* */) - 条件编译 :根据
#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
功能
编译阶段是整个编译过程的核心,主要做以下工作:
- 语法检查:检查代码是否符合 C 语言语法规范
- 语义分析:检查类型是否匹配、变量是否定义等
- 代码优化:根据优化级别对代码进行优化
- 生成汇编代码:将 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)与库文件拼接在一起,生成最终可以执行的程序。
链接主要做两件事:
- 符号解析 :把代码中调用的外部函数(如
printf)与库文件中的实现"对接"起来 - 地址重定位:把各个目标文件的代码和数据安排到最终的内存地址中
一句话理解:链接就是"组装"------把各个零件(目标文件)和说明书(库文件)拼装成一辆可以开的"车"(可执行程序)。
示例
------演示链接:将 .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 就包含了 printf、malloc、strlen 等常用函数的实现。
- 静态库(
.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提供的数学函数(sin、cos、sqrt等)不在默认的 C 标准库里,而在单独的数学库libm.so中。-lm就是告诉链接器去链接这个数学库(-l是链接选项,m是库名libm的简写)。
七、总结
本篇我们学习了 GCC 编译器的基本使用,核心内容回顾:
- GCC 简介 :GCC 是 Linux 下最常用的编译器,
gcc编译 C 程序,g++编译 C++ 程序 - 编译四大步骤 :预处理(
-E)→ 编译(-S)→ 汇编(-c)→ 链接,每步都可以单独执行 - 常用编译选项 :
-o(指定输出名)、-g(调试信息)、-Wall(显示警告)、-O(优化级别)、-static(静态链接) - 多文件编译:可以一次性编译,也可以分步编译再链接(大型项目推荐)
- 动态链接与静态链接:动态链接(默认)文件小但依赖环境,静态链接文件大但独立运行
学习建议:
- 理解编译的四个步骤是本文最核心的内容,建议手动操作一遍,观察每个阶段生成的文件
- 养成加
-Wall编译的习惯,让编译器帮你发现问题- 日常开发用
gcc -Wall -g hello.c -o hello这个组合就够了- 理解动态链接和静态链接的区别,这在以后部署程序时很重要
下一篇我们将学习 Makefile 的编写,让多文件项目的编译变得自动化、高效化,敬请期待!
专栏目录:
- [第一篇:Xshell 连接腾讯云服务器](#第一篇:Xshell 连接腾讯云服务器)
- [第二篇:Linux 基本指令入门](#第二篇:Linux 基本指令入门)
- [第三篇:Linux 文件与目录管理](#第三篇:Linux 文件与目录管理)
- [第四篇:Vim 编辑器的基本使用](#第四篇:Vim 编辑器的基本使用)
- 第五篇:GCC 编译器的基本使用(本文)
- 第六篇:Makefile 的自动化构建(敬请期待)
- ...
