# 〇、从机器语言到 Hello World


<!--more-->

> [!abstract] 这篇文章能帮你解决什么？
> 如果你对编程毫无概念，或者只有一些零碎的了解；如果你照着老师给的步骤走完了，却完全不知道这些操作对自己的电脑做了什么；如果你只会用 Dev-C++或 VScode 点按钮，不清楚代码到底是怎么变成程序的
>
> 这篇文章会从零开始带你走完：**认识代码怎么变成程序 → 装编译器 → 配置环境变量 → 用最“土”的办法跑通第一个程序**。
> 全程以 Windows 操作系统为主视角，每一个操作都能跟着做，不需要任何前置知识。

## 写在前面：为什么“写个代码”这么难？

你可能经历过这样的场景。

照着老师的步骤一步一步做，完全不理解这些操作到底对电脑做了什么。抄完代码，满怀期待地按下运行键 —— 结果屏幕上冒出一行红字：

```plain {title="终端里让人抓狂的报错"}
'g++' 不是内部或外部命令，也不是可运行的程序或批处理文件。
```

折腾半天，`Hello World` 终于跑出来了，成就感爆棚 —— 但你完全不知道刚才发生了什么。想自己写点东西，又无从下手。

问题通常不在你。**问题在于没有人告诉你“中间发生了什么”。**

很多人学编程的第一步就跳得太快了：直接打开一个编辑器开始敲代码。于是“**编译器**”、“**环境变量**”、“**终端**”这些词，全都变成了一堆只能硬背的操作步骤 —— 因为没人解释它们在干嘛。

所以这篇文章会反着来：**先把原理讲清楚，再动手。** 你会知道每个操作为什么要做、不做会出什么问题。

## 第0步 · 从开关到人话：认识三种语言

在装任何软件之前，先花十分钟搞明白一件事 —— 你写的那几行字，是怎么一步步变成 CPU 能执行的程序的。

### 0.1 电脑的世界里，只有“开”和“关”

把电脑拆开看，里面是一大堆电路。电路上的每一个位置只有两种状态：**通电**，或者**断电**。

我们用两个数字来记它们：`1` 表示通电，`0` 表示断电。这就是“二进制”这个词的由来。

- 一个开关，叫 **1 位（bit）**
- 八个开关排成一排，叫 **1 字节（byte）**
- 一排排开关按照某种顺序排列，就组成了一条“指令”

在键盘和显示器普及之前，程序员真的是在纸带上**打孔**来写程序的：有孔是 1，没孔是 0。

![一排拨动开关，两个亮着橙光表示通电，三个熄灭表示断电，下方延展出一条打孔纸带](images/illus-01-machine-language.jpg "电脑的底层世界：开与关、1 与 0")

> [!info]机器语言
> 直接用 `0` 和 `1` 写出来、CPU 能**直接看懂**的指令，就叫**机器语言**。
> 它是唯一一种 CPU 生来就认识的语言，**其他所有语言最终都得翻译成它**。

一条真实的机器指令（十六进制）长这样：

```plain {title="一条 x86 机器指令"}
B8 01 00 00 00
```

这 5 个字节其实只在说一件事：**把数字 1 放进 `eax` 这个小盒子里。**

> [!warning] 那为什么不能一直用机器语言？
> 因为它根本就不是给人用的。上面那 5 个字节只完成了“放一个数字”这么小的动作；写一个能算平均分的程序，你得手算出成百上千个字节，而且**错一个 0 就全盘皆输**，报错信息还不会给你任何线索。
> 难写、难读、难查错 —— 这三条足以把它从日常开发里淘汰掉。

### 0.2 汇编语言：给每条指令起个名字

既然 CPU 只认 0 和 1，那我们能不能换个思路：**给每一种“开关组合”起个好记的名字**，写代码的时候用名字代替？

![一排机械开关，每个开关旁贴着写有几何符号的小标签，一只手正把新标签贴到另一个开关上](images/illus-02-assembly.jpg "给机器指令贴上标签")

于是 `mov`（move，搬运）、`add`（add，相加）这样的**助记符**出现了。刚才那条机器指令就被写成了一行：

| 机器语言 | 汇编语言 | 含义 |
| --- | --- | --- |
| `B8 01 00 00 00` | `mov eax, 1` | 把 1 放进 eax |
| `83 C0 02` | `add eax, 2` | 让 eax 再加 2 |

> [!info]汇编语言
> 用**助记符**（`mov`、`add`、`jmp` 这些短单词）代替 0 和 1 的语言。
> 它和机器语言**几乎一一对应**：一条汇编指令基本就对应一条机器指令。

这就是它的问题所在：**汇编语言并没有比机器语言“高级”多少。** 它只是换了一套更顺手的符号，思考方式仍然是“操作哪个寄存器、搬哪个字节” —— 你依然得用 CPU 的脑子去想问题。

不过别小看它。汇编到今天依然重要：操作系统内核、驱动程序、逆向工程都离不开它。

### 0.3 高级语言：干脆说人话

能不能再往前一步 —— **我不想知道寄存器，我只想描述“我要做什么”**？

![左边的人对着对话气泡说话，右边的机器人一脸困惑，中间隔着一道由细密竖线构成的墙](images/illus-03-high-level.jpg "你说的话，机器听不懂")

于是有了 C 语言这样的**高级语言**：

```c {title="用 C 语言描述同一件事"}
int sum = 1 + 2;
printf("%d", sum);
```

这两行不需要你知道 `eax` 是什么、字节长什么样。你只描述了两件事：**把 1 和 2 加起来，把结果打印出来。**

> [!info]高级语言
> 用接近人类语言和数学表达式来描述“要做什么”的语言。C、C++、Python、Java、Go、JavaScript、TypeScript 等都属于高级语言。
> 它的**抽象层次更高**：一句话往往能顶几十上百条机器指令。

代价也很直接：**CPU 完全看不懂。**

### 0.4 三层语言放在一起看

把刚才那道加法题用三种语言各写一遍，差别一目了然：

![同一道加法题在高级语言、汇编语言、机器语言三种层次下的写法对照，右侧箭头表示越往上越接近人话](images/langs-compare.svg "同一件事，三种写法")

- **越往上**，越接近人话，写起来越省事；
- **越往下**，越接近机器，执行起来越直接。

> [!tip] 记住这句话！！！
> 你写的是**高级语言**，而 CPU 只认**机器语言**。
> 所以中间必须有一个角色，把“人话”翻译成“0 和 1” —— 这个角色就叫**编译器**（compiler）。

### 0.5 所以第一步不是装编辑器，而是装编译器

![画面中央一位翻译官站在人和机器中间，把左边人说的话翻译给右边的机器人](images/illus-04-compiler.jpg "编译器：站在人和机器之间的翻译官")

>[!warning]初学者刚开始最容易搞混的一件事就是分不清编辑器和编译器，以为DevC++、VSCode之类的就能把所有活全干了
>**编辑器**（记事本、VSCode）是你**写**代码的地方 —— 它本质上只是个高级一点的打字软件；
>
>**编译器**（我们接下来要装的 GCC）才是把代码**翻译**成程序的地方 —— 它才是让代码真正“跑起来”的那个“翻译官”。


所以顺序很清楚：

```mermaid
graph LR
  A["① 装编译器<br/>"] --> B["② 配置环境<br/>让电脑找得到它"]
  B --> C["③ 纯终端跑通<br/>第一个程序"]
  C --> D["④ 搬进 VSCode<br/>开始日常写代码"]
```

> [!question] 为什么不是“直接装 VSCode”？
> 当然可以。但你会变成“只会按按钮的人” —— 程序跑不起来的时候，完全不知道发生了什么。
> 所以我们先用最“土”的办法：**记事本 + 黑色终端窗口**，把程序跑通一次。之后再搬进 VSCode，你会发现它到底替你做了什么，你一清二楚。

---

## 第1步 · 请一位翻译官：安装 MinGW {#install-mingw}

第0步的结论只有一句：**你写的是高级语言，CPU 只看机器语言，中间必须有编译器来翻译。**

那问题就很现实了 —— 该到哪里去下载编译器？

答案是去网上下一份 **MinGW**，解压到 C 盘（或者随便你喜欢的哪个位置） —— 这一步就干这一件事。

### 1.1 Windows 为什么不自带编译器

有些操作系统是自带编译器的：装好系统它就已经在里面躺着了，打开终端直接能用。Windows 没有这个待遇。

![一张干净的书桌，桌上放着打开的笔记本电脑，桌旁站着一个手足无措的简笔人物，旁边一把椅子空着](images/illus-06-winlibs.jpg "翻译官还没到岗，书桌和椅子先摆好了")

{{< tabs defaultTab="0" type="underline" >}}

{{% tab title=":(fa-brands fa-windows fi-c-blue): Windows" %}}

原因不难理解。Windows 的用户绝大多数是拿它办公、追剧、打游戏的，写代码只占很小一部分。微软自己也有一整套开发工具（Visual Studio），但那是几个 GB 的大件，默认不会塞进系统。

所以在 Windows 上写 C / C++，第一步永远是**安装一个编译器**。

**下面 [1.2](#1.2) 到 [1.5](#1.5) 讲的就是windows系统如何安装编译器。**

{{% /tab %}}

{{% tab title=":(fa-brands fa-apple fi-c-slate): macOS" %}}

Mac 属于“自带”那一类：苹果把 **Clang** 这套编译器随系统一起装好了，只是默认锁着，要用一条命令解锁：

```bash {title="在终端里执行"}
xcode-select --install
```

敲完会弹出对话框，点 **安装**、同意许可协议，等它把命令行工具下完（几百 MB，需要点时间）就绪。之后 `clang`、`clang++`、`lldb` 都能直接用了。

> [!info] Mac 上的名字对照
> 编译器叫 `clang` / `clang++`（不是 `gcc` / `g++`），调试器叫 `lldb`（不是 `gdb`）。
> 本文后面凡是出现 `gcc`、`g++`、`gdb` 的地方，Mac 读者按这张表替换 —— 每个涉及命令的标签页里都会再提醒一次。

{{% /tab %}}

{{< /tabs >}}

> [!note] Mac 系统请看这里
> 本节 1.2 到 1.5（下载 MinGW、解压、配置 PATH）**整段与你无关**，[**可直接跳转至第2步**](#第2步)。
<span id="1.2"></span>
### 1.2 MinGW-w64 到底是什么

**MinGW** 是 *Minimalist GNU for Windows* 的缩写，直译是“给 Windows 用的最小化 GNU”。

**GNU** 是个自由软件项目，最出名的作品是 **GCC**（GNU Compiler Collection，GNU 编译器套装）—— 一套能编译 C、C++、Fortran 的编译器。它最初并不是为 Windows 准备的，于是有人做了件事：**把 GCC 搬到 Windows 上**。这个搬运工作的产物，就叫 MinGW。

**MinGW-w64** 是它的一个分支：最早的 MinGW 只面向 32 位系统，后来有人补上了 64 位支持，一直维护到今天。现在网上下到的“MinGW”，基本都是 MinGW-w64。

> [!info]工具链
> MinGW-w64 不是一个软件，而是**一整箱工具**。它们衔接起来像一条流水线，所以这种组合被叫作**工具链**（toolchain）。
> 一次编译要好几个小工具接力，`gcc` 只是站在最前面的总指挥。

这里我们要认识以下三个程序：

| 工具 | 全称 | 干什么 | 什么时候用 |
| --- | --- | --- | --- |
| `gcc` | GNU C Compiler | 把 C 语言代码翻译成可执行程序 | 写 `.c` 文件时 |
| `g++` | GNU C++ Compiler | 把 C++ 代码翻译成可执行程序 | 写 `.cpp` 文件时 |
| `gdb` | GNU Debugger | 调试：让程序停下来、一步步走、看变量 | 程序跑不对时 |

一句话记法：**`gcc` 管 C，`g++` 管 C++，`gdb` 管找 bug。**

> [!note]**MinGW 不是 IDE**
> 它没有窗口、没有菜单、也没有“运行”按钮，就是几个命令行的 `.exe`。

### 1.3 去 winlibs.com 下载

MinGW-w64 是开源项目，可在 **WinLibs** 网站下载—— 它把 MinGW-w64 预编译打包好，下压缩包解压就能用。

{{< link href="https://winlibs.com/" content="WinLibs 官网" title="下载预编译好的 MinGW-w64" card=true card-icon="fa-solid fa-download" >}}

![一个由云端落下的快递纸箱，下方一台笔记本电脑和一个张开双手准备接收的简笔小人](images/illus-07-download.jpg "下载：从云端把工具包送进你的电脑")

打开页面往下滚，找到 **Release versions** 那一段。它分成 **UCRT runtime** 和 **MSVCRT runtime** 两组 —— 这是两套不同的系统运行库，**UCRT 更新、更贴近现代 Windows**，新手直接选 UCRT。

每个版本下面会列出两种架构、两种压缩格式，规则一张表说清：

| 页面上看到的选项 | 选哪个 | 为什么 |
| --- | --- | --- |
| UCRT runtime / MSVCRT runtime | **UCRT runtime** | 更新，更贴近现代 Windows 的系统库 |
| Win32 - i686 / Win64 - x86_64 | **Win64 - x86_64** | 现在的电脑基本都是 64 位 |
| 7-Zip archive / Zip archive | **Zip archive** | Windows 自带解压，不用额外装 7-Zip |

标着 **LATEST** 的就是最新版。下载下来的文件名很长，形如：

```plain {title="下载到的文件大致长这样"}
winlibs-x86_64-posix-seh-gcc-16.2.0-mingw-w64ucrt-14.0.0-r1.zip
```

数字会随版本更新而变，认准两个词就行：**`x86_64`**（64 位）和 **`ucrt`**（UCRT 运行库）。

> [!warning] 别被“一键安装包”骗了
> 网上还能搜到第三方打的“MinGW 一键安装版”，能用，但来源和版本都不好确认，**甚至还TM需要付费**？
>
> ~~比如某SDN~~。
>
> WinLibs 的压缩包解压即用，更干净也更省心。

### 1.4 解压到 C:\mingw64
> [!note] 你也可以自己选一个目录，不过后续配置 PATH 时要用你自己选好的安装目录
>
在 C 盘根目录新建一个文件夹，取名 `mingw64`，完整路径就是 `C:\mingw64`。右键压缩包选 **全部解压缩**，解压到这里。解压完检查一下，里面应该长这样：

```file-tree
- name: C:/mingw64
  type: dir
  children:
    - name: bin
      type: dir
      children:
        - name: gcc.exe
          type: file
        - name: g++.exe
          type: file
        - name: gdb.exe
          type: file
    - name: include
      type: dir
    - name: lib
      type: dir
    - name: libexec
      type: dir
    - name: share
      type: dir
    - name: x86_64-w64-mingw32
      type: dir
```

![C:\mingw64 目录结构示意图，bin 文件夹高亮，右侧列出 gcc、g++、gdb 三个工具各自的用途](images/illus-08-mingw-layout.svg "你的工具箱：三个工具都住在 bin 里")

目录名不用记，认准一个就行：**`bin`**。点进去会看到一大堆 `.exe`，其中就有 `gcc.exe`、`g++.exe` 和 `gdb.exe`。

> [!warning] 两条铁律：路径不含中文，也不含空格
> 安装位置必须是 `C:\mingw64` 这种干干净净的路径，别放进 `D:\我的软件\C++ 工具\` 这类地方。
> 编译器和构建工具处理这类路径时容易出错，更麻烦的是**报错信息会因为编码问题变成乱码**，你连错在哪都看不出来。

> [!note] 解压出来多套了一层目录？
> 有些包会变成 `C:\mingw64\mingw64\bin`。把里层的东西全部剪切出来，让路径回到 `C:\mingw64\bin` 这一层就行。
<span id="1.5"></span>
### 1.5 验收：敲一下 g++，结果报错了

工具链已经安装完毕，该验收一下它到底能不能干活。

先按 **Win + R**，输入 **cmd**，回车，你会看到一个黑底白字的窗口。（终端和 PowerShell 是什么，下一节讲；现在只要会用这个窗口。）在里面敲这一行：

```bash {title="在终端里执行"}
g++ --version
```

正常的话，屏幕上会打印出一串版本信息。但现在你多半会看到这一行：

```plain {title="终端里的报错"}
'g++' 不是内部或外部命令，也不是可运行的程序或批处理文件。
```

**这行字你也许已经见过 —— 现在轮到你亲手把它敲出来。**

> [!tip] 这是预期之内的，不是你哪一步做错了
> 编译器确实已经安装到你的电脑上了：`C:\mingw64\bin\g++.exe` 就安装在你的硬盘上。
> 只是你还没**把它的住址告诉系统**。操作系统会在几个默认位置找一个叫 `g++` 的程序，没找着，只好回你“不是内部或外部命令”。
> 东西在，路没通 —— 就这么简单。
---
<span id="第2步"></span>
## 第2步 · 让电脑找到它：环境变量与 PATH

### 2.1 明明装好了，系统凭什么说“找不到”

想象一间工作室。你把扳手放进了新买的三层抽屉，第二天让助手去拿，他翻了半小时回来说：“我们这儿没有扳手。”

助手没错。他手里有一份**工具清单**，上面写的是“扳手在工作台左边第二个柜子”。你换了地方，清单没更新 —— 他不认识你的新抽屉，只认清单。

Windows 面对你敲下的 `g++` 就是这个助手。它手里也有一份“去哪里找程序”的清单，上面没有 `C:\mingw64\bin`，自然找不到 `g++.exe`，只能回你一句“不是内部或外部命令”。

![一个简笔人物站在敞开的抽屉柜前摊手，抽屉里几乎空空荡荡，脚边飘落一张空白小卡片](images/illus-09-command-not-found.jpg "东西就在抽屉里，可系统手里的清单上没有这个地址")

> [!tip] 这不是安装失败
> 报错里没有一个字提到“安装”，因为安装那一步完全成功了。
> 你要做的不是重装，而是**更新那份清单**。

### 2.2 环境变量到底是什么

那份清单，学名就叫**环境变量**。

> [!info]环境变量
> 环境变量是一组 **“名字 = 值”** 的配置，由操作系统统一保管。程序在启动的那一刻，会把这批配置读进自己脑子里。
> 它不属于某一个程序，而是**整个系统共享**的一份公共设置。

举两个你天天在用、却从没注意过的例子：

| 环境变量 | 值大概是 | 谁在读它 |
| --- | --- | --- |
| `USERPROFILE` | `C:\Users\你的用户名` | 想知道“当前用户的文件夹在哪”的软件 |
| `TEMP` | `C:\Users\你的用户名\AppData\Local\Temp` | 想找地方放临时文件的软件 |

它们的存在让程序**不必把路径写死**：换个用户名，读到的值就跟着变，代码一行都不用改。说白了，环境变量是**系统启动程序时顺手递过去的一张纸条**。

### 2.3 PATH：系统的程序通讯录

环境变量有很多个，但只有一个决定“你敲的命令能不能跑起来”。它叫 **`Path`**（习惯写作 PATH）。

> [!info]PATH
> PATH 的值是一串**文件夹路径**，路径之间用**英文分号 `;`** 隔开。
> 它只干一件事：告诉系统，**敲下命令之后该去哪些文件夹里找这个程序**。

你敲下 `g++ --version` 的那一瞬间，系统做的是：

1. 把 `g++` 补全成 `g++.exe` —— Windows 上的程序都带这个后缀；
2. **从上到下、按顺序**去 PATH 里的每一个文件夹看一眼：“这里有 `g++.exe` 吗？”
3. 一旦找到，**立刻停手**，执行它；
4. 整份清单翻完还是没有，就打印那行“不是内部或外部命令”。

![PATH 查找原理图：系统按顺序翻 PATH 清单，前几个文件夹都没有 g++.exe，在 C:\mingw64\bin 里找到了](images/illus-10-path-lookup.svg "系统按顺序翻 PATH 清单，直到在 C:\mingw64\bin 找到 g++.exe")

**找到就停**，说明 PATH 的顺序会影响结果；**全都没有才报错**，说明那行报错的含义只有一个 —— 你的程序不在清单上的任何一个文件夹里。

> [!note] 那终端里的 `cmd`、`notepad`、`ping` 命令为什么直接就能用？
> 因为 `C:\Windows\system32` 本来就躺在 PATH 里。

### 2.4 动手：把 C:\mingw64\bin 交给 PATH

原理讲完了，动手只有一件事：**在 PATH 清单末尾追加一行 `C:\mingw64\bin`。**

> [!note] Mac 读者：这一节只看不做
> Mac 装上命令行工具之后，`clang` 已经在 PATH 里了，不需要手动配置。
> 2.4 和 2.5 的动手步骤只针对 Windows，[你可以直接跳到 2.6](#2.6)。

> [!warning] 加的是 bin，不是 mingw64
> 要交给系统的是**装着 `.exe` 的那个文件夹**，也就是 `C:\mingw64\bin`。
> 写成 `C:\mingw64` 没有用 —— 那个文件夹里没有 `g++.exe`。

按 **Win** 键，输入 **环境变量** 四个字，搜索结果里点 **编辑系统环境变量**。

![一个简笔人物正往一本厚重的通讯录里写入新卡片，旁边站着一个小小的拟人化扳手在等待](images/illus-11-add-to-path.jpg "把 bin 的住址登记进系统手里的通讯录")

在“系统属性”窗口右下角点 **环境变量(N)...**。新窗口分成上下两半：**上半部分是“用户变量”**，我们操作这一半。

1. 在上半部分的列表里选中 **`Path`** 那一行；
2. 点它下面的 **编辑**；
3. 在新窗口里点 **新建**，列表末尾会多出一个空行；
4. 把 `C:\mingw64\bin` 粘进去（`Ctrl + V`）；
5. **逐层点“确定”**，直到所有窗口都关掉。

> [!warning] 注意！
> **① 只点右上角的叉等于白干。** 那个叉的含义是“放弃修改”，必须逐层按下 **确定**，改动才会真正写进去。
> **② 改完必须把终端和 VSCode 完全关掉再重开。** 环境变量是程序**启动的那一刻**读进去的，已经打开着的程序拿的还是旧清单（旧PATH）。

{{< details summary="“用户变量”和“系统变量”，我到底该选哪个？" >}}
**用户变量**只对当前登录的账号生效；**系统变量**对这台电脑上的所有用户生效。

自己电脑自己用，选**用户变量**就够了：更安全，不会影响别人的账号，改错了也更好收拾。这也是本文推荐的做法。只有当这台电脑还有别人也要用 `g++` 时，才考虑系统变量 —— 那通常需要管理员权限。
{{< /details >}}

### 2.5 验收：这次真的成功了

先把那个黑窗口**彻底关掉**，再按 **Win + R**、输入 **cmd** 回车，开一个全新的窗口，敲这两行：

```bash {title="在终端里执行"}
g++ --version
gdb --version
```

配置对了的话，屏幕会打印两串版本信息：

```plain {title="终端输出"}
g++ (MinGW-W64 x86_64-ucrt-posix-seh, built by Brecht Sanders) 16.2.0
Copyright (C) 2026 Free Software Foundation, Inc.
```

`gdb --version` 的格式类似，只是换成它自己的版本号。**看到版本号，这块石头就落地了。**

{{< center-quote >}}
**没有消息就是最好的消息。** 命令行只有在出错时才会说话。
{{< /center-quote >}}

这句话值得记很久：真正成功的命令往往一个字都不打印，终端安静地什么都不说，反倒说明一切正常。

验收清单：

- [x] “用户变量”的 `Path` 里多了一行 `C:\mingw64\bin`
- [x] 每一层窗口都是点 **确定** 关掉的，不是点叉
- [x] 关掉了旧终端，重新开了一个
- [x] `g++ --version` 打印出了版本号
- [x] `gdb --version` 打印出了版本号

> [!success] 到这里，你的电脑已经是一台能写 C 的电脑了
> 编译器已经安装，操作系统也认得了它的名字。接下来只差一件事：写一个真正的程序。
<span id="2.6"></span>
### 2.6 顺便，把终端彻底搞明白

环境变量配好了，你和编译器之间还隔着一个东西 —— 那个黑色窗口，也就是**终端**。往后你的每一次编译、每一次调试都在这里发生。趁现在把它一次讲透。

![一个简笔人物坐在桌前，面对一个发着柔和光的窗口，窗口里伸出几个装着几何图形的对话气泡](images/illus-12-terminal.jpg "终端：你和电脑说话的那个窗口")

#### 2.6.1 终端、控制台、Shell、cmd、PowerShell 都是什么？

一张表拆开这几个词：

| 名词 | 是什么 | 一句话记法 |
| --- | --- | --- |
| **终端**（Terminal） | 你看到的那个窗口，显示文字、接收键盘输入 | 它是**壳** |
| **控制台**（Console） | 更早的说法，概念上和终端基本重叠 | 老名字，知道就行 |
| **Shell** | 终端里真正**读懂并执行**你敲的命令的那个程序 | 它是**里子** |
| **cmd** | Windows 传统的 shell，从 DOS 时代继承下来 | 里子 A |
| **PowerShell** | 更新的 shell，命令风格和 cmd 不一样 | 里子 B |

**终端是壳，shell 是里子；cmd 和 PowerShell 是 Windows 上两种不同的里子。**

![层叠关系示意图：终端窗口是最外层的壳，Shell 是壳里真正执行命令的那个程序，cmd 和 PowerShell 是两种可以互换的 Shell](images/illus-13-terminal-shell.svg "终端是壳，Shell 是里子：壳不变，里子可以换")

上面这张图把这几层叠在一起看：最外层那个黑框是**终端**（壳），壳里真正执行命令的是 **Shell**（里子），而同一个窗口同一时刻只装一种里子 —— 要么 cmd，要么 PowerShell。想分辨自己站在哪一种里子里，看**提示符**最快：`C:\Users\你>` 是 cmd，`PS C:\Users\你>` 是 PowerShell。

> [!tip] 那本文的命令用哪个敲？
> **两个都能用。** 后面的 `g++`、`gcc`、`gdb` 在 cmd 和 PowerShell 里都跑得通。
>
> 只有一处写法不同，就在 2.6.4 表格的最后一条。
>
> 建议以后在windows系统上只用powershell，因为cmd功能相对残缺，语法也与现代shell有差异，只是历史遗留下来的程序。

#### 2.6.2 打开终端的四种方式

| 方式 | 怎么操作 | 适合什么场景 |
| --- | --- | --- |
| `Win + R` | 按下后输入 `cmd` 或`powershell` 回车 | 最快，但起始目录是用户文件夹，还得自己 `cd` |
| 资源管理器地址栏 | 打开文件夹，在**地址栏**里输入 `cmd`或`powershell` 回车 | 想在某个具体目录干活，最省事的一种 |
| `Shift + 右键` | 文件夹空白处按住 **Shift** 点右键，选 **在此处打开 PowerShell 窗口** / **在终端中打开** | 同上，但更直观 |
| 地址栏输入 `powershell` | 同第 2 种，换成 PowerShell | 想用 PowerShell 时 |

记住一条就够用：**在哪个文件夹里开终端，终端就默认待在哪个文件夹。** 学会第 2 种，你几乎不用再手敲 `cd` 了。

#### 2.6.3 第一批必须会的命令

调出你系统的那一栏，照着敲几遍就熟了：

{{< tabs defaultTab="0" type="underline" >}}

{{% tab title=":(fa-brands fa-windows fi-c-blue): Windows" %}}

| 命令 | 作用 | 什么时候会用到 |
| --- | --- | --- |
| `cd 路径` | 切换到某个目录 | 想进 `D:\code\c` 干活：`cd /d D:\code\c` |
| `cd ..` | 返回上一级目录 | 走错文件夹了 |
| `dir` | 列出当前目录里的所有内容 | 确认 `hello.exe` 到底生成了没有 |
| `cls` | 清屏 | 输出刷了一大堆，想从干净的一屏重新开始 |
| `mkdir 名字` | 新建文件夹 | 给新作业建一个目录 |
| `echo 文字` | 输出文字（配合 `>` 还能写进文件） | 快速造一个测试用的小文件 |
| `type 文件名` | 显示文本文件的内容 | 不想打开记事本，只想瞄一眼代码 |

> [!warning] `cd` 换盘必须加 `/d`
> `cd D:\code\c` 在 cmd 里**不会**跳过去 —— 它只是记下了这个路径，当前盘还是 C 盘。跨盘切换必须写 `cd /d D:\code\c`。
> 这个 `/d` 是 cmd 特有的坑，第一次遇到基本都会踩。

{{% /tab %}}

{{% tab title=":(fa-brands fa-apple fi-c-slate): macOS" %}}

| 命令 | 作用 | 什么时候会用到 |
| --- | --- | --- |
| `cd 路径` | 切换到某个目录 | 想进 `~/code/c` 干活：`cd ~/code/c` |
| `cd ..` | 返回上一级目录 | 走错文件夹了 |
| `ls` | 列出当前目录里的所有内容 | 确认 `hello` 到底生成了没有 |
| `pwd` | 显示当前目录的完整路径 | 迷路了，想知道自己身在何处 |
| `clear` | 清屏 | 输出刷了一大堆，想从干净的一屏重新开始 |
| `mkdir 名字` | 新建文件夹 | 给新作业建一个目录 |
| `echo 文字` | 输出文字（配合 `>` 还能写进文件） | 快速造一个测试用的小文件 |
| `cat 文件名` | 显示文本文件的内容 | 不想打开编辑器，只想瞄一眼代码 |

> [!tip] Mac 上没有 `/d` 那个坑
> 路径从 `/` 开始，没有 C 盘 D 盘的概念，`cd` 想去哪就去哪，不用加任何开关。

{{% /tab %}}

{{< /tabs >}}

#### 2.6.4 让终端好用十倍的七个技巧

每一格都值得当场试一遍：

| # | 技巧 | 为什么好用 |
| --- | --- | --- |
| 1 | :(fa-solid fa-keyboard fi-c-blue): **Tab 补全**（强烈推荐） | 路径敲一半按一下 **Tab** 自动补齐；有多个候选时连按 Tab 会依次列出 |
| 2 | :(fa-solid fa-arrows-rotate fi-c-teal): **上下方向键翻历史** | 上一条命令按 **↑** 就回来了；cmd 里按 **F7** 还能弹出历史列表 |
| 3 | :(fa-solid fa-xmark fi-c-red): **`Ctrl + C` 强制中断** | 程序卡死、日志刷不停时掐掉它，比直接关窗口安全 |
| 4 | :(fa-solid fa-folder-open fi-c-amber): **路径含空格要加双引号** | `cd "D:\My Code\c"`。不加引号，空格会被当成两个参数的分界 |
| 5 | :(fa-solid fa-arrow-right fi-c-violet): **把文件拖进终端** | 从资源管理器拖文件或文件夹到终端窗口，完整路径自动填好 |
| 6 | :(fa-solid fa-clipboard-list fi-c-green): **右键即粘贴** | 选中文字就是复制，点右键就是粘贴。cmd 里 `Ctrl + C` 是**中断，不是复制** |
| 7 | :(fa-solid fa-play fi-c-orange): **运行当前目录的程序** | cmd 里写 `hello`；PowerShell 里必须写 `.\hello`；Mac 上是 `./hello` |

第 7 条先记住结论，下一节马上就会用到：你编译出 `hello.exe` 之后，会发现两种终端里运行它的写法**不一样**。`.\` 的意思是“**就在当前文件夹里找**” —— PowerShell 出于安全考虑，默认不去当前目录找程序，所以必须显式指明。

---

到这里，你已经把两件最不“所见即所得”的事情啃下来了：安装编译器使电脑具备编译C/C++的能力，配置环境变量让操作系统能够找到编译器位置；终端那堆名词和命令，也从黑话变成了手上的工具。

现在只差最后一个动作 —— **让你的计算机真的跑出一行字来。**

下一节，我们打开记事本（对，就是最普通的那个），写出人生第一个 `.c` 文件，用 `gcc` 把它变成 `hello.exe`，亲眼看着屏幕跳出 `Hello, C!`。

---
<span id="第3步"></span>
## 第3步 · 用最“土”的工具跑通第一个程序

编译器装好了，环境变量也配好了。现在，你写下的代码终于可以变成程序。

这一节我们**故意不用任何编辑器插件、不按任何按钮** —— 一个记事本加一个黑色终端窗口，从头手敲一遍。看清了每一步在干什么，再搬进 VSCode 才不是照抄步骤。

### 3.1 用记事本写 hello.c

先给代码找个目录。打开**文件资源管理器**，进 D 盘，新建文件夹 `code`，再进去新建一个 `c`。你以后写的代码可以都放这里，路径就是 `D:\code\c`。

> [!warning] 路径里不要有中文和空格
> 编译器、调试器、构建脚本处理路径时，遇到空格容易把一整个路径当成两个参数拆开，遇到中文则可能因为编码对不上而找不到文件。`D:\code\c` 这种短、纯英文、无空格的路径最省心。
> `桌面`、`我的文档`、`新建文件夹 (2)` 都属于“雷区”。

![木桌上摊开一本空白的笔记本和一支铅笔，旁边立着一台屏幕发着微光的台式电脑，桌上还有键盘和鼠标](images/illus-14-notepad.jpg "最朴素的工具，也能写出真正的程序")

然后打开记事本，**文件 → 新建**，把下面这段代码附近进去：

```c {title="hello.c 的完整内容", linenos=true}
#include <stdio.h> // 引入标准输入输出

int main() {
    printf("Hello, C!\n");
    return 0;
}
```
> [!warning] 保存时的三个坑
> 新手第一次存 C 文件，基本都栽在这里。三个坑，一个都别踩：


| 坑 | 踩了会怎样 | 正确做法 |
| --- | --- | --- |
| **保存类型** | 被存成 `hello.c.txt`，后缀变成 `.txt`，编译器根本不认这个文件 | 把**保存类型**改成**所有文件 (\*.\*)** |
| **文件名** | 只写了 `hello`，存成没有后缀的文件 | 文件名写全：`hello.c` |
| **编码** | 程序里的中文变成乱码 | 看右下角的**编码**，选 **UTF-8** |

> [!info] 记事本的编码在哪里看
> 新版记事本窗口的**右下角状态栏**会直接显示当前编码，点一下就能改；保存对话框里也有**编码**下拉框。

存好之后**先别急着编译**，回到终端，在 `D:\code\c` 里确认一下文件名：

```plain {title="在终端里敲 dir"}
 D:\code\c 的目录

2026/09/15  20:14               312 hello.c
               1 个文件            312 字节
```

看到干干净净的 `hello.c`，就可以往下走了。

> [!danger] 如果看到的是 hello.c.txt
> 说明**保存类型**没改。最快的补救办法是在终端里敲 `ren hello.c.txt hello.c`；更保险的是删掉它，改好**保存类型**后重新保存一次。

### 4.2 编译：一条命令把源码变成程序

现在，让第1步里那位“翻译官”正式上岗。在终端里敲：

```bash {title="在终端里执行", group="build", name="Windows"}
gcc -g hello.c -o hello
```

```bash {group="build", name="macOS"}
clang -g hello.c -o hello
```

然后——**屏幕上什么都没有发生。**

没有进度条，没有“编译成功”，终端只是安静地回到下一行等你敲新命令。这不是卡住了，这就是成功。

{{< center-quote >}}
**没有消息就是最好的消息。**
{{< /center-quote >}}

再 `dir` 看一眼，`D:\code\c` 里已经多出一个 `hello.exe`。刚才那一行命令，把一份人类能读的文本，变成了一份 CPU 能执行的程序。

#### 这一条命令背后，其实跑了四个程序

你敲的是 `gcc`，但 `gcc` 自己并不会翻译代码。它更像一个**总调度**：接到命令后，依次叫来四个小工具，一个接一个地干活。

![编译流程：预处理、编译、汇编、链接四个阶段，每一步的输入与产物](images/illus-15-compile-pipeline.svg "gcc 一条命令，四条工序")

| 阶段 | 干活的程序 | 它做了什么 |
| --- | --- | --- |
| 预处理 | `cpp` | 把 `#include` 展开成真正的头文件内容，把宏替换掉 |
| 编译 | `cc1` | 把 C 代码翻译成汇编代码 —— “翻译”就发生在这里 |
| 汇编 | `as` | 把汇编代码变成机器码，产出目标文件 `hello.o` |
| 链接 | `ld` | 把你的代码和标准库、启动代码拼在一起，产出 `hello.exe` |

> [!note] 为什么非要拆成这么多步
> 根本原因就是提高复用性和可维护性。因为每一步都能单独复用：一百个源文件可以先各自汇编成 `.o`，改动了哪个就重新汇编哪个，最后统一链接 —— 大型项目靠这个才撑得住。一句话：**`gcc` 是总调度，四道工序各有专人。**

> [!warning] 编译报错长什么样
> 报错开头通常是这样一串：`hello.c:8:15: error: ...`，读法是**文件 : 行号 : 列号**。
> 一个实用习惯：**从上往下改，先改第一个错误** —— 第一个错误经常会让编译器在后面看什么都像错的。

### 4.3 读懂这条命令的每一个字符

别急着往下敲，把刚才那行命令拆成四段看：

| 这一段 | 它在说什么 |
| --- | --- |
| `gcc` | 执行gcc编译器程序 |
| `-g` | 在生成的可执行文件里**额外塞进调试信息**（行号、函数名、变量名） |
| `hello.c` | 要翻译的**源文件** |
| `-o hello` | 指定**输出文件名**。`-o` 就是 output 的缩写 |

**`-o` 后面写什么，产物就叫什么。** 写 `-o hello`，Windows 自动补上 `.exe` 就是 `hello.exe`；写 `-o myfirst`，产物就是 `myfirst.exe`。

> [!warning] 少了 -g 这个参数，断点就是一句空话
> 其实 `gcc hello.c -o hello` 也能编译、也能跑，但生成的程序里**不含行号和变量名** —— gdb 拿到它只能看到一堆内存地址：`break hello.c:12` 找不到第 12 行，`print sum` 也不知道 `sum` 是什么。
> 所以记住这条判据：**只要你还打算调试，`-g` 就必须在。** 代价只是可执行文件大一点点，运行速度不受影响。

{{< details summary="顺手记住：以后天天要用的几个参数" >}}

| 参数 | 作用 |
| --- | --- |
| `-Wall` | 打开所有常用警告。它不阻止编译，但会提醒你“这里很可能写错了” |
| `-O2` | 开启二级优化，让程序跑得更快。**调试时不要用** —— 优化会打乱代码和行号的对应关系 |
| `-std=c++17` | 指定 C++ 标准的版本。用 C++ 时源文件是 `hello.cpp`，命令要把 `gcc` 换成 `g++` |

所以等你熟练以后，那条命令大概率会长成这个样子：

```bash {title="熟练之后"}
gcc -g -Wall hello.c -o hello
```

{{< /details >}}

> [!info]你可能已经发现了规律
> 命令行程序的不同行为都是通过`-`加上某个英文字母或单词来实现的，比如对于gcc编译器来说，携带`-o`表明我要给编译好的文件设置文件名，`-Wall`就是在编译时提供警告信息，如果不加则不提供。
>
> **这种通过携带参数(flag)来指定不同行为的用法，适用于所有的命令行工具。**
>
> **不清楚具体的参数和行为时，大多可以用`--help`或`-h`来查看具体的参数行为，比如在终端中输入`gcc --help`后回车。**

### 4.4 运行它

![一台电脑屏幕上冒出由圆点和线条组成的小烟花，桌上放着一杯冒热气的水](images/illus-16-run-it.jpg "程序跑起来了")

运行一个程序，不同终端里的**敲法是不一样的**：

| 终端 | 你要敲的 |
| --- | --- |
| cmd（Windows 传统命令提示符） | `hello` |
| PowerShell（Windows 新版终端） | `.\hello` |
| macOS 的 zsh | `./hello` |

在 cmd 里敲 `hello`，它会去当前目录找 `hello.exe` 执行。PowerShell 则为了安全，**不会自动去当前目录找程序**（防止你误执行一个同名的恶意程序），所以必须显式写出 `.\`，意思是“就在当前目录这里”。

> [!tip] 记不住就只记这一条
> 运行当前目录里的程序，前面统一加 `.\`（Mac 上是 `./`）—— 这条在 cmd 里同样成立。
> 另外，`exe` 后缀可以省略，Windows 会自动补上；Mac 编译出来的可执行文件本来就没有后缀。

敲回车之后：

```plain {title="程序跑起来的样子"}
Hello, C!
```
**到这里，你的第一个 C 程序真的跑通了。**

### 4.5 让程序停下来给你看：认识 gdb

#### 4.5.1 为什么需要调试器

最原始的办法是在代码里到处插 `printf`，把中间结果打出来看。这招叫“printf 大法”，能用但很笨：改一次要重新编译一次，看完还得把 `printf` 一条条删掉。

调试器解决的就是这件事 —— **让程序在半路上停下来，然后慢慢看**：

- **断点**：告诉程序“跑到这一行就停”
- **单步**：一次只往前走一行
- **查看变量**：随时问一句“现在 `sum` 是多少”

这三件套是**用过就回不去**的。而 gdb 就是第1步里跟着 MinGW 一起装进来的那个调试器 —— 之前 `gdb --version` 能打印版本号，说明它已经安装到你的硬盘上。

![一个简笔人物举着巨大的放大镜，观察一条由虚线铺成的道路，路中央停着一个方形标记](images/illus-17-gdb.jpg "让程序停下来，然后凑近看")

#### 4.5.2 调试器的四条命令

调来调去，真正要记的就四条。选你系统那一栏 —— **两个系统的命令名不一样，别照抄错栏**：

{{< tabs defaultTab="0" type="underline" >}}

{{% tab title=":(fa-brands fa-windows fi-c-blue): gdb" %}}

| # | 命令 | 简写 | 干什么 |
| --- | --- | --- | --- |
| 1 | `gdb hello.exe` | — | 启动调试器，把程序交给它 |
| 2 | `break hello.c:4` | `b hello.c:4` | 在 `hello.c` 的第 4 行下一个断点 |
| 3 | `run` | `r` | 让程序跑起来，撞到断点就停 |
| 4 | `next` | `n` | 执行下一行（不钻进函数里面去） |

看完了想退出，敲 `quit`（简写 `q`）就行。

{{% /tab %}}

{{% tab title=":(fa-brands fa-apple fi-c-slate): lldb" %}}

Mac 的调试器叫 `lldb`，思路和 gdb 完全一致，但**下断点和查看变量的写法不一样**：

| # | 命令 | 简写 | 干什么 |
| --- | --- | --- | --- |
| 1 | `lldb hello` | — | 启动调试器，把程序交给它 |
| 2 | `breakpoint set --file hello.c --line 4` | `b hello.c:4` | 在 `hello.c` 的第 4 行下一个断点 |
| 3 | `run` | `r` | 让程序跑起来，撞到断点就停 |
| 4 | `next` | `n` | 执行下一行（不钻进函数里面去） |

`run`、`r`、`n`、`q` 在 lldb 里是**为 gdb 老用户保留的兼容别名**，可以放心用；只有 `break` 和 `print` 得按上表换写法。

下面那段会话示例以 gdb 为例，Mac 读者把命令换成这一栏就行，看到的现象是一样的。

{{% /tab %}}

{{< /tabs >}}

先跑通最基础的三步 —— 启动、下断点、运行：

```plain {title="启动 → 下断点 → 运行"}
D:\code\c> gdb hello.exe
GNU gdb (GDB) 14.2
Copyright (C) 2023 Free Software Foundation, Inc.
（后面还有几行版本与许可信息，这里略过）

(gdb) break hello.c:4
Breakpoint 1 at 0x140001533: file hello.c, line 4.
(gdb) run
Starting program: D:\code\c\hello.exe
Hello, C!
```

有三件事值得注意：

- **`(gdb)` 是调试器的提示符。** 它后面的命令是敲给调试器听的，不是敲给你的程序的 —— 这一点非常容易搞混。
- **地址那串十六进制数每台机器都不一样**，不用管。看到 `Breakpoint 1 at ...: file hello.c, line 4.` 就说明断点下好了。
- **程序停在了第 4 行之前。** 那一行还没执行。

> [!note] 行号对不上了怎么办
> `break hello.c:4` 里的 4，就是**你文件里 `printf("Hello, C!\n");` 那一行的行号**，空行不一样它就不一样。
> 对着上面那份带行号的代码数一下，或者在 gdb 里敲 `list`，它会把当前位置附近的代码连行号一起列出来。

### 4.6 到这里，你已经掌握了“原理层”

回头看看，这一板块里你真正拿到手的东西：

- [x] 会用记事本写出能编译的源文件，并且知道**保存类型 / 后缀 / 编码**这三个坑
- [x] 会用 `gcc -g hello.c -o hello` 把源码变成可执行文件，并知道背后跑了**预处理 → 编译 → 汇编 → 链接**四道工序
- [x] 说得清 `-g` 给了你什么：**没有调试信息，断点就是空话**
- [x] 会在 cmd 和 PowerShell 里分别正确地运行当前目录的程序
- [x] 会用 gdb 下断点、单步、查看变量。

这条链路 —— **源码 → 可执行文件 → 运行 → 调试** —— 是后面所有工具的地基。VSCode、Visual Studio、CLion，做的都是同一件事，只是把这套动作包在了按钮后面。

> [!question] 可是……每次都要手敲这么长的命令，不累吗？
> 累。而且命令只会越来越长：`gcc -g -Wall -fexec-charset=GBK hello.c -o hello`，敲错一个字符就得重来。gdb 的纯文本界面也确实不直观 —— 想看一个变量怎么变，得反复敲 `next` 和 `print`。
> 下一篇文章，我们把这一切搬进 VSCode：让编辑器记住编译命令、一键编译运行、在某一行点一下就设好断点，还能**在侧边栏里直接看到所有变量的值随着单步一起变化**。
> 而到那时候你会发现 —— **VSCode 并没有替你多做任何一件事，它只是把上面这些命令，翻译成了按钮。**

接下来就是通过使用VScode，搞定大一期间所有的关于C/C++的代码编写…………

---
## 参考文献

下面这些链接都是本文提到的工具的官方来源，需要查证细节时首选它们。

{{< link href="https://winlibs.com/" content="WinLibs — GCC + MinGW-w64 for Windows" title="下载本文使用的编译器，附版本与运行时说明" card=true card-icon="fa-solid fa-download" >}}

完整清单：

1. [MinGW-w64 官网](https://www.mingw-w64.org/) —— 编译器背后那套 Windows 头文件和运行库的官方项目页。想知道“MinGW 到底是什么”时，从这一页开始。
2. [Pre-built Toolchains · mingw-w64](https://www.mingw-w64.org/downloads/) —— 官方列出的各家预编译工具链，WinLibs 就在名单里；这一页也把 **UCRT 与 MSVCRT 两种运行时的区别**讲清楚了，选版本犯难时来查。
3. [GCC online documentation](https://gcc.gnu.org/onlinedocs/) —— 所有 `gcc` 参数（`-g`、`-Wall`、`-O2`、`-std=`……）的权威出处。网上搜到的参数解释拿不准，就来手册里对一遍。
4. [GDB Documentation](https://sourceware.org/gdb/documentation/) —— `break`、`run`、`next`、`print` 的完整手册。
5. [Environment Variables · Win32 文档](https://learn.microsoft.com/en-us/windows/win32/procthread/environment-variables) —— 环境变量的官方说明：为什么程序在启动那一刻才读到它们。第2步里讲的现象，这里能看到原理层面。
6. [C 参考手册 · cppreference（中文）](https://zh.cppreference.com/w/c) —— 遇到 `printf`、`scanf`、`for` 这些语法细节想查准确写法时最省事的一个中文站点。**注意：它是参考手册，不是教程**，适合“我知道要用什么、只想确认怎么用”的时候翻。


---

> 作者: [青梧](https://zzzheng.com)  
> URL: https://zzzheng.com/posts/2c2f588/  

