Menu

如何编译和运行 C 程序(gcc 分步教程)

把一个 .c 文件变成可以运行的程序:gcc 命令,预处理、编译和链接到底做了什么,从第一天起就值得使用的编译选项,以及编译失败时如何读懂错误信息。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

C 是一门编译型语言:在编译器把你的源代码翻译成适合你的 CPU 和操作系统的机器码之前,什么都运行不了。这一步只需要一条命令,但了解它做了什么,能让大多数错误信息从莫名其妙变得一目了然。

两条命令

把下面的代码保存为 hello.c:

#include <stdio.h>

int main(void) {
    printf("Hello, C!\n");
    return 0;
}

然后在同一个文件夹中运行:

gcc hello.c -o hello
./hello

第一条命令负责编译,第二条运行编译结果。在 Windows 上,运行命令在命令提示符中是 hello,在 PowerShell 中是 .\hello:不需要 ./ 前缀,因为 Windows 默认会搜索当前目录,而 Unix shell 有意不这样做。

如果省略 -o hello,gcc 会把输出命名为 a.out(或 a.exe),这就是为什么老教程的最后一步都是 ./a.out。始终给输出文件命名吧,只多写四个字符,却能省去很多困惑。

下面是同一个程序,放在浏览器编辑器中,它会替你完成这两步:

“编译”到底做了什么

gcc hello.c -o hello 看起来是一步,其实是四步,每一步都可能以自己特有的方式出错。

1. 预处理。 在编译任何 C 代码之前,预处理器会处理每一个以 # 开头的行。#include <stdio.h> 会被原样替换成那个头文件的内容;#define 宏会被展开;#ifdef 块会被保留或删除。输出结果是一个不再含有 # 行的大 C 文件。你可以亲眼看看:

gcc -E hello.c

这会打印出几百行,几乎全是粘贴进来的 stdio.h。

2. 编译。 预处理后的 C 代码会经过解析、类型检查,并被翻译成适合你 CPU 的汇编代码。语法错误、类型错误和警告都出自这一步。

3. 汇编。 汇编代码变成目标文件:一种机器码,在调用其他文件中函数的地方留有占位符。

gcc -c hello.c   # produces hello.o, stops before linking

4. 链接。 目标文件与 C 标准库拼接在一起,每个占位符都被填上真实的地址,最终得到可执行文件。“undefined reference” 错误就出自这一步。

实际的结论是:编译错误会指向源代码中的某一行,而链接错误不会,因为链接发生在每一行代码都已经通过检查之后。

从第一天起就值得使用的编译选项

不加任何选项的 gcc file.c -o prog 会悄悄接受很多危险的代码。四个选项可以改变这一点。

gcc -std=c17 -Wall -Wextra -g hello.c -o hello
  • -Wall 打开常见的警告。尽管名字如此,它并不是“全部”警告,而是一组合理的警告。
  • -Wextra 再加上更多警告,包括未使用的参数和一些比较错误。
  • -std=c17 固定语言标准,让你的代码在每台机器上都表示同样的意思。如果你在看较老的资料,就用 -std=c99。
  • -g 保留调试信息,这样程序崩溃时,gdb 或 lldb 能显示出你实际的源代码行。

还有两个你迟早会用到:

  • -O2 为发布版本打开优化。学习期间先别用:优化后的代码更难调试,警告有时也会变化。
  • -fsanitize=address,undefined(gcc 和 clang 都支持)会让程序在越界读取或遇到未定义行为的那一刻中止,并给出清晰的信息。这是学习 C 语言时最有用的一个选项。

自己试试这些警告。下面这个程序可以编译和运行,但它有两个真正的 bug:

count 从来没有被赋值,所以程序打印的是那块内存里碰巧存着的字节。加上 -Wall,编译器会指出这一点:'count' is used uninitialized。C 语言的警告几乎从来不是噪音,把它们当作你还没碰上的错误。

编译多个文件

真实的程序会拆分到多个文件中。把它们全部传给 gcc:

gcc -std=c17 -Wall main.c utils.c -o myprog

或者分别编译每个文件,最后再链接。构建系统就是这样做的,这样修改一个文件时不必重新构建所有文件:

gcc -c main.c      # -> main.o
gcc -c utils.c     # -> utils.o
gcc main.o utils.o -o myprog

有些库需要显式的链接选项。数学库是每个初学者都会碰到的那一个:

gcc calc.c -o calc -lm

不加 -lm 的话,使用 math.h 中的 sqrt 可以正常编译,但在链接时会失败,报出 undefined reference to sqrt:声明在头文件里,代码却在一个没人要求链接的库里。

读懂错误信息

C 语言的错误信息很简短,但很一致。下面三个例子涵盖了你早期会遇到的大部分情况。

漏写分号会在下一行报错,因为编译器一直往下读:

hello.c:5:5: error: expected ';' before 'return'

要改的是第 4 行,而不是第 5 行。每当错误指向一行看起来没问题的代码时,就检查它的上一行。

缺少头文件看起来像是一个谜:你明明把函数名拼对了:

hello.c:4:5: warning: implicit declaration of function 'printf'

这表示编译器从没见过 printf 的声明,也就是你忘了 #include <stdio.h>。在 C99 及以后的标准中,这是一个错误,而不只是警告。

链接失败根本没有行号:

/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x1a): undefined reference to `helper'

编译器相信你说的 helper 存在于某个地方;链接器去找了,却没找到。要么你根本没写它的定义,要么拼写不一样,要么忘了把包含它的文件传给 gcc。

始终先修复第一个错误。C 语言的错误会连锁反应:一行错误的代码可能产生二十条信息,修好第一个,其余十九条就消失了。

退出码

main 中的 return 0 不是装饰。它是程序的退出状态,shell 可以读取它:

运行之后,在 macOS/Linux 上执行 echo $?(在 Windows 上执行 echo %errorlevel%)会打印出这个数字。脚本和构建工具根据它来决定是否继续。这个约定是绝对的:0 表示成功,非零值是由你自己定义的错误码。如果你更喜欢用名字,stdlib.h 定义了 EXIT_SUCCESS 和 EXIT_FAILURE。

从 C99 开始,main 执行到末尾而没有 return 时,会被当作 return 0 处理;但写出来更清楚,而且其他所有函数都必须写。

常见问题

怎么编译和运行 C 程序?

把代码保存为 hello.c,然后运行 gcc hello.c -o hello 进行编译,再运行 ./hello 执行程序(在 Windows 上只需输入 hello)。如果找不到 gcc,你需要先安装编译器,或者使用在线 C 编译器。

gcc -o 是什么意思?

-o 用来指定输出文件的名字。gcc hello.c -o hello 会生成一个名为 hello 的可执行文件。不加 -o 时,gcc 输出到 a.out(在 Windows 上是 a.exe),这就是为什么很多老教程都运行 ./a.out。

gcc 应该始终使用哪些编译选项?

gcc -std=c17 -Wall -Wextra -g yourfile.c -o yourprog。-Wall -Wextra 打开能发现真正 bug 的警告,-std=c17 固定语言版本,-g 保留调试符号,让调试器能显示你的源代码。想在发布版本中提升速度时,再加上 -O2。

C 语言中 “undefined reference to” 是什么意思?

这是一个链接错误:编译器接受了某个函数调用,却找不到这个函数的定义。常见原因有名字拼错、忘了编译另一个 .c 文件,或者使用了数学函数却没有链接数学库(-lm)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始