2.第二部分:语言核心

4 阅读26分钟

第二部分:语言核心

[!IMPORTANT] C++ 最核心的思维转换——从 GC 到 RAII,从引用语义到值语义。(详见 [[01-fundamental-differences]])

Java/Kotlin 开发者转型 C++ 时,最大的障碍不是语法,而是两套根本不同的资源管理哲学:

  • Java/Kotlin:对象默认在堆上,通过引用访问,GC 在不确定的时机回收。你几乎不需要思考"对象什么时候被销毁"。
  • C++:对象默认在栈上,通过值访问,离开作用域时立即销毁。堆对象的生命周期由你显式管理——但通过 RAII,这种管理是自动的、确定的、零开销的。

本章覆盖 C++ 语言核心的五大支柱:值语义、RAII、内存管理、错误处理、引用与指针。掌握这些,你就拥有了编写安全、高效 C++ 代码的基础能力。


2.1 值语义与对象模型 [L2]

值类型 vs 引用类型对比

这是 Java/Kotlin 和 C++ 之间最根本的语义差异。

特性Java/KotlinC++
默认语义引用语义(变量是引用)值语义(变量是对象本身)
赋值b = a → 两个引用指向同一对象b = a → 创建 a 的完整副本
函数传参传递引用(对象地址)传递副本(值传递)
函数返回返回引用返回副本(RVO/NRVO 优化为零拷贝)
null引用可以为 null值类型不存在 nullstd::optional 表示可能缺失)
比较== 比较引用(默认)或值(.equals()== 比较值(默认,可重载)
对象位置永远在堆上栈上(默认)或堆上(显式选择)
基本类型值语义(int, double 等)值语义(所有类型统一)
// Java:一切都是引用语义(基本类型除外)
public class Point {
    int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
}

Point a = new Point(1, 2);
Point b = a;          // b 和 a 指向同一个对象
b.x = 10;            // a.x 也变成了 10!
System.out.println(a.x); // 10
// 代码片段(无 main 函数)
// C++:默认值语义
struct Point {
    int x, y;
};

Point a{1, 2};
Point b = a;    // b 是 a 的完整拷贝!两个独立对象
b.x = 10;       // a.x 仍然是 1
// a.x == 1, b.x == 10

// 需要引用语义时,显式声明
Point& ref = a;   // ref 是 a 的引用(别名)
ref.x = 20;       // a.x 变成 20

Point* ptr = &a;  // ptr 是指向 a 的指针
ptr->x = 30;      // a.x 变成 30

栈上对象与自动析构

C++ 中,栈上对象在离开作用域时自动调用析构函数。这是 RAII 的基础(详见 [[01-fundamental-differences]]),也是 Java/Kotlin 中不存在的概念。

// 代码片段(无 main 函数)
#include <iostream>

class Resource {
public:
    Resource()  { std::cout << "  构造 Resource\n"; }
    ~Resource() { std::cout << "  析构 Resource(自动释放)\n"; }
};

void demo() {
    std::cout << "进入作用域\n";
    Resource r;        // 栈上构造
    std::cout << "使用 Resource\n";
}   // ← r 在这里自动析构!不需要手动释放
    // Java 对比:对应对象要等 GC 回收,时机不确定

int main() {
    demo();
    std::cout << "离开 demo()\n";
}
// 输出:
// 进入作用域
//   构造 Resource
// 使用 Resource
//   析构 Resource(自动释放)
// 离开 demo()

Java 对比:Java 中没有"析构函数"。AutoCloseable/try-with-resources 是最接近的概念,但它只适用于显式声明的资源,不像 C++ 析构函数那样适用于所有栈上对象。

拷贝构造函数 / 拷贝赋值运算符

当你拷贝一个对象时,C++ 编译器会根据情况调用两个特殊成员函数之一:

// 代码片段(无 main 函数)
#include <iostream>
#include <cstring>

class Buffer {
    char* data_;
    size_t size_;
public:
    // 构造函数
    explicit Buffer(size_t size) : size_(size), data_(new char[size]) {
        std::cout << "  构造 Buffer(" << size << ")\n";
    }

    // 拷贝构造函数:用另一个对象初始化新对象
    // Buffer b2 = b1; 或 Buffer b2(b1); 时调用
    Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) {
        std::cout << "  拷贝构造(深拷贝)\n";
        std::memcpy(data_, other.data_, size_);
    }

    // 拷贝赋值运算符:用另一个对象替换已有对象
    // b2 = b1; 时调用(b2 已存在)
    Buffer& operator=(const Buffer& other) {
        std::cout << "  拷贝赋值(深拷贝)\n";
        if (this != &other) {  // 自赋值检查
            delete[] data_;
            size_ = other.size_;
            data_ = new char[size_];
            std::memcpy(data_, other.data_, size_);
        }
        return *this;
    }

    // 析构函数
    ~Buffer() {
        std::cout << "  析构 Buffer\n";
        delete[] data_;
    }
};

Java 对比:Java 没有"拷贝构造函数"的概念。Java 的 clone() 方法是显式调用的,且默认是浅拷贝。C++ 的拷贝是隐式的(函数传参、返回值、赋值都会触发),这是 C++ 性能问题的常见来源。

移动语义 (C++11):右值引用、std::move、std::forward

移动语义是 C++11 引入的最重要的性能特性。它解决了"拷贝大对象时代价过高"的问题。

核心思想:当一个对象"即将被销毁"(右值)时,与其拷贝它的数据,不如直接"偷走"它的资源。

// 代码片段(无 main 函数)
#include <iostream>
#include <utility>

class Buffer {
    char* data_;
    size_t size_;
public:
    explicit Buffer(size_t size) : size_(size), data_(new char[size]) {
        std::cout << "  构造 Buffer(" << size << ")\n";
    }

    // 拷贝构造(深拷贝——昂贵)
    Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) {
        std::cout << "  拷贝构造(深拷贝 " << size_ << " 字节)\n";
        std::memcpy(data_, other.data_, size_);
    }

    // 移动构造(窃取资源——廉价)
    // 参数是右值引用(Buffer&&),表示"other 即将被销毁,可以偷它的资源"
    Buffer(Buffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        std::cout << "  移动构造(零拷贝!)\n";
        other.data_ = nullptr;  // 置空,防止 other 析构时释放
        other.size_ = 0;
    }

    // 移动赋值
    Buffer& operator=(Buffer&& other) noexcept {
        std::cout << "  移动赋值(零拷贝!)\n";
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    ~Buffer() {
        if (data_) {
            std::cout << "  析构 Buffer(释放 " << size_ << " 字节)\n";
            delete[] data_;
        } else {
            std::cout << "  析构 Buffer(已移动,无资源)\n";
        }
    }
};

Buffer create_buffer() {
    Buffer tmp{1024};  // 构造
    return tmp;        // 返回局部变量 → 触发移动(或 NRVO 优化掉)
}

int main() {
    std::cout << "=== 拷贝 vs 移动 ===\n";

    Buffer a{1024};          // 构造
    Buffer b = a;            // 拷贝构造(深拷贝 1024 字节)

    Buffer c = std::move(a); // 移动构造(零拷贝!a 被掏空)

    Buffer d = create_buffer(); // 移动构造(或 NRVO 直接构造到 d)

    std::cout << "\n=== std::move 的本质 ===\n";
    // std::move 不移动任何东西!它只是将左值转换为右值引用
    // 真正的"移动"由移动构造函数/移动赋值运算符执行
    Buffer e{2048};
    Buffer f = static_cast<Buffer&&>(e);  // 等价于 std::move(e)
}

关键概念总结:

概念含义Java/Kotlin 对应
左值 (lvalue)有名字、可以取地址的对象所有变量都是左值
右值 (rvalue)临时的、即将销毁的值方法返回的临时对象
T&&右值引用,绑定到右值不存在
std::move(x)x 转为右值引用(不移动!)不存在
std::forward<T>(x)完美转发,保持值的类别不存在
移动构造窃取资源而非拷贝不存在(GC 世界不需要)

std::forward(完美转发) 用于模板中,当函数需要将参数"原封不动"地转发给另一个函数时:

// 代码片段(无 main 函数)
#include <iostream>
#include <utility>

// 完美转发的工厂函数
template<typename T, typename... Args>
T create(Args&&... args) {
    // std::forward 保持参数的值类别(左值还是右值)
    return T(std::forward<Args>(args)...);
}

class Widget {
public:
    Widget(int x)      { std::cout << "  Widget(int)\n"; }
    Widget(int x, int y) { std::cout << "  Widget(int, int)\n"; }
};

int main() {
    auto w1 = create<Widget>(42);       // 转发为右值
    int v = 10;
    auto w2 = create<Widget>(v);        // 转发为左值
    auto w3 = create<Widget>(1, 2);     // 多参数完美转发
}

Rule of Five / Rule of Zero

Rule of Five:如果你定义了以下五个特殊成员函数中的任何一个,通常需要全部定义:

  1. 析构函数 (~T())
  2. 拷贝构造函数 (T(const T&))
  3. 拷贝赋值运算符 (T& operator=(const T&))
  4. 移动构造函数 (T(T&&))
  5. 移动赋值运算符 (T& operator=(T&&))

Rule of Zero:更好的做法——如果你不需要自定义析构函数、拷贝或移动操作,就不要定义它们。让编译器自动生成,你的类天然正确。

// 代码片段(无 main 函数)
#include <iostream>
#include <string>
#include <memory>

// === Rule of Zero 的最佳实践 ===
// 使用标准库类型作为成员,编译器自动生成正确的拷贝/移动操作

class User_RuleOfZero {
    std::string name_;           // std::string 自带正确的拷贝/移动
    std::vector<int> scores_;    // std::vector 自带正确的拷贝/移动
    std::unique_ptr<Config> cfg_; // unique_ptr 自带正确的移动(禁止拷贝)
public:
    User_RuleOfZero(std::string name, std::unique_ptr<Config> cfg)
        : name_(std::move(name)), cfg_(std::move(cfg)) {}

    // 不需要定义任何特殊成员函数!
    // 编译器自动生成:
    // - 拷贝构造/赋值:因为 unique_ptr 不可拷贝,所以自动删除
    // - 移动构造/赋值:正确移动所有成员
    // - 析构函数:正确销毁所有成员
};

// === Rule of Five:管理原始资源的类 ===
class RawBuffer {
    int* data_;
    size_t size_;
public:
    // 1. 构造函数
    explicit RawBuffer(size_t size) : size_(size), data_(new int[size]) {}

    // 2. 析构函数
    ~RawBuffer() { delete[] data_; }

    // 3. 拷贝构造
    RawBuffer(const RawBuffer& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }

    // 4. 拷贝赋值
    RawBuffer& operator=(const RawBuffer& other) {
        if (this != &other) {
            auto tmp = RawBuffer(other);  // 拷贝构造
            std::swap(data_, tmp.data_);
            std::swap(size_, tmp.size_);
        }  // tmp 析构,释放旧资源
        return *this;
    }

    // 5. 移动构造
    RawBuffer(RawBuffer&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
    }

    // 6. 移动赋值
    RawBuffer& operator=(RawBuffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
};

Java 对比:Java 中不存在这些概念。Java 的赋值永远是引用拷贝(浅拷贝),没有移动语义。如果你需要深拷贝,必须手动实现 clone() 或拷贝工厂方法。

Pimpl 惯用法 [L3]

Pimpl (Pointer to Implementation) 通过将私有成员隐藏在堆上的实现类中,实现编译防火墙——修改实现不触发用户代码重编译。

// 代码片段(无 main 函数)
// widget.h —— 公共头文件(用户可见)
#include <memory>

class Widget {
public:
    Widget();
    ~Widget();  // 析构函数必须在头文件中声明

    void do_work();       // 公共接口
    void set_data(int v); // 公共接口

    // 禁止拷贝(或根据需要实现)
    Widget(const Widget&) = delete;
    Widget& operator=(const Widget&) = delete;

    // 允许移动
    Widget(Widget&&) noexcept;
    Widget& operator=(Widget&&) noexcept;

private:
    // 前向声明:用户看不到 Impl 的定义
    class Impl;
    std::unique_ptr<Impl> impl_;  // 指向实现的指针
};
// 代码片段(无 main 函数)
// widget.cpp —— 实现文件(用户不可见)
#include "widget.h"
#include <iostream>

// 实现类的完整定义——只在 .cpp 中可见
class Widget::Impl {
public:
    int data = 0;
    std::string description;

    void do_internal_work() {
        std::cout << "内部工作: data=" << data
                  << ", desc=" << description << "\n";
    }
};

// 构造/析构必须定义在 .cpp 中(因为 Impl 是不完整类型)
Widget::Widget() : impl_(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // unique_ptr 需要完整类型来析构

Widget::Widget(Widget&&) noexcept = default;
Widget& Widget::operator=(Widget&&) noexcept = default;

void Widget::do_work() { impl_->do_internal_work(); }
void Widget::set_data(int v) { impl_->data = v; }

Java 对比:Java 不需要 Pimpl,因为 Java 的编译模型不同——.class 文件在运行时链接。C++ 中,头文件的任何修改都会导致所有 #include 它的文件重编译。Pimpl 通过隐藏实现细节,将修改影响隔离在 .cpp 文件中。

Kotlin 对比:Kotlin 的 internal 可见性修饰符提供了模块级的封装,但仍然不需要 Pimpl。Pimpl 是 C++ 编译模型的特有需求。

Demo:值语义拷贝 vs 移动语义的性能差异,Rule of Five 示例

// demo_2_1_value_semantics.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>
#include <chrono>
#include <numeric>
#include <cstring>

// ============================================================
// 第一部分:值语义 vs 移动语义性能对比
// ============================================================

class LargeBuffer {
    static constexpr size_t SIZE = 1'000'000;  // 1MB
    std::vector<int> data_;
public:
    explicit LargeBuffer() : data_(SIZE) {
        std::iota(data_.begin(), data_.end(), 0);  // 填充 0, 1, 2, ...
    }

    // 拷贝构造(深拷贝——拷贝 1MB 数据)
    LargeBuffer(const LargeBuffer& other) : data_(other.data_) {
        std::cout << "    [拷贝构造] 拷贝了 " << data_.size() * sizeof(int) << " 字节\n";
    }

    // 移动构造(窃取资源——零拷贝)
    LargeBuffer(LargeBuffer&& other) noexcept : data_(std::move(other.data_)) {
        std::cout << "    [移动构造] 零拷贝!仅转移内部指针\n";
    }

    // 拷贝赋值
    LargeBuffer& operator=(const LargeBuffer& other) {
        std::cout << "    [拷贝赋值] 拷贝了 " << other.data_.size() * sizeof(int) << " 字节\n";
        if (this != &other) data_ = other.data_;
        return *this;
    }

    // 移动赋值
    LargeBuffer& operator=(LargeBuffer&& other) noexcept {
        std::cout << "    [移动赋值] 零拷贝!\n";
        if (this != &other) data_ = std::move(other.data_);
        return *this;
    }

    size_t size() const { return data_.size(); }
};

void copy_vs_move_demo() {
    std::cout << "=== 值语义拷贝 vs 移动语义性能对比 ===\n\n";

    LargeBuffer original;
    std::cout << "原始 buffer 大小: " << original.size() * sizeof(int) << " 字节\n\n";

    // 拷贝:深拷贝 1MB 数据
    std::cout << "--- 拷贝 ---\n";
    LargeBuffer copy = original;  // 调用拷贝构造

    // 移动:零拷贝,仅转移指针
    std::cout << "\n--- 移动 ---\n";
    LargeBuffer moved = std::move(original);  // 调用移动构造

    // 性能基准测试
    std::cout << "\n--- 性能基准(1000 次操作)---\n";

    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000; ++i) {
        LargeBuffer tmp;
        LargeBuffer c = tmp;  // 拷贝
    }
    auto copy_time = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::high_resolution_clock::now() - start).count();

    start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < 1000; ++i) {
        LargeBuffer tmp;
        LargeBuffer m = std::move(tmp);  // 移动
    }
    auto move_time = std::chrono::duration_cast<std::chrono::milliseconds>(
        std::chrono::high_resolution_clock::now() - start).count();

    std::cout << "拷贝 1000 次: " << copy_time << " ms\n";
    std::cout << "移动 1000 次: " << move_time << " ms\n";
    std::cout << "加速比: " << (copy_time > 0 ? static_cast<double>(copy_time) / move_time : 999.0) << "x\n";
}

// ============================================================
// 第二部分:Rule of Five 完整示例
// ============================================================

class StringHolder {
    char* data_;
    size_t size_;
public:
    // 构造函数
    explicit StringHolder(const char* s) {
        size_ = std::strlen(s);
        data_ = new char[size_ + 1];
        std::memcpy(data_, s, size_ + 1);
        std::cout << "  构造: \"" << data_ << "\"\n";
    }

    // 1. 析构函数
    ~StringHolder() {
        std::cout << "  析构: \"" << (data_ ? data_ : "(null)") << "\"\n";
        delete[] data_;
    }

    // 2. 拷贝构造
    StringHolder(const StringHolder& other) : data_(new char[other.size_ + 1]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_ + 1);
        std::cout << "  拷贝构造: \"" << data_ << "\"\n";
    }

    // 3. 拷贝赋值(copy-and-swap 惯用法)
    StringHolder& operator=(const StringHolder& other) {
        std::cout << "  拷贝赋值: \"" << other.data_ << "\"\n";
        if (this != &other) {
            StringHolder tmp(other);  // 拷贝构造
            std::swap(data_, tmp.data_);
            std::swap(size_, tmp.size_);
        }  // tmp 析构,释放旧资源
        return *this;
    }

    // 4. 移动构造
    StringHolder(StringHolder&& other) noexcept : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
        std::cout << "  移动构造: \"" << data_ << "\"\n";
    }

    // 5. 移动赋值
    StringHolder& operator=(StringHolder&& other) noexcept {
        std::cout << "  移动赋值: \"" << (other.data_ ? other.data_ : "(null)") << "\"\n";
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    const char* c_str() const { return data_ ? data_ : "(null)"; }
};

void rule_of_five_demo() {
    std::cout << "\n=== Rule of Five 示例 ===\n";

    std::cout << "--- 构造 ---\n";
    StringHolder a{"Hello"};

    std::cout << "\n--- 拷贝构造 ---\n";
    StringHolder b = a;  // 拷贝构造

    std::cout << "\n--- 移动构造 ---\n";
    StringHolder c = std::move(a);  // 移动构造(a 被掏空)

    std::cout << "\n--- 拷贝赋值 ---\n";
    StringHolder d{"World"};
    d = b;  // 拷贝赋值

    std::cout << "\n--- 移动赋值 ---\n";
    StringHolder e{"Temp"};
    e = std::move(c);  // 移动赋值(c 被掏空)

    std::cout << "\n--- 状态检查 ---\n";
    std::cout << "a: " << a.c_str() << " (已移动,应为 null)\n";
    std::cout << "b: " << b.c_str() << "\n";
    std::cout << "c: " << c.c_str() << " (已移动,应为 null)\n";
    std::cout << "d: " << d.c_str() << "\n";
    std::cout << "e: " << e.c_str() << "\n";

    std::cout << "\n--- 析构(逆序)---\n";
    // e, d, c, b, a 逆序析构
}

// ============================================================
// 第三部分:Rule of Zero 示例
// ============================================================

class ModernStringHolder {
    std::string data_;  // std::string 自带正确的拷贝/移动/析构
public:
    explicit ModernStringHolder(const char* s) : data_(s) {
        std::cout << "  构造: \"" << data_ << "\"\n";
    }
    ~ModernStringHolder() {
        std::cout << "  析构: \"" << data_ << "\"\n";
    }
    const char* c_str() const { return data_.c_str(); }
    // 不需要定义拷贝/移动!编译器自动生成正确的版本。
    // 这就是 Rule of Zero:什么都不写,编译器帮你做对。
};

void rule_of_zero_demo() {
    std::cout << "\n=== Rule of Zero 示例 ===\n";
    ModernStringHolder a{"Hello"};
    ModernStringHolder b = a;           // 自动生成的拷贝构造
    ModernStringHolder c = std::move(a); // 自动生成的移动构造
    std::cout << "a: \"" << a.c_str() << "\" (已移动)\n";
    std::cout << "b: \"" << b.c_str() << "\"\n";
    std::cout << "c: \"" << c.c_str() << "\"\n";
}

int main() {
    copy_vs_move_demo();
    rule_of_five_demo();
    rule_of_zero_demo();
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_2_1_value_semantics.cpp -o demo_2_1 && ./demo_2_1

RVO/NRVO(返回值优化)

RVO (Return Value Optimization)NRVO (Named Return Value Optimization) 是 C++ 中最重要的编译器优化之一。当函数返回一个局部对象时,编译器可以直接在调用者的内存中构造该对象,完全消除拷贝/移动

概念含义示例
RVO返回临时对象时,直接构造到调用者空间return T(args);
NRVO返回命名局部变量时,直接构造到调用者空间T result; ...; return result;
C++17 强制拷贝消除编译器必须省略返回值的拷贝/移动return T(args); 保证零拷贝

Java 对比:Java 中所有对象返回都是引用传递,不存在"拷贝返回值"的概念。Java 方法返回对象时,只是传递引用(4/8 字节的指针),无论对象多大。C++ 中如果不做 RVO/NRVO,返回大对象会触发深拷贝。

C++17 强制拷贝消除:当返回一个 prvalue(如 return T(args))时,C++17 保证不会调用任何拷贝/移动构造函数。这不仅是优化,而是语言保证。

// demo_rvo_nrvo.cpp
#include <iostream>
#include <string>

class Tracer {
    std::string name_;
public:
    explicit Tracer(std::string name) : name_(std::move(name)) {
        std::cout << "  构造: " << name_ << "\n";
    }
    Tracer(const Tracer& other) : name_(other.name_) {
        std::cout << "  拷贝构造: " << name_ << "\n";
    }
    Tracer(Tracer&& other) noexcept : name_(std::move(other.name_)) {
        std::cout << "  移动构造: " << name_ << "\n";
    }
    ~Tracer() {
        std::cout << "  析构: " << name_ << "\n";
    }
};

// RVO:返回临时对象(C++17 强制省略拷贝/移动)
Tracer make_tracer_rvo(std::string name) {
    return Tracer(name);  // C++17 保证:直接在调用者空间构造,零拷贝/零移动
}

// NRVO:返回命名局部变量(编译器通常优化,但不保证)
Tracer make_tracer_nrvo(std::string name) {
    Tracer result(name);  // 命名局部变量
    // ... 一些操作 ...
    return result;  // NRVO:编译器可能直接在调用者空间构造 result
    // 如果 NRVO 未生效,会尝试移动构造(需要移动构造函数)
}

int main() {
    std::cout << "=== RVO(强制拷贝消除)===\n";
    auto t1 = make_tracer_rvo("RVO");
    // 预期输出:只有"构造",没有拷贝或移动
    std::cout << "\n=== NRVO(命名返回值优化)===\n";
    auto t2 = make_tracer_nrvo("NRVO");
    // 预期输出(-O2):只有"构造",NRVO 生效
    // 预期输出(无优化):构造 + 移动构造

    std::cout << "\n=== Java 对比 ===\n";
    std::cout << "Java 中返回对象只是传递引用(4/8 字节),无拷贝概念\n";
    std::cout << "C++ 中 RVO/NRVO 让返回值语义和 Java 一样高效\n";
    return 0;
}

编译运行:

g++ -std=c++17 -Wall -Wextra -O2 demo_rvo_nrvo.cpp -o demo_rvo_nrvo && ./demo_rvo_nrvo

std::move 误用案例

std::move 是 C++ 中最容易被误用的特性之一。记住:std::move 本身不移动任何东西,它只是一个 static_cast<T&&>,将左值转换为右值引用。真正的"移动"由移动构造函数/移动赋值运算符执行。

误用 1:move 后使用对象
// 代码片段(无 main 函数)
#include <iostream>
#include <string>
#include <vector>

void move_then_use_mistake() {
    std::string data = "important information";

    std::string stolen = std::move(data);  // data 被移动,处于"有效但未指定"状态

    // 误用!data 的内容已经被移走
    std::cout << data << "\n";  // UB 或输出空字符串(取决于实现)
    std::cout << data.size() << "\n";  // 可能为 0

    // 正确做法:如果之后还需要使用 data,就不要 move 它
    // 如果只是想传递给函数,用 const 引用
}
误用 2:move 局部变量返回(无意义)
// 代码片段(无 main 函数)
std::vector<int> create_data_mistake() {
    std::vector<int> result = {1, 2, 3, 4, 5};
    return std::move(result);  // 无意义!
    // NRVO 或 C++17 强制拷贝消除已经保证零拷贝
    // 加 std::move 反而会阻止 NRVO(因为 result 变成了右值,不再是命名局部变量)
    // 编译器只能走移动构造路径,而不是直接在调用者空间构造
}

// 正确写法:
std::vector<int> create_data_correct() {
    std::vector<int> result = {1, 2, 3, 4, 5};
    return result;  // NRVO:直接在调用者空间构造,零拷贝零移动
}

规则:返回局部变量时不要 std::move。NRVO 或强制拷贝消除比你手动 move 更高效。只有返回函数参数或成员变量时才需要 std::move


2.2 RAII 与资源管理 [L2]

[!TIP] C++ 最核心惯用法

RAII (Resource Acquisition Is Initialization) 是 C++ 最重要的惯用法,没有之一。它不仅是内存管理的方案,更是所有资源(文件、锁、网络连接、数据库连接、GPU 缓冲区等)的统一管理模式。

Java 的 try-with-resources 只能管理实现了 AutoCloseable 的对象,且需要显式声明。C++ 的 RAII 是语言级别的保证——任何栈上对象离开作用域时,析构函数必定被调用,无论是因为正常返回、异常抛出、还是 break/continue

RAII 原理

RAII 的核心思想:将资源的生命周期绑定到对象的生命周期。

  1. 获取资源:在构造函数中获取资源
  2. 使用资源:在对象存活期间使用资源
  3. 释放资源:在析构函数中释放资源(自动、确定、异常安全)
// 代码片段(无 main 函数)
// Java 的 try-with-resources
try (FileInputStream fis = new FileInputStream("data.bin");
     BufferedInputStream bis = new BufferedInputStream(fis)) {
    // 使用资源
} // 自动关闭——但只对 AutoCloseable 有效

// C++ 的 RAII——对任何资源都有效,且不需要特殊语法
void process_file() {
    std::ifstream file{"data.bin"};  // 构造时打开文件
    std::lock_guard lock{mutex};     // 构造时加锁
    std::vector<int> data;           // 构造时分配内存

    // 使用资源...

    // 离开作用域时,file、lock、data 自动释放
    // 无论是因为 return、异常、还是其他控制流
}

std::unique_ptr(独占所有权)

std::unique_ptr 是 C++ 中最常用的智能指针,表示独占所有权——同一时刻只有一个 unique_ptr 拥有对象。

// 代码片段(无 main 函数)
#include <memory>
#include <iostream>

struct Widget {
    int value;
    explicit Widget(int v) : value(v) {
        std::cout << "  构造 Widget(" << v << ")\n";
    }
    ~Widget() {
        std::cout << "  析构 Widget(" << value << ")\n";
    }
};

void unique_ptr_demo() {
    // 创建
    auto w1 = std::make_unique<Widget>(42);  // 推荐:make_unique(C++14)
    // Widget w2{*w1};  // 编译错误!unique_ptr 不可拷贝

    // 移动——所有权转移
    auto w2 = std::move(w1);  // w1 变为 nullptr,w2 拥有对象
    std::cout << "w1: " << (w1 ? "有值" : "null") << "\n";  // null
    std::cout << "w2: " << w2->value << "\n";               // 42

    // 自定义删除器(类似 Java 的 Cleaner)
    auto file_deleter = [](FILE* f) {
        if (f) fclose(f);
    };
    std::unique_ptr<FILE, decltype(file_deleter)> file{fopen("test.txt", "w"), file_deleter};

    // 数组版本
    auto arr = std::make_unique<int[]>(10);
    arr[0] = 42;
}
特性std::unique_ptr<T>Java 独占引用
所有权独占(不可拷贝,可移动)Java 没有独占引用的概念
大小与裸指针相同(零开销)N/A
释放时机离开作用域或 reset()GC 决定
空值可以为空(nullptr可以为 null
数组unique_ptr<T[]>不适用

std::shared_ptr(共享所有权)

std::shared_ptr 通过引用计数实现共享所有权。多个 shared_ptr 可以指向同一个对象,最后一个 shared_ptr 销毁时释放对象。

// 代码片段(无 main 函数)
#include <memory>
#include <iostream>

struct Widget {
    int value;
    explicit Widget(int v) : value(v) {
        std::cout << "  构造 Widget(" << v << ")\n";
    }
    ~Widget() {
        std::cout << "  析构 Widget(" << value << ")\n";
    }
};

void shared_ptr_demo() {
    // 创建
    auto w1 = std::make_shared<Widget>(42);  // 推荐:make_shared(单次分配)
    std::cout << "use_count: " << w1.use_count() << "\n";  // 1

    // 拷贝——引用计数增加
    auto w2 = w1;
    std::cout << "use_count: " << w1.use_count() << "\n";  // 2

    {
        auto w3 = w1;
        std::cout << "use_count: " << w1.use_count() << "\n";  // 3
    }  // w3 离开作用域
    std::cout << "use_count: " << w1.use_count() << "\n";  // 2

    // reset——显式释放
    w2.reset();
    std::cout << "use_count: " << w1.use_count() << "\n";  // 1
}  // w1 离开作用域,对象被释放

Java 对比:Java 中所有对象引用本质上都是"共享引用"——GC 通过可达性分析判断对象是否可以回收。shared_ptr 的引用计数是确定性的(计数归零立即释放),但有线程安全开销(原子操作)。

std::weak_ptr(打破循环引用)

std::weak_ptr 是指向 shared_ptr 管理对象的弱引用,不增加引用计数。用于打破 shared_ptr 的循环引用。

// 代码片段(无 main 函数)
#include <memory>
#include <iostream>

struct Node;
struct Node {
    std::string name;
    std::shared_ptr<Node> next;        // 强引用
    std::weak_ptr<Node> prev;          // 弱引用——打破循环

    explicit Node(std::string n) : name(std::move(n)) {
        std::cout << "  构造 " << name << "\n";
    }
    ~Node() {
        std::cout << "  析构 " << name << "\n";
    }
};

void weak_ptr_demo() {
    std::cout << "--- 循环引用问题 ---\n";
    // 如果 next 和 prev 都是 shared_ptr,会内存泄漏!
    // 因为 A→B→A 形成循环,引用计数永远不为 0

    auto a = std::make_shared<Node>("A");
    auto b = std::make_shared<Node>("B");

    a->next = b;  // b.use_count = 2
    b->prev = a;  // a.use_count 不变!weak_ptr 不增加计数

    std::cout << "a.use_count: " << a.use_count() << "\n";  // 1
    std::cout << "b.use_count: " << b.use_count() << "\n";  // 2

    // 通过 weak_ptr 访问对象
    if (auto locked = b->prev.lock()) {  // lock() 返回 shared_ptr
        std::cout << "b->prev: " << locked->name << "\n";  // A
    } else {
        std::cout << "b->prev 已过期\n";
    }

    // a 和 b 正常析构——因为 prev 是 weak_ptr,不阻止释放
    std::cout << "--- 离开作用域 ---\n";
}

智能指针选择策略表

场景选择原因
独占所有权,单个所有者std::unique_ptr零开销,不可拷贝,语义清晰
共享所有权,多个所有者std::shared_ptr引用计数管理共享生命周期
观察 shared_ptr 对象但不拥有std::weak_ptr打破循环引用,缓存观察
工厂函数返回堆对象std::unique_ptr调用者获得独占所有权
类成员指向多态对象std::unique_ptr独占所有权,多态析构
两个对象互相引用一方用 shared_ptr,另一方用 weak_ptr打破循环引用
不需要堆分配不用智能指针栈对象更高效,优先使用栈

核心原则:默认使用 unique_ptr。只有确实需要共享所有权时才用 shared_ptr。能用栈对象就不要用堆对象。

std::optional (C++17) 对比 Java Optional / Kotlin T?

std::optional<T> 表示一个值可能存在也可能不存在,是 null 的类型安全替代。

// 代码片段(无 main 函数)
#include <iostream>
#include <optional>
#include <string>

// Java: Optional<String> findUser(long id)
// Kotlin: fun findUser(id: Long): String?
std::optional<std::string> find_user(int id) {
    if (id == 1) return "Alice";
    if (id == 2) return "Bob";
    return std::nullopt;  // 类似 Java Optional.empty() / Kotlin null
}

void optional_demo() {
    // Java 对比:Optional<String> result = findUser(1);
    auto result = find_user(1);

    // 检查是否有值
    if (result.has_value()) {
        std::cout << "找到: " << result.value() << "\n";
    }

    // 带默认值(类似 Java orElse / Kotlin ?: )
    auto name = find_user(999).value_or("Unknown");
    std::cout << "name: " << name << "\n";  // Unknown

    // 访问(如果无值会抛异常,类似 Java get())
    try {
        auto user = find_user(999).value();  // 抛 std::bad_optional_access
    } catch (const std::bad_optional_access& e) {
        std::cout << "异常: " << e.what() << "\n";
    }

    // C++23: 还可以用 monadic 操作
    // auto upper = find_user(1).transform([](auto s) { /* 转大写 */ return s; });
}
特性Java Optional<T>Kotlin T?C++ std::optional<T>
空值表示.empty()nullstd::nullopt
获取值.get() / .orElse()!! / ?:.value() / .value_or()
检查.isPresent()== null.has_value()
映射.map().let{}.transform() (C++23)
链式调用.flatMap()多个 ?..and_then() (C++23)
性能堆分配(对象引用)栈分配(内联)栈分配(内联)

std::expected (C++23) 对比 Kotlin Result

std::expected<T, E> 表示一个操作要么成功返回 T,要么失败返回 E。这是 C++23 引入的错误处理利器。

// 注意:需要 GCC 12+ 支持。GCC 11 可使用下方的自定义 expected 实现
#include <iostream>
#include <expected>
#include <string>
#include <string_view>

// 错误类型
enum class ParseError {
    EmptyInput,
    InvalidFormat,
    OutOfRange
};

// Java: throw new ParseException("...") 或 return Result<T>
// Kotlin: fun parseInt(s: String): Result<Int, ParseError>
std::expected<int, ParseError> parse_int(std::string_view s) {
    if (s.empty()) return std::unexpected(ParseError::EmptyInput);

    try {
        size_t pos{};
        int value = std::stoi(std::string{s}, &pos);
        if (pos != s.size()) return std::unexpected(ParseError::InvalidFormat);
        return value;
    } catch (const std::out_of_range&) {
        return std::unexpected(ParseError::OutOfRange);
    }
}

void expected_demo() {
    // 成功路径
    auto result1 = parse_int("42");
    if (result1.has_value()) {
        std::cout << "解析成功: " << result1.value() << "\n";
    }

    // 失败路径
    auto result2 = parse_int("");
    if (!result2.has_value()) {
        auto error = result2.error();
        std::cout << "解析失败: error code " << static_cast<int>(error) << "\n";
    }

    // C++23 monadic 操作:链式错误处理
    // 类似 Kotlin: parse("42").map { it * 2 }.getOrElse { -1 }
    auto doubled = parse_int("21")
        .transform([](int v) { return v * 2; })
        .value_or(-1);
    std::cout << "翻倍: " << doubled << "\n";  // 42

    auto failed = parse_int("abc")
        .transform([](int v) { return v * 2; })
        .value_or(-1);
    std::cout << "失败时默认值: " << failed << "\n";  // -1

    // error_or: 获取错误或默认错误
    auto err = parse_int("").error_or(ParseError::InvalidFormat);
    std::cout << "错误码: " << static_cast<int>(err) << "\n";
}

Demo:unique_ptr / shared_ptr / weak_ptr 使用,optional / expected 错误处理

// demo_2_2_raii.cpp
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <string_view>
#include <fstream>
#include <optional>
#include <variant>

// 简化版 unexpected 包装(GCC 13+ 可直接 #include <expected> 使用 std::unexpected)
template<typename E>
struct unexpected { E value; explicit unexpected(E v) : value(std::move(v)) {} };

template<typename T, typename E>
class expected {
    std::variant<T, E> var_;
public:
    expected(T val) : var_(std::move(val)) {}
    expected(unexpected<E> err) : var_(std::move(err.value)) {}
    bool has_value() const { return std::holds_alternative<T>(var_); }
    const T& value() const { return std::get<T>(var_); }
    const T* operator->() const { return &std::get<T>(var_); }
    const E& error() const { return std::get<E>(var_); }
    T value_or(T alt) const { return has_value() ? value() : std::move(alt); }
    template<typename F> auto transform(F f) const -> expected<decltype(f(value())), E> {
        if (has_value()) return f(value());
        return unexpected(error());
    }
    template<typename F> auto and_then(F f) const -> decltype(f(value())) {
        if (has_value()) return f(value());
        return unexpected(error());
    }
};

// ============================================================
// 第一部分:智能指针使用
// ============================================================

struct Node {
    std::string name;
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;

    explicit Node(std::string n) : name(std::move(n)) {
        std::cout << "    构造 " << name << "\n";
    }
    ~Node() {
        std::cout << "    析构 " << name << "\n";
    }
};

void smart_pointers_demo() {
    std::cout << "=== 智能指针演示 ===\n\n";

    // --- unique_ptr:独占所有权 ---
    std::cout << "--- unique_ptr ---\n";
    {
        auto ptr = std::make_unique<Node>("unique_node");
        std::cout << "    use: " << ptr->name << "\n";

        // 不可拷贝
        // auto copy = ptr;  // 编译错误!

        // 可以移动
        auto ptr2 = std::move(ptr);
        std::cout << "    ptr: " << (ptr ? "有值" : "null") << "\n";  // null
        std::cout << "    ptr2: " << ptr2->name << "\n";
    }
    std::cout << "    (unique_ptr 离开作用域,自动释放)\n\n";

    // --- shared_ptr:共享所有权 ---
    std::cout << "--- shared_ptr ---\n";
    {
        auto a = std::make_shared<Node>("A");
        std::cout << "    a.use_count = " << a.use_count() << "\n";  // 1

        auto b = a;  // 拷贝,引用计数 +1
        std::cout << "    拷贝后 a.use_count = " << a.use_count() << "\n";  // 2

        {
            auto c = a;  // 再拷贝
            std::cout << "    再拷贝 a.use_count = " << a.use_count() << "\n";  // 3
        }
        std::cout << "    c 离开后 a.use_count = " << a.use_count() << "\n";  // 2
    }
    std::cout << "    (所有 shared_ptr 离开作用域,对象释放)\n\n";

    // --- weak_ptr:打破循环引用 ---
    std::cout << "--- weak_ptr 打破循环引用 ---\n";
    {
        auto x = std::make_shared<Node>("X");
        auto y = std::make_shared<Node>("Y");

        x->next = y;  // y.use_count = 2
        y->prev = x;  // x.use_count 不变(weak_ptr)

        std::cout << "    x.use_count = " << x.use_count() << "\n";  // 1
        std::cout << "    y.use_count = " << y.use_count() << "\n";  // 2

        // 通过 weak_ptr 访问
        if (auto locked = y->prev.lock()) {
            std::cout << "    y->prev = " << locked->name << "\n";  // X
        }
    }
    std::cout << "    (循环引用被打破,X 和 Y 正常释放)\n\n";

    // --- 对比:循环引用导致泄漏 ---
    std::cout << "--- 循环引用导致泄漏(反面教材)---\n";
    // 如果 prev 也是 shared_ptr,x 和 y 永远不会被释放
    // 这就是为什么需要 weak_ptr
    std::cout << "    (上面的代码中 prev 是 weak_ptr,所以没有泄漏)\n";
}

// ============================================================
// 第二部分:optional 错误处理
// ============================================================

struct Config {
    int port;
    std::string host;
};

// 类似 Kotlin: fun loadConfig(path: String): Config?
std::optional<Config> load_config(std::string_view path) {
    std::ifstream file{std::string{path}};
    if (!file.is_open()) return std::nullopt;

    Config cfg;
    file >> cfg.host >> cfg.port;
    if (cfg.port <= 0 || cfg.port > 65535) return std::nullopt;
    return cfg;
}

void optional_demo() {
    std::cout << "=== std::optional 错误处理 ===\n\n";

    // value_or 提供默认值
    auto cfg = load_config("nonexistent.txt").value_or(Config{8080, "localhost"});
    std::cout << "默认配置: " << cfg.host << ":" << cfg.port << "\n";

    // has_value 检查
    auto result = load_config("nonexistent.txt");
    if (!result.has_value()) {
        std::cout << "配置文件不存在,使用默认值\n";
    }

    // C++23 transform / and_then(GCC 13+ 可用)
    // auto port = load_config("nonexistent.txt")
    //     .transform([](const Config& c) { return c.port; })
    //     .value_or(8080);
    // GCC 11 兼容写法:
    auto opt_result = load_config("nonexistent.txt");
    int port = opt_result.has_value() ? opt_result->port : 8080;
    std::cout << "端口: " << port << "\n";
}

// ============================================================
// 第三部分:expected 错误处理
// ============================================================

enum class AppError {
    FileNotFound,
    ParseError,
    ValidationError
};

// 类似 Kotlin: fun loadConfig(path: String): Result<Config, AppError>
expected<Config, AppError> load_config_v2(std::string_view path) {
    std::ifstream file{std::string{path}};
    if (!file.is_open()) return unexpected(AppError::FileNotFound);

    Config cfg;
    file >> cfg.host >> cfg.port;
    if (file.fail()) return unexpected(AppError::ParseError);
    if (cfg.port <= 0 || cfg.port > 65535) return unexpected(AppError::ValidationError);
    return cfg;
}

std::string error_message(AppError e) {
    switch (e) {
        case AppError::FileNotFound:    return "文件不存在";
        case AppError::ParseError:      return "解析错误";
        case AppError::ValidationError: return "验证失败";
    }
    return "未知错误";
}

void expected_demo() {
    std::cout << "\n=== std::expected 错误处理 ===\n\n";

    // 成功路径
    auto result = load_config_v2("nonexistent.txt");

    // 简单的错误处理方式
    if (result.has_value()) {
        std::cout << "  成功: " << result->host << ":" << result->port << "\n";
    } else {
        std::cout << "  失败: " << error_message(result.error()) << "\n";
    }

    // value_or
    auto cfg = load_config_v2("nonexistent.txt").value_or(Config{8080, "localhost"});
    std::cout << "  默认配置: " << cfg.host << ":" << cfg.port << "\n";

    // transform 链式处理
    auto host = load_config_v2("nonexistent.txt")
        .transform([](const Config& c) { return c.host; })
        .value_or("localhost");
    std::cout << "  host: " << host << "\n";

    // and_then 链式处理
    auto port_str = load_config_v2("nonexistent.txt")
        .and_then([](const Config& c) -> expected<std::string, AppError> {
            if (c.port == 80) return unexpected(AppError::ValidationError);
            return std::to_string(c.port);
        })
        .value_or("8080");
    std::cout << "  port: " << port_str << "\n";
}

int main() {
    smart_pointers_demo();
    optional_demo();
    expected_demo();
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_2_2_raii.cpp -o demo_2_2 && ./demo_2_2

2.3 内存管理深度 [L3]

new/delete 与 operator new/operator delete

C++ 的内存分配分为两个层次:

// 代码片段(无 main 函数)
#include <iostream>

class Widget {
    int data_[100];
public:
    Widget() { std::cout << "  Widget 构造\n"; }
    ~Widget() { std::cout << "  Widget 析构\n"; }
};

void memory_allocation_demo() {
    // 层次 1:表达式 new/delete(分配 + 构造 / 析构 + 释放)
    Widget* w = new Widget;    // 1. 调用 operator new 分配内存  2. 调用构造函数
    delete w;                  // 1. 调用析构函数  2. 调用 operator delete 释放内存

    // 层次 2:operator new/operator delete(仅分配/释放内存,不构造/析构)
    void* raw = operator new(sizeof(Widget));   // 仅分配内存,不调用构造函数
    new (raw) Widget;                           // 定位 new:在 raw 上构造对象
    static_cast<Widget*>(raw)->~Widget();       // 显式调用析构函数
    operator delete(raw);                       // 释放内存

    // 数组版本
    Widget* arr = new Widget[5];  // 分配 5 个 Widget
    delete[] arr;                  // 必须用 delete[]!delete 只释放第一个

    // nothrow 版本
    Widget* w2 = new (std::nothrow) Widget;  // 失败返回 nullptr 而非抛异常
    if (w2) delete w2;
}
操作JavaC++
分配 + 构造new Widget()new Widget
释放GC 自动回收delete ptr
数组分配new Widget[5]new Widget[5]
数组释放GC 自动回收delete[] ptr
分配失败OutOfMemoryErrorstd::bad_alloc(或 nothrow 返回 nullptr

Java 对比:Java 的 new 只做一件事——分配内存并构造对象。C++ 的 new 表达式做了两件事(分配 + 构造),delete 也做了两件事(析构 + 释放)。C++ 允许你将这两步分开执行,这是 Java 做不到的。

内存对齐 alignas/alignof

每个类型都有一个对齐要求 (alignment),即其地址必须是某个值的整数倍。正确的对齐对性能至关重要,错误的对齐会导致未定义行为。

// 代码片段(无 main 函数)
#include <iostream>
#include <cstddef>

// 默认对齐
struct Default {
    char c;     // 1 字节,对齐 1
    int i;      // 4 字节,对齐 4
    double d;   // 8 字节,对齐 8
};

// 自定义对齐
struct alignas(16) Aligned16 {
    int data[4];  // 强制 16 字节对齐(适合 SIMD)
};

struct alignas(64) CacheLine {
    long data[8];  // 强制 64 字节对齐(匹配缓存行大小)
};

void alignment_demo() {
    std::cout << "=== 内存对齐 ===\n\n";

    std::cout << "char 对齐: " << alignof(char) << "\n";       // 1
    std::cout << "int 对齐: " << alignof(int) << "\n";         // 4
    std::cout << "long 对齐: " << alignof(long) << "\n";       // 8
    std::cout << "double 对齐: " << alignof(double) << "\n";   // 8
    std::cout << "指针 对齐: " << alignof(int*) << "\n";       // 8

    std::cout << "\nDefault 结构体:\n";
    std::cout << "  sizeof: " << sizeof(Default) << " 字节\n";     // 16(不是 13!)
    std::cout << "  alignof: " << alignof(Default) << "\n";       // 8

    std::cout << "\nAligned16 结构体:\n";
    std::cout << "  sizeof: " << sizeof(Aligned16) << " 字节\n";
    std::cout << "  alignof: " << alignof(Aligned16) << "\n";     // 16

    std::cout << "\nCacheLine 结构体:\n";
    std::cout << "  sizeof: " << sizeof(CacheLine) << " 字节\n";
    std::cout << "  alignof: " << alignof(CacheLine) << "\n";     // 64

    // alignas 变量
    alignas(64) int simd_buffer[16];  // 64 字节对齐的数组
    std::cout << "\nsimd_buffer 地址: " << reinterpret_cast<uintptr_t>(simd_buffer) << "\n";
    std::cout << "是否 64 字节对齐: "
              << (reinterpret_cast<uintptr_t>(simd_buffer) % 64 == 0 ? "是" : "否") << "\n";
}

对象布局:虚表指针(vptr)、多重继承布局

理解 C++ 对象在内存中的布局,对性能优化和调试至关重要。

// 代码片段(无 main 函数)
#include <iostream>

// === 单继承布局 ===
class Base {
public:
    int base_data;
    virtual void foo() { std::cout << "Base::foo\n"; }
    virtual ~Base() = default;
    // 布局:[vptr(8)] [base_data(4)] [padding(4)] = 16 字节
};

class Derived : public Base {
public:
    int derived_data;
    void foo() override { std::cout << "Derived::foo\n"; }
    // 布局:[vptr(8)] [base_data(4)] [padding(4)] [derived_data(4)] [padding(4)] = 24 字节
};

// === 多重继承布局 ===
class Interface1 {
public:
    virtual void method1() = 0;
    virtual ~Interface1() = default;
    // 布局:[vptr1(8)] = 8 字节
};

class Interface2 {
public:
    virtual void method2() = 0;
    virtual ~Interface2() = default;
    // 布局:[vptr2(8)] = 8 字节
};

class MultiImpl : public Interface1, public Interface2 {
public:
    int data;
    void method1() override { std::cout << "method1\n"; }
    void method2() override { std::cout << "method2\n"; }
    // 布局:[vptr1(8)] [vptr2(8)] [data(4)] [padding(4)] = 24 字节
};

// === 虚继承布局 ===
class VirtualBase {
public:
    int vb_data;
    virtual ~VirtualBase() = default;
};

class Left : virtual public VirtualBase {
public:
    int left_data;
};

class Right : virtual public VirtualBase {
public:
    int right_data;
};

class Diamond : public Left, public Right {
public:
    int diamond_data;
    // 布局更复杂:包含 vptr、vbptr(虚基类指针)、各成员
};

void layout_demo() {
    std::cout << "=== 对象布局 ===\n\n";

    std::cout << "--- 单继承 ---\n";
    std::cout << "  sizeof(Base):    " << sizeof(Base) << " 字节\n";
    std::cout << "  sizeof(Derived): " << sizeof(Derived) << " 字节\n";
    std::cout << "  (Base 有 vptr + int,Derived 增加了 derived_data)\n";

    std::cout << "\n--- 多重继承 ---\n";
    std::cout << "  sizeof(Interface1): " << sizeof(Interface1) << " 字节\n";
    std::cout << "  sizeof(Interface2): " << sizeof(Interface2) << " 字节\n";
    std::cout << "  sizeof(MultiImpl):  " << sizeof(MultiImpl) << " 字节\n";
    std::cout << "  (MultiImpl 有两个 vptr + data)\n";

    std::cout << "\n--- 虚继承(菱形继承)---\n";
    std::cout << "  sizeof(VirtualBase): " << sizeof(VirtualBase) << " 字节\n";
    std::cout << "  sizeof(Left):        " << sizeof(Left) << " 字节\n";
    std::cout << "  sizeof(Right):       " << sizeof(Right) << " 字节\n";
    std::cout << "  sizeof(Diamond):     " << sizeof(Diamond) << " 字节\n";
    std::cout << "  (虚继承通过 vbptr 间接访问共享基类)\n";

    // 验证多态行为
    std::cout << "\n--- 多态验证 ---\n";
    Base* poly = new Derived{};
    poly->foo();  // 调用 Derived::foo(通过 vptr 动态分发)
    delete poly;

    // 多重继承指针转换
    MultiImpl* multi = new MultiImpl{};
    Interface1* i1 = multi;
    Interface2* i2 = multi;
    std::cout << "  multi:  " << static_cast<void*>(multi) << "\n";
    std::cout << "  i1:     " << static_cast<void*>(i1) << "\n";
    std::cout << "  i2:     " << static_cast<void*>(i2) << "\n";
    std::cout << "  (i1 和 multi 地址相同,i2 地址不同——指针被调整了!)\n";
    delete multi;
}

Java 对比:Java 对象的内存布局由 JVM 决定,每个对象都有一个对象头(mark word + class pointer)。Java 没有多重继承(只有单继承 + 接口实现),所以不存在 C++ 中的多重继承布局问题。Java 接口方法的调用通过 itable 或 vtable 实现,对程序员透明。

定位 new (placement new) [L4]

定位 new 允许在指定的内存地址上构造对象,常用于自定义内存分配器和嵌入式开发。

// 代码片段(无 main 函数)
#include <iostream>
#include <new>

class Widget {
    int value_;
public:
    explicit Widget(int v) : value_(v) {
        std::cout << "  构造 Widget(" << value_ << ") 在 " << this << "\n";
    }
    ~Widget() {
        std::cout << "  析构 Widget(" << value_ << ") 在 " << this << "\n";
    }
    int value() const { return value_; }
};

void placement_new_demo() {
    std::cout << "=== 定位 new ===\n\n";

    // 1. 分配原始内存(不调用构造函数)
    char buffer[sizeof(Widget) * 2];  // 栈上的内存池
    std::cout << "buffer 地址: " << static_cast<void*>(buffer) << "\n\n";

    // 2. 在指定位置构造对象
    Widget* w1 = new (buffer) Widget{42};
    Widget* w2 = new (buffer + sizeof(Widget)) Widget{99};

    std::cout << "\nw1->value() = " << w1->value() << "\n";
    std::cout << "w2->value() = " << w2->value() << "\n";

    // 3. 必须显式调用析构函数!
    w1->~Widget();
    w2->~Widget();

    // 4. 不需要释放内存(buffer 在栈上,自动释放)
    std::cout << "\n(buffer 在栈上,自动释放)\n";
}

std::pmr (C++17) 多态内存资源 [L4]

std::pmr 提供了多态内存资源抽象,允许运行时切换内存分配策略,而不改变使用代码。

// 代码片段(无 main 函数)
#include <iostream>
#include <memory_resource>
#include <vector>
#include <array>

void pmr_demo() {
    std::cout << "=== std::pmr 多态内存资源 ===\n\n";

    // 1. 栈上的缓冲区(避免堆分配)
    std::array<std::byte, 1024> buffer;
    std::pmr::monotonic_buffer_resource pool{buffer.data(), buffer.size()};

    // 2. 使用 pmr 容器(接口与标准容器相同)
    std::pmr::vector<int> vec{&pool};  // 从 pool 分配内存
    vec.push_back(1);
    vec.push_back(2);
    vec.push_back(3);

    std::cout << "vec: ";
    for (int v : vec) std::cout << v << " ";
    std::cout << "\n";

    // 3. 可以无缝切换分配策略
    // 例如:从栈缓冲区切换到堆分配
    std::pmr::vector<int> heap_vec;  // 默认使用 new/delete
    heap_vec.push_back(42);

    // 4. 同步缓冲区(线程安全)
    std::pmr::synchronized_pool_resource sync_pool;

    // Java 对比:
    // Java 没有等价物。最接近的是自定义 ClassLoader 或 ByteBuffer.allocateDirect()
    // 但 pmr 的灵活性和零抽象开销是 Java 无法比拟的
}

Demo:内存对齐展示,对象布局大小验证

// demo_2_3_memory.cpp
#include <iostream>
#include <cstddef>
#include <cstdint>
#include <new>
#include <array>
#include <memory_resource>

// ============================================================
// 第一部分:内存对齐
// ============================================================

struct PaddedStruct {
    char a;      // offset 0, size 1
                 // padding 3
    int b;       // offset 4, size 4
    char c;      // offset 8, size 1
                 // padding 7
    double d;    // offset 16, size 8
};  // total: 24 bytes (不是 14!)

struct alignas(32) SimdFriendly {
    float data[8];  // 32 字节,适合 AVX-256
};

struct Empty {};  // 空类——C++ 保证 sizeof >= 1

struct WithVptr {
    virtual void foo() {}
    int data;
};

// 紧凑排列(避免 padding)
#pragma pack(push, 1)
struct PackedStruct {
    char a;     // offset 0
    int b;      // offset 1(不对齐!访问可能较慢)
    char c;     // offset 5
    double d;   // offset 6(不对齐!x86 上可以工作,但很慢;ARM 上可能崩溃)
};  // total: 14 bytes
#pragma pack(pop)

void alignment_demo() {
    std::cout << "=== 内存对齐 ===\n\n";

    std::cout << "--- 基本类型对齐 ---\n";
    std::cout << "  alignof(char):    " << alignof(char) << "\n";
    std::cout << "  alignof(int):     " << alignof(int) << "\n";
    std::cout << "  alignof(double):  " << alignof(double) << "\n";
    std::cout << "  alignof(int64_t): " << alignof(int64_t) << "\n";
    std::cout << "  alignof(int*):    " << alignof(int*) << "\n";

    std::cout << "\n--- 结构体大小与对齐 ---\n";
    std::cout << "  PaddedStruct:\n";
    std::cout << "    sizeof:  " << sizeof(PaddedStruct) << " 字节\n";
    std::cout << "    alignof: " << alignof(PaddedStruct) << "\n";
    std::cout << "    布局: [char(1)][pad(3)][int(4)][char(1)][pad(7)][double(8)] = 24\n";

    std::cout << "  PackedStruct (#pragma pack 1):\n";
    std::cout << "    sizeof:  " << sizeof(PackedStruct) << " 字节\n";
    std::cout << "    alignof: " << alignof(PackedStruct) << "\n";
    std::cout << "    布局: [char(1)][int(4)][char(1)][double(8)] = 14\n";
    std::cout << "    警告:packed 会导致未对齐访问,影响性能甚至导致 UB\n";

    std::cout << "  SimdFriendly (alignas(32)):\n";
    std::cout << "    sizeof:  " << sizeof(SimdFriendly) << " 字节\n";
    std::cout << "    alignof: " << alignof(SimdFriendly) << "\n";

    std::cout << "  Empty:\n";
    std::cout << "    sizeof:  " << sizeof(Empty) << " 字节 (C++ 保证 >= 1)\n";

    std::cout << "  WithVptr:\n";
    std::cout << "    sizeof:  " << sizeof(WithVptr) << " 字节\n";
    std::cout << "    布局: [vptr(8)][int(4)][pad(4)] = 16\n";

    // 验证对齐
    alignas(64) int cache_aligned;
    auto addr = reinterpret_cast<uintptr_t>(&cache_aligned);
    std::cout << "\n--- 对齐验证 ---\n";
    std::cout << "  cache_aligned 地址: " << addr << "\n";
    std::cout << "  是否 64 字节对齐: " << (addr % 64 == 0 ? "是" : "否") << "\n";
}

// ============================================================
// 第二部分:对象布局验证
// ============================================================

class Base1 {
public:
    int a;
    virtual void f1() {}
    virtual ~Base1() = default;
};

class Base2 {
public:
    int b;
    virtual void f2() {}
    virtual ~Base2() = default;
};

class Derived : public Base1, public Base2 {
public:
    int c;
    void f1() override {}
    void f2() override {}
};

class SingleBase {
public:
    int x;
    virtual void v() {}
    virtual ~SingleBase() = default;
};

class SingleDerived : public SingleBase {
public:
    int y;
    void v() override {}
};

void layout_demo() {
    std::cout << "\n=== 对象布局验证 ===\n\n";

    std::cout << "--- 单继承 ---\n";
    std::cout << "  sizeof(SingleBase):    " << sizeof(SingleBase) << " 字节\n";
    std::cout << "  sizeof(SingleDerived): " << sizeof(SingleDerived) << " 字节\n";
    std::cout << "  差值: " << (sizeof(SingleDerived) - sizeof(SingleBase)) << " 字节 (int y + padding)\n";

    std::cout << "\n--- 多重继承 ---\n";
    std::cout << "  sizeof(Base1):   " << sizeof(Base1) << " 字节 [vptr + int]\n";
    std::cout << "  sizeof(Base2):   " << sizeof(Base2) << " 字节 [vptr + int]\n";
    std::cout << "  sizeof(Derived): " << sizeof(Derived) << " 字节 [vptr1 + a + vptr2 + b + c + pad]\n";

    // 多重继承的指针调整
    Derived d{};
    Base1* b1 = &d;
    Base2* b2 = &d;

    std::cout << "\n--- 多重继承指针调整 ---\n";
    std::cout << "  &d:  " << static_cast<void*>(&d) << "\n";
    std::cout << "  b1:  " << static_cast<void*>(b1) << " (偏移 0)\n";
    std::cout << "  b2:  " << static_cast<void*>(b2) << " (偏移 " << (reinterpret_cast<char*>(b2) - reinterpret_cast<char*>(&d)) << ")\n";
    std::cout << "  注意:b2 != &d,编译器自动调整了指针!\n";
}

// ============================================================
// 第三部分:pmr 演示
// ============================================================

void pmr_demo() {
    std::cout << "\n=== std::pmr 多态内存资源 ===\n\n";

    // 栈缓冲区——小对象不需要堆分配
    std::array<std::byte, 256> stack_buffer;
    std::pmr::monotonic_buffer_resource stack_pool{stack_buffer.data(), stack_buffer.size()};

    std::pmr::vector<int> vec{&stack_pool};
    for (int i = 0; i < 10; ++i) vec.push_back(i * i);

    std::cout << "  栈上 pmr vector: ";
    for (int v : vec) std::cout << v << " ";
    std::cout << "\n  容量: " << vec.capacity() << "\n";

    // 带上游的池分配器(溢出到堆)
    std::pmr::unsynchronized_pool_resource pool;
    std::pmr::vector<std::pmr::string> strings{&pool};
    strings.emplace_back("hello");
    strings.emplace_back("world");
    strings.emplace_back("pmr");

    std::cout << "\n  pmr string vector: ";
    for (const auto& s : strings) std::cout << s << " ";
    std::cout << "\n";
}

int main() {
    alignment_demo();
    layout_demo();
    pmr_demo();
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_2_3_memory.cpp -o demo_2_3 && ./demo_2_3

2.4 错误处理 [L2]

异常机制对比表

特性JavaC++
异常类型checked exception(编译期检查)+ unchecked统一异常(无 checked 概念)
throws 声明throws IOException(编译期强制)noexcept / throw()(运行期承诺)
catch 语法catch (IOException e)catch (const std::exception& e)
finallyfinally { ... }RAII(析构函数自动执行)
try-with-resourcestry (Resource r = ...) { }RAII(不需要特殊语法)
异常层次Throwable → Exception → IOExceptionstd::exception → std::runtime_error
性能异常创建有栈跟踪开销异常创建开销小(栈展开时收集信息)
使用惯例广泛使用 checked exception仅用于真正"异常"的情况

核心差异:Java 的 checked exception 强制调用者处理错误,但实践中被广泛诟病(导致大量 catch (Exception e) { log(e); })。C++ 没有 checked exception,依靠 RAII 保证异常安全,用 std::expected (C++23) 处理可预期的错误。

异常安全保证:Basic/Strong/Nothrow

C++ 定义了三个级别的异常安全保证:

// 代码片段(无 main 函数)
#include <iostream>
#include <vector>
#include <stdexcept>

class Stack {
    std::vector<int> data_;
public:
    // Basic Guarantee(基本保证):
    // 如果操作失败,对象处于有效状态,但不保证不变
    void push_basic(int value) {
        data_.push_back(value);  // 如果 push_back 抛异常(内存不足)
        // data_ 仍然有效,但可能没有包含新元素
    }

    // Strong Guarantee(强保证):
    // 如果操作失败,状态回滚到操作前(事务语义)
    void push_strong(int value) {
        data_.push_back(value);  // std::vector::push_back 提供强保证
        // 如果失败,data_ 和调用前完全一样
    }

    // Nothrow Guarantee(不抛保证):
    // 操作绝对不会抛异常
    void clear() noexcept {
        data_.clear();  // std::vector::clear 是 noexcept 的
    }

    size_t size() const noexcept { return data_.size(); }
    bool empty() const noexcept { return data_.empty(); }
};

// === 异常安全实践 ===
class Database {
    std::vector<std::string> records_;
public:
    // 强保证:copy-and-swap 惯用法
    void add_record(const std::string& record) {
        auto new_records = records_;           // 1. 拷贝当前状态
        new_records.push_back(record);         // 2. 在副本上修改(可能抛异常)
        std::swap(records_, new_records);      // 3. 成功后交换(noexcept)
    }  // 4. new_records 析构,释放旧数据

    // nothrow 移动
    Database(Database&& other) noexcept = default;
    Database& operator=(Database&& other) noexcept = default;
};
保证级别含义Java 对应示例
Nothrow操作绝不抛异常不会抛异常的方法swap()clear()、析构函数
Strong失败时状态回滚(事务语义)事务回滚std::vector::push_back
Basic失败时对象有效但不特定无直接对应大多数操作

Java 对比:Java 没有异常安全级别的概念。Java 的 finally 块保证了资源释放,但对象状态的一致性需要程序员自己保证。C++ 的 RAII 让异常安全更容易实现——只要析构函数是 noexcept 的(默认就是),资源就不会泄漏。

错误码 vs 异常的工业界争议

维度异常错误码 + std::expected
性能正常路径零开销,异常路径较慢始终有检查开销(但很小)
可读性正常路径清晰,错误处理分离错误处理和业务逻辑混合
强制性不处理就崩溃(默认)不处理就忽略(危险)
适用场景真正异常的情况(文件不存在、网络断开)可预期的失败(解析失败、查找未命中)
与 RAII 配合完美——析构函数自动清理也很好——但需要手动检查
工业界Google 禁止异常,大部分项目使用越来越流行(C++23 std::expected

现代 C++ 推荐策略

  1. 析构函数、移动操作、swap → 必须 noexcept
  2. 构造函数 → 可以抛异常(对象还没构造成功,无法返回错误码)
  3. 可预期的失败std::expected<T, E> (C++23)
  4. 真正的异常情况 → 异常
  5. 性能关键路径 → 错误码或 std::expected

std::expected (C++23) 值类型错误处理

std::expected 是 C++23 引入的值类型错误处理方案,融合了错误码和异常的优点。

// 注意:需要 GCC 12+ 支持。GCC 11 可使用下方的自定义 expected 实现
#include <iostream>
#include <expected>
#include <string>
#include <string_view>
#include <charconv>
#include <system_error>

// 错误类型
enum class HttpError {
    ConnectionFailed,
    Timeout,
    NotFound,
    ServerError
};

std::string http_error_message(HttpError e) {
    switch (e) {
        case HttpError::ConnectionFailed: return "连接失败";
        case HttpError::Timeout:          return "超时";
        case HttpError::NotFound:         return "未找到";
        case HttpError::ServerError:      return "服务器错误";
    }
    return "未知";
}

// 返回 expected 而非抛异常
std::expected<std::string, HttpError> http_get(std::string_view url) {
    if (url == "https://api.example.com/users/42") {
        return R"({"name": "Alice", "age": 30})";
    }
    if (url == "https://api.example.com/notfound") {
        return std::unexpected(HttpError::NotFound);
    }
    return std::unexpected(HttpError::ConnectionFailed);
}

// 链式处理
std::expected<int, HttpError> parse_age(std::string_view json) {
    // 简化:从 JSON 中提取 age
    auto pos = json.find("\"age\":");
    if (pos == std::string_view::npos) return std::unexpected(HttpError::ServerError);

    auto num_start = json.find_first_of("0123456789", pos);
    if (num_start == std::string_view::npos) return std::unexpected(HttpError::ServerError);

    int age = 0;
    auto [ptr, ec] = std::from_chars(json.data() + num_start, json.data() + json.size(), age);
    if (ec != std::errc{}) return std::unexpected(HttpError::ServerError);
    return age;
}

void expected_chain_demo() {
    std::cout << "=== std::expected 链式处理 ===\n\n";

    // 链式调用:类似 Kotlin 的 Result.flatMap { }
    auto result = http_get("https://api.example.com/users/42")
        .and_then([](const std::string& json) {
            return parse_age(json);
        })
        .transform([](int age) {
            return age * 2;
        });

    if (result.has_value()) {
        std::cout << "  年龄翻倍: " << result.value() << "\n";
    } else {
        std::cout << "  错误: " << http_error_message(result.error()) << "\n";
    }

    // 错误路径
    auto error_result = http_get("https://api.example.com/notfound")
        .and_then([](const std::string& json) {
            return parse_age(json);
        })
        .transform([](int age) {
            return age * 2;
        });

    if (!error_result.has_value()) {
        std::cout << "  错误: " << http_error_message(error_result.error()) << "\n";
    }
}

static_assert 编译期断言

static_assert编译期检查条件,不满足时产生编译错误。这是 C++ 强大的编译期检查能力。

// 代码片段(无 main 函数)
#include <type_traits>
#include <cstdint>

// 编译期断言:类型大小检查
static_assert(sizeof(int) >= 4, "int 至少需要 4 字节");
static_assert(sizeof(void*) == 8, "本代码要求 64 位系统");

// 模板约束
template<typename T>
class NumericOnly {
    static_assert(std::is_arithmetic_v<T>, "T 必须是算术类型");
    T value_;
public:
    explicit NumericOnly(T v) : value_(v) {}
    T get() const { return value_; }
};

// C++17: static_assert 单参数版本(消息可选)
static_assert(std::is_move_constructible_v<std::string>);

// C++20: concepts 提供更好的编译期约束
// template<std::integral T>  // 比 static_assert 更清晰
// void func(T value) { }

Java 对比:Java 没有编译期断言(assert 是运行时的)。Java 的泛型通过类型擦除实现,无法在编译期做类型大小等检查。C++ 的 static_assert + type_traits + concepts 提供了强大的编译期检查能力。

Demo:异常安全保证示例,expected 错误处理

// demo_2_4_error_handling.cpp
#include <iostream>
#include <string>
#include <vector>
#include <stdexcept>
#include <charconv>
#include <type_traits>
#include <variant>

// 简化版 unexpected 包装(GCC 13+ 可直接 #include <expected> 使用 std::unexpected)
template<typename E>
struct unexpected { E value; explicit unexpected(E v) : value(std::move(v)) {} };

template<typename T, typename E>
class expected {
    std::variant<T, E> var_;
public:
    expected(T val) : var_(std::move(val)) {}
    expected(unexpected<E> err) : var_(std::move(err.value)) {}
    bool has_value() const { return std::holds_alternative<T>(var_); }
    const T& value() const { return std::get<T>(var_); }
    const T& operator*() const { return value(); }
    const E& error() const { return std::get<E>(var_); }
    T value_or(T alt) const { return has_value() ? value() : std::move(alt); }
    template<typename F> auto transform(F f) const -> expected<decltype(f(value())), E> {
        if (has_value()) return f(value());
        return unexpected(error());
    }
    template<typename F> auto and_then(F f) const -> decltype(f(value())) {
        if (has_value()) return f(value());
        return unexpected(error());
    }
};

// ============================================================
// 第一部分:异常安全保证
// ============================================================

class TransactionLog {
    std::vector<std::string> entries_;
public:
    // Strong Guarantee: copy-and-swap
    void add_entry(std::string entry) {
        auto backup = entries_;          // 1. 拷贝当前状态
        backup.push_back(std::move(entry)); // 2. 在副本上修改
        entries_.swap(backup);           // 3. noexcept 交换
    }  // 4. backup 析构,释放旧数据

    // Nothrow Guarantee
    void clear() noexcept { entries_.clear(); }

    size_t size() const noexcept { return entries_.size(); }

    const std::string& operator[](size_t i) const {
        if (i >= entries_.size()) throw std::out_of_range{"索引越界"};
        return entries_[i];
    }
};

class ExceptionSafeStack {
    std::vector<int> data_;
public:
    // push_back 本身提供强保证
    void push(int value) { data_.push_back(value); }

    // pop 需要自己保证强安全
    expected<int, std::string> pop() {
        if (data_.empty()) {
            return unexpected(std::string{"栈为空"});
        }
        int top = data_.back();
        data_.pop_back();  // noexcept
        return top;
    }

    size_t size() const noexcept { return data_.size(); }
};

void exception_safety_demo() {
    std::cout << "=== 异常安全保证 ===\n\n";

    // --- Strong Guarantee ---
    std::cout << "--- 强保证(copy-and-swap)---\n";
    TransactionLog log;
    log.add_entry("entry1");
    log.add_entry("entry2");
    std::cout << "  条目数: " << log.size() << "\n";

    // 模拟异常安全的 push
    std::cout << "\n--- 异常安全栈 ---\n";
    ExceptionSafeStack stack;
    stack.push(10);
    stack.push(20);
    stack.push(30);

    auto result = stack.pop();
    std::cout << "  pop: " << (result.has_value() ? std::to_string(result.value()) : result.error()) << "\n";
    std::cout << "  size: " << stack.size() << "\n";

    auto empty = stack.pop();
    auto empty2 = stack.pop();
    auto empty3 = stack.pop();
    auto empty4 = stack.pop();
    std::cout << "  pop (空栈): " << (empty4.has_value() ? std::to_string(empty4.value()) : empty4.error()) << "\n";

    // --- noexcept 的重要性 ---
    std::cout << "\n--- noexcept 的重要性 ---\n";
    std::cout << "  std::vector<int>::push_back is noexcept: "
              << std::boolalpha << noexcept(std::declval<std::vector<int>&>().push_back(0)) << "\n";
    std::cout << "  std::vector<int>::swap is noexcept: "
              << noexcept(std::declval<std::vector<int>&>().swap(std::declval<std::vector<int>&>())) << "\n";
    std::cout << "  std::vector<int>::clear is noexcept: "
              << noexcept(std::declval<std::vector<int>&>().clear()) << "\n";
}

// ============================================================
// 第二部分:expected 错误处理
// ============================================================

enum class ParseError {
    Empty,
    NotANumber,
    OutOfRange
};

std::string parse_error_msg(ParseError e) {
    switch (e) {
        case ParseError::Empty:      return "输入为空";
        case ParseError::NotANumber: return "不是数字";
        case ParseError::OutOfRange: return "超出范围";
    }
    return "未知";
}

expected<int, ParseError> safe_parse_int(std::string_view s) {
    if (s.empty()) return unexpected(ParseError::Empty);

    int value = 0;
    auto [ptr, ec] = std::from_chars(s.data(), s.data() + s.size(), value);

    if (ec == std::errc::invalid_argument) return unexpected(ParseError::NotANumber);
    if (ec == std::errc::result_out_of_range) return unexpected(ParseError::OutOfRange);
    if (ptr != s.data() + s.size()) return unexpected(ParseError::NotANumber);

    return value;
}

expected<int, ParseError> safe_divide(int a, int b) {
    if (b == 0) return unexpected(ParseError::OutOfRange);
    return a / b;
}

void expected_demo() {
    std::cout << "\n=== std::expected 错误处理 ===\n\n";

    // 基本使用
    auto r1 = safe_parse_int("42");
    std::cout << "parse(\"42\"): " << (r1.has_value() ? std::to_string(r1.value()) : parse_error_msg(r1.error())) << "\n";

    auto r2 = safe_parse_int("abc");
    std::cout << "parse(\"abc\"): " << (r2.has_value() ? std::to_string(r2.value()) : parse_error_msg(r2.error())) << "\n";

    auto r3 = safe_parse_int("");
    std::cout << "parse(\"\"): " << (r3.has_value() ? std::to_string(r3.value()) : parse_error_msg(r3.error())) << "\n";

    // value_or
    auto r4 = safe_parse_int("not_a_number").value_or(0);
    std::cout << "\nvalue_or(0): " << r4 << "\n";

    // 链式处理
    std::cout << "\n--- 链式处理 ---\n";

    // parse → transform → value_or
    auto doubled = safe_parse_int("21")
        .transform([](int v) { return v * 2; })
        .value_or(-1);
    std::cout << "  parse(\"21\") * 2 = " << doubled << "\n";

    // parse → and_then → transform
    auto result = safe_parse_int("10")
        .and_then([](int v) { return safe_divide(100, v); })
        .transform([](int v) { return v + 1; })
        .value_or(-1);
    std::cout << "  100 / parse(\"10\") + 1 = " << result << "\n";

    // 错误传播
    auto error_chain = safe_parse_int("abc")
        .and_then([](int v) { return safe_divide(100, v); })
        .transform([](int v) { return v + 1; })
        .value_or(-1);
    std::cout << "  错误传播: " << error_chain << " (默认值)\n";

    // 除以零
    auto div_zero = safe_parse_int("10")
        .and_then([](int v) { return safe_divide(v, 0); })
        .value_or(-1);
    std::cout << "  除以零: " << div_zero << " (默认值)\n";
}

// ============================================================
// 第三部分:static_assert 编译期断言
// ============================================================

// 编译期检查
static_assert(sizeof(void*) == 8, "本代码要求 64 位系统");
static_assert(sizeof(int) >= 4, "int 至少需要 4 字节");
static_assert(std::is_move_constructible_v<std::string>, "string 必须可移动");

// 模板中的 static_assert
template<typename T>
class SafeInt {
    static_assert(std::is_integral_v<T>, "SafeInt 只支持整数类型");
    T value_;
public:
    explicit SafeInt(T v) : value_(v) {}
    T get() const { return value_; }
};

void static_assert_demo() {
    std::cout << "\n=== static_assert 编译期断言 ===\n\n";
    std::cout << "  sizeof(void*) = " << sizeof(void*) << " (已通过编译期检查)\n";
    std::cout << "  sizeof(int) = " << sizeof(int) << " (已通过编译期检查)\n";

    SafeInt<int> si{42};
    std::cout << "  SafeInt<int>: " << si.get() << "\n";

    // SafeInt<std::string> ss{"hello"};  // 编译错误!static_assert 失败
    std::cout << "  (SafeInt<std::string> 会在编译期报错)\n";
}

int main() {
    exception_safety_demo();
    expected_demo();
    static_assert_demo();
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_2_4_error_handling.cpp -o demo_2_4 && ./demo_2_4

2.5 引用与指针 [L2]

指针基础:T* / const T* / T* const

C++ 的指针声明语法需要仔细理解 const 的位置:

// 代码片段(无 main 函数)
int x = 42;

// T* —— 指向可变 int 的可变指针
int* p1 = &x;
*p1 = 10;   // OK:修改指向的值
p1 = nullptr; // OK:修改指针本身

// const T*(或 T const*)—— 指向常量 int 的可变指针
const int* p2 = &x;
// *p2 = 10;  // 错误:不能通过 p2 修改值
p2 = nullptr; // OK:可以修改指针本身

// T* const —— 指向可变 int 的常量指针
int* const p3 = &x;
*p3 = 10;     // OK:可以修改值
// p3 = nullptr; // 错误:不能修改指针本身

// const T* const —— 指向常量 int 的常量指针
const int* const p4 = &x;
// *p4 = 10;   // 错误
// p4 = nullptr; // 错误

记忆技巧:从右往左读:

  • const int* p → p 是指针,指向 const int
  • int* const p → p 是 const 指针,指向 int
  • const int* const p → p 是 const 指针,指向 const int

Java 对比:Java 只有 final 修饰符,且 final 只能修饰变量引用(不能重新赋值),不能修饰指向的对象(对象始终可变)。Java 没有 const 的概念。Kotlin 有 val(只读引用)和 const val(编译期常量),但同样不能让引用指向的对象不可变。

引用 vs 指针对比表

特性引用 (T&)指针 (T*)
声明int& ref = x;int* ptr = &x;
必须初始化是(声明时必须绑定)否(可以声明为 nullptr
可以为空不可以(永远绑定到有效对象)可以(nullptr
重新绑定不可以(一旦绑定不能更改)可以(ptr = &other;
语法和普通变量一样(ref.x需要解引用(ptr->x*ptr
sizeof等于被引用类型的大小指针大小(8 字节)
数组不存在引用数组可以有指针数组
作为参数推荐用于必须非空的参数用于可选参数或需要重新绑定
多态可以(基类引用绑定派生类)可以(基类指针指向派生类)
Java 对应类似 Java 的变量引用类似 Java 的引用(但 Java 没有 nullptr 的概念——有 null
// 代码片段(无 main 函数)
#include <iostream>

void by_value(int x)     { x = 100; }  // 修改副本
void by_ref(int& x)      { x = 100; }  // 修改原对象
void by_ptr(int* x)      { if (x) *x = 100; }  // 修改原对象(可能为空)
void by_const_ref(const int& x) { /* x = 100; */ }  // 只读,不能修改

int main() {
    int a = 42;

    by_value(a);   std::cout << a << "\n";  // 42(未改变)
    by_ref(a);     std::cout << a << "\n";  // 100(已改变)
    by_ptr(&a);    std::cout << a << "\n";  // 100(已改变)

    int* null_ptr = nullptr;
    by_ptr(null_ptr);  // 安全:函数内部检查了空指针

    // by_ref 的参数不能为空——这是引用的优势
    // by_ref(*null_ptr);  // UB!解引用空指针

    // by_const_ref 可以接受临时值
    by_const_ref(42);  // OK:绑定到临时值
    // by_ref(42);       // 错误:非 const 引用不能绑定到右值
}

悬挂引用/指针的危险

悬挂引用/指针是指向已销毁对象的引用/指针。这是 C++ 中最常见的 UB 来源之一。

// 代码片段(无 main 函数)
#include <iostream>

// === 悬挂指针 ===
int* dangling_pointer() {
    int local = 42;
    return &local;  // 警告:返回局部变量地址
}  // local 被销毁,返回的指针是悬挂的

// === 悬挂引用 ===
int& dangling_reference() {
    int local = 42;
    return local;  // 警告:返回局部变量引用
}  // local 被销毁,返回的引用是悬挂的

// === 容器导致的悬挂 ===
int* container_dangling() {
    std::vector<int> v = {1, 2, 3};
    int* ptr = &v[0];
    v.push_back(4);  // 可能导致重新分配!ptr 变成悬挂指针
    return ptr;      // UB!
}

// === 安全做法 ===
int* safe_pointer() {
    auto* p = new int{42};  // 堆分配
    return p;  // 安全(但调用者必须 delete)
    // 更好的做法:返回 unique_ptr
}

std::unique_ptr<int> safe_smart_pointer() {
    return std::make_unique<int>(42);  // 所有权明确转移
}

// === Java 对比 ===
// Java 中不可能出现悬挂引用——GC 保证对象在使用期间存活
// C++ 中,你必须自己保证引用/指针的生命周期不超过被引用对象

防护规则

  1. 优先使用值语义(拷贝/移动),减少引用/指针的使用
  2. 优先使用引用而非指针(引用不能为空,语义更清晰)
  3. 使用智能指针管理堆对象的生命周期
  4. 不要返回局部变量的引用/指针
  5. 注意容器操作(push_backinsert 等)可能导致迭代器/指针失效

智能指针选择决策树

需要管理对象的生命周期吗?
├── 否 → 使用值语义(栈对象)
│   └── 对象太大? → 使用值语义 + 移动语义
│
└── 是 → 对象在堆上吗?
    ├── 否 → 栈对象 + 引用/指针(注意生命周期)
    │
    └── 是 → 谁拥有这个对象?
        ├── 单一所有者 → std::unique_ptr
        │   ├── 需要自定义删除器? → unique_ptr<T, Deleter>
        │   └── 数组? → unique_ptr<T[]>
        │
        ├── 多个所有者 → std::shared_ptr
        │   ├── 有循环引用? → 其中一方用 std::weak_ptr
        │   └── 性能敏感? → 重新考虑是否真的需要共享所有权
        │
        └── 不拥有(只是观察) → 原始指针 T* 或引用 T&
            ├── 可能为空? → T*
            └── 一定非空? → T&

Structured Bindings 生命周期陷阱 (C++17)

C++17 的 structured bindings (auto [x, y] = ...) 看起来像 Python/Kotlin 的解构声明,但语义完全不同,容易踩坑。

auto [x, y] 是拷贝
// 代码片段(无 main 函数)
#include <utility>

std::pair<std::string, int> get_user() {
    return {"Alice", 30};
}

void structured_binding_copy_demo() {
    auto [name, age] = get_user();
    // name 是 get_user() 返回值的拷贝!
    // 等价于:auto __tmp = get_user();  // __tmp 是 pair<string, int>
    //           string& name = __tmp.first;
    //           int& age = __tmp.second;
    // name 和 age 是匿名变量 __tmp 的成员的引用
    // 修改 name 不会影响原始数据
    name = "Bob";  // 只修改了 __tmp.first
}
auto& [x, y] 绑定到临时对象时悬挂
// 代码片段(无 main 函数)
#include <utility>

auto& [name, age] = std::pair<std::string, int>{"Alice", 30};
// 危险!std::pair{"Alice", 30} 是临时对象(右值)
// auto& 绑定到这个临时对象,临时对象在语句结束后销毁
// name 和 age 立即成为悬挂引用!
// 使用 name 或 age 是 UB

// 正确写法 1:使用 auto(拷贝)
auto [name1, age1] = std::pair<std::string, int>{"Alice", 30};

// 正确写法 2:使用 const auto&(延长临时对象生命周期)
const auto& [name2, age2] = std::pair<std::string, int>{"Alice", 30};

// 正确写法 3:绑定到已存在的左值
auto user = std::pair<std::string, int>{"Alice", 30};
auto& [name3, age3] = user;  // OK:user 是左值,生命周期足够长

Kotlin/Java 对比:Kotlin 的 val (name, age) = getPair() 是解构声明,nameage 直接绑定到 Pair 的组件。C++ 的 structured bindings 更复杂——auto [x, y] 实际上创建了一个匿名变量,xy 是该匿名变量的引用。理解这一点对避免悬挂引用至关重要。

Demo:指针与引用的使用场景,const 正确性

// demo_2_5_pointers_refs.cpp
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <chrono>

// ============================================================
// 第一部分:const 正确性
// ============================================================

struct Person {
    std::string name;
    int age;

    // const 成员函数:不修改对象状态
    // 类似 Java 的 final 方法(但语义不同)
    std::string greet() const {
        return "Hello, I'm " + name + ", age " + std::to_string(age);
    }

    // 非 const 成员函数:修改对象状态
    void have_birthday() {
        ++age;
    }
};

// 函数参数的 const 正确性
void print_name(const std::string& name) {  // const 引用:不拷贝,不修改
    std::cout << "  Name: " << name << "\n";
    // name += "!";  // 编译错误!const 引用不能修改
}

void print_name_ptr(const std::string* name) {  // const 指针:不修改指向的值
    if (name) std::cout << "  Name: " << *name << "\n";
    // *name += "!";  // 编译错误!
}

void update_name(std::string& name) {  // 非 const 引用:可以修改
    name += " (updated)";
}

// 返回值的 const 正确性
const std::string& get_reference(const std::string& s) {
    return s;  // 返回 const 引用:调用者不能修改
}
// 注意:不要返回局部变量的引用!

void const_correctness_demo() {
    std::cout << "=== const 正确性 ===\n\n";

    // const 变量
    const int max_size = 100;
    // max_size = 200;  // 编译错误!

    // const 指针
    int x = 42, y = 99;
    const int* p1 = &x;     // 指向 const int 的指针
    // *p1 = 10;            // 错误:不能修改值
    p1 = &y;                // OK:可以修改指针

    int* const p2 = &x;     // const 指针,指向 int
    *p2 = 10;               // OK:可以修改值
    // p2 = &y;             // 错误:不能修改指针

    const int* const p3 = &x; // const 指针,指向 const int
    // *p3 = 10;            // 错误
    // p3 = &y;             // 错误

    std::cout << "  x = " << x << ", y = " << y << "\n";

    // const 成员函数
    const Person alice{"Alice", 30};
    std::cout << "  " << alice.greet() << "\n";
    // alice.have_birthday();  // 编译错误!const 对象不能调用非 const 方法

    Person bob{"Bob", 25};
    bob.have_birthday();  // OK
    std::cout << "  " << bob.greet() << "\n";

    // const 引用绑定到临时值
    const std::string& ref = std::string{"temporary"};
    std::cout << "  const ref to temporary: " << ref << "\n";
}

// ============================================================
// 第二部分:引用 vs 指针使用场景
// ============================================================

// 场景 1:函数参数——必须非空 → 引用
void process_must_exist(Person& person) {
    person.have_birthday();
}

// 场景 2:函数参数——可能为空 → 指针
void process_may_be_null(Person* person) {
    if (!person) {
        std::cout << "  person 为空,跳过\n";
        return;
    }
    person->have_birthday();
}

// 场景 3:只读参数 → const 引用
void read_only(const Person& person) {
    std::cout << "  " << person.greet() << "\n";
}

// 场景 4:需要重新绑定 → 指针
void rebind_example() {
    int a = 1, b = 2;
    int* ptr = &a;
    ptr = &b;  // 重新绑定——引用做不到
    std::cout << "  ptr 指向: " << *ptr << "\n";
}

// 场景 5:多态——基类引用/指针
struct Shape {
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

struct Circle : Shape {
    double radius;
    explicit Circle(double r) : radius(r) {}
    double area() const override { return 3.14159265 * radius * radius; }
};

struct Rectangle : Shape {
    double width, height;
    Rectangle(double w, double h) : width(w), height(h) {}
    double area() const override { return width * height; }
};

void print_area(const Shape& shape) {  // 基类引用——多态
    std::cout << "  面积: " << shape.area() << "\n";
}

void pointer_ref_demo() {
    std::cout << "\n=== 引用 vs 指针使用场景 ===\n\n";

    Person alice{"Alice", 30};

    // 引用:必须非空
    std::cout << "--- 引用(必须非空)---\n";
    process_must_exist(alice);
    std::cout << "  " << alice.greet() << "\n";

    // 指针:可以为空
    std::cout << "\n--- 指针(可以为空)---\n";
    Person* maybe_null = nullptr;
    process_may_be_null(maybe_null);
    process_may_be_null(&alice);

    // const 引用:只读
    std::cout << "\n--- const 引用(只读)---\n";
    read_only(alice);

    // 多态
    std::cout << "\n--- 多态(基类引用)---\n";
    Circle c{5.0};
    Rectangle r{3.0, 4.0};
    print_area(c);  // 动态分发到 Circle::area
    print_area(r);  // 动态分发到 Rectangle::area

    // 智能指针 + 多态
    std::cout << "\n--- 智能指针 + 多态 ---\n";
    std::vector<std::unique_ptr<Shape>> shapes;
    shapes.push_back(std::make_unique<Circle>(5.0));
    shapes.push_back(std::make_unique<Rectangle>(3.0, 4.0));

    for (const auto& shape : shapes) {
        std::cout << "  面积: " << shape->area() << "\n";
    }
}

// ============================================================
// 第三部分:生命周期与悬挂引用
// ============================================================

void lifetime_demo() {
    std::cout << "\n=== 生命周期与安全 ===\n\n";

    // 安全:引用的生命周期不超过被引用对象
    {
        int x = 42;
        int& ref = x;  // OK:ref 和 x 同生命周期
        std::cout << "  ref = " << ref << "\n";
    }

    // 安全:返回值绑定到 const 引用(临时值生命周期延长)
    const std::string& name = Person{"Charlie", 28}.greet();
    std::cout << "  " << name << "\n";  // OK:临时值生命周期延长到 name 的作用域

    // 危险(已注释):返回局部变量引用
    // int& bad_ref() { int x = 42; return x; }  // 悬挂引用!
    // int& ref = bad_ref();  // UB:ref 指向已销毁的 x

    // 安全:使用智能指针
    auto safe = std::make_unique<Person>("Diana", 35);
    std::cout << "  " << safe->greet() << "\n";
    // safe 离开作用域时自动释放——不会悬挂
}

int main() {
    const_correctness_demo();
    pointer_ref_demo();
    lifetime_demo();
    return 0;
}

编译运行:

g++ -std=c++23 -Wall -Wextra -O2 demo_2_5_pointers_refs.cpp -o demo_2_5 && ./demo_2_5

本节总结

概念Java/KotlinC++核心认知
值语义引用语义(默认)值语义(默认)C++ 赋值是拷贝,不是引用共享
移动语义不存在std::move、右值引用零拷贝转移资源,C++ 性能优势的核心
RAIItry-with-resources析构函数自动调用C++ 最核心惯用法,管理所有资源
智能指针无(GC 管理)unique_ptr/shared_ptr/weak_ptr默认 unique_ptr,避免 shared_ptr 滥用
optionalOptional<T> / T?std::optional<T>类型安全的"可能为空"
expectedResult<T> (Kotlin)std::expected<T,E> (C++23)值类型错误处理,替代异常
异常安全finally 块RAII + noexcept析构函数必须是 noexcept
constfinal(引用不可变)const(值不可变 + 引用不可变)C++ 的 const 更强大,覆盖值和引用
引用 vs 指针只有引用(可 null)引用(非空)+ 指针(可空)优先引用,需要空值或多态时用指针