第二部分:语言核心
[!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/Kotlin | C++ |
|---|---|---|
| 默认语义 | 引用语义(变量是引用) | 值语义(变量是对象本身) |
| 赋值 | b = a → 两个引用指向同一对象 | b = a → 创建 a 的完整副本 |
| 函数传参 | 传递引用(对象地址) | 传递副本(值传递) |
| 函数返回 | 返回引用 | 返回副本(RVO/NRVO 优化为零拷贝) |
| null | 引用可以为 null | 值类型不存在 null(std::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:如果你定义了以下五个特殊成员函数中的任何一个,通常需要全部定义:
- 析构函数 (
~T()) - 拷贝构造函数 (
T(const T&)) - 拷贝赋值运算符 (
T& operator=(const T&)) - 移动构造函数 (
T(T&&)) - 移动赋值运算符 (
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 的核心思想:将资源的生命周期绑定到对象的生命周期。
- 获取资源:在构造函数中获取资源
- 使用资源:在对象存活期间使用资源
- 释放资源:在析构函数中释放资源(自动、确定、异常安全)
// 代码片段(无 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() | null | std::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;
}
| 操作 | Java | C++ |
|---|---|---|
| 分配 + 构造 | new Widget() | new Widget |
| 释放 | GC 自动回收 | delete ptr |
| 数组分配 | new Widget[5] | new Widget[5] |
| 数组释放 | GC 自动回收 | delete[] ptr |
| 分配失败 | OutOfMemoryError | 抛 std::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]
异常机制对比表
| 特性 | Java | C++ |
|---|---|---|
| 异常类型 | checked exception(编译期检查)+ unchecked | 统一异常(无 checked 概念) |
| throws 声明 | throws IOException(编译期强制) | noexcept / throw()(运行期承诺) |
| catch 语法 | catch (IOException e) | catch (const std::exception& e) |
| finally | finally { ... } | RAII(析构函数自动执行) |
| try-with-resources | try (Resource r = ...) { } | RAII(不需要特殊语法) |
| 异常层次 | Throwable → Exception → IOException | std::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++ 推荐策略:
- 析构函数、移动操作、swap → 必须
noexcept - 构造函数 → 可以抛异常(对象还没构造成功,无法返回错误码)
- 可预期的失败 →
std::expected<T, E>(C++23) - 真正的异常情况 → 异常
- 性能关键路径 → 错误码或
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 intint* const p→ p 是 const 指针,指向 intconst 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++ 中,你必须自己保证引用/指针的生命周期不超过被引用对象
防护规则:
- 优先使用值语义(拷贝/移动),减少引用/指针的使用
- 优先使用引用而非指针(引用不能为空,语义更清晰)
- 使用智能指针管理堆对象的生命周期
- 不要返回局部变量的引用/指针
- 注意容器操作(
push_back、insert等)可能导致迭代器/指针失效
智能指针选择决策树
需要管理对象的生命周期吗?
├── 否 → 使用值语义(栈对象)
│ └── 对象太大? → 使用值语义 + 移动语义
│
└── 是 → 对象在堆上吗?
├── 否 → 栈对象 + 引用/指针(注意生命周期)
│
└── 是 → 谁拥有这个对象?
├── 单一所有者 → 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()是解构声明,name和age直接绑定到 Pair 的组件。C++ 的 structured bindings 更复杂——auto [x, y]实际上创建了一个匿名变量,x和y是该匿名变量的引用。理解这一点对避免悬挂引用至关重要。
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/Kotlin | C++ | 核心认知 |
|---|---|---|---|
| 值语义 | 引用语义(默认) | 值语义(默认) | C++ 赋值是拷贝,不是引用共享 |
| 移动语义 | 不存在 | std::move、右值引用 | 零拷贝转移资源,C++ 性能优势的核心 |
| RAII | try-with-resources | 析构函数自动调用 | C++ 最核心惯用法,管理所有资源 |
| 智能指针 | 无(GC 管理) | unique_ptr/shared_ptr/weak_ptr | 默认 unique_ptr,避免 shared_ptr 滥用 |
| optional | Optional<T> / T? | std::optional<T> | 类型安全的"可能为空" |
| expected | Result<T> (Kotlin) | std::expected<T,E> (C++23) | 值类型错误处理,替代异常 |
| 异常安全 | finally 块 | RAII + noexcept | 析构函数必须是 noexcept |
| const | final(引用不可变) | const(值不可变 + 引用不可变) | C++ 的 const 更强大,覆盖值和引用 |
| 引用 vs 指针 | 只有引用(可 null) | 引用(非空)+ 指针(可空) | 优先引用,需要空值或多态时用指针 |