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 若要访问需先声明 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;)。
  1. C++17 inline static
    • 在 C++17 之前,静态成员变量必须在 .cpp 文件中定义一次,否则链接报错。
    • C++17 引入 inline static 后,允许在头文件的类定义中直接初始化静态成员,极大简化了代码。
  2. constexpr 隐含 inline
    • 类内的 static constexpr 成员默认就是 inline 的,不需要显式写 inline。
    • 它是编译期常量,适合用于数组大小、模板参数等场景。
  3. 非静态 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 变量的操作
相关推荐
indexsunny2 小时前
互联网大厂Java面试实录:Spring Boot与微服务在电商场景中的应用
java·jvm·spring boot·微服务·面试·mybatis·电商
白太岁2 小时前
Muduo:(4) 主从 Reactor、事件循环、跨线程无锁唤醒及其线程池
c++·网络协议·tcp/ip
喜欢吃燃面2 小时前
基础算法:枚举(上)
c++·学习·算法
郝学胜-神的一滴2 小时前
计算思维:数字时代的超级能力
开发语言·数据结构·c++·人工智能·python·算法
兵哥工控2 小时前
mfc 线程启动、挂起、恢复、停止实例
c++·mfc·线程
m0_531237172 小时前
C语言-数组练习
c语言·开发语言·算法
今天你TLE了吗2 小时前
JVM学习笔记:第四章——虚拟机栈
java·jvm·笔记·后端·学习
xiaoye-duck2 小时前
《算法题讲解指南:优选算法-滑动窗口》--09长度最小的子数串,10无重复字符的最长字串
c++·算法
二年级程序员2 小时前
一篇文章掌握“双向链表”
c语言·数据结构·链表