
文章目录
-
- [1. 编译与运行](#1. 编译与运行)
-
- [GCC、gcc 与 g++](#GCC、gcc 与 g++)
- 常用选项的选择
- [2. 编译过程与中间文件](#2. 编译过程与中间文件)
- [3. 多文件编译与链接错误](#3. 多文件编译与链接错误)
- [4. Makefile 的规则与增量构建](#4. Makefile 的规则与增量构建)
-
- 先写最小规则
- 把规则分解到目标文件
- [`.PHONY` 与 clean](#
.PHONY与 clean) - 更精简的写法:在读懂规则后使用
- [5. 小结](#5. 小结)
上一篇已经基本介绍了工具的安装和 Vim 编辑。本篇将从一条编译命令开始,观察它生成了什么,再把这些命令组织成可以重复使用的构建规则。
1. 编译与运行
可以先在自己的学习目录中操作:
bash
mkdir -p ~/linux-dev-study/build
cd ~/linux-dev-study/build
vim hello.c
保存以下完整文件 hello.c:
c
#include <stdio.h>
#define MESSAGE "hello linux"
int main()
{
puts(MESSAGE);
return 0;
}
编译,再运行:
bash
gcc hello.c -o hello
./hello
运行示例:

gcc 在这里是编译驱动程序,它会按需要调用各阶段的工具。hello.c 是输入文件,-o hello 指定输出文件名。正常情况下编译成功可以没有任何输出。
./hello 表示运行当前目录中的 hello。
Shell 通常不会默认搜索当前目录,所以直接输入
hello可能提示找不到命令。
Linux 可执行文件不靠 .exe 后缀识别。起名 hello.exe 也能使用,但不会因此变成 Windows 程序。
GCC、gcc 与 g++
GCC 是编译器工具集合的名称,gcc、g++ 是常见驱动命令。
不能简单说 gcc 只能处理 C、g++ 只能处理 C++:文件后缀和选项会影响语言的选择,两者的默认链接行为也有区别。
对于当前阶段,保持这条实用习惯即可:C 程序使用 gcc,C++ 程序使用 g++,C++ 程序的最终链接也交给 g++,由它处理 C++ 标准库的常规链接。
一个独立的 C++ 小例子,保存为 hello.cpp:
cpp
#include <iostream>
int main()
{
std::cout << "hello cpp" << std::endl;
return 0;
}
bash
g++ hello.cpp -o hello_cpp
./hello_cpp
运行示例:

常用选项的选择
| 选项 | 当前需要理解的作用 |
|---|---|
-o 文件名 |
指定该次命令的输出文件名,不是专指可执行文件 |
-g |
生成调试信息,便于调试器联系源码、变量与机器代码 |
-O0 |
关闭通常的优化,便于我们按源码观察执行 |
-O2 |
常见优化级别;本篇实践先不启用 |
-g和优化选项相互独立,可以同时使用。没有指定优化选项时 GCC 通常相当于-O0
2. 编译过程与中间文件
我们常说的"编译程序",有时指的是整条构建链;而狭义的编译阶段,只是其中的一步。

在 hello.c 所在目录执行:
bash
gcc -E hello.c -o hello.i
gcc -S hello.i -o hello.s
gcc -c hello.s -o hello.o
gcc hello.o -o hello
./hello
运行示例:


四条命令分别留下各阶段产物,最后的输出仍为
hello linux。平常是不必手动执行四步的,这里是为了观察。
预处理:处理头文件与宏
-E 表示在预处理后停止。预处理包括头文件包含、宏展开、条件编译等。观察:
bash
head -n 15 hello.i
grep -n 'hello linux' hello.i
运行示例:

stdio.h 及其间接包含的内容会被展开,结果通常比原文件长得多。
不加 -o 时,gcc -E hello.c 默认把结果写到标准输出;既可以重定向,也可以用 -o。
编译:产生汇编文本
gcc -S hello.i -o hello.s 把预处理后的代码转换为汇编文本。可以使用 head hello.s 查看头部部分信息。

不同编译器版本、CPU 架构、优化选项可能会生成不同的汇编。
汇编:产生可重定位目标文件
gcc -c hello.s -o hello.o 让汇编代码成为目标文件。.o 通常含有机器代码、数据和链接所需的信息,但还没有完成生成最终程序所需的链接。
如果从源码开始,下面这条命令会自动做前面的必要处理:
bash
gcc -c hello.c -o hello.o
所以 -c 不是"只接受汇编文件",而是处理到目标文件为止,不执行最终链接。
链接:把所需部分组织成程序
gcc hello.o -o hello 组织目标文件,并按工具链约定连接运行所需的库等内容,最终生成可执行程序。
3. 多文件编译与链接错误
只有一个源文件时,很难体会到链接的必要性。所以现在我们编写一个求和的函数,把求和功能分开,在新的子目录练习:
bash
mkdir -p ~/linux-dev-study/build/sum
cd ~/linux-dev-study/build/sum
创建三个完整文件。sum.h 声明接口:
c
#ifndef SUM_H
#define SUM_H
int sum_range(int first, int last);
#endif
sum.c 提供实现:
c
#include "sum.h"
int sum_range(int first, int last)
{
int total = 0;
for (int i = first; i <= last; ++i)
{
total += i;
}
return total;
}
main.c 调用接口:
c
#include <stdio.h>
#include "sum.h"
int main()
{
printf("sum(1..100) = %d\n", sum_range(1, 100));
return 0;
}
先分别生成目标文件,再进行链接:
bash
gcc -std=c11 -c main.c -o main.o
gcc -std=c11 -c sum.c -o sum.o
gcc main.o sum.o -o sum_demo
./sum_demo
运行示例:

也可以一步完成:
bash
gcc -std=c11 main.c sum.c -o sum_demo
-std=c11选项指定 C11,兼顾一些默认语言标准较旧的 GCC 环境。
根据错误位置判断原因
若 main.c 漏写一个分号,编译器在翻译源码时就会报语法错误。若 main.c 本身语法正确,但链接时漏掉 sum.o:
bash
gcc main.o -o missing_demo
通常会看到关于 sum_range 的 undefined reference:声明存在,所以对函数的调用可以被编译,但是最终找不到定义,链接无法完成。
库的作用:了解即可
多个目标文件可以直接一起链接,也可以把可复用实现组织为库。Linux 下常见静态库后缀为 .a,共享库常见为 .so 及其版本形式。
静态库链接通常把需要的目标模块纳入最终程序,不是无条件把整个库全部复制进去。动态链接通常保留对共享库的依赖,在装载、运行过程中由相应机制完成连接。
在常见 Linux GCC 工具链环境中,默认生成的程序通常动态链接 C 运行库。
bash
file sum_demo
ldd ./sum_demo
运行示例:

输出中的路径、加载器名称和地址因系统而异。
4. Makefile 的规则与增量构建
当我们编写的代码文件多起来之后,每次都手敲一串编译命令就容易遗漏。如果把命令写进普通脚本可以实现批量执行,但如果只改了一个 .c 文件,仍然每次全量编译就有些浪费。
make 的价值是:根据目标与依赖关系,判断哪些结果需要更新,再执行相应命令。 make 是工具,Makefile 是它读取的规则文件,make 自己并不承担 C 语言编译。
先写最小规则
在刚才的 sum 目录中创建 Makefile:
makefile
sum_demo: main.c sum.c sum.h
gcc -std=c11 main.c sum.c -o sum_demo
.PHONY: clean
clean:
rm -f sum_demo
配方行 gcc ... 与 rm ... 开头是Tab ,不是若干空格。复制后若出现 missing separator,首先检查这一点。
规则的一般形式为:
makefile
目标: 前置依赖
生成或更新目标的命令
在示例中,sum_demo 依赖三个源码文件,只要其中一个比它新,就需要重新生成它。把
bash
make
./sum_demo
make
运行示例:

第一次需要时执行编译命令;第二次若文件没有变化,通常提示目标已是最新,不再次执行配方。
把规则分解到目标文件
上面的写法能够工作,但每次有变化仍一起编译两个源文件。所以现在将刚才的 Makefile 整体替换为:
makefile
CC = gcc
CFLAGS = -std=c11 -g -O0
sum_demo: main.o sum.o
$(CC) main.o sum.o -o sum_demo
main.o: main.c sum.h
$(CC) $(CFLAGS) -c main.c -o main.o
sum.o: sum.c sum.h
$(CC) $(CFLAGS) -c sum.c -o sum.o
.PHONY: clean
clean:
rm -f main.o sum.o sum_demo
CC 与 CFLAGS 是变量,$(变量名) 取出内容。链接命令接收 .o 文件,不再重复编译源文件。

使用以下命令观察完整过程:
bash
make clean
make
make
touch sum.c
make
touch sum.h
make
运行示例:

- 依次观察到:清理后生成两个
.o并链接;无改动时不重建; - 更新
sum.c的时间后重建sum.o并链接; - 更新
sum.h的时间后,两个.o都重建并链接。 touch在这里用于模拟依赖"变新",即使文件内容没变,也会影响判断。
默认的文件目标更新主要比较修改时间 mtime:目标不存在,或前置依赖的 mtime 更新,通常就需要重建。
系统时钟异常、时间戳未发生有效变化,也可能影响判断。
这套关系还解释了执行顺序:虽然最终目标写在前面,但要得到它,得先确保 main.o、sum.o 可用。通常从所需目标沿依赖向下检查,再执行必要的生成命令。
.PHONY 与 clean
clean 表达的是清理动作,不是生成一个名叫 clean 的文件。
makefile
.PHONY: clean
这条声明告诉 make:clean 是伪目标,不以同名文件是否存在、是否最新来判断它。在本篇示例中,每一次的请求 make clean 都会执行清理配方。
不要为了"保证编译"把最终可执行文件随便声明成伪目标,否则会失去它本来的增量判断。本例中 all、clean 等动作或聚合入口适合声明,真实 .o、可执行文件保留为文件目标。
更精简的写法:在读懂规则后使用
同一个小项目可以改写为下面的完整 Makefile:
makefile
CC = gcc
CFLAGS = -std=c11 -g -O0
OBJ = main.o sum.o
sum_demo: $(OBJ)
$(CC) $^ -o $@
%.o: %.c sum.h
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJ) sum_demo
| 写法 | 本例含义 |
|---|---|
$@ |
当前规则的目标 |
$^ |
当前规则的前置依赖列表,去掉重复项 |
$< |
第一个前置依赖;模式规则里对应匹配的 .c |
%.o: %.c sum.h |
为同名 .c 与 .o 建立一类规则,并显式依赖 sum.h |
- 这里的
%匹配共同的文件名主体,不是 Shell 的*.c展开。 - 在命令的开头加上
@可隐藏命令回显,初学时可以保留回显来方便观察。
若文件数量增加,可以用
SRC = $(wildcard *.c)获取当前目录匹配的源码,再用OBJ = $(SRC:.c=.o)得到目标文件列表。
5. 小结
-
GCC/G++ 编译分为四阶段:预处理、编译、汇编、链接;可分步生成
.i、.s、.o目标文件,也可一步完成编译链接。 -
多文件项目:
.h头文件存放函数声明,.c源文件存放实现;编译阶段只检查声明,链接阶段需要找到函数实现,缺失实现会报undefined reference。 -
常用编译选项:
-std指定 C 语言标准,-g添加调试信息,-O0关闭优化,-c只生成.o目标文件不链接。 -
Make 通过文件修改时间 mtime 判断是否重建,只重新编译过期的文件,减少重复编译耗时。
-
touch命令修改文件时间戳,常用于测试 Makefile 的依赖关系。
