五、C++ 新特性、关键字与编译原理(进阶)(一)

五、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__、跨编译器属性封装。普通数值、字符串和简单函数应使用 constexprinline 函数、枚举替代。


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::mutexstd::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;
};

还包括右值引用、noexceptdecltype、尾置返回类型、变参模板、initializer_list、委托构造、继承构造、std::thread/mutex/future/asyncstd::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 constexprinline 变量、折叠表达式、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_viewspan 是非拥有视图,用"指针 + 长度"借用已有内存,避免字符串和数组复制;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
相关推荐
2601_962298271 小时前
交易开拓者通常使用什么编程语言?这种语言如何帮助自动化交易?
java·c++·python·编程语言·自动化交易
秋名RG1 小时前
Java IO 体系深度剖析:从流式编程到 JDK 21 高并发陷阱
java·开发语言
码匠许师傅1 小时前
【设计模式精讲】23.备忘录模式(Memento)
c++·设计模式·软件工程·uml·备忘录模式
Pocker_Spades_A1 小时前
Python快速入门专业版(五十九):re实战——用正则爬取豆瓣电影Top250(全流程解析)
开发语言·python
键盘会跳舞2 小时前
【C++多线程】thread_local 深度剖析:线程局部存储的生命周期与实现
c++·多线程·thread_local
whitelbwwww2 小时前
c++ 多线程
开发语言·c++·算法
乌药ice2 小时前
c#中一个多线程安全的HashSet
开发语言·c#
秋名RG2 小时前
Java 异常处理全攻略:从入门到实战(JDK 21 版)
java·开发语言
努力努力再努力wz2 小时前
【Docker入门系列】:从架构演进到容器化:一文建立 Docker、虚拟化与 Namespace 的底层心智模型
运维·开发语言·数据结构·c++·docker·容器·架构