C/C++ 入门:从编译环境到 HelloWorld
最近想系统补一下计算机基础,从 c/c++ 开始吧。
📚 基本概念速读
| 名称 | 定义 | 省流 |
|---|---|---|
| 源码 | 程序员写的文本代码,比如 .c、.cpp
文件 |
人能读 |
| 机器码 | CPU 能直接执行的二进制指令 | 机器能跑 |
| 目标文件 | 源码经过编译和汇编后得到的中间文件,比如
.o、.obj |
还不能独立运行 |
| 可执行程序 | 链接完成后的最终程序,比如 Linux 下的 a.out、Windows
下的 .exe |
可以运行 |
| 编译器 | 把高级语言翻译成更低层表示的工具 | 负责翻译 |
| 链接器 | 把多个目标文件和库组合成可执行程序的工具 | 负责拼装 |
| 标准库 | 语言自带的一组常用功能,比如输入输出、字符串、容器 | 省掉重复造轮子 |
| 头文件 | 通常保存声明的文件,比如
iostream、.h、.hpp |
告诉编译器有什么 |
🧩 核心概念
源码、机器码、可执行程序
源码是给人看的,机器码是给 CPU 执行的。C/C++ 程序不能直接把
.cpp 文件丢给操作系统运行,中间必须经过编译工具链处理。
flowchart TD
A[源码 hello.cpp] --> B[目标文件 hello.o]
B --> C[可执行程序 hello]
操作系统真正启动的是可执行程序,不是源码文件。
编程语言分类
常见分类不是绝对边界,但有助于理解 C/C++ 的位置。
| 分类 | 代表语言 | 特点 |
|---|---|---|
| 编译型语言 | C、C++、Rust、Go | 通常先生成可执行文件,再运行 |
| 解释型语言 | Python、JavaScript | 通常由解释器边解释边执行 |
| 字节码语言 | Java、C# | 先编译成字节码,再由虚拟机运行 |
| 低级语言 | 汇编语言 | 接近 CPU 指令,可移植性较弱 |
| 高级语言 | C++、Python、Java | 更接近人的思考方式 |
C 和 C++ 通常被归到编译型语言。它们离硬件更近,性能和控制力强,但也要求程序员理解更多底层细节。
🛠️ 编译环境
常见编译器
| 编译器 | 常用命令 | 常见平台 | 说明 |
|---|---|---|---|
| GCC | gcc、g++ |
Linux、macOS、Windows MinGW | GNU 编译器集合 |
| Clang | clang、clang++ |
Linux、macOS、Windows | LLVM 项目的前端编译器 |
| MSVC | cl |
Windows | Visual Studio 自带编译器 |
C++ 代码建议使用 g++、clang++ 或
cl 编译。虽然 gcc 也能参与 C++
编译,但它默认更偏 C 语言入口,链接 C++ 标准库时容易让初学者困惑。
简单理解三者:
| 工具链 | 特点 | 适合场景 |
|---|---|---|
| GCC | 历史久、生态成熟、Linux 上非常常见 | Linux 开发、开源项目、竞赛和课程环境 |
| Clang | 错误提示清晰、模块化设计好、和 LLVM 生态绑定紧密 | macOS、现代 C++ 项目、静态分析和编译器相关工具 |
| MSVC | 和 Visual Studio、Windows SDK 集成紧密 | Windows 原生开发、Visual Studio 工程 |
GNU 是一个自由软件项目,GCC 是 GNU
生态里最重要的编译器工具链之一。Linux 发行版里常见的
gcc、g++、ld、make
等工具,很多都来自 GNU 生态。
LLVM 更像一套可复用的编译器基础设施。Clang 负责解析 C/C++/Objective-C 源码,LLVM 负责中间表示、优化和后端代码生成。很多现代工具都会复用 LLVM,例如静态分析器、格式化工具、语言服务器和新的语言编译器。
怎么选:
| 场景 | 建议 |
|---|---|
| Linux 初学 | g++,资料最多,环境最常见 |
| macOS 初学 | clang++,系统工具链默认就是 Clang |
| Windows + Visual Studio | cl,和 IDE、调试器、SDK 配合最好 |
| Windows + 命令行练习 | MinGW 的 g++ 或 LLVM 的 clang++
都可以 |
| 课程、OJ、竞赛 | 优先使用平台指定的编译器和标准版本 |
最小编译命令
假设文件名是 hello.cpp:
1 | # 使用 g++ 编译 C++ 源码,并生成名为 hello 的可执行程序 |
在 Windows 的 PowerShell 中,使用 MinGW 或 MSVC 时,运行方式通常是:
1 | # 运行当前目录下的 hello.exe |
常见编译参数
| 参数 | 示例 | 作用 |
|---|---|---|
-o |
g++ hello.cpp -o hello |
指定输出文件名 |
-Wall |
g++ hello.cpp -Wall -o hello |
打开常见警告 |
-Wextra |
g++ hello.cpp -Wall -Wextra -o hello |
打开更多警告 |
-std=c++17 |
g++ hello.cpp -std=c++17 -o hello |
指定 C++ 标准版本 |
-g |
g++ hello.cpp -g -o hello |
生成调试信息 |
-O2 |
g++ hello.cpp -O2 -o hello |
开启较常用的优化级别 |
-E |
g++ -E hello.cpp -o hello.i |
只做预处理 |
-S |
g++ -S hello.cpp -o hello.s |
生成汇编代码 |
-c |
g++ -c hello.cpp -o hello.o |
只生成目标文件,不链接 |
-std=c++17 的意思是:让编译器按照 C++17
标准理解这份代码。C++ 语言会不断演进,常见标准包括
C++98、C++11、C++14、C++17、C++20、C++23。
| 标准 | 简单印象 | 选择建议 |
|---|---|---|
| C++11 | 现代 C++ 的起点,引入 auto、lambda、智能指针等 |
维护旧项目时常见 |
| C++14 | 对 C++11 做了小幅增强 | 老项目可能使用 |
| C++17 | 稳定、普及度高,很多课程和工程仍在使用 | 初学和普通项目的稳妥选择 |
| C++20 | 引入 concepts、ranges、coroutines 等较大特性 | 新项目可考虑,但要看编译器支持 |
| C++23 | 更新,生态仍在跟进 | 学习新特性或特定项目需要时使用 |
初学阶段建议从 -std=c++17
开始:资料多、编译器支持成熟,也不会一开始就被 C++20/23
的新特性分散注意力。如果课程或项目明确要求某个版本,以课程或项目为准。
-O2
是优化选项,告诉编译器在不明显拉长编译时间的前提下,尽量生成运行更快的机器码。常见优化级别有:
| 参数 | 含义 | 常见用途 |
|---|---|---|
-O0 |
不优化,默认通常接近这个级别 | 调试最友好 |
-O1 |
开启基础优化 | 较少手动使用 |
-O2 |
开启多数稳妥优化 | 发布版本常用 |
-O3 |
更激进优化,可能增大代码体积 | 计算密集型程序可测试 |
-Os |
优化体积 | 嵌入式或体积敏感场景 |
-Og |
兼顾调试体验和部分优化 | 调试优化相关问题 |
优化可能做的事情包括:删除无用代码、减少重复计算、内联小函数、调整循环、改善寄存器使用。它不改变语言语义,但如果程序本身有未定义行为,比如数组越界、使用未初始化变量,优化后问题可能更明显。
调试时优先用:
1 | g++ hello.cpp -std=c++17 -Wall -Wextra -g -O0 -o hello |
发布或性能测试时再考虑:
1 | g++ hello.cpp -std=c++17 -Wall -Wextra -O2 -o hello |
初学阶段推荐命令:
1 | g++ hello.cpp -std=c++17 -Wall -Wextra -g -o hello |
⚙️ 程序完整运行流程
从源码到运行,C/C++ 一般经历四个构建阶段:
flowchart LR
A[源码 .cpp] --> B[预处理 .i]
B --> C[编译 .s]
C --> D[汇编 .o]
D --> E[链接 可执行程序]
如果把“编译”这一步继续拆开,编译器内部大致会经历下面的流程:
flowchart LR
A[源码 .cpp] --> B[预处理]
B --> C[词法/语法分析]
C --> D[语义分析]
D --> E[生成 IR]
E --> F[IR 优化]
F --> G[指令选择]
G --> H[寄存器分配]
H --> I[生成汇编/目标文件 .s/.o]
预处理
预处理器处理
#include、#define、条件编译等内容。
1 | g++ -E hello.cpp -o hello.i |
#include <iostream>
会在这个阶段展开相关声明。可以粗略理解为:把编译所需的接口信息带进来,让后续阶段知道
cout、endl 等名字是什么。
常见头文件:
| 头文件 | 作用 | 常见内容 |
|---|---|---|
<iostream> |
标准输入输出 | cin、cout、cerr、endl |
<string> |
字符串 | std::string |
<vector> |
动态数组 | std::vector |
<array> |
固定长度数组封装 | std::array |
<algorithm> |
常用算法 | sort、find、max、min |
<cmath> |
数学函数 | sqrt、pow、sin、cos |
<cstdlib> |
通用工具 | exit、abs、随机数相关旧接口 |
<cstdio> |
C 风格输入输出 | printf、scanf |
<fstream> |
文件输入输出 | ifstream、ofstream |
<unordered_map> |
哈希表 | std::unordered_map |
C++ 标准库头文件通常写成
<iostream>、<vector>
这种形式;自己写的头文件通常写成 "my_header.h"。
1 |
两种写法的主要区别是搜索路径不同:
| 写法 | 常见用途 |
|---|---|
#include <...> |
标准库或系统/第三方库头文件 |
#include "..." |
当前项目自己的头文件 |
头文件不等于库本体。头文件通常提供声明,让编译器知道“有什么函数、类、变量”;真正的实现可能在源码文件里,也可能在已经编译好的库文件里,最后由链接阶段接上。
预处理阶段还会处理宏:
1 |
以及条件编译:
1 |
|
这些内容会在正式编译前先被展开、替换或裁剪。
编译
编译器把预处理后的代码翻译成汇编代码,并做语法检查、类型检查和优化。
1 | g++ -S hello.cpp -o hello.s |
如果少写分号、类型不匹配、函数调用不合法,通常会在这个阶段报错。
汇编
汇编器把汇编代码转换为目标文件。
1 | g++ -c hello.cpp -o hello.o |
目标文件里已经有机器指令,但它通常还缺少一些外部符号,比如标准库函数的最终地址。
链接
链接器把目标文件、启动代码、标准库和其他库拼起来,生成可执行程序。
1 | g++ hello.o -o hello |
常见的链接错误包括:
| 错误类型 | 常见原因 |
|---|---|
undefined reference |
声明了函数,但没有提供实现,或链接时漏了库 |
multiple definition |
同一个函数或全局变量被重复定义 |
| 找不到库 | -l 或 -L 参数不正确,或库没有安装 |
🚀 HelloWorld:C++ 版本
完整示例代码:
1 | // 引入标准输入输出头文件 |
编译运行:
1 | g++ hello.cpp -std=c++17 -Wall -Wextra -o hello |
输出:
1 | Hello World! |
🔍 HelloWorld 逐行拆解
#include <iostream>
include 是预处理指令,用来引入头文件。
iostream 是 C++ 标准库里的输入输出头文件,里面提供了
cin、cout、cerr、endl
等常用对象和工具。
注意:#include
不是在运行时加载模块,而是在编译前的预处理阶段让编译器看到相关声明。
using namespace std;
std 是 C++ 标准库使用的命名空间。cout
的完整名字其实是 std::cout,endl 的完整名字是
std::endl。
写了:
1 | using namespace std; |
就可以简写:
1 | cout << "Hello World!" << endl; |
如果不写 using namespace std;,推荐写成:
1 | std::cout << "Hello World!" << std::endl; |
初学时使用 using namespace std;
可以减少干扰;项目代码里更推荐显式写
std::,避免名字冲突。
int main()
main 是程序入口函数。操作系统启动程序后,C/C++
运行时会调用 main,程序从这里开始执行。
int
表示这个函数会返回一个整数。这个整数会作为进程退出码交给操作系统。
{ }
花括号表示函数体。main 里面的语句会按顺序执行。
1 | int main() |
cout << "Hello World!" << endl;
cout 是标准输出对象,通常对应控制台。
<<
是输出运算符,可以把右侧内容送到左侧输出流。
"Hello World!" 是字符串字面量。
endl 表示换行,并刷新输出缓冲区。日常输出换行也可以使用
'\n':
1 | cout << "Hello World!\n"; |
return 0;
return 0; 表示 main 正常结束,并把退出码
0 返回给操作系统。
常见约定:
| 返回码 | 含义 |
|---|---|
0 |
程序正常结束 |
非 0 |
程序异常结束或表示某种错误状态 |
在 Linux/macOS Shell 中,可以用下面的命令查看上一个程序的返回码:
1 | echo $? |
在 Windows PowerShell 中,可以查看:
1 | $LASTEXITCODE |
🧪 把四个阶段跑一遍
用同一个 hello.cpp,可以手动观察中间产物:
1 | # 预处理:生成 hello.i |
这些文件的关系是:
flowchart TD
A[hello.cpp 源码] --> B[hello.i 预处理结果]
B --> C[hello.s 汇编代码]
C --> D[hello.o 目标文件]
D --> E[hello 可执行程序]
⚠️ 常见误区
| 误区 | 正解 |
|---|---|
| C 和 C++ 是同一种语言 | C++ 兼容大量 C 语法,但它们是两门语言 |
.cpp 文件可以直接运行 |
源码必须先编译链接成可执行程序 |
#include 会把库代码全部复制进来 |
更准确地说,它让编译器看到声明;真正库代码通常在链接阶段接入 |
using namespace std; 必须写 |
不是必须,也可以写 std::cout |
return 0; 是给用户看的 |
它主要是返回给操作系统或调用者看的退出码 |
| 编译成功就一定没有问题 | 编译通过只代表语法和链接基本通过,不代表逻辑正确 |
✅ 总结
C/C++ 入门的主线是:人写源码,编译器生成目标文件,链接器生成可执行程序,操作系统加载后从
main开始运行。
后续学习可以沿着三条线继续:语法基础、内存模型、工程构建工具。先把
g++ hello.cpp -o hello
背后的流程弄清楚,后面学指针、类、CMake 和调试器都会轻松很多。
Happy Hacking! 🎉