目录
[一、认识 GCC/G++:编译器界的 "全能选手"](#一、认识 GCC/G++:编译器界的 “全能选手”)
[1.1 检查与安装 GCC/G++](#1.1 检查与安装 GCC/G++)
[1.1.1 检查是否已安装](#1.1.1 检查是否已安装)
[输出结果示例(Ubuntu 20.04):](#输出结果示例(Ubuntu 20.04):)
[1.1.2 安装 GCC/G++(CentOS 系统)](#1.1.2 安装 GCC/G++(CentOS 系统))
[1.1.3 安装 GCC/G++(Ubuntu 系统)](#1.1.3 安装 GCC/G++(Ubuntu 系统))
[1.2 GCC/G++ 的核心能力](#1.2 GCC/G++ 的核心能力)
[二、编译四步曲:从源代码到可执行文件的 "变身之旅"](#二、编译四步曲:从源代码到可执行文件的 “变身之旅”)
[2.1 准备示例代码](#2.1 准备示例代码)
[2.2 第一步:预处理(Preprocessing)------"整理" 源代码](#2.2 第一步:预处理(Preprocessing)——“整理” 源代码)
[2.2.1 预处理命令](#2.2.1 预处理命令)
[2.2.2 预处理做了什么?](#2.2.2 预处理做了什么?)
[2.2.3 为什么需要预处理?](#2.2.3 为什么需要预处理?)
[2.3 第二步:编译(Compilation)------"翻译" 成汇编语言](#2.3 第二步:编译(Compilation)——“翻译” 成汇编语言)
[2.3.1 编译命令](#2.3.1 编译命令)
[2.3.2 编译输出结果](#2.3.2 编译输出结果)
[2.3.3 语法错误示例](#2.3.3 语法错误示例)
[2.4 第三步:汇编(Assembly)------"翻译" 成机器码](#2.4 第三步:汇编(Assembly)——“翻译” 成机器码)
[2.4.1 汇编命令](#2.4.1 汇编命令)
[2.4.2 查看目标文件信息](#2.4.2 查看目标文件信息)
[2.5 第四步:链接(Linking)------"组装" 成可执行文件](#2.5 第四步:链接(Linking)——“组装” 成可执行文件)
[2.5.1 链接命令](#2.5.1 链接命令)
[2.5.2 运行可执行文件](#2.5.2 运行可执行文件)
[2.5.3 链接的核心:解决 "依赖" 问题](#2.5.3 链接的核心:解决 “依赖” 问题)
[2.6 一键编译:跳过中间文件](#2.6 一键编译:跳过中间文件)
[三、常用编译选项:让 GCC/G++"听你的话"](#三、常用编译选项:让 GCC/G++“听你的话”)
[3.1 基础选项:控制输出与阶段](#3.1 基础选项:控制输出与阶段)
[3.2 调试选项:生成调试信息(配合 GDB)](#3.2 调试选项:生成调试信息(配合 GDB))
[3.3 优化选项:平衡程序性能与编译速度](#3.3 优化选项:平衡程序性能与编译速度)
[3.4 警告选项:提前发现代码隐患](#3.4 警告选项:提前发现代码隐患)
[3.5 头文件与库文件选项:解决 "找不到" 问题](#3.5 头文件与库文件选项:解决 “找不到” 问题)
[3.5.1 头文件路径选项-I(大写 i)](#3.5.1 头文件路径选项-I(大写 i))
[3.5.2 库文件路径与链接选项](#3.5.2 库文件路径与链接选项)
[示例 1:链接系统库(数学库libm.so)](#示例 1:链接系统库(数学库libm.so))
[示例 2:链接自定义库](#示例 2:链接自定义库)
[四、静态链接与动态链接:程序 "依赖" 的两种方式](#四、静态链接与动态链接:程序 “依赖” 的两种方式)
[4.1 静态链接:"自给自足" 的程序](#4.1 静态链接:“自给自足” 的程序)
[4.1.1 静态链接的特点](#4.1.1 静态链接的特点)
[4.1.2 静态链接实战](#4.1.2 静态链接实战)
[4.1.3 对比静态与动态程序](#4.1.3 对比静态与动态程序)
[4.2 动态链接:"按需加载" 的程序](#4.2 动态链接:“按需加载” 的程序)
[4.2.1 动态链接的特点](#4.2.1 动态链接的特点)
[4.2.2 动态链接的两种库文件](#4.2.2 动态链接的两种库文件)
[4.2.3 动态链接实战:自定义动态库](#4.2.3 动态链接实战:自定义动态库)
[4.3 静态链接 vs 动态链接:如何选择?](#4.3 静态链接 vs 动态链接:如何选择?)
[五、GCC/G++ 实战:多文件编译与常见问题解决](#五、GCC/G++ 实战:多文件编译与常见问题解决)
[5.1 多文件编译实战:学生成绩管理程序](#5.1 多文件编译实战:学生成绩管理程序)
[5.1.1 步骤 1:创建文件](#5.1.1 步骤 1:创建文件)
[5.1.2 步骤 2:多文件编译](#5.1.2 步骤 2:多文件编译)
[方式 1:直接指定所有源文件(简单快捷)](#方式 1:直接指定所有源文件(简单快捷))
[方式 2:先编译目标文件,再链接(适合大型项目)](#方式 2:先编译目标文件,再链接(适合大型项目))
[5.2 常见编译问题及解决方案](#5.2 常见编译问题及解决方案)
[5.2.1 问题 1:头文件找不到(No such file or directory)](#5.2.1 问题 1:头文件找不到(No such file or directory))
[5.2.2 问题 2:未定义引用(undefined reference to)](#5.2.2 问题 2:未定义引用(undefined reference to))
[5.2.3 问题 3:重复定义(multiple definition of)](#5.2.3 问题 3:重复定义(multiple definition of))
前言
在 Linux 开发领域,GCC(GNU Compiler Collection)堪称 "编译器之王"------ 它不仅是 C/C++ 程序的核心编译工具,更是支撑 Linux 生态中无数开源项目的基石。无论是编写一个简单的 "Hello World" 程序,还是构建 Linux 内核这样的超大型项目,GCC 都以其强大的兼容性、丰富的优化选项和跨平台特性,成为开发者的首选工具。而 G++ 作为GCC家族中针对 C++ 的专用编译器,完美继承了 GCC 的核心能力,同时对 C++ 标准(从 C++98 到 C++20)提供了全面支持。
本文将从 "原理 + 实战" 双视角出发,带你彻底掌握 GCC/G++ 编译器:从编译的四个核心阶段(预处理、编译、汇编、链接)讲起,到常用编译选项的灵活运用,再到静态链接与动态链接的底层差异,最后结合实际案例讲解优化技巧与调试配置,让你不仅 "知其然",更 "知其所以然"。下面就让我我们正式开始吧!
一、认识 GCC/G++:编译器界的 "全能选手"
在开始实操前,我们先搞清楚一个常见疑问:GCC 和 G++ 到底是什么关系?简单来说,GCC 是一个编译器套件,支持 C、C++、Java、Fortran 等多种语言;而 G++ 是 GCC 套件中专门用于编译 C++ 程序的前端工具,本质上是对 GCC 的**"封装"**------ 当你用 G++ 编译代码时,它会自动调用 GCC 的核心编译能力,并默认链接 C++ 标准库(这是它与 GCC 编译 C 程序的关键区别)。

1.1 检查与安装 GCC/G++
Linux 系统( CentOS、Ubuntu等)通常预装了 GCC,但版本可能较旧。我们先检查当前版本,再根据需求安装或升级。
1.1.1 检查是否已安装
打开终端,执行以下命令查看 GCC/G++ 版本:
bash
# 检查GCC版本(支持C编译)
gcc --version
# 检查G++版本(支持C++编译)
g++ --version
输出结果示例(Ubuntu 20.04):
gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
g++ (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
如果终端提示 "command not found",说明未安装,需执行以下命令安装。
1.1.2 安装 GCC/G++(CentOS 系统)
CentOS 使用**yum包管理器**,安装命令如下:
bash
# 安装GCC(C编译器)和G++(C++编译器)
sudo yum install -y gcc gcc-c++
输出结果示例:
Loaded plugins: fastestmirror, langpacks
Loading mirror speeds from cached hostfile
Resolving Dependencies
--> Running transaction check
---> Package gcc.x86_64 0:4.8.5-44.el7 will be installed
---> Package gcc-c++.x86_64 0:4.8.5-44.el7 will be installed
--> Processing Dependency: libstdc++-devel = 4.8.5-44.el7 for package: gcc-c++-4.8.5-44.el7.x86_64
--> Processing Dependency: libmpfr.so.4()(64bit) for package: gcc-4.8.5-44.el7.x86_64
--> Running transaction check
---> Package libmpfr.x86_64 0:3.1.1-4.el7 will be installed
---> Package libstdc++-devel.x86_64 0:4.8.5-44.el7 will be installed
--> Finished Dependency Resolution
Dependencies Resolved
================================================================================
Package Arch Version Repository Size
================================================================================
Installing:
gcc x86_64 4.8.5-44.el7 base 16 M
gcc-c++ x86_64 4.8.5-44.el7 base 7.2 M
Installing for dependencies:
libmpfr x86_64 3.1.1-4.el7 base 203 k
libstdc++-devel x86_64 4.8.5-44.el7 base 1.5 M
Transaction Summary
================================================================================
Install 2 Packages (+2 Dependent packages)
Total download size: 25 M
Installed size: 85 M
Downloading packages:
(1/4): libmpfr-3.1.1-4.el7.x86_64.rpm | 203 kB 00:00:00
(2/4): libstdc++-devel-4.8.5-44.el7.x86_64.rpm | 1.5 MB 00:00:00
(3/4): gcc-c++-4.8.5-44.el7.x86_64.rpm | 7.2 MB 00:00:01
(4/4): gcc-4.8.5-44.el7.x86_64.rpm | 16 MB 00:00:02
--------------------------------------------------------------------------------
Total 9.8 MB/s | 25 MB 00:00:02
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
Installing : libmpfr-3.1.1-4.el7.x86_64 1/4
Installing : gcc-4.8.5-44.el7.x86_64 2/4
Installing : libstdc++-devel-4.8.5-44.el7.x86_64 3/4
Installing : gcc-c++-4.8.5-44.el7.x86_64 4/4
Verifying : libmpfr-3.1.1-4.el7.x86_64 1/4
Verifying : libstdc++-devel-4.8.5-44.el7.x86_64 2/4
Verifying : gcc-4.8.5-44.el7.x86_64 3/4
Verifying : gcc-c++-4.8.5-44.el7.x86_64 4/4
Installed:
gcc.x86_64 0:4.8.5-44.el7 gcc-c++.x86_64 0:4.8.5-44.el7
Dependency Installed:
libmpfr.x86_64 0:3.1.1-4.el7 libstdc++-devel.x86_64 0:4.8.5-44.el7
Complete!
1.1.3 安装 GCC/G++(Ubuntu 系统)
Ubuntu 使用**apt包管理器**,安装命令更简洁:
bash
# 更新软件源(可选,确保安装最新版本)
sudo apt update
# 安装GCC和G++
sudo apt install -y gcc g++
输出结果示例:
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
binutils binutils-common binutils-x86-64-linux-gnu cpp cpp-9 gcc-9 gcc-9-base
libasan5 libatomic1 libbinutils libc6-dev libcc1-0 libcrypt-dev libctf-nobfd0
libctf0 libexpat1-dev libfakeroot libfile-fcntllock-perl libgcc-9-dev
libgomp1 libisl22 libitm1 liblsan0 libmpc3 libmpfr6 libmpx2 libnsl-dev
libquadmath0 libstdc++6 libtsan0 libubsan1 libxau-dev libxdmcp-dev linux-libc-dev
manpages manpages-dev
Suggested packages:
binutils-doc cpp-doc gcc-9-locales gcc-multilib make manpages-posix
manpages-posix-dev glibc-doc libstdc++-9-doc
The following NEW packages will be installed:
binutils binutils-common binutils-x86-64-linux-gnu cpp cpp-9 gcc gcc-9
gcc-9-base g++ libasan5 libatomic1 libbinutils libc6-dev libcc1-0 libcrypt-dev
libctf-nobfd0 libctf0 libexpat1-dev libfakeroot libfile-fcntllock-perl
libgcc-9-dev libgomp1 libisl22 libitm1 liblsan0 libmpc3 libmpfr6 libmpx2
libnsl-dev libquadmath0 libstdc++6 libtsan0 libubsan1 libxau-dev libxdmcp-dev
linux-libc-dev manpages manpages-dev
0 upgraded, 39 newly installed, 0 to remove and 0 not upgraded.
Need to get 49.7 MB of archives.
After this operation, 201 MB of additional disk space will be used.
Get:1 http://mirrors.aliyun.com/ubuntu focal/main amd64 gcc-9-base amd64 9.4.0-1ubuntu1~20.04.1 [20.2 kB]
Get:2 http://mirrors.aliyun.com/ubuntu focal/main amd64 libstdc++6 amd64 10.3.0-1ubuntu1~20.04 [515 kB]
...(中间省略部分下载过程)
Setting up gcc (4:9.3.0-1ubuntu2) ...
Setting up g++ (4:9.3.0-1ubuntu2) ...
Processing triggers for man-db (2.9.1-1) ...
安装完成后,再次执行gcc --version和g++ --version,确认版本正确即可。
1.2 GCC/G++ 的核心能力
为什么 GCC 能成为 Linux 开发的 "标配"?因为它具备以下三大核心优势:
- 多语言支持:除了 C/C++,还支持 Java、Python、Go 等数十种语言,一套工具搞定多语言开发。
- 跨平台编译:可以为不同架构(x86、ARM、RISC-V)和系统(Linux、Windows、macOS)生成可执行文件,比如在 x86 Linux 上编译 ARM 嵌入式程序。
- 强大的优化与调试:提供从 O0(无优化)到 O3(最高优化)的多级优化选项,同时支持生成调试信息(配合 GDB 调试),兼顾开发效率与程序性能。
二、编译四步曲:从源代码到可执行文件的 "变身之旅"
很多初学者会以为 "编译" 是一步完成的 ------ 输入gcc hello.c -o hello,按下回车就得到可执行文件。但实际上,这个命令背后隐藏了四个关键阶段 :预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)、链接(Linking)。理解这四个阶段,是掌握 GCC 高级用法的基础。
我们以一个简单的 C 程序hello.c为例,逐步拆解每个阶段的作用和输出结果。
2.1 准备示例代码
先创建一个hello.c文件,包含宏定义、头文件引用和主函数:
cpp
#include <stdio.h> // 引入标准输入输出头文件
#define NAME "Linux" // 定义宏NAME
int main() {
// 使用宏和printf函数
printf("Hello, %s! This is GCC compiler.\n", NAME);
return 0;
}
2.2 第一步:预处理(Preprocessing)------"整理" 源代码
预处理阶段的核心任务是:处理源代码中的 "特殊指令" (以#开头的指令,如#include、#define、#if等,大家如果忘了这部分内容,可以去看看我在C语言篇写过的预处理相关的内容),生成预处理后的代码文件(后缀为**.i**)。
2.2.1 预处理命令
使用**-E选项触发预处理,-o**指定输出文件(若不指定,会直接输出到终端):
bash
gcc -E hello.c -o hello.i
2.2.2 预处理做了什么?
打开hello.i文件(约 1300 行,这里只看关键部分),你会发现三个变化:
- 头文件展开 :#include <stdio.h>被替换成stdio.h头文件的所有内容(包括
printf函数的声明)。 - 宏替换 :**#define NAME "Linux"**被替换 ------所有
NAME都变成了"Linux",比如printf中的%s对应的值从NAME变成了"Linux"。 - 删除注释 :源代码中的**
// 使用宏和printf函数被直接删除**,避免注释影响编译。
hello.i关键内容示例:
cpp
// (前面省略1200多行stdio.h的内容)
extern int fprintf (FILE *__restrict __stream, const char *__restrict __format, ...);
extern int printf (const char *__restrict __format, ...); // stdio.h中printf的声明
// (中间省略部分内容)
int main() {
// 注释被删除,宏NAME被替换为"Linux"
printf("Hello, %s! This is GCC compiler.\n", "Linux");
return 0;
}
2.2.3 为什么需要预处理?
因为编译器(后续的编译阶段)只认识 "纯 C 代码" ,不认识#include这类指令。预处理相当于**"翻译官"** ,把带有特殊指令的源代码,转换成编译器能理解的**"纯净代码"**。
2.3 第二步:编译(Compilation)------"翻译" 成汇编语言
编译阶段的核心任务是:将预处理后的.i文件(C 代码)翻译成汇编语言代码 (后缀为.s),同时进行语法检查 ------ 如果代码有语法错误(如少写分号、变量未定义),会在这个阶段报错。
2.3.1 编译命令
使用**-S**选项触发编译(注意是大写的 S),生成.s汇编文件:
bash
gcc -S hello.i -o hello.s
2.3.2 编译输出结果
打开hello.s文件,会看到类似下面的汇编代码(不同架构的汇编指令略有差异,这里是 x86_64 架构):
cpp
.file "hello.c"
.text
.section .rodata
.LC0:
.string "Hello, %s! This is GCC compiler."
.string "Linux"
.text
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $.LC1, %esi # 将"Linux"的地址存入esi寄存器
movl $.LC0, %edi # 将字符串常量的地址存入edi寄存器
movl $0, %eax
call printf # 调用printf函数
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
.section .note.GNU-stack,"",@progbits
这段代码的核心是:将main函数的逻辑翻译成汇编指令,比如**movl(移动数据)、call printf(调用 printf 函数)**。
2.3.3 语法错误示例
如果我们故意把hello.c中的printf写成print(少个f),再执行编译命令:
bash
gcc -S hello.c -o hello.s
会看到编译报错,直接终止编译过程:
cpp
hello.c: In function 'main':
hello.c:6:5: warning: implicit declaration of function 'print' [-Wimplicit-function-declaration]
print("Hello, %s! This is GCC compiler.\n", NAME);
^~~~~
hello.c:6:5: error: incompatible implicit declaration of built-in function 'print'
hello.c:6:5: note: include '<stdio.h>' or provide a declaration of 'print'
这说明编译阶段会严格检查代码语法,只有语法正确的代码才能进入下一阶段。
2.4 第三步:汇编(Assembly)------"翻译" 成机器码
汇编阶段的核心任务是:将汇编语言.s文件翻译成机器能识别的二进制目标文件 (后缀为**.o**),这个文件包含了 CPU 可执行的指令,但还不能直接运行(因为缺少依赖的库函数,如printf的实现)。
2.4.1 汇编命令
使用**-c选项触发汇编,生成.o**目标文件:
bash
gcc -c hello.s -o hello.o
2.4.2 查看目标文件信息
.o文件是二进制文件,直接打开会看到乱码,我们可以用file命令查看它的属性:
bash
file hello.o
输出结果:
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
解释一下关键信息:
- ELF 64-bit:表示是 64 位 ELF 格式文件(Linux 下可执行文件和目标文件的标准格式)。
- relocatable:表示是 "可重定位目标文件"------ 意味着它还需要和其他目标文件或库文件链接,才能生成可执行文件。
2.5 第四步:链接(Linking)------"组装" 成可执行文件
链接阶段是最后一步,核心任务是:将目标文件(.o)与依赖的库文件(如 C 标准库libc.so)合并,生成可执行文件。
2.5.1 链接命令
直接使用gcc命令,输入目标文件,指定输出可执行文件名称:
bash
gcc hello.o -o hello
2.5.2 运行可执行文件
执行生成的hello文件,验证结果:
bash
./hello
输出结果:
Hello, Linux! This is GCC compiler.
成功运行!这说明四个阶段全部完成,源代码最终变成了可执行程序。
2.5.3 链接的核心:解决 "依赖" 问题
为什么需要链接?因为我们的代码中使用了printf函数,但hello.o中只有printf的调用指令(call printf),没有printf的实现代码 ------printf的实现放在系统的 C 标准库(libc.so)中。链接阶段的作用就是:找到printf的实现代码,并将其 "拼接" 到我们的可执行文件中(或者记录库文件的位置,运行时动态加载)。
我们可以用ldd命令查看可执行文件依赖的库:
bash
ldd hello
输出结果:
linux-vdso.so.1 (0x00007ffd7b7f7000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b3a800000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8b3aa1a000)
其中libc.so.6就是 C 标准库 ,ld-linux-x86-64.so.2是动态链接器(负责运行时加载库文件)。
2.6 一键编译:跳过中间文件
在实际开发中,我们很少单独执行四个阶段的命令,而是用一条命令直接生成可执行文件:
bash
# 编译C程序
gcc hello.c -o hello
# 编译C++程序(将gcc换成g++,源文件后缀为.cpp)
g++ hello.cpp -o hello_cpp
这条命令会自动完成**"预处理→编译→汇编→链接"四个阶段,中间生成的.i、.s、.o**文件会被自动删除,只保留最终的可执行文件。
三、常用编译选项:让 GCC/G++"听你的话"
GCC 提供了数百个编译选项,但常用的只有十几个。掌握这些选项,能让你灵活控制编译过程 ------ 比如生成调试信息、开启优化、指定头文件路径等。我们按 "功能分类" 讲解最实用的选项,每个选项都搭配示例。
3.1 基础选项:控制输出与阶段
这类选项用于指定输出文件、控制编译阶段,是最常用的 "入门级" 选项。
| 选项 | 功能描述 | 示例 |
|---|---|---|
| -o <file> | 指定输出文件名称(可用于中间文件或可执行文件) | gcc hello.c -o hello(生成可执行文件 hello) |
| -E | 只执行预处理,生成.i文件 |
gcc -E hello.c -o hello.i |
| -S | 执行预处理 + 编译,生成.s汇编文件 |
gcc -S hello.c -o hello.s |
| -c | 执行预处理 + 编译 + 汇编,生成.o目标文件 |
gcc -c hello.c -o hello.o |
| -v | 显示编译过程的详细信息(包括调用的工具、参数) | gcc -v hello.c -o hello |
示例:用-v查看编译细节
执行gcc -v hello.c -o hello,会输出大量信息,其中关键部分是 "调用的工具链" 和 "搜索路径":
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ...(省略配置信息)
Thread model: posix
gcc version 9.4.0 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
COLLECT_GCC_OPTIONS='-v' '-o' 'hello' '-mtune=generic' '-march=x86-64'
/usr/lib/gcc/x86_64-linux-gnu/9/cc1 -quiet -v -imultiarch x86_64-linux-gnu hello.c -quiet -dumpbase hello.c -mtune=generic -march=x86-64 -auxbase hello -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/ccX7Zk8G.s
GNU C17 (Ubuntu 9.4.0-1ubuntu1~20.04.1) version 9.4.0 (x86_64-linux-gnu)
compiled by GNU C version 9.4.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP
warning: GMP header version 6.2.0 differs from library version 6.2.1.
warning: MPFR header version 4.0.2 differs from library version 4.0.3.
warning: MPC header version 1.1.0 differs from library version 1.2.1.
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring duplicate directory "/usr/local/include/x86_64-linux-gnu"
ignoring duplicate directory "/usr/lib/gcc/x86_64-linux-gnu/9/include"
...(省略头文件搜索路径)
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/9/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
GNU C17 (Ubuntu 9.4.0-1ubuntu1~20.04.1) version 9.4.0 (x86_64-linux-gnu)
compiled by GNU C version 9.4.0, GMP version 6.2.0, MPFR version 4.0.2, MPC version 1.1.0, isl version isl-0.22.1-GMP
...(省略后续汇编和链接步骤)
通过**-v选项,你可以看到 GCC 在预处理阶段搜索头文件的路径(如/usr/include)、调用的编译器(cc1)和汇编器(as)**,这对解决 "头文件找不到" 等问题非常有用。
3.2 调试选项:生成调试信息(配合 GDB)
如果需要用 GDB 调试程序(后续我会详细介绍GDB),必须在编译时生成调试信息(记录变量、行号等信息),否则 GDB 无法定位代码。核心选项是-g。
| 选项 | 功能描述 | 示例 |
|---|---|---|
| -g | 生成调试信息(默认包含行号、变量信息,配合 GDB 使用) | gcc -g hello.c -o hello |
| -g3 | 生成更详细的调试信息(包括宏定义、注释等) | gcc -g3 hello.c -o hello |
| -ggdb | 生成 GDB 专用的调试信息(优化调试体验) | gcc -ggdb hello.c -o hello |
示例:生成调试信息并验证
(1)编译时添加**-g**选项:
bash
gcc -g hello.c -o hello_debug
(2)用file命令查看调试信息是否生成:
bash
file hello_debug
输出结果:
bash
hello_debug: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0, with debug_info, not stripped
其中with debug_info表示包含调试信息,not stripped表示调试信息未被剥离(strip命令会删除调试信息,减小文件体积)。
(3)用 GDB 验证调试功能:
bash
gdb hello_debug
进入 GDB 后,输入list main查看main函数的代码(如果没有调试信息,会提示 "No line number information available"):
cpp
(gdb) list main
1 #include <stdio.h>
2 #define NAME "Linux"
3
4 int main() {
5 printf("Hello, %s! This is GCC compiler.\n", NAME);
6 return 0;
7 }
能看到行号和代码,说明调试信息生成成功。
3.3 优化选项:平衡程序性能与编译速度
GCC 提供了从-O0到-O3的四级优化选项,还有-Os(优化代码体积),不同选项对应不同的优化策略。
| 选项 | 功能描述 | 适用场景 |
|---|---|---|
| -O0 | 无优化(默认选项) | 开发阶段,编译速度快,便于调试(变量值不会被优化) |
| -O1(或-O) | 基础优化(优化代码大小和执行速度,不增加编译时间) | 日常开发,兼顾性能和编译速度 |
| -O2 | 中级优化(比-O1更全面,如循环展开、函数内联) |
生产环境,追求更高性能,编译时间适中 |
| -O3 | 高级优化(在-O2基础上增加向量优化、循环优化等) |
对性能要求极高的场景(如科学计算),编译时间长 |
| -Os | 优化代码体积(减少可执行文件大小,适合嵌入式设备) | 存储空间有限的场景(如 ARM 嵌入式程序) |
示例:对比不同优化选项的效果
我们用一个计算 1 到 1000000 求和的程序sum.c,测试不同优化选项对程序体积和运行时间的影响:
cpp
#include <stdio.h>
int main() {
long long sum = 0;
for (int i = 1; i <= 1000000; i++) {
sum += i;
}
printf("Sum: %lld\n", sum);
return 0;
}
-
用不同优化选项编译:
bash# O0(无优化) gcc -O0 sum.c -o sum_O0 # O2(中级优化) gcc -O2 sum.c -o sum_O2 # O3(高级优化) gcc -O3 sum.c -o sum_O3 # Os(优化体积) gcc -Os sum.c -o sum_Os -
对比文件大小(用
ls -l命令):bashls -l sum_O0 sum_O2 sum_O3 sum_Os
输出结果:
-rwxrwxr-x 1 user user 16824 11月 15 10:00 sum_O0
-rwxrwxr-x 1 user user 16304 11月 15 10:00 sum_O2
-rwxrwxr-x 1 user user 16304 11月 15 10:00 sum_O3
-rwxrwxr-x 1 user user 16280 11月 15 10:00 sum_Os
我们可以看到:
sum_O0(无优化)体积最大(16824 字节)。sum_Os(优化体积)体积最小(16280 字节)。O2和O3体积相近,略小于O0。
-
对比运行时间(用time命令):
bash# 测试O0版本 time ./sum_O0 # 测试O2版本 time ./sum_O2
O0 版本输出:
Sum: 500000500000
real 0m0.003s
user 0m0.000s
sys 0m0.003s
O2 版本输出:
Sum: 500000500000
real 0m0.001s
user 0m0.001s
sys 0m0.000s
虽然程序简单,优化效果不明显,但仍能看出**O2版本的运行时间更** 短 ------ 因为编译器对循环做了优化(如循环展开、变量缓存)。对于复杂程序,O2和O3的优化效果会更显著。
3.4 警告选项:提前发现代码隐患
GCC 的警告功能非常强大,能检测出代码中的潜在问题(如未初始化变量、类型不匹配、无用变量等)。建议大家始终开启警告选项,避免 "隐性 bug"。
| 选项 | 功能描述 | 示例 |
|---|---|---|
| -Wall | 开启所有常见警告(推荐必加) | gcc -Wall hello.c -o hello |
| -Wextra | 在-Wall基础上,开启更多警告(如未使用的参数) |
gcc -Wall -Wextra hello.c -o hello |
| -Werror | 将警告视为错误(强制修复所有警告才能编译通过) | gcc -Wall -Werror hello.c -o hello |
| -Wunused | 检测未使用的变量、函数(如定义了变量但未使用) | gcc -Wall -Wunused hello.c -o hello |
示例:用-Wall检测潜在问题
我们故意写一个有隐患的代码warn.c:
cpp
#include <stdio.h>
// 定义了函数但未使用
int unused_func() {
return 100;
}
int main() {
int a; // 定义了变量但未初始化
printf("a = %d\n", a); // 使用未初始化的变量
return 0;
}
使用-Wall选项编译:
bash
gcc -Wall warn.c -o warn
输出警告信息:
bash
warn.c:3:5: warning: 'unused_func' defined but not used [-Wunused-function]
int unused_func() {
^~~~~~~~~~~
warn.c: In function 'main':
warn.c:8:6: warning: variable 'a' is used uninitialized in this function [-Wuninitialized]
int a;
^
warn.c:9:22: note: 'a' was declared here
printf("a = %d\n", a);
^
这些警告提示了两个问题:
- unused_func函数定义了但未使用。
- 变量**
a未初始化就被使用**(运行时可能输出随机值)。
如果加上**-Werror**选项,警告会变成错误,编译直接失败:
bash
gcc -Wall -Werror warn.c -o warn
输出结果:
bash
warn.c:3:5: error: 'unused_func' defined but not used [-Werror=unused-function]
int unused_func() {
^~~~~~~~~~~
warn.c: In function 'main':
warn.c:8:6: error: variable 'a' is used uninitialized in this function [-Werror=uninitialized]
int a;
^
warn.c:9:22: note: 'a' was declared here
printf("a = %d\n", a);
^
cc1: all warnings being treated as errors
-Werror适合团队开发,强制所有人修复警告,保证代码质量。
3.5 头文件与库文件选项:解决 "找不到" 问题
当你的代码引用了**"非系统默认路径"**的头文件或库文件时(如自己写的头文件、第三方库),需要用以下选项指定路径,否则 GCC 会报错 "头文件找不到" 或 "库文件找不到"。
3.5.1 头文件路径选项-I(大写 i)
-I <path>:指定头文件搜索路径(GCC 会先搜索-I指定的路径,再搜索系统默认路径)。
示例 :假设我们有一个自定义头文件myheader.h,放在./include目录下,内容如下:
cpp
// ./include/myheader.h
#define MAX_NUM 100
void print_max(); // 函数声明
对应的实现文件myfunc.c放在**./src**目录下:
cpp
// ./src/myfunc.c
#include "myheader.h"
#include <stdio.h>
void print_max() {
printf("Max number is: %d\n", MAX_NUM);
}
主程序main.c在当前目录,引用myheader.h:
cpp
// main.c
#include "myheader.h"
int main() {
print_max();
return 0;
}
如果直接编译,会报错**"myheader.h: No such file or directory",因为 GCC 默认只搜索/usr/include等系统路径,找不到./include**下的头文件。
正确的编译命令需要用**-I ./include**指定头文件路径,同时指定所有源文件:
bash
gcc main.c ./src/myfunc.c -I ./include -o myprog
运行程序:
bash
./myprog
输出结果:
Max number is: 100
3.5.2 库文件路径与链接选项
如果代码依赖第三方库(如数学库、网络库),需要用**-L指定库文件路径,-l**(小写 L)指定库名称。
示例 1:链接系统库(数学库libm.so)
C 标准库中的数学函数(如sin、sqrt)不在默认链接的libc.so中,而是在libm.so中,需要手动链接。
创建math_test.c:
cpp
#include <stdio.h>
#include <math.h> // 包含数学库头文件
int main() {
double x = 2.0;
double result = sqrt(x); // 使用sqrt函数(在libm.so中)
printf("sqrt(%.1f) = %.2f\n", x, result);
return 0;
}
直接编译会报错:
bash
gcc math_test.c -o math_test
错误信息:
bash
/tmp/ccY6Zk7G.o: In function `main':
math_test.c:(.text+0x2a): undefined reference to `sqrt'
collect2: error: ld returned 1 exit status
错误原因是 "找不到sqrt的引用"------ 因为没有链接libm.so库。正确的命令需要加-lm(-l指定库名称m,GCC 会自动补全为libm.so):
gcc math_test.c -o math_test -lm
运行程序:
bash
./math_test
输出结果:
sqrt(2.0) = 1.41
示例 2:链接自定义库
如果我们将myfunc.c编译成静态库libmyfunc.a,再链接到主程序中,步骤如下:
-
编译
myfunc.c生成目标文件:bashgcc -c ./src/myfunc.c -I ./include -o myfunc.o -
用
ar命令创建静态库 (ar是归档工具,用于打包目标文件):bashar rcs libmyfunc.a myfunc.o- r:替换库中的旧文件。
- c:创建新库(若库不存在)。
- s:生成库的索引(加快链接速度)。
-
链接静态库编译主程序:
bash# -L .:指定库文件路径为当前目录(libmyfunc.a在当前目录) # -lmyfunc:指定链接libmyfunc.a库(-l后加库名称myfunc) gcc main.c -o myprog_lib -I ./include -L . -lmyfunc -
运行程序:
bash./myprog_lib
输出结果:
Max number is: 100
四、静态链接与动态链接:程序 "依赖" 的两种方式
在链接阶段,GCC 支持两种链接方式:静态链接(Static Linking)和动态链接(Dynamic Linking)。这两种方式的核心区别是 "库文件是否被打包到可执行文件中",直接影响程序的体积、运行效率和可移植性。
4.1 静态链接:"自给自足" 的程序
静态链接的原理是:将程序依赖的库文件(如libc.a、libmyfunc.a)的代码 "完整复制" 到可执行文件中。生成的可执行文件不依赖外部库,可以单独运行。
4.1.1 静态链接的特点
优点:
- 可移植性强:程序不依赖外部库,复制到其他相同架构的 Linux 系统中即可运行。
- 运行速度快:库代码已包含在程序中,无需运行时加载库文件。
- 缺点 :
- 程序体积大:每个程序都包含一份库代码,若多个程序依赖同一个库,会浪费磁盘空间和内存。
- 更新麻烦:若库文件有 bug 修复或功能更新,需要重新编译链接程序才能生效。
4.1.2 静态链接实战
GCC 默认使用动态链接,要启用静态链接,需添加**-static**选项。
以hello.c为例,静态链接 C 标准库:
bash
# 静态链接,生成静态可执行文件
gcc -static hello.c -o hello_static
4.1.3 对比静态与动态程序
(1)对比文件大小:
bash
# 动态链接版本(之前生成的hello)
gcc hello.c -o hello_dynamic
# 查看两个文件的大小
ls -l hello_static hello_dynamic
输出结果:
bash
-rwxrwxr-x 1 user user 16824 11月 15 11:00 hello_dynamic
-rwxrwxr-x 1 user user 846448 11月 15 11:01 hello_static
可以看到,静态链接 的hello_static体积(846KB)远大于动态链接 的hello_dynamic(16KB)------ 因为hello_static包含了 C 标准库的完整代码。
(2)查看依赖的库:
bash
# 动态链接程序的依赖库
ldd hello_dynamic
# 静态链接程序无依赖库(ldd会提示不是动态可执行文件)
ldd hello_static
动态链接程序输出:
linux-vdso.so.1 (0x00007ffd7b7f7000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b3a800000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8b3aa1a000)
静态链接程序输出:
not a dynamic executable
- 测试的可移植性:将
hello_static复制到另一台未安装 C 标准库的 Linux 系统中,执行./hello_static,能正常输出 "Hello, Linux! ...";而hello_dynamic会报错 "error while loading shared libraries: libc.so.6: cannot open shared object file: No such file or directory",因为找不到依赖的libc.so.6。
4.2 动态链接:"按需加载" 的程序
动态链接的原理是:不将库代码复制到可执行文件中,而是在程序运行时,由动态链接器(ld-linux-x86-64.so.2)加载依赖的库文件。生成的可执行文件体积小,且多个程序可共享同一个库文件。
4.2.1 动态链接的特点
- 优点 :
- 程序体积小:仅包含自身代码和库的 "引用信息",不包含库代码。
- 共享库资源:多个程序可共享同一个库文件,节省磁盘空间和内存(库文件只需加载一次到内存)。
- 更新方便:若库文件更新,无需重新编译程序,直接替换库文件即可(前提是接口兼容)。
- 缺点 :
- 可移植性差:程序依赖外部库,若目标系统缺少对应的库文件,无法运行。
- 运行速度略慢:需要在运行时加载库文件,增加少量启动时间。
4.2.2 动态链接的两种库文件
Linux 下的动态库文件有两种后缀:
- libxxx.so:动态共享库(Shared Object),是编译后的二进制文件,可直接被动态链接器加载。
- libxxx.so.x.y.z:版本化动态库(如
libc.so.6),x是主版本号(接口不兼容),y是次版本号(接口兼容,新增功能),z是修订号(bug 修复)。
4.2.3 动态链接实战:自定义动态库
我们将myfunc.c编译成动态库libmyfunc.so,再链接到主程序中。
(1)编译动态库:
bash
# -fPIC:生成位置无关代码(Position Independent Code),动态库必须加此选项
# -shared:生成动态库
gcc -fPIC -shared ./src/myfunc.c -I ./include -o libmyfunc.so
(2)链接动态库编译主程序:
bash
# -L .:指定动态库路径为当前目录
# -lmyfunc:链接libmyfunc.so动态库
gcc main.c -o myprog_dyn -I ./include -L . -lmyfunc
(3)运行程序:
直接运行会报错 "找不到动态库",因为动态链接器默认只搜索/lib、/usr/lib等系统路径,找不到当前目录的libmyfunc.so。
解决方法有三种:
-
方法 1:将动态库复制到系统库路径(需要 root 权限):
bashsudo cp libmyfunc.so /usr/lib ./myprog_dyn -
方法 2:设置
LD_LIBRARY_PATH环境变量(临时生效,重启终端后失效):bashexport LD_LIBRARY_PATH=$LD_LIBRARY_PATH:. # 添加当前目录到库搜索路径 ./myprog_dyn -
方法 3:修改
/etc/ld.so.conf配置文件(永久生效):bashsudo echo "./" >> /etc/ld.so.conf # 添加当前目录到配置文件(实际开发中建议用绝对路径) sudo ldconfig # 更新动态链接器缓存 ./myprog_dyn
运行结果:
Max number is: 100
-
查看程序依赖的动态库:
bashldd myprog_dyn
输出结果:
linux-vdso.so.1 (0x00007ffd7b7f7000)
libmyfunc.so => ./libmyfunc.so (0x00007f8b3a7f0000) # 依赖我们的动态库
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b3a600000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8b3a81a000)
可以看到,程序成功依赖了libmyfunc.so动态库。
4.3 静态链接 vs 动态链接:如何选择?
| 场景 | 推荐链接方式 | 原因 |
|---|---|---|
| 嵌入式设备(存储空间有限) | 动态链接 | 多个程序共享库文件,节省存储空间 |
| 独立部署的工具(如脚本解释器) | 静态链接 | 无需依赖系统库,复制即可运行 |
| 企业内部服务(如后端 API) | 动态链接 | 库更新时无需重新部署程序,降低维护成本 |
| 对性能要求极高的程序(如实时系统) | 静态链接 | 避免运行时加载库的开销,提升响应速度 |
五、GCC/G++ 实战:多文件编译与常见问题解决
在实际开发中,我们很少写 "单文件程序",而是将代码拆分到多个.c/.cpp文件和头文件中。掌握多文件编译的方法,是应对中大型项目的基础。同时,我们还会讲解开发中常见的编译问题及解决方案。
5.1 多文件编译实战:学生成绩管理程序
我们以一个简单的 "学生成绩管理程序" 为例,包含 3 个文件:
student.h:头文件,声明结构体和函数。student.c:源文件,实现成绩计算相关函数。main.c:主程序,调用student.c中的函数。
5.1.1 步骤 1:创建文件
(1)student.h(头文件):
cpp
#ifndef STUDENT_H
#define STUDENT_H // 防止头文件重复包含(头文件保护)
// 定义学生结构体
typedef struct {
char name[50];
int id;
float scores[3]; // 3门课程成绩
float average; // 平均分
} Student;
// 函数声明:计算学生平均分
void calculate_average(Student *stu);
// 函数声明:打印学生信息
void print_student(Student *stu);
#endif // STUDENT_H
注意 :头文件中必须加 "头文件保护"(#ifndef/#define/#endif),防止因多次#include导致结构体和函数重复声明。
(2)student.c(源文件):
cpp
#include "student.h"
#include <stdio.h>
// 实现计算平均分的函数
void calculate_average(Student *stu) {
float sum = 0;
for (int i = 0; i < 3; i++) {
sum += stu->scores[i];
}
stu->average = sum / 3;
}
// 实现打印学生信息的函数
void print_student(Student *stu) {
printf("ID: %d\n", stu->id);
printf("Name: %s\n", stu->name);
printf("Scores: %.1f, %.1f, %.1f\n",
stu->scores[0], stu->scores[1], stu->scores[2]);
printf("Average: %.1f\n", stu->average);
}
(3)main.c(主程序):
cpp
#include "student.h"
#include <stdio.h>
#include <string.h>
int main() {
// 定义一个学生变量并初始化
Student stu;
stu.id = 1001;
strcpy(stu.name, "Zhang San");
stu.scores[0] = 85.5;
stu.scores[1] = 92.0;
stu.scores[2] = 78.5;
// 计算平均分
calculate_average(&stu);
// 打印学生信息
print_student(&stu);
return 0;
}
5.1.2 步骤 2:多文件编译
多文件编译有两种方式:直接指定所有源文件,或先编译成目标文件再链接。
方式 1:直接指定所有源文件(简单快捷)
bash
gcc main.c student.c -o student_manage
运行程序:
bash
./student_manage
输出结果:
bash
ID: 1001
Name: Zhang San
Scores: 85.5, 92.0, 78.5
Average: 85.3
方式 2:先编译目标文件,再链接(适合大型项目)
对于包含数十个源文件的项目,直接编译所有文件会很慢(修改一个文件需要重新编译所有文件)。更好的方式是:将每个源文件编译成目标文件(.o),再链接所有目标文件------ 修改一个文件时,只需重新编译对应的目标文件,节省时间。
bash
# 1. 编译main.c生成main.o
gcc -c main.c -o main.o
# 2. 编译student.c生成student.o
gcc -c student.c -o student.o
# 3. 链接所有目标文件,生成可执行文件
gcc main.o student.o -o student_manage_obj
运行程序,结果与方式 1 一致:
bash
./student_manage_obj
5.2 常见编译问题及解决方案
在多文件编译中,初学者常遇到 "头文件找不到""未定义引用""重复定义" 等问题,我们逐一讲解解决方案。
5.2.1 问题 1:头文件找不到(No such file or directory)
错误示例:
bash
gcc main.c student.c -o student_manage
错误信息:
bash
main.c:1:10: fatal error: student.h: No such file or directory
#include "student.h"
^~~~~~~~~~~
compilation terminated.
原因 :student.h不在当前目录,或不在 GCC 的默认搜索路径中。
解决方案:
-
若头文件在
./include目录,用**-I ./include**指定路径:bashgcc main.c student.c -I ./include -o student_manage -
若头文件在其他路径,将路径替换为实际路径(如
-I /home/user/project/include)。
5.2.2 问题 2:未定义引用(undefined reference to)
错误示例 :只编译main.c,未编译student.c:
bash
gcc main.c -o student_manage
错误信息:
/tmp/ccX7Zk8G.o: In function `main':
main.c:(.text+0x5a): undefined reference to `calculate_average'
main.c:(.text+0x66): undefined reference to `print_student'
collect2: error: ld returned 1 exit status
原因 :main.c中调用了calculate_average和print_student函数,但这两个函数的实现在student.c中,未被编译链接。
解决方案:编译时包含所有相关的源文件或目标文件:
bash
# 包含student.c
gcc main.c student.c -o student_manage
# 或包含student.o(已提前编译)
gcc main.c student.o -o student_manage
5.2.3 问题 3:重复定义(multiple definition of)
错误示例 :在student.h中定义全局变量,然后在main.c和student.c中都#include "student.h":
bash
// student.h中错误定义全局变量
int global_var = 10;
编译时会报错:
bash
gcc main.c student.c -o student_manage
错误信息:
/tmp/ccY6Zk7G.o:(.data+0x0): multiple definition of `global_var'
/tmp/ccX7Zk8G.o:(.data+0x0): first defined here
collect2: error: ld returned 1 exit status
原因 :头文件被多个源文件#include后,全局变量global_var会在每个源文件中被定义一次,链接时出现重复定义。
解决方案:
-
头文件中只声明 全局变量(用
extern),不定义 :cpp// student.h中声明全局变量 extern int global_var; -
在一个源文件(如
student.c)中定义 全局变量:cpp// student.c中定义全局变量 int global_var = 10;
总结
GCC/G++ 是 Linux 开发的 "基石工具",掌握它不仅能让你高效编译 C/C++ 程序,更能帮助你理解程序从源代码到可执行文件的底层逻辑。希望本文能成为你学习 GCC/G++ 的 "入门钥匙",后续可结合实际项目不断实践,逐步解锁更多高级用法!