第8课:Linux开发工具(三):gcc
一、GCC与g++的区别与使用建议
基本定位
- GCC:纯C语言编译器,只能编译C语言代码
- g++:C++编译器,既能编译C++代码,也能编译C语言代码,但编译C语言时会按照C++的语法规则进行编译
推荐使用方式
- 编译C语言代码:优先使用
gcc - 编译C++代码:优先使用
g++
- 编译C语言代码:优先使用
【核心结论】虽然g++可以编译C语言,但本质是将C语言当作C++来处理,可能存在语法兼容性问题。使用对应编译器编译对应语言是最佳实践。
- 历史与环境兼容性说明
- 部分老旧Linux环境可能默认只安装了GCC,未安装g++
- C语言编写内核、操作系统等底层代码时,使用GCC编译的代码质量更高、更贴近硬件
二、程序翻译的四个阶段(核心重点)
C/C++代码从源文件到可执行程序,必须经过预处理、编译、汇编、链接四个阶段。GCC默认会一步完成所有阶段,但我们可以通过选项让它在每个阶段结束后停下来,方便观察和调试。
1. 预处理(Preprocessing)
核心工作:
- 头文件展开:将
#include包含的头文件内容完整拷贝到源文件中 - 宏替换:将代码中所有的宏定义(
#define)替换为实际的值 - 去除注释:删除代码中所有的单行注释(
//)和多行注释(/* */) - 条件编译:根据宏定义条件,保留或裁剪对应的代码段
- 头文件展开:将
对应GCC选项与文件后缀:
- 选项:
-E(大写E) - 作用:从当前开始进行程序翻译,完成预处理后立即停止
- 输出文件后缀:
.i(约定俗成,非强制) - 命令格式:
gcc -E 源文件.c -o 预处理文件.i
- 选项:
代码演示与验证:
我们使用以下测试代码code.c:#include<stdio.h>#defineM100#defineVERSION11intmain(){#ifdefVERSION1printf("我是version1版本的功能\n");#elseprintf("我是version2版本的功能\n");#endif//printf("hello world 1, %d\n", M);//printf("hello world 2, %d\n", M);return0;}执行预处理命令:
gcc-Ecode.c-ocode.i观察
code.i文件可以发现:- 宏
M和VERSION1已经被完全替换 - 所有注释已经被删除
- 代码从原来的20多行变成了800多行,这是因为
stdio.h头文件被完整展开了
- 宏
头文件展开的本质与意义
本质:将头文件的内容原封不动地拷贝到源文件的对应位置
为什么要把头文件和源文件分开?
【补充说明】主要是为了支持库的开发。当我们制作库时,只需要将头文件(包含函数声明)交给用户,而将源文件编译成二进制库文件隐藏起来,这样既可以让用户知道如何使用库,又能保护源代码不被泄露。
条件编译的本质与应用场景
- 本质:对代码进行裁剪
- 核心应用场景:
- 软件版本管理:同一套代码维护免费版、社区版、企业版等不同版本
- 跨平台开发:根据不同操作系统编译不同的代码段
- 调试与发布:在调试版本中保留调试代码,发布版本中裁剪掉
【核心结论】公司内部通常只维护一套完整的代码,通过条件编译选项生成不同功能的版本,大大降低了维护成本。
防止头文件重复包含的原理
我们经常在头文件中看到这样的写法:#ifndefCODE_H#defineCODE_H// 头文件内容#endif或者:
#pragmaonce这两种写法的本质都是条件编译。当第一次包含头文件时,宏未定义,会保留头文件内容并定义该宏;当第二次及以后包含时,宏已经定义,会直接裁剪掉头文件内容,从而避免重复包含导致的重定义错误。
2. 编译(Compilation)
核心工作:将预处理后的干净C语言代码,翻译成汇编语言代码
对应GCC选项与文件后缀:
- 选项:
-S(大写S) - 作用:从当前开始进行程序翻译,完成编译后立即停止
- 输出文件后缀:
.s(汇编语言源文件的标准后缀) - 命令格式:
gcc -S 预处理文件.i -o 汇编文件.s
- 选项:
为什么要翻译成汇编语言,而不是直接翻译成二进制?
这是由计算机语言的发展历史决定的:- 最早的编程方式是二进制编程(打孔纸带),不需要编译器
- 为了提高效率,人们发明了汇编语言,用助记符代替二进制指令,同时诞生了汇编器
- 后来为了进一步提高效率,发明了C语言。C语言的设计者没有从零开始写一个直接生成二进制的编译器,而是站在巨人的肩膀上,先将C语言翻译成汇编语言,再利用已有的汇编器生成二进制。
编译器与语言的关系
【核心结论】编程语言的语法本质上是编译器的翻译规则。我们学习C语言语法,其实就是在学习GCC等编译器如何理解和翻译我们写的代码。
编译器自举原理
这是一个非常经典的"鸡生蛋,蛋生鸡"问题:- 第一代汇编编译器:用二进制直接编写
- 有了汇编编译器后,就可以用汇编语言编写更强大的汇编编译器
- 第一代C语言编译器:用汇编语言编写
- 有了C语言编译器后,就可以用C语言编写更高级的C语言编译器
- 现代的GCC、Clang等编译器,都是用C/C++编写的,自己编译自己
这个过程就叫做编译器自举,是编程语言发展的核心动力。
3. 汇编(Assembly)
核心工作:将汇编语言代码,翻译成可重定位目标二进制文件
对应GCC选项与文件后缀:
- 选项:
-c(小写c) - 作用:从当前开始进行程序翻译,完成汇编后立即停止
- 输出文件后缀:
.o(可重定位目标文件的标准后缀) - 命令格式:
gcc -c 汇编文件.s -o 目标文件.o
- 选项:
可重定位目标文件(.o)的特点
- 它已经是二进制文件,用文本编辑器打开会显示乱码
- 它不能直接运行!即使给它加上可执行权限也不行
【易错警告】很多初学者会误以为二进制文件就能运行,实际上.o文件只是半成品,它里面只包含了我们自己写的代码的二进制实现,但没有和系统库关联起来。
4. 链接(Linking)
核心工作:将我们生成的目标文件(.o)与系统库文件进行关联,生成最终的可执行程序
头文件与库的关系
- 头文件(.h):只包含函数的声明,告诉编译器函数的名称、参数和返回值类型
- 库文件:包含函数的具体实现,已经被编译成二进制
【补充说明】开发C语言程序必须具备三个要素:
- 编译器(GCC/g++)
- 头文件(提供函数声明)
- 库文件(提供函数实现)
我们安装VS时下载的"C++开发工具包",本质上就是下载了Windows平台下的C/C++头文件和库文件。
查看库依赖的命令:
ldd
格式:ldd 可执行文件
示例:ldd code输出中会显示该可执行文件依赖的所有动态库,其中最重要的是
libc.so.6,这就是C语言标准库,它包含了printf、scanf等所有标准函数的实现。为什么.o文件不能直接运行?
因为.o文件中只有我们自己写的代码的二进制,但我们调用的printf等标准函数的实现并不在.o文件中。链接阶段的核心任务就是找到这些函数在库中的实现,并将它们与我们的代码关联起来。
三、动静态库详解(感性认识)
库是预编译好的二进制代码集合,分为静态库和动态库两种。
1. 库的分类与命名规则
| 系统 | 静态库后缀 | 动态库后缀 | 命名格式 | 库的真名 |
|---|---|---|---|---|
| Linux | .a | .so | libxxx.a/libxxx.so | 去掉前缀lib和后缀后的部分(如libc.so的真名是c) |
| Windows | .lib | .dll | xxx.lib/xxx.dll | 去掉后缀后的部分 |
【补充说明】Linux系统本身不关心文件后缀,但编译器和链接器会根据后缀来识别文件类型。
2. 动态库(共享库)
形象比喻:网吧。所有学生(程序)都去同一个网吧(动态库)上网(调用函数)。
工作原理:
- 链接阶段:链接器不会将库函数的代码拷贝到可执行文件中,只会在可执行文件中记录库的路径和函数的地址
- 运行阶段:当程序执行到库函数时,操作系统会根据记录的地址,将动态库加载到内存中,然后跳转到库函数中执行
- 多个程序可以共享内存中的同一份动态库
优缺点:
- ✅ 优点:节省内存(所有程序共享一份库);可执行文件体积小;库更新后不需要重新编译程序
- ❌ 缺点:运行时需要跳转,速度稍慢;强依赖库,库缺失或版本不兼容会导致程序无法运行
3. 静态库
形象比喻:买电脑回家。每个学生(程序)都把自己需要的电脑(库函数)买回家(拷贝到自己的可执行文件中)。
工作原理:
链接阶段:链接器会将程序中用到的所有库函数的代码,完整地拷贝到可执行文件中优缺点:
- ✅ 优点:不依赖库,编译成功后可以独立运行;运行时不需要跳转,速度稍快
- ❌ 缺点:可执行文件体积大;多个程序运行时,内存中会存在多份相同的库代码,浪费内存;库更新后需要重新编译程序
4. GCC链接方式控制
默认方式:动态链接。GCC默认会优先使用动态库进行链接,这也是工业界的最佳实践。
强制静态链接:使用
-static选项
命令格式:gcc 源文件.c -o 可执行文件 -static静态链接常见报错与解决:
如果你执行静态链接命令时出现以下错误:/usr/bin/ld: cannot find -lc collect2: error: ld returned 1 exit status这表示系统中没有安装C语言静态库。
【易错警告】Linux系统默认只安装动态库,不安装静态库。
解决方法(以CentOS/RHEL为例):
sudoyuminstall-yglibc-static动静态链接生成文件的体积对比:
老师课堂演示结果:- 动态链接生成的可执行文件:约8KB
- 静态链接生成的可执行文件:约860KB
体积相差了100多倍,这直观地体现了静态链接的缺点。
四、GCC常用选项总结
| 选项 | 作用 | 对应阶段 | 输出文件后缀 |
|---|---|---|---|
-E | 完成预处理后停止 | 预处理 | .i |
-S | 完成编译后停止 | 编译 | .s |
-c | 完成汇编后停止 | 汇编 | .o |
-o | 指定输出文件的名称 | 所有阶段 | - |
-Wall | 显示所有编译警告 | 编译阶段 | - |
-static | 强制使用静态链接 | 链接阶段 | - |
【记忆技巧】
- 程序翻译的三个阶段选项:ESC(键盘左上角的三个键,注意S是大写)
- 对应生成的文件后缀:ISO(
.i.s.o)
五、核心命令演示
一步生成可执行文件(默认动态链接):
gcc code.c-ocode分步编译:
# 1. 预处理gcc-Ecode.c-ocode.i# 2. 编译gcc-Scode.i-ocode.s# 3. 汇编gcc-ccode.s-ocode.o# 4. 链接gcc code.o-ocode强制静态链接:
gcc code.c-ocode-static-static查看可执行文件依赖的库:
ldd code查看文件类型:
filecodefilecode-static