C++:(4) 内存布局、编译流程、关键字及其链接性

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&

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 变量的操作

Read more

C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

C++ 面试题常用总结 详解(满足c++ 岗位必备,不定时更新)

📚 本文主要总结了一些常见的C++面试题,主要涉及到语法基础、STL标准库、内存相关、类相关和其他辅助技能,掌握这些内容,基本上就满足C++的岗位技能(红色标记为重点内容),欢迎大家前来学习指正,会不定期去更新面试内容。  Hi~!欢迎来到碧波空间,平时喜欢用博客记录学习的点滴,欢迎大家前来指正,欢迎欢迎~~ ✨✨ 主页:碧波 📚 📚 专栏:C++ 系列文章 目录 一、C ++ 语法基础 🔥 谈谈变量的使用和生命周期,声明和初始化 🔥 谈谈C++的命名空间的作用 🔥  include " " 和 <> 的区别 🔥 指针是什么? 🔥 什么是指针数组和数组指针 🔥 引用是什么? 🔥 指针和引用的区别 🔥 什么是函数指针和指针函数以及区别 🔥 什么是常量指针和指针常量以及区别 🔥 智能指针的本质是什么以及实现原理 🔥 weak_ptr 是否有计数方式,在那分配空间? 🔥 类型强制转换有哪几种? 🔥 函数参数传递时,

By Ne0inhk
Re:从零开始的 C++ 进阶篇(三)彻底搞懂 C++ 多态:虚函数、虚表与动态绑定的底层原理

Re:从零开始的 C++ 进阶篇(三)彻底搞懂 C++ 多态:虚函数、虚表与动态绑定的底层原理

◆ 博主名称: 晓此方-ZEEKLOG博客大家好,欢迎来到晓此方的博客。⭐️C++系列个人专栏: 主题曲:C++程序设计⭐️ 踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰 0.1概要&序論 这里是此方,好久不见。 多态是 C++ 中最核心而且是最难理解的机制之一。它不仅是语法层面的特性,更牵涉到 C++ 的对象模型、对象内存布局以及多态机制的底层实现原理。本文将从底层原理出发,系统全面解析多态的真实运作机制。这里是「此方」。让我们现在开始吧! 一,多态的概念 通俗来说,多态就是多种形态。多态分为编译时多态(静态多态) 和 运行时多态(动态多态),这里我们重点讲运行时多态。 1.1编译时多态(静态多态) 编译时多态主要就是我们前面讲的 函数重载和函数模板。 它们通过传递不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态。之所以叫编译时多态,是因为实参传递给形参的参数匹配是在编译时完成的,

By Ne0inhk
第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

第十六届蓝桥杯省赛(软件类真题)C/C++ 大学A组

大纲: A.寻找质数 B:黑白棋 题目&解析&代码 A题 题目解析 本题的目标是枚举质数并计数,直到数到第2025个。由于2025不算太大,第2025个质数大约在17000~18000之间,完全可以在合理时间内通过简单枚举得到。 解题步骤: 从2开始遍历每个整数,判断它是否是质数。 质数判断采用试除法:对于一个数n,只需检查从2到√n的所有整数是否能整除n。若存在能整除的数,则n不是质数;否则是质数。 每找到一个质数,计数器加1。 当计数器达到2025时,输出当前的质数并结束。 优化点: 除了2以外,偶数不可能是质数,因此可以跳过偶数判断(直接步进2)。 在isPrime函数中,可以先处理特殊情况(n<2返回false),然后单独判断偶数,再对奇数进行试除,步进也可以设为2。 C++ 参考代码 以下代码实现了上述算法,并输出第2025个质数。 cpp

By Ne0inhk