02 · 编译链接与内存模型
定位:面试第一类必考基础题,也是理解所有内存话题(智能指针、内存池、ECS)的地基。 学完标准:能画出编译四阶段流程图;能手算 sizeof;能说清静态/动态库区别;能写代码判断大小端。
1. 从 .cpp 到可执行文件:编译四阶段
Section titled “1. 从 .cpp 到可执行文件:编译四阶段”源代码 a.cpp b.cpp │ ① 预处理(Preprocessing) gcc -E ▼翻译单元(头文件展开、宏替换、条件编译、去注释) │ ② 编译(Compilation) gcc -S ▼汇编代码 a.s b.s │ ③ 汇编(Assembly) gcc -c ▼目标文件 a.o b.o(机器码 + 符号表 + 重定位表) │ ④ 链接(Linking) gcc 链接器 ▼可执行文件(a.out / a.exe)各阶段做什么:
| 阶段 | 输入→输出 | 核心工作 |
|---|---|---|
| 预处理 | .cpp → .i | 展开 #include、替换 #define 宏、处理 #if/#ifdef 条件编译、删除注释 |
| 编译 | .i → .s | 词法分析(拆单词)→ 语法分析(建语法树)→ 语义分析(类型检查)→ 优化 → 生成汇编 |
| 汇编 | .s → .o | 把汇编翻译成机器码,生成符号表(函数/变量名→地址)和重定位表(哪些地址待定) |
| 链接 | .o + 库 → 可执行 | 符号解析(把每个符号引用找到定义) + 重定位(把符号填成真实地址) |
面试常问的衍生题:
- 编译错误 vs 链接错误怎么区分?
- 编译错误:语法/类型问题,报错里带
.cpp:行号,比如未定义标识符、int i = "str";。 - 链接错误:每个文件单独编译都过了,但连起来时符号找不到,典型报错
undefined reference to 'xxx'(漏实现、漏链接库、名字不一致)。
- 编译错误:语法/类型问题,报错里带
- 同一个变量在多文件引用:定义只能有一份(ODR 一个定义规则),声明(
extern)可以有很多份。 - 头文件只放声明、不放定义,否则多个 .cpp include 同一个头文件就会“重复定义”链接错误。两个例外见第 2 节(inline / 模板)。
2. 头文件机制与关键字
Section titled “2. 头文件机制与关键字”2.1 防止头文件重复包含
Section titled “2.1 防止头文件重复包含”// 方式一:#ifndef(老式、跨平台,靠宏名避免冲突)#ifndef MY_HEADER_H#define MY_HEADER_H// 内容#endif
// 方式二:#pragma once(现代主流,编译器保证只展开一次)#pragma once区别:#ifndef 依赖宏名唯一——两个不同头文件若 guard 宏同名,后一个会被跳过;#pragma once 由编译器按文件去重,更快,但同一文件经硬链接/软链接/不同路径包含时可能被当成两个文件。前者是标准写法,后者现在所有主流编译器都支持,现代工程常用。
💡 什么是 guard? guard = “头文件守卫”,就是
#ifndef XXX_H / #define XXX_H / #endif这三行宏,作用是防止同一个头文件被重复包含:一个 .cpp 里#include "a.h"两次(或 a.h 又 include b.h、b.h 又 include a.h),没有 guard 时头文件内容被展开两遍,里面若含变量/函数定义就报“重复定义”。guard 宏名必须全局唯一、全大写、下划线分隔,如MYMATH_H、MY_PROJECT_UTIL_H,否则两个头文件撞名会互相跳过。#pragma once本质是让编译器帮你做同一件事(按文件路径去重)。
2.2 声明 vs 定义(ODR,One Definition Rule)
Section titled “2.2 声明 vs 定义(ODR,One Definition Rule)”- 声明:告诉编译器“存在这个东西”,不分配内存:
extern int x;、void foo(); - 定义:实际创建实体、分配内存:
int x;、void foo(){} - ODR:程序里每个实体只能有一个定义。
- 两个例外(一个头文件里可以放定义、多个 .cpp 包含也不冲突):
inline函数 /inline变量(C++17):允许多个相同定义,链接器会合并。- 类模板/函数模板:实例化时才生成代码,天然满足“多份也没事”。
💡 为什么 inline / 模板能放头文件?例子:
// math.h —— 头文件里允许放这些"定义"inline int square(int x) { return x * x; } // ① inline 函数:多份相同定义,链接器自动合并inline int version = 1; // ② C++17 inline 变量:同理合并成一份template <typename T> T my_max(T a, T b) { // ③ 模板:只是"图纸",用到才实例化return a > b ? a : b;}int add(int a, int b); // 普通函数:只能放声明普通函数
add若把定义写进 math.h,a.cpp、b.cpp 都 include → 链接时出现两份定义 → 报“重复定义”。
- inline 函数:每个 .cpp 各编译一份,链接器发现多份相同定义时自动合并取一份,不报错。
- inline 变量(C++17):同理多份合并成一份,解决“头文件里定义全局变量”的老大难。
- 模板:模板本身不生成符号;哪个 .cpp 用到
my_max<int>就在哪个翻译单元实例化出代码,多份实例化同样由链接器合并(COMDAT 机制)。⚠️ 易错点(面试常考):inline 函数在头文件里、10 个 .cpp 都调用 → 最终链接成 1 份副本,不是 10 份! 两阶段理解:编译期每个 .cpp 各生成一份(确实有 10 份)→ 链接期链接器发现这 10 份定义完全相同(同签名 + 同函数体),按 COMDAT 规则合并成 1 份。这就是 inline 能放头文件的底层原因。 反过来:普通函数定义放头文件 → 链接器视为多份不同定义(违反 ODR)→ 报 multiple definition 重定义错误。
// 正确做法:声明放 .h,定义放 .cppint add(int a, int b); // 声明// mymath.cpp#include "mymath.h"int add(int a, int b) { return a + b; } // 定义2.3 extern “C” 与名字修饰(name mangling)
Section titled “2.3 extern “C” 与名字修饰(name mangling)”- C++ 支持函数重载,靠编译器给函数名“改名”来区分:
void foo(int)和void foo(double)编译后符号名不同,这个过程叫名字修饰。C 语言没有重载,不改名。 - 所以 C 写的库(符号名是原始名),在 C++ 里链接时会找不到——用
extern "C"声明,告诉编译器“这段按 C 规则链接”:
extern "C" { #include "some_c_library.h"}// 或者声明单个函数extern "C" int c_func(int x);💡 extern “C” 展开讲:核心是名字修饰(name mangling)。C++ 为支持重载会改函数名:
void foo(int)和void foo(double)编译后符号分别是_Z3fooi、_Z3food(名字不同才能共存)。而 C 语言没有重载,符号名就是原名foo。 链接阶段靠符号名找函数。你在 C++ 里extern int my_add(int,int);去链 C 写的库libmylib.a,C++ 编译器找的是_Z6my_addii,库里却只有my_add→undefined reference。extern "C"就是告诉 C++ 编译器“这段按 C 规则处理、不要改名”,于是链接时找的就是my_add,能对上。 头文件常见写法#ifdef __cplusplus extern "C" { ... }:让同一个头文件既能被 C 也能被 C++ 包含(C 没有extern "C"语法,必须条件编译包住)。 面试补充:extern “C” 块里不能重载(C 规则无重载);它只影响链接,不影响函数本身的行为。
追问:static 全局变量/函数的作用?→ 把符号的链接属性改为内部链接(internal linkage),只在本 .cpp 可见,不参与跨文件链接,避免和其他文件符号冲突。
3. 静态库与动态库
Section titled “3. 静态库与动态库”| 静态库(.a / .lib) | 动态库(.so / .dll) | |
|---|---|---|
| 链接时机 | 编译链接时,把用到的库代码拷贝进可执行文件 | 链接时只记录依赖,运行时(或加载时)才加载 |
| 生成文件大小 | 大(代码重复包含) | 小 |
| 部署 | 单文件即可跑,独立 | 必须带上 .dll/.so,缺了就崩(找不到XXX.dll) |
| 内存占用 | 每个进程各一份 | 多个进程可共享同一份物理内存 |
| 更新 | 改库要重新编译整个可执行文件 | 只替换 .dll/.so 即可(热更新方便) |
| 启动速度 | 快(代码已在) | 稍慢(要加载+重定位) |
| 常见报错 | 链接期 undefined reference |
运行期 无法定位程序输入点 / not found |
Windows 注意:
.lib既可能是静态库,也可能是 DLL 的导入库(import library,只含导入符号,不含真正实现)。
制作静态库(Linux 视角):gcc -c foo.c -o foo.o && ar rcs libfoo.a foo.o,链接 -lfoo。
制作动态库:gcc -shared -fPIC foo.c -o libfoo.so。
面试金句:静态库“静态链接、以空间换独立性”;动态库“动态加载、以启动开销换体积与共享、更新灵活”。
4. 进程内存布局(重点背这张图)
Section titled “4. 进程内存布局(重点背这张图)”高地址┌─────────────────────┐│ 栈 Stack │ ← 向下增长,局部变量、函数调用帧,自动分配释放│ (大小默认约1~8MB)│├─────────────────────┤│ ↓ ││ 空 闲 区 ││ ↑ │├─────────────────────┤│ 堆 Heap │ ← 向上增长,new/malloc,手动管理,碎片化├─────────────────────┤│ 未初始化数据区 BSS │ ← 未初始化的全局变量/static 变量(运行时清零)├─────────────────────┤│ 已初始化数据区 .data│ ← 已初始化的全局变量/static 变量├─────────────────────┤│ 只读常量区 .rodata │ ← 字符串常量、const 全局常量├─────────────────────┤│ 代码段 .text │ ← 程序指令(只读)└─────────────────────┘低地址4.1 各种变量放哪(必背表格)
Section titled “4.1 各种变量放哪(必背表格)”| 变量 | 存放位置 | 生命周期 | 是否默认初始化 |
|---|---|---|---|
| 全局变量 | .data / .bss | 程序启动→结束 | 是(0) |
| static 全局变量 | .data / .bss | 程序启动→结束 | 是 |
| static 局部变量 | .data / .bss | 首次执行到声明处初始化,程序结束析构(作用域仅函数内) | 是 |
| 局部变量 | 栈 | 函数调用期间 | 否(垃圾值) |
| new/malloc 出的对象 | 堆 | 直到 delete/free | 堆内存不自动清零:new int 未初始化、new int() 为 0、new T 调构造 |
字符串常量 "hello" |
.rodata(只读) | 程序周期 | — |
| const 局部变量 | 栈(编译期可优化) | 同局部 | — |
| 虚函数表 vtable | .rodata(只读) | 程序周期 | — |
| 函数指令 | .text | 程序周期 | — |
必考题示例:int a;(文件作用域)→ .bss;int b = 5; → .data;static int c; → .bss;void f(){ int d; } → 栈;int* p = new int; → 堆上分配、p 本身在栈。
💡 一样。“程序周期”就是“程序从启动到结束”的简称:变量在程序启动时创建(全局/static 在 main 之前完成初始化)、程序结束时销毁。注意区分两个概念:
- 生命周期/存储期:全局/static 是“静态存储期”,从启动到结束(与“程序周期”同义)。
- 初始化时机:static 局部变量的初始化发生在“第一次执行到它的声明处”(之后不再重新初始化),但它初始化后存活到程序结束——这两个说法不矛盾。
💡 意思是:栈上局部变量如果不写初始化,拿到的是垃圾值(不确定值)。栈是别人用过的内存,编译器不会帮你清零。
void f() {int x; // 未初始化 → 值不确定(可能 0,也可能任意残留)std::cout << x; // 读未初始化变量 = 未定义行为(UB),-Wall 会警告int y{}; // 值初始化 → 确定是 0int z = 0; // 显式初始化 → 0}对比记忆:全局/static 默认 0(.bss 由系统清零);栈和堆(
new int)默认不初始化;new int()才是 0。规则:栈/堆默认不初始化,全局/static 默认是 0。
4.2 栈 vs 堆(背熟这张对比)
Section titled “4.2 栈 vs 堆(背熟这张对比)”| 对比项 | 栈 | 堆 |
|---|---|---|
| 分配方式 | 编译器自动分配/释放 | 程序员手动 new/delete |
| 速度 | 快(只移动栈指针) | 慢(要查找空闲块、可能系统调用) |
| 容量 | 小,默认约 1~8MB,递归太深会栈溢出 | 大(取决于系统,可达 GB 级) |
| 碎片 | 无(严格 LIFO) | 有,会产生内存碎片 |
| 方向 | 向下增长(高地址→低地址) | 向上增长 |
| 越界后果 | 段错误/程序崩溃 | 破坏堆管理数据结构,可能延迟崩溃 |
追问点:栈溢出的原因?→ 无限递归、超大局部数组。怎么避免?→ 检查递归基、大数组用堆(new/vector)。
5. 内存对齐(Alignment)
Section titled “5. 内存对齐(Alignment)”5.1 为什么需要对齐?
Section titled “5.1 为什么需要对齐?”CPU 访问内存不是按字节的,而是按“字”(如 8 字节)一次读取。如果数据跨了两个字(不对齐),需要两次访问甚至出错;对齐后一次就能取到。所以编译器默认会给结构体成员排位置,让每个成员都落在自身对齐数(通常等于大小)整数倍的地址上。
5.2 对齐规则(默认可背诵)
Section titled “5.2 对齐规则(默认可背诵)”- 每个成员的起始偏移必须是
alignof(成员)的整数倍;若用了#pragma pack(n),则取min(n, alignof(成员))。 - 结构体自身的对齐值 = 成员中最大的
alignof;总大小必须是这个对齐值的整数倍(最后可能补 padding)。 - 空类/空结构体大小为 1(见 04 章)。
💡 alignof(T) 和 sizeof(T) 是两件事:
sizeof(T):这个类型/对象占多少字节(含尾部 padding)。alignof(T):这个类型要求起始地址必须是几的倍数(对齐数)。 例:sizeof(int)=4, alignof(int)=4;sizeof(double)=8, alignof(double)=8;但struct S { char a; int b; }; // sizeof(S)=8,alignof(S)=4S 里 b 要 4 字节对齐,a 后面垫 3 字节 → 总大小 8;但整个结构体对齐数是最大成员对齐 4(不是 8)。 记忆:sizeof 回答“占地多大”,alignof 回答“住址要求”。内存布局时:每个成员的偏移 = 上一个成员结束位置向上取整到
alignof(该成员)的倍数;结构体总大小再向上取整到alignof(结构体)的倍数。
5.3 例题(面试必考)
Section titled “5.3 例题(面试必考)”struct S1 { // 分析偏移: char a; // 0,对齐1,占 [0] int b; // 对齐4 → 偏移4,占 [4,7],中间 [1,3] 是 padding double c; // 对齐8 → 偏移8,占 [8,15]}; // 最大对齐8,总大小16(已是8的倍数)// sizeof(S1) = 16
struct S2 { // 调换顺序: double c; // 偏移0,占 [0,7] int b; // 偏移8,占 [8,11] char a; // 偏移12,占 [12]}; // 最大对齐8,总大小必须8的倍数 → 补到16// sizeof(S2) = 16(顺序改变不影响这里,但下面这个顺序有影响)
struct S3 { char a; // [0] char b; // [1] int c; // 对齐4 → 偏移4,占 [4,7]}; // 最大对齐4 → 8// sizeof(S3) = 8
struct S4 { char a; // [0] double c; // 对齐8 → 偏移8!占 [8,15] int b; // 对齐4 → 偏移16,占 [16,19]}; // 最大对齐8,总大小 → 24// sizeof(S4) = 24 —— 看出"把大成员放前面"可以省内存总结一句话:按成员大小从大到小排,padding 最少。
5.4 手动控制对齐
Section titled “5.4 手动控制对齐”#pragma pack(push, 1) // 强制 1 字节对齐(省内存、但访问变慢)struct Packed { char a; int b; }; // sizeof = 5#pragma pack(pop)
struct Aligned { alignas(16) char buf[64]; }; // C++11 alignas:强制16字节对齐(如SIMD数据)
> 💡 **alignas(16) 的作用**:`char` 自然对齐是 1(放哪都行),`alignas(16)` 强制这个结构体的**对齐数变成 16**——即 `alignof(Aligned) == 16`,编译器保证起始地址是 16 的倍数(`sizeof` 也必须是 16 的倍数)。> **sizeof(Aligned) = 64**:buf 占 64 字节,64 本来就是 16 的倍数,无需补 padding。> 若 buf 是 `char[65]`:65 不是 16 的倍数 → sizeof 补到 **80**。> 用途:SIMD(SSE 要求 16 字节对齐)、原子操作、缓存行对齐(`alignas(64)`)提升性能。追问:pragma pack 用在什么场景?→ 网络协议报文、文件格式、硬件寄存器映射,需要和外部结构严格对应时。 追问:
offsetof能拿成员偏移;alignof(T)拿类型对齐数。
💡 offsetof(struct, member):标准库宏(
#include <cstddef>),返回成员在结构体内的字节偏移。struct S { char a; int b; double c; };offsetof(S, a) == 0offsetof(S, b) == 4 // int 对齐 4offsetof(S, c) == 8 // double 对齐 8原理(编译器魔法):
((size_t)&((S*)0)->b)—— 假装结构体放在地址 0,取成员的地址就是它的偏移。 用途:序列化/反序列化、反射系统、游戏引擎属性系统。注意:对非标准布局的类(含虚函数、多重继承等)用 offsetof 是未定义行为,标准只保证标准布局类型。
6. 大小端(Endianness)
Section titled “6. 大小端(Endianness)”- 大端(Big-Endian):高位字节存低地址。网络字节序是大端。
- 小端(Little-Endian):低位字节存低地址。x86/ARM 主流是小端。
判断方法(背代码):
#include <iostream>bool isLittleEndian() { int x = 1; // 内存里低字节是 0x01 return *(char*)&x == 1; // 取首字节:小端 → 1,大端 → 0}
// 或用 union(更常见于面试)union { int i; char c[4]; } u;u.i = 1;std::cout << (u.c[0] == 1 ? "小端" : "大端");⚠️ 易错点(面试高频坑):小端机器上
int x = 0x12345678;,内存从低地址到高地址依次是78 56 34 12,不是12 34 56 78! 小端 = 低位字节存低地址,0x78 是最低位字节 → 先落地。12 34 56 78是大端/网络字节序的排列。 记忆:小端 = “小尾端”,尾巴(低位)先存。判断:*(char*)&x == 0x78→ 小端;== 0x12→ 大端。
为什么考:网络协议、跨平台存档/二进制格式、序列化时字节序不一致会读错数据。游戏联机或存二进制存档时要用 htonl/ntohl 或统一转成小端。
💡 htonl / ntohl:字节序转换函数(Linux 在
arpa/inet.h,Windows 在winsock2.h)。
htonl= host to network long:把“主机字节序”(x86 是小端)转成“网络字节序”(规定是大端)。发送数据前调用。ntohl= network to host long:接收后转回主机字节序。- 还有 16 位版本
htons / ntohs(s = short)。这里的 “long” 指 32 位、“short” 指 16 位(与 C 的 long 类型大小无关)。uint32_t v = 0x12345678;uint32_t net = htonl(v); // 发送前:小端 → 大端uint32_t back = ntohl(net); // 接收后:大端 → 小端本质:网络协议/跨平台文件格式必须约定一种字节序(大端),各平台收发时转换。自己设计的存档格式也可直接统一用某一种,但用 htonl/ntohl 更通用。
标准提示:union 类型双关在 C++ 中严格是未定义行为(GCC/MSVC 作为扩展允许);更标准的写法是
memcpy、C++20std::bit_cast,或读unsigned char对象表示。
💡 展开讲(union 类型双关):上面判断大小端的 union 写法:
union { int i; char c[4]; } u;u.i = 1;std::cout << u.c[0]; // 写 i、读 c —— 类型双关C 语言允许“写一个成员读另一个成员”(类型双关);C++ 标准认为这是未定义行为(UB),但 GCC/MSVC 把它当扩展支持,实践中能跑,所以面试/笔试可以这么答。更标准的做法:
// 方式一:memcpy(C++03 起合法)int x = 1; char bytes[4];std::memcpy(bytes, &x, sizeof x); // 把字节拷出来看// 方式二:C++20 std::bit_cast(编译期、最现代)auto bytes = std::bit_cast<std::array<char, 4>>(x);// 方式三:用 unsigned char* 读对象表示(标准明确允许)const unsigned char* p = reinterpret_cast<const unsigned char*>(&x);p[0] // 小端 == 1面试话术:先答 union 法(考官预期答案),再补一句“严格 C++ 里这是 UB,规范做法是 memcpy / bit_cast”,加分。
7. 高频面试题 Q&A(合上书能讲)
Section titled “7. 高频面试题 Q&A(合上书能讲)”Q1:从 .cpp 到可执行文件经历哪几步? 预处理(展开头文件、宏替换)→ 编译(词法/语法/语义分析,生成汇编)→ 汇编(生成目标文件 .o)→ 链接(符号解析+重定位,和库拼成可执行文件)。
Q2:静态库和动态库区别? 静态库链接期拷入代码,体积大、独立部署、启动快;动态库运行期加载,体积小、可共享、可热更新,但依赖缺失会运行时报错。
Q3:全局变量、static、局部变量存哪?生命周期? 全局/static → 数据段(.data/.bss),程序启动到结束;局部 → 栈,函数返回即销毁;new → 堆,直到 delete。
Q4:栈和堆的区别? 栈:自动管理、快、容量小、无碎片;堆:手动管理、慢、容量大、有碎片。
Q5:sizeof 一个结构体怎么算?
成员按自身大小对齐 + 总大小是最大对齐数整数倍 + 顺序影响 padding。例:{char,int,double} → 16;{char,char,int} → 8。
Q6:为什么要内存对齐? CPU 按字访问,对齐保证一次读取、访问更快,也避免跨页/跨字访问。
Q7:怎么判断大小端?
union 或 char* 强转取首字节,看低字节是不是低地址。
Q8:#pragma once 和 #ifndef 区别? #ifndef 靠宏名防重、跨平台;#pragma once 编译器按文件去重、更快。现代工程用 #pragma once 居多。
Q9:extern “C” 干嘛的? 关闭 C++ 名字修饰,让 C++ 里能链接 C 编译出的符号;反过来 C 里也常用。
Q10:链接报 undefined reference 可能原因? 函数只声明没实现;源文件没参与编译/链接;忘了链接对应的库(-lxxx);符号名不一致(extern “C” 或 const/static 链接属性问题)。
Q11:const 全局变量和字符串常量放哪? 只读数据段 .rodata,改写会崩溃(段错误)。
Q12:一个进程的虚拟地址空间为什么 32 位只有 4G?栈为什么不能无限大? 虚拟地址由地址线位数决定(32 位 → 2^32 = 4G);栈大小由系统限制(可 ulimit 调整),防止无限递归把整个空间耗尽。
8. 本章自测(10 题,限时 15 分钟,先自己答再看答案)
Section titled “8. 本章自测(10 题,限时 15 分钟,先自己答再看答案)”1. 预处理和编译阶段的分界产物是什么?.i .s
2. `.text`、`.data`、`.bss`、`.rodata`、栈、堆分别装什么?
我的原答:代码、初始化的全局变量和static、未初始化的全局变量和static、const以及常量字符串
3. `int x = 1;`(文件级)和 `void f(){ int x = 1; }` 的 x 存哪?
我的原答:.data、栈
4. `struct { int a; char b; char c; }` 的 sizeof?
我的原答:写理由。4+1+1=6 需要时4的倍数 8
5. `struct { char a; int b; char c; }` 的 sizeof?
我的原答:4(1)+4+1 = 9 12
6. 静态库和动态库各举一个"为什么选它"的场景。
我的原答:需要保持程序独立性,启动需要快;经常更新库,共享
7. 用代码判断大小端(手写)。
我的原答:`int x = 1; *(char*)&x == 1`
8. `#pragma once` 能防住所有重复包含问题吗?
我的原答:有什么局限?不能,按照文件识别重复,但是有时候因为软硬链接等问题会把同一个文件识别成两个文件
9. 链接时报 `undefined reference to main` 说明什么?
我的原答:未实现定义;未链接库;extern "C";未把当前文件编译/链接
10. 为什么 const 成员函数里修改成员会编译报错?
我的原答:(提示:先想 const 指针,这个在 03 章有呼应)
📖 答案(先自己答完再展开)
- 预处理产出 .i 文件(展开后的翻译单元);编译从它开始生成汇编 .s。
- 代码段=指令;.data=已初始化全局/static;.bss=未初始化全局/static;.rodata=字符串常量等只读数据;栈=局部变量/调用帧;堆=动态内存。
- 前者 .data;后者栈(局部)。
int(4)+char(1)+char(1)→ 对齐 4 → 8。(char 连续放,4+1+1=6 补到 8)char[0]+ pad[1..3] + int[4..7] + char[8] → 最大对齐 4 → 12。- 静态库:需要单文件发布/离线环境;动态库:插件化、频繁更新、多进程共享。
- 见第 6 节代码。
- 不能:只防“同一路径被多次包含”;同一文件经硬链接/软链接/不同路径包含时可能被当成两个文件(工程上 #pragma once 足够用)。
- 入口函数 main 没被链接进来(缺 main 定义/没编 main.cpp/编译链接命令漏了文件)。
- const 成员函数里 this 是
const T*,所有成员都只读——呼应下一章“const 指针”。
- 动态库加载时“重定位”做了什么?(PIC 地址无关代码)
- 栈为什么向下增长、堆为什么向上?(历史约定,避免相撞)
alignas对齐超出类型默认对齐的场景?(SIMD、原子操作 16 字节对齐)- 什么是 TLB、页表?(性能章 11 会讲)