C++:(4) 内存布局、编译流程、关键字及其链接性
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 若要访问需先声明 |
inline int x = 0 | 外 | ✅ 允许重定义(C++17 起),适合放在头文件中 |
static int x = 0 | 内 | 其他 cpp 无法访问。 |
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 | 外 | ✅ 允许重定义,编译期常量。 |
3.2 类内成员
有 inline / constexpr 就必须有static
inline(static) : 确保外部链接性, 全局唯一实例
- 允许 static 直接在类内定义,
- 加 const / constexpr 后确保为编译期常量
类内成员声明 | 链接性 | 注释 |
|---|---|---|
int x = 0 | 无 | 非静态成员。允许类内初始化 (C++11)。 |
static int x | 外 | 静态成员声明。不是定义。 |
inline static int x = 0 | 外 | C++17 起。是完整的定义。 |
const int x | 无 | 非静态常量成员。每个对象一份副本。 |
static const int x = 0 | 内 | 整型/枚举特例。是编译时常量,视为完整定义。 |
static const int x | 外 | 声明。需在 .cpp 中定义 |
static const double x | 外 | 非整型静态常量。C++17 前类内不能初始化。 |
inline static const int x = 0 | 外 | C++17 起。是完整的定义,编译时常量。 |
static constexpr int x = 0 | 外 | 隐含 inline static。是完整的定义,编译期常量。 |
extern const int 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 | 不变 | 异常规范。表示该函数承诺不会抛出异常。 |
inline | 外 | 允许重定义(ODR 例外)。适合放在头文件中。 |
static | 内 | 内部链接性(仅限自由函数)。 |
constexpr | 外 | 编译期求值。若输入参数是常量表达式,则结果在编译期计算。 |
类内函数修饰符 | 链接性/特性 | 注释 |
|---|---|---|
= delete | 无 | 显式删除函数。禁止使用某些构造函数、操作符或成员函数。 |
= default | 无 | 显式默认。要求编译器生成默认实现。 |
explicit | 无 | 显式转换。防止隐式类型转换。 |
static | 类作用域 | 静态成员函数。属于类而非对象实例。 |
const | 无 | 常量成员函数。承诺不修改对象状态。 |
constexpr | 外 (隐含 inline) | 编译期求值。C++11/14/17 非静态成员函数隐含 const。 |
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&
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 变量的操作