
五、C++ 新特性、关键字与编译原理
一句话总览:预处理指令在正式编译前完成文本替换;关键字决定类型、作用域、生命周期、链接属性和编译器优化;C++11 之后的现代特性用类型安全、RAII、移动语义、零拷贝视图和标准库组件替代大量旧式 C 写法。
知识点关系图
源代码
├─ 预处理阶段
│ ├─ 1. #define 宏
│ └─ 8. #include <> 与 #include ""
│
├─ 类型与声明
│ ├─ 2. typedef
│ ├─ 3. register
│ ├─ 4. const
│ ├─ 5. extern
│ ├─ 6. volatile
│ └─ 10. static
│
├─ 运行时与编译机制
│ ├─ 7. dynamic_cast / RTTI
│ ├─ 22/23 类知识:头文件保护、inline 不在本批次
│ └─ 13. sizeof / strlen
│
├─ C 风格接口
│ └─ 11. sprintf / strcpy / memcpy
│
├─ 现代 C++
│ ├─ 9. const 替代 #define
│ ├─ 12. C++11
│ ├─ 14. C++17/20/23
│ ├─ 16. string_view / span / optional
│ └─ 17. RAII
│
└─ 工程选择
└─ 15. STL 容器在数据处理与推理服务中的选择
1. C/C++ 中宏定义的相关知识
核心:宏由预处理器处理,发生在正式语法编译之前,本质是文本替换。宏没有类型系统、不遵循普通作用域,使用方便但容易产生优先级、重复求值和调试困难等问题。
1.1 对象式宏与函数式宏
#define BUFFER_SIZE 1024
#define PI 3.14159
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))
宏参数必须充分加括号,否则会因运算符优先级出错:
#define BAD_ADD(a, b) a + b
int x = BAD_ADD(1, 2) * 10;
// 文本展开:1 + 2 * 10 = 21,而不是 30
函数式宏还可能多次求值参数:
int n = 3;
int y = SQUARE(n++);
// 展开为 ((n++) * (n++)),n 被自增两次,结果和求值顺序都危险
普通内联函数或模板不会这样:
template <class T>
inline T square(T x) {
return x * x;
}
1.2 字符串化与标记拼接
# 把宏参数转成字符串,## 拼接标记:
#define TO_STRING(x) #x
#define CONCAT(a, b) a##b
const char* s = TO_STRING(hello); // "hello"
int CONCAT(value, 1) = 10; // int value1 = 10;
1.3 条件编译与头文件保护
#ifdef DEBUG
std::cout << "debug\n";
#else
std::cout << "release\n";
#endif
#ifndef MODULE_CORE_H
#define MODULE_CORE_H
class Core {};
#endif
常见预定义宏:
std::cout << __FILE__; // 文件名
std::cout << __LINE__; // 行号
std::cout << __DATE__; // 编译日期
std::cout << __TIME__; // 编译时间
1.4 使用建议
宏适合条件编译、头文件保护、平台判断、日志中自动带文件名和行号;但普通常量优先使用 constexpr,小函数优先使用 inline 或模板,枚举集合优先使用 enum class。现代 C++ 的原则是:能用语言类型系统解决,就不要让预处理器绕过类型系统。
2. C/C++ 中 typedef 关键字的相关知识
核心:typedef 用于给已有类型起别名,提高可读性、屏蔽平台差异、简化复杂函数指针声明。它不是创建新类型,而是为原类型增加一个同义名称。
2.1 基本用法
typedef unsigned long size_type;
typedef int Integer;
size_type len = 100;
Integer x = 10;
2.2 结构体与函数指针
typedef struct Point {
int x;
int y;
} Point;
typedef int (*CompareFunc)(int, int);
void sort_array(int* arr, int n, CompareFunc cmp) {
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (cmp(arr[i], arr[j]) > 0) {
std::swap(arr[i], arr[j]);
}
}
}
}
2.3 typedef 与 #define 的区别
typedef int* IntPtr;
#define PTR_INT int*
IntPtr a, b; // a、b 都是 int*
PTR_INT c, d; // 展开为 int* c, d; c 是 int*,d 是 int
| 对比项 | typedef | #define |
|---|---|---|
| 处理阶段 | 编译阶段 | 预处理阶段 |
| 类型检查 | 有 | 无 |
| 作用域 | 遵循命名空间和块作用域 | 文本生效直到 #undef |
| 复杂声明 | 更安全 | 容易被展开规则坑害 |
| 模板别名 | 不直接支持 | 不支持 |
2.4 现代 C++ 推荐 using
using IntPtr = int*;
using CompareFunc = int (*)(int, int);
template <class T>
using Vec = std::vector<T>;
Vec<int> nums;
Vec<float> scores;
using 的阅读顺序是"新名字 = 原类型",更直观,而且支持别名模板,这是 typedef 做不到的。需要注意,typedef int UserId; typedef int OrderId; 并不会形成强类型,二者仍可互相赋值;若需要编译期防止不同 ID 混用,应封装结构体。
3. 介绍一下 register 关键字
核心:register 是早期 C/C++ 的存储类说明符,用来建议编译器把频繁访问的变量放入 CPU 寄存器。现代编译器的寄存器分配能力远强于人工提示,因此该关键字基本失去实际意义。
旧式写法:
long long sum(int n) {
register int i;
register long long result = 0;
for (i = 1; i <= n; ++i) {
result += i;
}
return result;
}
它只是建议,不是强制:
程序员:希望变量在寄存器中
编译器:根据寄存器压力、循环频率和优化级别决定
├─ 有空闲寄存器 → 可能放寄存器
└─ 寄存器不足 → 仍放栈上
传统 C 中,register 变量不能取地址,因为寄存器没有内存地址:
register int x = 1;
// int* p = &x; // 传统 C 中错误
它与其他存储类区别如下:
| 关键字 | 生命周期 | 作用 |
|---|---|---|
| 自动局部变量 | 作用域内 | 默认栈上分配 |
| register | 作用域内 | 建议寄存器分配 |
| static 局部变量 | 程序运行期 | 只初始化一次 |
| 全局变量 | 程序运行期 | 静态存储区 |
现代代码直接写普通变量并开启优化即可:
g++ -O2 main.cpp -o main
C++17 起,register 不再作为存储类说明符使用。性能优化应依靠 profiler、算法优化、数据布局和缓存命中,而不是盲目添加 register。
4. 介绍一下 const 关键字
核心:const 表示只读约束,用于变量、指针、引用、参数、返回值和成员函数。它让编译器帮助防止意外修改,是 const correctness 的基础。
4.1 const 变量和引用
const int max_count = 100;
// max_count = 200; // 错误
void print(const std::string& s) {
// s = "new"; // 错误
std::cout << s;
}
4.2 const 与指针
int x = 10, y = 20;
const int* p1 = &x;
// *p1 = 30; // 错误:不能通过 p1 修改值
p1 = &y; // 正确:可以改指向
int* const p2 = &x;
*p2 = 30; // 正确:可以修改值
// p2 = &y; // 错误:不能改指向
const int* const p3 = &x;
// *p3 = 30; // 错误
// p3 = &y; // 错误
记忆:const 在 * 左边限制指向值,在 * 右边限制指针本身。
4.3 const 成员函数
class Account {
private:
int balance;
public:
explicit Account(int b) : balance(b) {}
int get_balance() const {
// balance = 0; // 错误
return balance;
}
};
const 对象只能调用 const 成员函数:
const Account account(100);
account.get_balance(); // 正确
// account.deposit(); // 错误
4.4 const 与 constexpr
const int a = get_runtime_value(); // 运行期初始化也可以
constexpr int b = 10 * 10; // 必须编译期可计算
const 表达只读,constexpr 表达编译期常量。mutable 是极少数例外,只用于锁、缓存等不影响对象逻辑状态的成员。合理使用 const 可以明确接口意图、提前暴露修改错误,并让 const 对象能够安全调用只读接口。
5. 介绍一下 external(extern)关键字
核心:C++ 中没有 external 关键字,正确名称是 extern。它用于声明变量或函数定义在其他翻译单元中,由链接器在链接阶段解析符号。
// global.cpp
int g_count = 10;
// main.cpp
#include <iostream>
extern int g_count; // 声明,不分配内存
int main() {
std::cout << g_count << '\n';
}
声明与定义的区别:
extern int x; // 声明
int x; // 定义
extern int y = 10; // 带初始化,是定义
C++ 命名空间作用域下的 const 默认内部链接,跨文件共享时要写:
// config.cpp
extern const int model_size = 128;
// main.cpp
extern const int model_size;
现代 C++ 更推荐头文件中使用:
inline constexpr int model_size = 128;
extern "C"
C++ 支持重载,会对函数名做 name mangling;C 不会。调用 C 库时要按 C 链接规则:
#ifdef __cplusplus
extern "C" {
#endif
int c_library_function(int);
#ifdef __cplusplus
}
#endif
编译链接关系:
a.cpp → a.o ┐
├─ 链接 → 可执行文件
b.cpp → b.o ┘
extern 表示符号可能在另一个目标文件中
常见错误包括只声明不定义导致 undefined reference、头文件定义全局变量导致重复定义、C++ 调 C 库忘记 extern "C" 导致符号不匹配。
6. 介绍一下 volatile 关键字
核心:volatile 告诉编译器,被修饰内存可能在当前控制流之外被改变,因此每次访问都必须真实读写,不能缓存到寄存器或优化掉。它主要用于硬件寄存器和信号处理,不等于多线程同步。
volatile int flag = 0;
void wait() {
while (flag == 0) {
// 每次循环都重新从内存读取 flag
}
}
嵌入式内存映射 IO:
#define UART_STATUS (*(volatile unsigned int*)0x40001000)
void wait_ready() {
while ((UART_STATUS & 0x01) == 0) {
// 每次都真实读取硬件寄存器
}
}
指针形式:
volatile int* p; // 指向 volatile int
int* volatile p2; // 指针本身 volatile
volatile int* volatile p3; // 值和指针都 volatile
volatile 不是线程同步工具
// 错误理解:volatile 不能保证原子性、内存顺序或 happens-before
volatile bool stop = false;
多线程应使用:
#include <atomic>
std::atomic<bool> stop{false};
void worker() {
while (!stop.load(std::memory_order_acquire)) {
}
}
| 场景 | 正确工具 |
|---|---|
| 硬件寄存器 | volatile |
| 信号处理标志 | volatile sig_atomic_t |
| 多线程共享标志 | std::atomic |
| 复杂临界区 | std::mutex |
const volatile 可用于程序只读但硬件会改变的状态寄存器:const 限制程序写入,volatile 保证每次读取。总结:volatile 管的是编译器内存访问优化,不管线程同步。
7. 介绍一下 dynamic 在什么时候使用
核心:C++ 没有普通关键字 dynamic,面试通常问 dynamic_cast。它用于继承层次中的运行时类型转换,最典型场景是把基类指针或引用安全转换为派生类类型。
class Animal {
public:
virtual void speak() const = 0;
virtual ~Animal() = default;
};
class Dog : public Animal {
public:
void speak() const override {}
void guard() const {}
};
void process(const Animal* animal) {
const Dog* dog = dynamic_cast<const Dog*>(animal);
if (dog) {
dog->guard();
}
}
指针转换失败返回 nullptr;引用没有空引用,失败抛 std::bad_cast:
void process_ref(const Animal& animal) {
try {
const Dog& dog = dynamic_cast<const Dog&>(animal);
dog.guard();
} catch (const std::bad_cast&) {
}
}
dynamic_cast 依赖 RTTI,因此基类必须至少有一个虚函数。它与 static_cast 的区别是:
| 转换 | 检查时机 | 类型错误时 |
|---|---|---|
| static_cast | 编译期 | 不检查,可能未定义行为 |
| dynamic_cast | 运行期 | 指针返回 nullptr,引用抛异常 |
向上转型 Dog* → Animal* 天然安全,可隐式完成;向下转型 Animal* → Dog* 才需要 dynamic_cast。若代码中频繁使用 dynamic_cast,通常说明基类接口抽象不足,应优先把通用行为设计成虚函数。它适合反序列化、事件分发、插件接口查询等少量恢复具体类型的场景。
8. #include <file.h> 与 #include "file.h" 的区别
核心:二者都在预处理阶段把头文件内容插入当前位置,区别主要在搜索路径和搜索顺序。尖括号通常用于系统和外部库头文件,双引号通常用于项目内部头文件。
#include <vector> // 标准库
#include <nlohmann/json.hpp> // 外部库
#include "student.h" // 当前项目头文件
#include "../common/config.h"
预处理过程:
main.cpp
│ #include "student.h"
▼
把 student.h 内容原样插入
▼
形成完整翻译单元,再编译
- 尖括号:主要搜索标准库路径、系统路径和
-I指定的外部 include 路径。 - 双引号:先按实现定义的本地文件规则搜索当前项目目录;找不到后再回退到尖括号路径。
| 写法 | 常见用途 | 搜索特征 |
|---|---|---|
<file.h> |
标准库、第三方库、系统头 | 偏系统/外部路径 |
"file.h" |
项目内部头文件 | 先本地,再系统路径 |
头文件应加保护:
#pragma once
class Student {};
或:
#ifndef PROJECT_STUDENT_H
#define PROJECT_STUDENT_H
class Student {};
#endif
推荐在构建系统中统一 include 根目录:
g++ main.cpp -Iinclude
然后写 #include "mylib/student.h",避免大量 ../../ 脆弱路径。C++ 标准库头文件通常没有 .h,C 头文件的 C++ 封装推荐 <cstdio>、<cstring>、<cstdint>。
9. const 和 #define 有什么区别?
核心:const 是语言层面的只读实体,有类型、作用域和调试信息;#define 是预处理文本替换,没有类型检查和作用域。现代 C++ 常量优先使用 constexpr/const。
#define MAX_SIZE 100
const int max_size = 100;
constexpr int compile_size = 100;
区别表
| 维度 | const/constexpr | #define |
|---|---|---|
| 处理阶段 | 编译阶段 | 预处理阶段 |
| 类型检查 | 有 | 无 |
| 作用域 | 类、命名空间、块作用域 | 文本全局生效直到 undef |
| 调试 | 可进入符号表 | 替换后原名可能消失 |
| 重复求值 | 不存在 | 函数式宏可能多次求值 |
| 现代推荐 | 推荐 | 常量不推荐 |
宏的问题:
#define DOUBLE(x) ((x) + (x))
int n = 1;
int y = DOUBLE(n++); // n++ 执行两次
const 不存在该问题:
const int n = 1;
int y = n + n;
宏没有命名空间和类作用域,容易污染后续代码;const 常量可以属于类:
class Config {
public:
static constexpr int timeout = 30;
};
宏仍适合条件编译、头文件保护、日志中的 __FILE__/__LINE__、跨编译器属性封装。普通数值、字符串和简单函数应使用 constexpr、inline 函数、枚举替代。
10. 介绍一下 static 关键字的作用
核心:static 同时管理生命周期、链接属性和类级共享,主要有五种用法:静态局部变量、静态全局变量、静态函数、静态成员变量、静态成员函数。
10.1 静态局部变量
int next_id() {
static int id = 0; // 只初始化一次
return ++id;
}
普通局部变量每次进入函数创建,离开销毁;static 局部变量生命周期持续到程序结束。C++11 保证其初始化线程安全。
10.2 内部链接
// a.cpp
static void helper() {} // 只在 a.cpp 可见
static int cache = 0; // 只在 a.cpp 可见
现代 C++ 更常用匿名命名空间:
namespace {
void helper() {}
}
10.3 类静态成员
class Student {
private:
inline static int total = 0;
public:
Student() { ++total; }
~Student() { --total; }
static int count() {
return total; // 静态函数没有 this
}
};
静态成员变量所有对象共享;静态成员函数没有 this,不能直接访问普通成员,不能是虚函数,也不能加 const。
| 场景 | static 作用 |
|---|---|
| 函数内 | 只初始化一次,保持状态 |
| 文件内变量/函数 | 内部链接,避免符号冲突 |
| 类成员变量 | 所有对象共享 |
| 类成员函数 | 属于类,不依赖对象 |
多线程下共享 static 状态要使用 std::mutex 或 std::atomic。不要滥用 static 制造隐藏全局状态,否则会降低可重入性和可测试性。
11. sprintf、strcpy、memcpy 有什么区别?
核心:sprintf 负责格式化输出到字符串;strcpy 复制以 \0 结尾的 C 字符串;memcpy 按字节复制指定长度内存。三者处理对象、结束条件、重叠规则和安全性不同。
sprintf
char buf[64];
sprintf(buf, "age=%d, name=%s", 20, "Tom");
它按格式串拼接文本,并自动补 \0,返回写入字符数,不包括 \0。但无法限制长度,容易溢出,应使用 snprintf:
snprintf(buf, sizeof(buf), "%s", long_text);
strcpy
const char* src = "hello";
char dest[16];
strcpy(dest, src); // 复制到并包含 \0
它遇到源字符串 \0 停止,不接收长度参数,调用者必须保证目标容量。strncpy 不一定自动补 \0,必须手动处理。
memcpy
int a[5] = {1, 2, 3, 4, 5};
int b[5];
memcpy(b, a, sizeof(a)); // 严格复制 20 字节
memcpy 不关心字符串,也不会因 \0 停止;源和目标重叠时是未定义行为,重叠场景使用 memmove。
| 函数 | 对象 | 长度依据 | 是否补 \0 |
重叠 |
|---|---|---|---|---|
| sprintf | 格式化文本 | 格式串和参数 | 是 | 不建议 |
| strcpy | C 字符串 | 遇到 \0 |
是 | 不允许 |
| memcpy | 任意内存 | 显式字节数 | 否 | 不允许 |
| snprintf | 安全格式化 | 缓冲区长度 | 是 | 不建议 |
| memmove | 任意内存 | 显式字节数 | 否 | 允许 |
含 std::string、虚函数、智能指针的非平凡 C++ 对象不能简单 memcpy,应使用拷贝构造/赋值或容器。
12. C++11 的新特性都有哪些?
核心:C++11 是现代 C++ 分水岭,引入自动类型推导、移动语义、lambda、智能指针、统一初始化、并发库、范围 for、强类型枚举和空指针常量。
12.1 auto 与范围 for
std::map<std::string, std::vector<int>> data;
for (auto& [k, v] : data) { // 结构化绑定是 C++17,此处展示 auto
}
for (auto& x : vector_data) {
x *= 2;
}
12.2 nullptr、enum class、统一初始化
void f(int);
void f(int*);
f(nullptr); // 明确空指针,不会误匹配 int
enum class Color { Red, Green, Blue };
Color c = Color::Red;
std::vector<int> nums{1, 2, 3};
Point p{1, 2};
12.3 lambda
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
int base = 10;
auto add = [base](int x) { return base + x; };
12.4 移动语义与智能指针
std::string s1 = "large text";
std::string s2 = std::move(s1); // 转移资源
auto p = std::make_unique<int>(10);
auto q = std::make_shared<int>(20);
12.5 其他高频特性
constexpr int square(int x) { return x * x; }
class Base {
public:
virtual void f() {}
virtual ~Base() = default;
};
class Derived : public Base {
void f() override {}
};
class NonCopy {
public:
NonCopy() = default;
NonCopy(const NonCopy&) = delete;
};
还包括右值引用、noexcept、decltype、尾置返回类型、变参模板、initializer_list、委托构造、继承构造、std::thread/mutex/future/async、std::array/tuple/unordered_map/function/chrono。分组记忆:
类型表达:auto/decltype
初始化:{} / initializer_list
函数:lambda/constexpr/noexcept
资源:移动语义/智能指针
安全:nullptr/enum class/override/final
并发:thread/mutex/condition_variable/future
13. sizeof 与 strlen 的区别
核心:sizeof 是编译期运算符,求类型或对象占用字节数;strlen 是库函数,在运行时统计 C 字符串第一个 \0 之前的字符数。
char s[] = "hello";
sizeof(s); // 6:h e l l o \0
strlen(s); // 5:不包含 \0
内存结构:
下标:0 1 2 3 4 5
内容:h e l l o \0
strlen → 5
sizeof → 6
| 维度 | sizeof | strlen |
|---|---|---|
| 本质 | 运算符 | C 库函数 |
| 求值时间 | 通常编译期 | 运行时 |
是否依赖 \0 |
不依赖 | 必须依赖 |
| 结果 | 字节大小 | 有效字符数 |
| 对指针 | 得到指针大小 | 从地址开始扫描 |
指针陷阱:
const char* p = "hello";
sizeof(p); // 64 位平台通常是 8
strlen(p); // 5
数组作为函数参数会退化为指针:
void bad(char s[100]) {
sizeof(s); // 不是 100,而是 char* 大小
}
没有 \0 时,strlen 会越界扫描:
char buf[5] = {'h', 'e', 'l', 'l', 'o'};
strlen(buf); // 未定义行为
sizeof 还会受结构体对齐影响:
struct Node {
char c;
int x;
};
sizeof(Node); // 通常是 8,不是 5
现代 C++ 固定数组可用 std::array::size(),动态元素数量用 std::vector::size()。
14. C++17/20/23 中哪些特性在工程中高频使用?
核心:C++17 让接口更安全、类型表达更清楚;C++20 提供 concepts、ranges、span、协程等高性能抽象;C++23 补齐 expected、mdspan、print、flat_map 等工程能力。在数据管线和推理服务中,它们主要用于减少拷贝、提前暴露类型错误、简化异步和多维数据处理。
C++17
#include <optional>
#include <string_view>
#include <variant>
void print(std::string_view name); // 不拥有字符串,避免临时 string
std::optional<int> find_index(int key) {
if (key < 0) return std::nullopt;
return key;
}
using Device = std::variant<CpuDevice, GpuDevice, NpuDevice>;
std::visit([](const auto& dev) { dev.run(); }, device);
高频特性还包括结构化绑定、if constexpr、inline 变量、折叠表达式、CTAD、std::filesystem、并行算法。
C++20
#include <concepts>
#include <ranges>
#include <span>
template <std::integral T>
T add(T a, T b) { return a + b; }
float sum(std::span<const float> x);
namespace rv = std::views;
for (int x : data | rv::filter([](int x) { return x > 0; })
| rv::transform([](int x) { return x * 2; })) {
}
高频特性包括 concepts 约束模板、ranges 惰性管道、span 连续内存视图、coroutines 协程、modules、jthread、format、latch/barrier/semaphore。
C++23
std::expected<int, ErrorCode> parse();
std::print("value={}\n", value);
std::mdspan<float, std::dextents<std::size_t, 2>> matrix(ptr, rows, cols);
expected 表达成功或带原因失败;mdspan 把连续内存解释成多维张量视图;print 提供类型安全输出;flat_map 使用连续有序存储,小配置和查找密集场景缓存友好;move_only_function 可持有只移动回调。
选型建议:C++17 是稳定基线,C++20 是现代工程主力,C++23 按编译器和部署环境逐步引入。
15. STL 容器在数据处理和推理服务中如何选择?
核心:热路径默认使用连续内存容器 vector/span,因为张量、batch 和特征数组要求缓存友好并能直接传给底层计算库;配置元数据用 map/unordered_map;任务调度用 queue/priority_queue;list 在高性能数据面通常应避免。
数据面
struct Tensor {
std::vector<float> data;
std::vector<int64_t> shape;
std::span<float> view() { return data; }
};
void infer(std::span<const float> input);
vector 连续内存可直接传给 C 接口或运行时:
runtime.set_input(batch.data(), batch.size() * sizeof(float));
控制面
std::unordered_map<std::string, std::string> config; // 精确查找
std::map<long long, Metric> time_series; // 有序范围查询
std::queue<Request> fifo; // FIFO
std::priority_queue<Request, std::vector<Request>, Compare> scheduler;
| 场景 | 推荐 |
|---|---|
| 张量、特征数组 | vector |
| 不拥有的数据窗口 | span |
| 固定大小数组 | array |
| 精确 key 查询 | unordered_map |
| 有序/范围查询 | map |
| FIFO | queue |
| 优先级调度 | priority_queue |
| 双端增删 | deque |
| 高频无锁管线 | 自定义环形缓冲/MPSC 队列 |
list 虽然已知位置插入删除 O(1),但节点独立分配、缓存差。批量过滤可用 vector 标记后压缩:
size_t w = 0;
for (size_t r = 0; r < items.size(); ++r) {
if (items[r].valid) items[w++] = std::move(items[r]);
}
items.resize(w);
STL 容器本身不负责线程安全,多线程读写必须外部加锁、分片锁或使用专用并发队列。选型不能只看理论复杂度,还要看缓存命中、内存分配次数、对象大小和队列背压。
16. std::string_view、std::span、std::optional 在高性能接口中的价值
核心:string_view 和 span 是非拥有视图,用"指针 + 长度"借用已有内存,避免字符串和数组复制;optional 用类型系统表达"可能没有值",避免魔法值和空指针歧义。
string_view
void process(std::string_view s) {
auto sub = s.substr(6); // O(1),只调整指针和长度
}
process("hello"); // 不构造 string
process(std::string("world")); // 不复制原字符串
它不保证 \0 结尾,不能无脑传给 const char* C 接口。
span
float mean(std::span<const float> values) {
float total = 0;
for (float x : values) total += x;
return values.empty() ? 0 : total / values.size();
}
float raw[3] = {1, 2, 3};
std::vector<float> vec = {4, 5};
std::array<float, 2> arr = {6, 7};
mean(raw);
mean(vec);
mean(arr);
span<T> 可写,span<const T> 只读,统一了数组、vector、array 和原始连续缓冲区。
optional
std::optional<std::string> get_header(const auto& headers,
const std::string& key) {
auto it = headers.find(key);
if (it == headers.end()) return std::nullopt;
return it->second;
}
if (auto token = get_header(headers, "token")) {
use(*token);
} else {
reject();
}
生命周期风险
std::string_view bad() {
std::string s = "temporary";
return s; // s 销毁,view 悬空
}
std::span<const float> bad2() {
std::vector<float> data(1024);
return data; // vector 销毁,span 悬空
}
关系图:
所有者:string / vector / array / buffer
├─ string_view:借用字符区间
├─ span:借用对象区间
└─ 所有者销毁 → 视图立即失效
视图适合作为函数参数和短期局部变量,不适合长期保存。optional 表达"可能不存在",expected 表达"可能失败且有错误原因",异常用于意外错误,三者分工不同。
17. 为什么 RAII 是 C++ 资源管理的核心思想?
核心:RAII,Resource Acquisition Is Initialization,即资源获取即初始化。它把资源生命周期绑定到对象生命周期:构造函数获取资源,析构函数释放资源;对象离开作用域时自动清理,即使异常导致栈展开也能释放。
手动管理问题:
void bad() {
FILE* fp = fopen("a.txt", "r");
void* buf = malloc(1024);
if (error) {
free(buf);
fclose(fp); // 每个分支都要手动释放
return;
}
free(buf);
fclose(fp);
}
RAII:
#include <fstream>
#include <vector>
void good() {
std::ifstream file("a.txt");
std::vector<char> buffer(1024);
if (error) throw std::runtime_error("failed");
// 离开作用域自动析构释放
}
自定义资源类:
class FileHandle {
private:
FILE* fp;
public:
explicit FileHandle(const char* path, const char* mode) {
fp = std::fopen(path, mode);
if (!fp) throw std::runtime_error("open failed");
}
~FileHandle() {
if (fp) std::fclose(fp);
}
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
FileHandle(FileHandle&& other) noexcept : fp(other.fp) {
other.fp = nullptr;
}
};
锁是最典型 RAII:
std::mutex mtx;
void push_safe(int x) {
std::lock_guard<std::mutex> lock(mtx);
data.push_back(x);
} // 自动 unlock
智能指针是内存 RAII:
auto p = std::make_unique<Buffer>(1024); // 作用域结束自动 delete
异常时构造与析构顺序:
构造:A → B → C
异常栈展开析构:C → B → A
资源不仅包括内存,还包括文件、socket、锁、线程、GPU context、模型会话和共享内存。现代 C++ 推荐 Rule of Zero:业务类尽量只使用 RAII 成员,不手写析构;若自己管理资源,则遵循 Rule of Five。RAII 把"记得释放"变成"不可能忘记释放",这是 C++ 资源管理的核心。
第 1~17 项总结表
| 题号 | 知识点 | 一句话结论 |
|---|---|---|
| 1 | 宏定义 | 预处理文本替换,无类型检查,常量优先 constexpr |
| 2 | typedef | 类型别名,现代 C++ 优先 using |
| 3 | register | 旧式寄存器提示,现代编译器自动优化,基本废弃 |
| 4 | const | 只读约束,构建 const correctness |
| 5 | extern | 外部链接声明,extern "C" 用于 C/C++ 混合 |
| 6 | volatile | 禁止省略硬件内存访问,不等于线程同步 |
| 7 | dynamic_cast | 运行时安全向下转型,依赖虚函数和 RTTI |
| 8 | include | 尖括号偏系统/外部库,双引号偏项目本地 |
| 9 | const vs define | const 有类型、作用域和调试信息 |
| 10 | static | 管理生命周期、内部链接和类级共享 |
| 11 | 三类 C 函数 | sprintf 格式化、strcpy 复制字符串、memcpy 复制字节 |
| 12 | C++11 | auto、lambda、移动语义、智能指针、并发库 |
| 13 | sizeof/strlen | sizeof 看字节数,strlen 看 \0 前长度 |
| 14 | C++17/20/23 | optional/view/concept/ranges/span/expected/mdspan |
| 15 | STL 选择 | 数据面 vector/span,控制面 map/unordered_map |
| 16 | 高性能视图 | view/span 借用内存,optional 表达可缺失 |
| 17 | RAII | 构造获取、析构释放,异常安全资源管理 |
最终记忆主线
预处理:#define / #include
类型声明:typedef / const / extern / volatile / static
运行时识别:dynamic_cast / RTTI
旧式 C 接口:sprintf / strcpy / memcpy / strlen
现代 C++:constexpr、using、auto、lambda、智能指针、移动语义
高性能接口:string_view / span / optional / expected
资源管理:RAII / unique_ptr / lock_guard
工程选型:连续内存优先 vector,视图优先 span,配置按有序性选 map