1. 内存布局
1.1 linux C++ 内存布局示意
高地址
+---------------------------+
| 栈 (Stack) | ← 局部变量、函数参数、返回地址
| ↓ 增长 | (向下增长)
+---------------------------+
| |
| 空闲区域 |
| |
+---------------------------+
| 堆 (Heap) | ← 动态分配 (new/malloc)
| ↑ 增长 | (向上增长)
+---------------------------+
| 内存映射段 (mmap) | ← 共享库、动态映射、大分配
+---------------------------+
| BSS 段 | ← 未初始化的全局/静态变量
+---------------------------+
| 数据段 (.data) | ← 已初始化的全局/静态变量 (可读写)
+---------------------------+
| 只读数据段 (.rodata) | ← 常量、字符串字面量 (只读)
+---------------------------+
| 代码段 (.text) | ← 程序指令 (只读 + 可执行)
+---------------------------+
低地址
1.2 内存布局由谁规定
链接脚本的作用:
- 内存映射:将程序逻辑段(代码/数据)映射到物理内存(FLASH/RAM)
- 地址分配:精确控制各段在内存中的位置和布局
- 符号定义:生成关键地址符号供启动文件和C代码使用
- 优化控制:决定哪些段保留/丢弃,影响最终固件大小
一个ARMv7-M架构的硬件的链接脚本如下,其内存布局与 linux C++ 不同:
/* ========================================================================
* 1. 程序入口点
* ======================================================================== */
ENTRY(Reset_Handler)
/* ========================================================================
* 2. 定义变量栈顶 _estack - 从RAM顶部开始向下生长
* 计算: 0x20000000 + 32KB = 0x20008000
* ======================================================================== */
_estack = ORIGIN(RAM) + LENGTH(RAM);
/* ========================================================================
* 3. 物理内存定义 - STM32G431RBT6固定配置
* ======================================================================== */
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K /* 程序存储区 */
RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 32K /* 数据运行区 */
}
/* ========================================================================
* 4. 内存段布局, 定义段映射规则
* 每个 { ... } 块是一个输出段
* 书写顺序决定了不同段在内存中的物理排列顺序
* VMA(Virtual Memory Address):程序运行时该段所在的地址
* LMA(Load Memory Address):该段在存储介质中的地址, AT 用于显示指定 LMA
* . 是当前位置计数器(location counter)
* 它的值是 当前输出段的 VMA, 即程序运行时该位置的地址
* ======================================================================== */
SECTIONS
{
/* --------------------------------------------------------------
* 4.1 中断向量表 - 位于 FLASH 起始地址: 0x08000000
* Cortex-M4硬件要求: 前8字节 = [栈顶, 复位地址]
* -------------------------------------------------------------- */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* 保留向量表不被优化,
KEEP(...): 强制保留括号内的段, 即使在链接优化时也不会被丢弃 */
KEEP(*(.vectors)) /* 兼容不同命名 */
. = ALIGN(4);
} > FLASH /* VMA 和 LMA 相同, 都在 FLASH 起始地址 */
/* --------------------------------------------------------------
* 4.2 代码段 - 所有函数机器码(main/port/tasks等代码)
* -------------------------------------------------------------- */
.text :
{
. = ALIGN(4);
*(.text*) /* 所有代码段 */
*(.glue_7) /* Thumb/ARM胶合代码(保留兼容性)*/
*(.glue_7t)
*(.eh_frame) /* 异常帧 */
. = ALIGN(4);
_etext = .; /* 定义变量 _etext 为代码段 VMA 结束地址(供启动文件使用)*/
} > FLASH /* VMA 和 LMA 相同, 都在 FLASH .isr_vector 段后面 */
/* --------------------------------------------------------------
* 4.3 只读数据 - 常量、字符串字面量等
* -------------------------------------------------------------- */
.rodata :
{
. = ALIGN(4);
*(.rodata*)
*(.rodata.*)
. = ALIGN(4);
} > FLASH /* VMA 和 LMA 相同, 都在 FLASH .text 段后面 */
/* 定义 _sidata 为.data 段初始值在 FLASH 中的起始地址(LMA)*/
_sidata = LOADADDR(.data);
/* --------------------------------------------------------------
* 4.4 已初始化数据 - 如: int x = 5;
* 运行时在RAM,初始值从Flash复制
* -------------------------------------------------------------- */
.data :
{
. = ALIGN(4);
_sdata = .; /* 定义 .data 段在 RAM 中起始地址 */
*(.data*)
*(.data.*)
. = ALIGN(4);
_edata = .; /* 定义 .data 段在 RAM 中结束地址 */
} > RAM AT > FLASH /* LMA=Flash, VMA=RAM */
. = ALIGN(4);
/* --------------------------------------------------------------
* 4.5 未初始化数据 - 如: int y;
* 启动时由启动文件清零 .bss 段
* -------------------------------------------------------------- */
.bss :
{
_sbss = .;
*(.bss*)
*(.bss.*)
*(COMMON) /* 传统未初始化全局变量 */
. = ALIGN(4);
_ebss = .;
} > RAM /* LMA=VMA=RAM */
/* --------------------------------------------------------------
* ABI属性段 - 必须保留,否则链接器警告
* 包含Thumb指令集等ABI信息
* -------------------------------------------------------------- */
.ARM.attributes 0 : { *(.ARM.attributes) }
/* --------------------------------------------------------------
* 丢弃无用段
* -------------------------------------------------------------- */
/DISCARD/ :
{
*(.ARM.exidx*) /* C++异常索引 */
*(.ARM.extab*) /* C++异常表 */
*(.init) /* C库初始化(直接跳main)*/
*(.fini)
*(.preinit_array*) /* 全局构造函数(C++)*/
*(.init_array*) /* 全局构造函数 */
*(.fini_array*) /* 全局析构函数 */
*(.note.gnu.build-id) /* 构建ID(调试用,生产可移除)*/
}
}
由该链接脚本的内容可知,链接脚本的作用就是规定程序的内存布局,并提供符号给启动文件以及其他目标文件,以便符合特定硬件的行为 ,详见:操作系统开发:(9) 从硬件复位到程序执行:如何编写符合硬件动作的启动文件与链接脚本。
2. 程序编译流程
2.1 预处理
作用:
- 处理以
#开头的预处理指令 - 展开宏定义
- 处理条件编译
- 插入头文件内容
- 删除注释
- 添加行号和文件名标识.
生成文件:
- .i 文件:预处理后的源代码(仍是文本文件)
2.2 编译(生成 .s)
作用:
- 词法分析:将代码拆分成 token
- 语法分析:构建抽象语法树 (AST)
- 语义分析:类型检查、符号解析
- 代码优化:优化中间代码
- 生成汇编代码:输出目标平台的汇编代码
生成文件:
- .s 文件:汇编代码(文本文件,可读)
2.3 汇编(生成 .o)
作用:
- 将汇编代码转换为机器指令
- 生成目标文件(二进制文件)
- 创建符号表(记录函数和变量)
- 生成重定位表(为链接做准备)
生成文件:
- .o 文件
2.4 链接
作用:
- 符号解析:解析未定义的符号引用
- 重定位:修正地址引用
- 合并段:合并相同类型的段
- 库处理:链接静态库或动态库
生成文件:
- 可执行文件:Linux 无后缀或 .out,Windows .exe
2.5 总结
| 阶段 | 输入 | 输出 | 文件类型 | 可读性 |
|---|---|---|---|---|
| 预处理 | .c/.cpp | .i | 文本 | ✅ 可读 |
| 编译 | .i | .s | 文本 | ✅ 可读 |
| 汇编 | .s | .o | 二进制 | ❌ 不可读 |
| 链接 | .o + 库 | 可执行文件 | 二进制 | ❌ 不可读 |
3. 关键字及其链接性
3.1 全局变量
| 全局变量定义 | 链接性 | 注释 |
|---|---|---|
| int x = 0 | 外 | 其他 cpp 若要访问需先声明 extern int x。 ⚠️ 不推荐:若放在头文件中会导致链接重定义错误 |
| inline int x = 0 | 外 | ✅ 允许重定义(C++17 起),适合放在头文件中 |
| static int x = 0 | 内 | 其他 cpp 无法访问。 ⚠️ 注意:放在 h 中则每个包含它的编译单元都会生成一个独立副本 |
| const int x = 0 | 内 | 其他 cpp 无法访问。C++ 中全局 const 默认内部链接, C 语言是外部 |
| constexpr int x = 0 | 内 | 其他 cpp 无法访问。隐含 inline 但默认内部链接,除非显式 extern |
| extern const int x = 0 | 外 | 其他 cpp 若要访问需先声明 extern const int x。通常定义在 .cpp 文件中 |
| inline const int x = 0 | 外 | ✅ 允许重定义,适合放在头文件中 |
| inline constexpr int x = 0 | 外 | ✅ 允许重定义 ,编译期常量。 推荐用于头文件中的全局常量(适用于 int、char 等字面类型) |
3.2 类内成员
有 inline / constexpr 就必须有static
inline(static) : 确保外部链接性, 全局唯一实例
- 允许 static 直接在类内定义,
- 加 const / constexpr 后确保为编译期常量
| 类内成员声明 | 链接性 | 注释 |
|---|---|---|
| int x = 0 | 无 | 非静态成员。允许类内初始化 (C++11)。 ⚠️ 只是个初始化建议,实际内存分配在对象实例化时,每个对象一份副本。 |
| static int x | 外 | 静态成员声明。不是定义 。 需在某个 .cpp 中定义 int Class::x;,防止头文件多次包含导致重定义。 |
| inline static int x = 0 | 外 | C++17 起。是完整的定义 。 可直接放在头文件中,链接器会合并多个编译单元的副本。 |
| const int x | 无 | 非静态常量成员。每个对象一份副本。 ⚠️ 必须在构造函数初始化列表中赋值 (规定在创建时初始化)。 |
| static const int x = 0 | 内 | 整型/枚举特例。是编译时常量,视为完整定义。 ⚠️ C++17 前若取地址 (ODR-use),仍需外部定义。 |
| static const int x | 外 | 声明。需在 .cpp 中定义 const int Class::x = val;。 同 static int x。 |
| static const double x | 外 | 非整型静态常量。C++17 前类内不能初始化。 需在 .cpp 中定义 const double Class::x = val;。 |
| inline static const int x = 0 | 外 | C++17 起。是完整的定义 ,编译时常量。 ✅ 适用于 double, char*, string_view 等所有类型,推荐放头文件。 |
| static constexpr int x = 0 | 外 | 隐含 inline static。是完整的定义 ,编译期常量。 ✅ 推荐用于头文件中的静态常量 (适用于字面类型)。 |
| extern const int x = 0 | 外 | ⚠️ 注意 :extern 通常用于全局变量 ,不用于类成员。 类外定义静态成员时不需要写 extern (直接写 const int Class::x = 0;)。 |
- C++17 inline static :
- 在 C++17 之前,静态成员变量必须在 .cpp 文件中定义一次,否则链接报错。
- C++17 引入 inline static 后,允许在头文件的类定义中直接初始化静态成员,极大简化了代码。
- constexpr 隐含 inline :
- 类内的 static constexpr 成员默认就是 inline 的,不需要显式写 inline。
- 它是编译期常量,适合用于数组大小、模板参数等场景。
- 非静态 const 成员 :
- 即使是 const int x,如果是非静态的,每个对象实例都有自己的一份内存。
- 必须在构造函数的初始化列表中初始化,不能在类内直接 = 0 (除非是静态整型常量)。
3.3 函数
| 函数修饰符 | 链接性 | 注释 |
|---|---|---|
| noexcept | 不变 | 异常规范。表示该函数承诺不会抛出异常。 ✅ 若抛出则调用 std::terminate。 ✅ 有助于编译器优化和重载决议。 |
| inline | 外 | 允许重定义(ODR 例外)。适合放在头文件中。 ✅ 类内定义的成员函数自动隐式 inline。 ⚠️ 只是建议编译器内联,实际由编译器决定。 |
| static | 内 | 内部链接性(仅限自由函数)。 ⚠️ 仅当前翻译单元可见,其他 cpp 无法访问。 💡 现代 C++ 推荐用 匿名命名空间 代替。 |
| constexpr | 外 | 编译期求值。若输入参数是常量表达式,则结果在编译期计算。 ✅ 隐含 inline。 ✅ C++14/17 后允许更复杂的函数体。 |
| 类内函数修饰符 | 链接性/特性 | 注释 |
|---|---|---|
| = delete | 无 | 显式删除函数。禁止使用某些构造函数、操作符或成员函数。 ✅ 常用于禁止拷贝 (MyClass(const MyClass&) = delete;)。 ✅ 比私有化更明确,报错更早。 |
| = default | 无 | 显式默认。要求编译器生成默认实现。 ✅ 常用于恢复被用户定义构造函数抑制的默认构造函数。 ✅ 比手写默认实现更高效且语义清晰。 |
| explicit | 无 | 显式转换。防止隐式类型转换。 ✅ 主要用于单参数构造函数。 ✅ C++11 起也可用于转换运算符 (explicit operator bool())。 |
| static | 类作用域 | 静态成员函数。属于类而非对象实例。 ✅ 无 this 指针,只能访问静态成员。 ⚠️ 链接性取决于定义位置(类内定义隐含 inline,类外定义通常外部链接)。 |
| const | 无 | 常量成员函数。承诺不修改对象状态。 ✅ 只能访问 const 成员变量。 ✅ mutable 成员变量可被修改。 ✅ 常对象只能调用 const 成员函数。 |
| constexpr | 外 (隐含 inline) | 编译期求值。C++11/14/17 非静态成员函数隐含 const。 ✅ C++20 起不再隐含 const。 ✅ 参数和返回类型必须是字面量类型。 ✅ 构造函数只能用初始化列表 (C++11 函数体需为空)。 |
| override | 无 | 显式重写。明确该函数是对父类虚函数的重写。 ✅ 编译器检查:若父类无对应虚函数则报错。 ✅ 增强代码可维护性,防止签名不匹配。 |
3.4 static
- 静态局部变量
在函数中为静态局部变量, 只会在第一次进入函数时初始化一次, 生命周期延长至整个程序运行结束。
- 静态全局变量和静态函数
内部链接性.
- 类中的静态成员变量和静态成员函数
外部链接性,
静态成员变量属于整个类,而不是类的对象, 必须在类外定义和初始化 (static const int x = 0 和 static constexpr int x = 0 需在类内初始化, 类外定义, 而 incline static constexpr int x = 0 以直接类内初始化并定义).
静态成员函数只能访问静态成员变量和其他静态成员函数.
3.5 decltype
用于在编译时推导表达式的类型。
如果( )中是一个任意其他表达式(如括号表达式、运算表达式等)
如果表达式是左值, decltype 返回左值引用T&
如果表达式是纯右值(prvalue), 返回非引用类型T
如果表达式是将亡值(xvalue), 返回右值引用T&
cpp
int x = 42;
decltype(x) y = x; // 推导出 y 的类型为 int
int x = 42;
decltype(x) a; // a 的类型为 int
decltype((x)) b = x; // b 的类型为 int&,因为 (x) 是一个左值
3.6 volatile
告诉编译器:
- 不要优化对该变量的读写操作
- 每次访问都必须从内存中重新读取,不能使用缓存的值
- 保持操作顺序,不重排与其他 volatile 变量的操作