备注:还记得第一次学习 C++ 是 2018 年,大学刚入学时,连写一个 Hello World 都要折腾半天。如今已熟练掌握各种 Hello World 写法 (¬‿¬)。毕业后从 Java 转到 C#,如今投身嵌入式开发也快一年了。趁着有空,把 C++ 再系统学一遍!
一、系统与编译器环境信息
1. 查看 Ubuntu 系统版本
css
lsb_release -a
输出示例:
yaml
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04.3 LTS
Release: 24.04
Codename: noble
Ubuntu 24.04 LTS 是长期支持版本,稳定性高,非常适合 Linux 学习。
2. 查看 g++ 编译器版本
css
g++ --version
输出示例:
scss
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
3. 主流 C++ 编译器对比
| 编译器 | 特点 | 适用场景 |
|---|---|---|
| GCC (g++) | 开源免费、兼容性强、多平台支持,嵌入式主流选择 | Ubuntu 默认 |
| Clang | 编译速度快、错误提示清晰、基于 LLVM 架构 | 对编译效率和诊断信息要求高的场景 |
二、基础实操:编写并运行第一个 C++ 程序
以经典的 Hello World 为例,完整演示从创建文件到运行程序的全流程。
1. 创建项目目录
bash
cd ~/Desktop
mkdir learncpp && cd learncpp
2. 使用 Vim 编写 C++ 代码
vim hello.cpp
输入以下代码:
c
#include <iostream>
using namespace std;
int main() {
cout << "Hello, world!" << endl;
return 0;
}
3. 编译并运行程序
bash
# 编译
g++ hello.cpp -o hello
# 运行
./hello
输出:
Hello, world!
三、深入理解:G++ 编译的四个阶段
C++ 源码 → 可执行文件的过程分为四步:预处理 → 编译 → 汇编 → 链接。
示例源文件 hello.cpp:
c
#include <iostream>
using namespace std;
int main() {
cout << "Hello, world!" << endl;
return 0;
}
3.1 阶段一:预处理(Preprocessing)
作用:
-
展开头文件(
#include),将指定的头文件内容原封不动地插入到 #include 指令所在的位置。-
示例:
arduino#include <iostream>预处理器会找到标准库中的 stdio.h 文件,并将其全部内容复制粘贴到当前源文件中。
注意:
#include <...>通常用于系统头文件,搜索路径由编译器预定义;#include "..."优先在当前目录或用户指定路径中查找,常用于自定义头文件。
-
-
替换宏(
#define),将宏名替换成其定义的内容,属于纯文本替换。-
示例:
对象式宏(无参数)
arduino#define PI 3.1415926所有出现
PI的地方都会被替换为3.14159。 -
函数式宏(有参数)
css#define MAX(a,b) ((a) > (b) ? (a) : (b))调用
MAX(x, y)会被替换为((x) > (y) ? (x) : (y)),这里为什么参数都要带括号,感兴趣的可以自己去了解一下。
-
-
处理 条件编译(
#if,#ifdef,#ifndef,#else,#elif,#endif) ,根据预定义的宏或表达式的真假,选择性地包含或排除代码块- 常见用于:跨平台兼容、调试开关、防止头文件重复包含
-
删除注释,字面意思,就是删除所有注释,减少后续编译器负担,预处理后的代码中不再包含任何注释。
-
注意:
- 注释被删除后,通常用空格或者换行代替,用来保持代码结构不变,避免两个token被意外拼接。
-
-
undef:取消已定义的宏 -
pragma:向编译器传递指定指令 -
行控制:添加行号和文件标识,以便编译时产生报告错误/警告时的行号和文件名
命令:
g++ -E hello.cpp -o hello.i
查看结果(末尾部分) :
bash
cat hello.i | tail -n 20
c
extern wistream wcin;
extern wostream wcout;
extern wostream wcerr;
extern wostream wclog;
# 82 "/usr/include/c++/13/iostream" 3
__extension__ __asm (".globl _ZSt21ios_base_library_initv");
}
# 2 "hello.cpp" 2
# 3 "hello.cpp"
using namespace std;
int main(void) {
cout << "Hello world!"<< endl;
return 0;
}
3.2 阶段二:编译(Compilation → 生成汇编)
作用:将 C++ 代码翻译为目标平台的汇编语言(如 x86-64)。
命令:
g++ -S hello.cpp -o hello.s
查看汇编代码:
bash
cat hello.s
perl
.file "hello.cpp"
.text
#APP
.globl _ZSt21ios_base_library_initv
.section .rodata
.LC0:
.string "Hello world!"
#NO_APP
.text
.globl main
.type main, @function
main:
.LFB1988:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
leaq .LC0(%rip), %rax
movq %rax, %rsi
leaq _ZSt4cout(%rip), %rax
movq %rax, %rdi
call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc@PLT
movq _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_@GOTPCREL(%rip), %rdx
......
3.3 阶段三:汇编(Assembly → 生成目标文件)
作用 :将 .s 汇编文件转为机器码,生成 .o 目标文件。
命令:
r
g++ -c hello.cpp -o hello.o
验证文件类型:
arduino
file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
.o文件是"可重定位"的,尚未链接库函数。
3.4 阶段四:链接(Linking → 生成可执行文件)
作用:
- 合并 .o 文件与系统库(如 libstdc++, libc )
- 解析未定义符号(如
std::cout) - 设置程序入口(
_start)
命令:
bash
g++ hello.o -o hello
# 或一步到位:
g++ hello.cpp -o hello
查看动态依赖:
ldd hello
输出:
bash
linux-vdso.so.1
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
...
所有外部符号均已正确链接。
四、常用 G++ 参数详解(按功能分类)
基础编译控制
| 参数 | 说明 |
|---|---|
-o file |
指定输出文件名 |
-c |
仅生成 .o,不链接 |
-E |
仅预处理 |
-S |
仅生成汇编(.s) |
-v |
显示详细编译过程 |
标准与警告
| 参数 | 说明 |
|---|---|
-std=c++17 / c++20 |
指定 C++ 标准 |
-Wall |
启用常见警告 |
-Wextra |
启用额外警告 |
-Werror |
警告视为错误 |
-pedantic |
严格遵循 ISO C++ 标准 |
推荐组合:
g++ -std=c++20 -Wall -Wextra -O2
优化选项
| 参数 | 说明 |
|---|---|
-O0 |
无优化(默认,便于调试) |
-O1 / -O2 |
中等/较高优化 |
-O3 |
激进优化(可能增大体积) |
-Os |
优化代码大小 |
-g |
生成调试信息(供 GDB 使用) |
预处理器控制
| 参数 | 说明 |
|---|---|
-DNAME=value |
定义宏(如 -DDEBUG=1) |
-UNAME |
取消定义宏 |
-I/path/to/include |
添加头文件搜索路径 |
-MM |
生成 Makefile 依赖关系 |
链接控制
| 参数 | 说明 |
|---|---|
-L/path/to/lib |
添加库搜索路径 |
-lxxx |
链接 libxxx.so(如 -lpthread) |
-static |
静态链接所有库(生成独立可执行文件) |
-shared |
生成共享库(.so) |
-Wl,option |
向链接器传递参数(如 -Wl,-rpath,/my/lib) |
调试与分析
| 参数 | 说明 |
|---|---|
-g |
生成调试符号 |
-fno-omit-frame-pointer |
保留帧指针,便于性能分析 |
-save-temps |
保留中间文件(.i, .s, .o) |
-ftime-report |
显示各阶段耗时 |
五、示例
csharp
# 1. 预处理(保留所有宏定义)
g++ -E -dD hello.cpp > hello.i
# 2. 编译为 Intel 汇编(带注释,-O2 优化)
g++ -S -fverbose-asm -masm=intel -O2 hello.cpp -o hello.s
# 3. 汇编为目标文件(可用 as 或 g++)
g++ -c hello.s -o hello.o
# 4. 静态链接 libstdc++ 和 libgcc
g++ hello.o -static-libstdc++ -static-libgcc -o hello_static
# 5. 查看符号表(确认 cout 是否链接)
nm hello | grep cout
六、调试与分析技巧
1. 查看完整编译过程
g++ -v hello.cpp -o hello
显示调用的 cpp → cc1plus → as → ld 等子工具。
2. 保留中间文件
bash
g++ -save-temps hello.cpp -o hello
# 自动生成 hello.i, hello.s, hello.o
3. 生成 Makefile 依赖
shell
g++ -MM hello.cpp
# 输出:hello.o: hello.cpp /usr/include/c++/.../iostream ...
附录:编译阶段速查表
| 阶段 | 输入 | 输出 | 命令标志 |
|---|---|---|---|
| 预处理 | .cpp | .i | -E |
| 编译 | .i | .s | -S |
| 汇编 | .s | .o | -c |
| 链接 | .o + 库 | 可执行文件 | (默认) |