C++ STL高级
1 variant
1.1 union
union 的所有成员 共享同一块内存的起始地址,不存在独立的内存空间。具体来说:
- 所有成员的内存起始地址完全相同
- union 的总大小 = 最大成员的大小(内存对齐)
- 对任意一个成员赋值,会覆盖其他成员的内存数据(因为共用一块空间)
1.1.1 经典用法示例
union Matrix
{
struct
{
double a11, a12, a13, a21, a22, a23, a31, a32, a33;
};
double elem[3][3];
// 结构体与数组共用内存,故既可按结构体成员访问,也可按数组索引
};
int main()
{
Matrix matrix{};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
matrix.elem[i][j] = i*j;
}
}
cout << &matrix.elem << endl;
cout << &matrix.a11 << endl; // 地址相同
cout << matrix.a22 << endl; // elem[1][1]
}
共享内存可以带来便捷,以下这个例子对共享内存的应用更为犀利
enum FIGURE_TYPE { LINE, RECTANGLE, ELLIPSE };
struct Line
{
FIGURE_TYPE t;
int x1, y1, x2, y2;
};
struct Rectangle
{
FIGURE_TYPE t;
int lef, top, rig, bot;
};
struct Ellipse
{
FIGURE_TYPE t;
int x, y, r;
};
union FIGURE
{
FIGURE_TYPE t;
Line line;
Rectangle rect;
Ellipse ellipse;
};
void input(FIGURE fig[], const int size)
{
int t;
for (int k = 0; k < size; k++)
{
cin >> t;
switch (t)
{
case LINE:
fig[k].t = LINE;
cin >> fig[k].line.x1 >> fig[k].line.y1
>> fig[k].line.x2 >> fig[k].line.y2;
break;
case RECTANGLE:; // 略
case ELLIPSE:; // 略
}
}
}
void draw(const FIGURE& figure)
{
switch (figure.t)
{
case LINE: cout << figure.line.x1 << endl; break;// 示意性,具体略
case RECTANGLE: cout << figure.rect.lef << endl; break;
case ELLIPSE: cout << figure.ellipse.x << endl; break;
}
}
int main()
{
FIGURE fig[100];
input(fig, 100);
for (const auto & f : fig) draw(f);
}
为什么各个结构体与联合体中第一个元素均为
FIGURE_TYPE t?
- 初始化时,先给联合体赋类型,再 “实例化” 为某个结构体的时候,因为这个类型变量的内存地址与结构体中类型变量的地址相同,故并未被 “覆盖” 而是继续保存了下来,并且后续可以直接调用
figure.t(而非结构体的类型变量,如figure.Line.t)来获取类型
多态的味道:“一个接口,多种形态” —— 消除调用者对具体类型的依赖,实现代码的通用性
如果还要加一个 color 与 size 的属性怎么办?
类似类型参数,必须每个结构体与联合体中保证内存一致的前提下再加一个 color 与 size 的变量,此时代码显得冗杂,不如使用类的继承
1.1.2 union 的局限
union variant_t { int i; double d; };
int main()
{
variant_t v{};
v.i = 6;
std::cout << v.d << std::endl; // UB
}
- 1、类型不安全
- 2、无法知道当前存储的类型
往联合体中塞一个用索引表示类型的结构体变量,再使用某种复杂的嵌套结构来发挥其作用?
struct Variant {
int m_index;
public:
Variant() : m_index(-1) {}
};
union variant_t
{
int i;
double d;
char c;
Variant v;
};
但实际上是无法使用的,由于 Variant 修改了默认构造函数,编译器无法为包含 non-trivial 成员的联合体生成合法的构造 / 析构函数
- 3、类型局限性
- 4、any 是运行时检查
- 5、继承与多态也有局限性
1.2 variant
variant 是 C++17 标准库引入的,其作用与 union 类似,但解决了 union 的局限性,并且是类型安全的、编译时检查的
std::get<>()一种模板函数,尖括号 <> 中是「索引(整数)」或「类型」
#include <variant>
using namespace std;
int main()
{
variant<int,double,string> v;
v = 42;
v = 3.14;
v = "hello";
try
{
cout << get<string>(v) << endl;
// cout << get<float>(v) << endl; // 编译器报错
// cout << get<int>(v) << endl; // 运行时异常,会被捕获
}
catch (bad_variant_access& e)
{
cout << "Wrong type acess: " << e.what() << endl;
}
}
当然也有避免异常处理的做法:
- varian 的
index()方法获取当前 variant 中存储的类型索引 holds_alternative<T>(v)检查 variant 中是否包含类型 T 的值
cout << "Current index: " << v.index() << endl; // 0-based,2
if (holds_alternative<int>(v))
cout << "Holds an int: " << get<int>(v) << endl;
else if (holds_alternative<double>(v))
cout << "Holds a double" << endl;
1.3 Visitor 模式
1.3.1 场景
- 开发者正在维护一个矢量图形库(Graphics Library),其中包含稳定的基础图形类(如 Circle, Rectangle)。这些类目前主要负责几何计算(如 area())
新增需求
- 现在的业务方提出新要求:需要将画布上的所有图形导出为 XML 格式,以便保存或传输
设计挑战与约束
- 单一职责原则 (SRP):Shape 类应该只关注几何属性(半径、长宽),不应该包含复杂的 IO 或格式化逻辑(如 XML 生成代码)
- 开闭原则 (OCP):我们预见到未来还会有导出 JSON、PDF 或进行压缩的需求,我们不希望每次增加新操作都去修改稳定的 Shape 基类
1.3.2 传统实现
每个操作都作为虚函数直接添加到 Shape 基类中
- 违背单一职责原则:图形类里塞满了业务逻辑
- 违背开闭原则:每次增加新操作,都需要修改基类和所有子类
1.3.3 类继承与 Visitor 模式
classDiagram
class Shape {
<>
+accept(IShapeVisitor*)
}
class Circle {
+radius: double
+accept(v)
}
class Rectangle {
+width: double
+height: double
+accept(v)
}
class IShapeVisitor {
<>
+visitCircle(Circle*)
+visitRectangle(Rectangle*)
}
class DrawVisitor {
+visitCircle(Circle*)
+visitRectangle(Rectangle*)
}
class XMLVisitor {
+visitCircle(Circle*)
+visitRectangle(Rectangle*)
}
class CompressVisitor {
+visitCircle(Circle*)
+visitRectangle(Rectangle*)
}
%% 继承关系
Shape <|-- Circle
Shape <|-- Rectangle
IShapeVisitor <|-- DrawVisitor
IShapeVisitor <|-- XMLVisitor
IShapeVisitor <|-- CompressVisitor
%% 依赖关系与伪代码注释
Circle ..> IShapeVisitor : v->visitCircle(this)
Rectangle ..> IShapeVisitor : v->visitRectangle(this)
class Circle;
class Rectangle;
class ShapeVisitor
{
public:
virtual ~ShapeVisitor() = default;
virtual void visitCircle(Circle *circle) = 0;
virtual void visitRectangle(Rectangle *rectangle) = 0;
};
class Shape // Shape 基类(只负责结构,不负责操作)
{
public:
virtual ~Shape() = default;
virtual void accept(ShapeVisitor *v) = 0;
virtual double area() = 0;
};
class Circle : public Shape
{
public:
double radius;
explicit Circle(double r) : radius(r) {}
void accept(ShapeVisitor *v) override
{
v->visitCircle(this); // 双重分发
}
double area() override { return this->radius * this->radius * 3.14; }
};
class Rectangle : public Shape
{
public:
int width, height;
Rectangle(int w, int h) : width(w), height(h) {}
void accept(ShapeVisitor *v) override
{
v->visitRectangle(this); // 双重分发
}
double area() override { return this->width * this->height; }
};
class DrawVisitor : public ShapeVisitor
{
public:
void visitCircle(Circle *c) override
{
std::cout << "Draw circle(r=" << c->radius << ")\n";
}
void visitRectangle(Rectangle *r) override
{
std::cout << "Draw rectangle\n";
}
};
class XMLVisitor : public ShapeVisitor
{
public:
void visitCircle(Circle *c) override
{
std::cout << "Exporting Circle (r=" << c->radius << ") to XML\n";
}
void visitRectangle(Rectangle *r) override
{
std::cout << "Exporting Rectangle to XML\n";
}
};
int main() {
const std::vector<Shape *> shapes = {
new Circle{5.0},
new Rectangle{10, 20}
};
XMLVisitor xmlExporter;
for (auto s : shapes) {
s->accept(&xmlExporter); // 通过 Visitor 执行操作
}
DrawVisitor drawExporter;
for (auto s : shapes)
{
s->accept(&drawExporter);
}
}
访问者模式 (Visitor Pattern) 是一种行为型设计模式,它允许你在不改变对象结构的前提下,定义作用于这些对象的新操作
核心思想:
- 将数据结构与数据操作分离
- 通过"双重分发" (Double Dispatch) 机制实现多态
- 遵循"开闭原则":对扩展开放,对修改关闭
双重分发:
- 单分发(普通多态):
Shape * s = new Circle();
s->draw(); // 只根据 s 的类型决定调用哪个 draw
- 双重分发(Visitor 模式):
Shape * s = new Circle();
Visitor * v = new DrawVisitor();
s->accept(v);
// 第一次分发:根据 s 的类型
// 第二次分发:根据 v 的类型
Visitor 需要知道每个具体 Element 的信息来创建 visit 函数
Element 需要知道 Visitor 来调用 visit 函数,将自己传给 Visitor
| 对比项 | 传统虚函数方式 | Visitor模式 |
|---|---|---|
| 添加新操作 | 需修改基类和所有子类 | 只需添加新的Visitor类 |
| 添加新类型 | 只需添加新子类 | 需修改所有Visitor类 |
| 代码耦合 | 操作与结构耦合 | 操作与结构解耦 |
| 编译影响 | 修改基类影响大 | 新操作不影响现有代码 |
| 适用场景 | 操作类型稳定 | 操作类型频繁变化 |
- ❌ 破坏封装:要求元素公开内部状态
- ❌ 增加新元素类困难
- ❌ 对象结构变化会影响所有访问者
1.3.4 variant 与 Visitor 模式
对于 variant 简单来说,就是将 variant 的类型信息作为参数传递给一个函数,由函数根据类型信息来处理 variant 中的值
优势
- 不需要基类 Shape
- 不需要虚函数
- 类型安全(编译期检查)
1.3.5 访问方式 std::visit
std::visit(Visitor visitor, Variant v);
核心功能
- std::visit 是 C++ 标准库提供的函数模板,用于将一个访问器(Visitor)应用到 std::variant 内部存储的具体数据上
工作原理
- 自动识别:检查 variant 当前存的是哪个类型
- 自动匹配:去 Visitor 里找对应的重载函数如:operator() (Circle) 或 operator() (Rectangle)。
- 编译期安全:如果少写了某种类型的处理逻辑,编译器会直接报错
// 纯数据类 (无需继承,无需 accept)
struct Circle { double radius; };
struct Rectangle { double w, h; };
// 定义类型集合
using Shape = variant<Circle, Rectangle>;
// 定义访问者 (使用 struct 重载 () 操作符)
struct DrawVisitor {
void operator()(const Circle &c) {
cout << "Drawing Circle r=" << c.radius << "\n";
}
void operator()(const Rectangle&r)
{
cout << "Drawing Rectangle r=" << r.w << " " << r.h << "\n";
}
};
struct XMLVisitor {
void operator()(const Circle &c) {}
void operator()(const Rectangle&r) {}
};
int main() {
vector<Shape> shapes = {
Circle{5.0},
Rectangle{10, 20}
};
// 使用 std::visit
DrawVisitor draw;
XMLVisitor xml;
for (const auto& s : shapes) {
visit(draw, s);
visit(xml, s);
}
return 0;
}
#include <variant>
using namespace std;
using MyVariant = variant<int, double, string>;
struct Visitor
{
void operator()(const int i) const
{
std::cout << "Got int: " << i << std::endl;
}
void operator()(const double d) const
{
std::cout << "Got double: " << d << std::endl;
}
void operator()(const std::string& s) const
{
std::cout << "Got string: " << s << std::endl;
}
};
void handleVariant(const MyVariant& v)
{
visit([](const auto& arg)
{
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>)
{
std::cout << "Got an int: " << arg << '\n';
}
else if constexpr (std::is_same_v<T, double>)
{
std::cout << "Got a double: " << arg << '\n';
}
else if constexpr (std::is_same_v<T, std::string>)
{
std::cout << "Got a string: " << arg << '\n';
}
}, v);
}
int main()
{
MyVariant v1 = 42;
MyVariant v2 = 3.14;
MyVariant v3 = "hello";
Visitor visitor;
std::visit(visitor, v1);
std::visit(visitor, v2);
std::visit(visitor, v3);
handleVariant(v1);
handleVariant(v2);
handleVariant(v3);
}
2 any
#include <any>
using namespace std;
int main() {
any a = 1;
a = any_cast<int>(a) + 1;
cout << any_cast<int>(a) << endl;
a = 1.5;
cout << any_cast<double>(a) << endl;
a = string("hello");
cout << any_cast<string>(a) << endl;
}
- 类型安全(type-safe)
- 可以安全封装任意类型的单个值
- 通过 std::any_cast 进行类型安全的取值
- 运行时多态
- 值的类型在运行时确定,而不是编译期固定
- 语义与限制
- 支持空值状态(默认构造时为空)
- 值必须可拷贝构造
- 支持拷贝、移动拷贝、移动赋值
2.1 .type()
const type_info& t = a.type();
void print_any(const any& a)
{
if (a.type() == typeid(int))
{
std::cout << std::any_cast<int>(a) << std::endl;
}
else if (a.type() == typeid(double))
{
std::cout << std::any_cast<double>(a) << std::endl;
}
else if (a.type() == typeid(std::string))
{
std::cout << std::any_cast<std::string>(a) << std::endl;
}
else
{
std::cout << "Unkonw" << std::endl;
}
}
2.2 any_cast
std::any_cast<T>(any_obj)
不匹配时抛出 std::bad_any_cast
any obj = 42;
try
{
double d = any_cast<int>(obj);
// double d = any_cast<double>(obj); // bad_any_cast异常
cout << "succeed: " << d << "\n";
}
catch (const bad_any_cast& e)
{
cout << "type error: " << e.what() << endl;
}
std::any_cast<T>(&any_obj)
当使用取指针的方式调用 any_cast 时,如果类型不匹配,并不会抛出异常,而是返回 nullptr
any obj = 42;
auto p = std::any_cast<int>(&obj);
// auto p = std::any_cast<double>(&obj); //type error
if (p != nullptr)
{
std::cout << "succeed: " << *p << "\n";
}
else
{
std::cout << "type error\n";
}
2.3 reset & has_value
- 默认构造为空值
- has_value() 判断是否有值
- reset() 清空内容
any obj;
cout << "obj type: " << obj.type().name() << endl; // v
cout << "has value: " << obj.has_value() << endl; // 0
obj = 42;
cout << "obj type: " << obj.type().name() << endl; // i
cout << "has value: " << obj.has_value() << endl; // 1
obj.reset();
cout << "obj type: " << obj.type().name() << endl; // v
cout << "has value: " << obj.has_value() << endl; // 0
注意:不要使用 typeid(obj).name() 来判断 any 的类型,否则只会返回编译器生成的类型名 St3any,而不是实际存储的类型名
2.4 any 的结构
细节(包括 variant 的结构)暂不展开,直接看 ppt
2.5 any vs. variant
Type
- std::any
- 编译期未确定,运行时检查
- std::variant
- 编译期确定、检查
- 运行时仅检查当前存放数据的激活索引是否匹配
Visit
- std::any
- 需通过 any_cast,即调用 handler,并通过 typeid 检查确认类型是否匹配
- 含一次函数调用开销和类型比较开销
- std::variant
std::get<T>(v)或std::get<I>(v)- 类型 T /索引 I 在编译期已知,内部直接转换为索引访问(调用
__get_alt<I>(...),直接返回存储对象的引用)
2.6 补充:struct 的内存对齐
struct A { char c; short s; int i; float f; };
struct B { char c; int i; short s; float f; };
struct A - 较优的排序
成员顺序:char c (1字节) -> short s (2字节) -> int i (4字节) -> float f (4字节)
内存布局:
- char 占 1 字节
- short 需要从偶数地址开始,所以中间补了 1 个填充字节
- 后续 int 和 float 均自然对齐
总大小:1 + 1(Padding) + 2 + 4 + 4 = 12 字节
struct B - 较差的排序
成员顺序:char c (1字节) -> int i (4字节) -> short s (2字节) -> float f (4字节)
内存布局:
- char 占 1 字节
- int 需要从 4 的倍数地址开始,所以中间必须补 3个填充字节
- short 占 2 字节。
float 需要 4 字节对齐,short 之后可能还需要填充
总大小:16 字节
3 tuple
std::tuple(元组)是一个固定大小的、可以包含不同类型数据的集合
可以把它看作是一个没有名字的结构体 (Struct),或者是 std::pair 的升级版(pair 只能存 2 个元素,tuple 可以存 N 个)
以下是 std::tuple 的主要用途和核心场景:
3.1 函数返回多个值 (最常见的用法)
// m1
void func1(int &i, float &f) { i = 1; f = 3.0f;}
void test1()
{
int i; float f;
func1(i,f);
cout << i << f;
}
// m2
struct Rtn { int i; float f; };
Rtn func2() { return { 1, 3.0f }; }
void test2()
{
auto r = func2();
cout << r.i << r.f;
}
// m3
tuple<int,float> func3() { return { 1, 3.0f }; }
void test3()
{
auto r = func3();
// cout << get<0>(r) << get<1>(r); // 丑且 magic number
// int i; float f;
// std::tie(i,f) = r; // 可用 std::tie 解包,但不如结构化绑定
// cout << i << f << endl;
auto [i,f] = r; // 结构化绑定,C++ 17
cout << i << f << endl;
}
3.2 临时组合相关数据
当你需要将几个不同类型的数据放在一起传递,但又不想专门为此定义一个 struct 时,tuple 非常好用
- 场景:你在一个容器中存储数据,比如
std::vector - 例子:存储一系列的三维坐标点 \((x, y, z)\)
std::vector<std::tuple<int, int, int>> points;
points.emplace_back(1, 2, 3);
points.emplace_back(4, 5, 6);
3.3 作为 std::map 的 Key
std::tuple 自带了比较运算符(它会按顺序比较元素:先比第一个,如果相等再比第二个……)
这使得它非常适合作为 std::map 或 std::set 的键值(Key),而不需要你需要自己重载 < 运算符
示例:需要根据坐标来索引地图上的物体
#include <map>
#include <tuple>
std::map<std::tuple<int, int>, std::string> mapData;
mapData[{10, 20}] = "Treasure";
mapData[{5, 5}] = "Trap";
// 自动支持查找
if (mapData.count({10, 20})) {
// 找到了宝藏
}
但如果是 unordered_map 则不允许这样用,只能
unordered_map<int,unordered_map<int,string>>
3.4 批量赋值与解包 (std::tie)
在 C++17 之前,如果你想把 tuple 的值解包给现有的变量,或者是为了实现某些排序逻辑,std::tie 很有用
int myInt;
double myDouble;
std::tuple<int, double> t(10, 3.14);
// 将 tuple 中的值“系”到变量上
std::tie(myInt, myDouble) = t;
// 现在 myInt = 10, myDouble = 3.14
std::tie(std::ignore, myInt) = t;
cout << myInt << endl; // 3,把 3.14 绑到了 int
注:C++17 引入结构化绑定后,std::tie 的使用频率降低了,但在只想要部分返回值时配合 std::ignore 还是很有用
3.4.1 Tuple vs Struct:什么时候用哪个?
这是很多开发者困惑的地方。既然 tuple 能存不同类型,struct 也能,怎么选?
| 特性 | std::tuple | struct / class |
|---|---|---|
| 可读性 | 差。成员通过 get<0>, get<1> 访问,没有语义名字 |
好。成员有名字(如 user.name, user.age) |
| 定义便捷性 | 高。不需要预先声明类型,直接写 tuple<int, float> |
低。需要先写 struct MyData {...}; |
| 用途 | 适合临时的数据组合、函数多返回值、通用编程 | 适合长期使用、业务逻辑核心的数据结构 |
| 比较操作 | 自动支持 (Lexicographical comparison) | 需要手动重载 operator< 或 operator== |
3.4.2 总结
std::tuple 就像是一个通用的、临时的容器包
- 用它:当你只是想快速把几个数据绑在一起传给别人,或者从函数丢出来,且不想费劲去给这个组合起名字的时候
- 不用它:如果这个数据组合在你的代码里到处都要用,还是老老实实定义一个
struct吧,否则几个月后你自己都忘了get<2>到底是 “分数” 还是 “身高”
4 optional
std::optional (C++17 引入) 是一个 “可能为空” 的容器
它专门用来表示一个值要么存在,要么不存在(std::nullopt)
它的出现是为了解决 C++ 中长期存在的一个痛点:如何优雅地表示 “没有值” 或 “失败” 的情况?
unordered_map<string, int> userData;
int findUserID1(const string& username)
{
if (userData.contains(username))
{
return userData[username];
}
return -1; // 用 -1 表示“未找到”
}
int* findUserID2(const string& username)
{
if (userData.contains(username))
{
return new int(userData[username]);
}
return nullptr; // 用空指针表示“未找到”
}
bool findUserID3(const string& username, int& outID)
{
if (userData.contains(username))
{
outID = userData[username];
return true;
}
return false;
}
- 方法1:magic number
- 方法2:memory leak
- 方法3:addiotnal parameter
都不够优雅
std::optional<T> 用法
#include <optional>
optional<int> findUserID4(const string& username)
{
if (userData.contains(username))
{
return userData[username];
}
return nullopt;
}
在主程序中
- 可以当指针一样使用
auto opt = findUserID4("Tom");
if (opt) // 这里也可以使用 opt.has_value() 作判断条件
{
cout << "Success " << *opt << endl; // 解引用
}
else
{
cout << "Fail" << endl;
}
- 使用
.value()方法获取值,如果为空则抛出异常
auto opt = findUserID4("Tom");
try
{
int id = opt.value();
}
catch (const bad_optional_access& e)
{
cout << e.what() << endl;
}
.value_or()若为空则返回默认值,一行代码即可,推荐
auto opt = findUserID4("Tom");
int id = opt.value_or(-1); // 若为空则 -1
5 malloc
并非 C 语言的 malloc
5.1 RAII
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 核心的编程范式,核心思想是利用对象的生命周期自动管理资源,从根本上解决资源泄漏问题
void old_use(Args a) {
auto q = new Blob(a);
// ...
if (foo) throw Bad(); // 会泄漏
if (bar) return; // 会泄漏
// ...
delete q; // 容易忘
}
class int_ptr
{
int* ptr;
public:
int_ptr(int *p = 0) : ptr(p) {}
~int_ptr() { delete ptr; }
int* operator->() const { return ptr;}
int& operator *() const { return *ptr; }
};
void newer_use(Args a) {
auto p = int_ptr(new Blob(a)); // 栈上对象
// ...
if (foo) throw Bad(); // 不会泄漏
if (bar) return; // 不会泄漏
// ...
}
5.2 auto_ptr(已弃用)
auto_ptr 是 C++98 标准 引入的第一个智能指针,定义在 <memory> 头文件的 std 命名空间中,核心目的是解决动态内存泄漏问题
但由于设计存在严重缺陷,C++11 已弃用,C++17 完全移除
template<typename T>
class auto_ptr {
T* ptr;
public:
explicit auto_ptr(T* p = nullptr) : ptr(p) {}
~auto_ptr() { delete ptr; }
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
auto_ptr(auto_ptr& other) : ptr(other.release()) {}
auto_ptr& operator=(auto_ptr& other) {
if (this != &other) {
delete ptr;
ptr = other.release();
// 反直觉的拷贝,实则转移
}
return *this;
}
T* release() {
T* old_ptr = ptr;
ptr = nullptr;
return old_ptr;
}
};
int main()
{
auto_ptr<int> p1(new int(8)); // p1 拥有 10
auto_ptr<int> p2 = p1; // 所有权从 p1 转移到 p2
cout << *p2 << endl; // 8
cout << *p1 << endl; // 崩溃,对 null 解引用
}
缺陷:
- 拷贝 / 赋值时的 “隐式所有权转移”
- 不能管理动态数组
- 析构函数仅调用 delete,如果用来管理 new[] 分配的数组,会导致内存泄漏 + 未定义行为
- 无法安全放入 STL 容器
- STL 容器(如 vector)的拷贝、排序等操作会触发元素的拷贝,而 auto_ptr 的拷贝会转移所有权,导致容器内的元素失效
5.3 unique_ptr
auto_ptr 的最佳替代品,定义在
<memory>头文件的 std 命名空间中
5.3.1 禁止隐式拷贝(编译期拦截)
// Deep Copy is denied
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;
// ...
unique_ptr<int> p1(new int(8));
unique_ptr<int> p2 = p1; // 编译错误
5.3.2 引入移动语义
unique_ptr(unique_ptr&& other) noexcept : ptr(other.release()) {}
unique_ptr& operator=(unique_ptr&& other) noexcept {
if (this != &other) {
reset(other.release());
}
return *this;
}
// ...
unique_ptr<int> p1(new int(8));
unique_ptr<int> p2 = move(p1);
// 尽管编译器不报错,但显式 move 程序员也心中有数
cout << *p1 << endl; // 崩溃
5.3.3 其他补充
两种初始化方式,优先用 std::make_unique
// 方式 1:直接初始化
unique_ptr<int> p1(new int(42)); // 接管 int 类型的动态内存
// 方式 2:make_unique 初始化(C++14 推荐,避免裸 new,更安全)
auto p2 = make_unique<int>(100); // auto 自动推导
常用成员函数(核心操作)
| 函数 | 作用 |
|---|---|
| get() | 返回管理的裸指针(仅查看,不要手动 delete) |
| reset() | 释放当前内存,若传新指针则接管新内存;传 nullptr 仅释放当前内存 |
| release() | 释放所有权(返回裸指针,unique_ptr 变为空,需手动 delete 裸指针) |
| swap() | 交换两个 unique_ptr 管理的内存 |
| empty() | 判断是否管理空指针(等价于 get() == nullptr) |
5.4 shared_ptr
C++11 引入的共享式智能指针,核心目标是解决 “多个对象需要共享同一块动态内存” 的场景,弥补 unique_ptr 独占所有权的不足
核心特性:
- 共享所有权:多个 shared_ptr 可以同时管理同一块动态内存
- 引用计数:内部维护一个 “引用计数器”,记录当前有多少个 shared_ptr 指向该内存
- 拷贝 / 赋值时,引用计数 +1
- shared_ptr 析构 / 释放所有权时,引用计数 -1
- 当引用计数变为 0 时,自动调用 delete 释放内存(RAII 机制)
shared_ptr<int> p1 = make_shared<int>(10);
cout << "p1 count: " << p1.use_count() << endl; // 1
auto p2 = p1;
cout << "p1 count: " << p1.use_count() << endl; // 2
*p2 = 100;
cout << *p1 << endl; // 100
shared_ptr<int> p3 = p2;
cout << "p3 count: " << p3.use_count() << endl; // 3
cout << "p1 count: " << p1.use_count() << endl; // 3
但 shared_ptr 也有其局限性:
- 循环引用:多个 shared_ptr 互相引用,导致引用计数无法归零,内存泄漏
struct B; // 前置声明
struct A {
shared_ptr<B> b_ptr; // A 持有 B 的共享指针
~A() { cout << "A 析构" << endl; } // 析构函数(验证是否释放)
};
struct B {
shared_ptr<A> a_ptr; // B 持有 A 的共享指针
~B() { cout << "B 析构" << endl; }
};
int main() {
{
auto a = make_shared<A>();
auto b = make_shared<B>();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 指向 A
cout << a.use_count() << endl; // 输出 2(a 自己 + b->a_ptr)
cout << b.use_count() << endl; // 输出 2(b 自己 + a->b_ptr)
} // 出作用域,a 和 b 析构
// 但实际上析构函数未执行!内存泄漏!
}
补充:make_shared
shared_ptr是 C++ 里管理动态对象(比如new出来的对象)的智能指针,靠 “引用计数” 记录有多少个指针在用这个对象(计数为 0 就自动释放对象)。但它的问题是:
引用计数的空间是单独分配的 —— 比如你写 shared_ptr<int> p(new int(10)) 时,系统会分两次内存:
- 给
int(10)分配动态对象空间 - 单独分配一块空间存 “引用计数”(初始是 1)
这两块空间是分开的,会多一次内存分配的开销
make_shared 的解决办法(让动态对象 “自带” 引用计数)
make_shared是 C++ 标准库的模板函数,它的作用是把 “动态对象的空间” 和 “引用计数的空间” 合并成一块分配:
比如写 auto p = make_shared<int>(10)时,系统只分配一次内存,里面既存 int(10) 这个对象,也存对应的引用计数
好处是:引用计数只占一份空间,所有指向这个对象的 shared_ptr 都共享它,同时少了一次内存分配,效率更高
5.5 weak_ptr
C++11 引入的弱引用智能指针,不能单独使用,核心作用是配合 shared_ptr 解决循环引用问题,不能单独管理任何资源
- 无所有权:weak_ptr 不拥有所指向的动态内存,仅作为 “观察者” 观察 shared_ptr 管理的资源
- 不影响引用计数:构造、拷贝、赋值 weak_ptr 时,不会增加 / 减少 shared_ptr 的引用计数
- 安全检测:可检测所观察的资源是否已被释放(避免访问已释放的内存)
- 间接访问:不能直接解引用访问资源,需先通过
lock()转为 shared_ptr 后才能安全访问
5.5.1 初始化
// 步骤 1:创建 shared_ptr(必须先有 “所有者”)
auto sp = make_shared<int>(100);
// 方式 1:从 shared_ptr 初始化 weak_ptr(最常用)
weak_ptr<int> wp1 = sp;
// wp1 仅观察 sp,sp 的引用计数仍为 1(关键!)
cout << "sp 的计数:" << sp.use_count() << endl; // 输出 1
// 方式 2:从另一个 weak_ptr 初始化
weak_ptr<int> wp2 = wp1;
cout << "sp 的计数仍为:" << sp.use_count() << endl; // 还是 1
// ❌ 错误:不能用裸指针初始化
// int* raw = new int(10);
// weak_ptr<int> wp3(raw);
// ❌ 错误:不能直接用 unique_ptr 初始化(需先转 shared_ptr)
// unique_ptr<int> up = make_unique<int>(20);
// weak_ptr<int> wp4 = up;
5.5.2 常用成员函数
| 函数 | 作用 |
|---|---|
| lock() | 返回一个 shared_ptr,资源未释放 → 返回有效 shared_ptr(计数 + 1);资源已释放 → 返回空 shared_ptr |
| expired() | 判断资源是否已被释放 |
| use_count() | 返回观察的 shared_ptr 当前的引用计数 |
| reset() | 清空 weak_ptr,不再观察任何资源 |
- 注意:不能直接解引用 weak_ptr
5.5.3 解决循环引用
struct B;
struct A {
shared_ptr<B> b_ptr; // A 仍持有 B 的强引用
~A() { cout << "A 析构" << endl; }
};
struct B {
weak_ptr<A> a_ptr; // B 改为持有 A 的弱引用
~B() { cout << "B 析构" << endl; }
};
int main() {
{
auto a = make_shared<A>(); // a 计数 = 1
auto b = make_shared<B>(); // b 计数 = 1
a->b_ptr = b; // a 指向 b → b 计数 = 2
b->a_ptr = a; // b 指向 a → a 计数仍为 1(弱引用不增加计数!)
cout << a.use_count() << endl; // 输出 1
cout << b.use_count() << endl; // 输出 2
// (可选)B 中访问 A 的资源:通过 lock() 转 shared_ptr
if (auto temp_a = b->a_ptr.lock()) {
cout << "B 成功访问 A 的资源" << endl;
cout << a.use_count() << endl; // 2
} // temp_a 析构
} // a 和 b 正常析构
}
- b->a_ptr 是 weak_ptr,绑定 a 后不会增加 a 的引用计数(a 计数始终为 1)
- 出作用域时,a 先析构 → 计数归 0 → A 内存释放
- a 释放后,a->b_ptr 也被销毁 → b 的计数从 2 减为 1,接着 b 析构 → 计数归 0 → B 内存释放
- 整个循环被打破,资源正常释放
5.5.4 应用场景
缓存
using Key = std::string;
class Resource {
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
void doSomething() { std::cout << "Doing something with Resource\n"; }
};
class Cache {
unordered_map<Key, std::shared_ptr<Resource>> m_cache;
public:
Cache() { std::cout << "Cache created\n"; }
void clear() { m_cache.clear(); }
~Cache() { clear(); std::cout << "Cache destroyed\n"; }
void addResource(Key key, std::shared_ptr<Resource> resource) {
m_cache[key] = resource;
}
std::shared_ptr<Resource> getResource(Key key) {
// ... Serach / create resource
return m_cache[key]; // 调用者共享所有权
}
Cache(const Cache&) = delete;
Cache& operator=(const Cache&) = delete;
Cache(Cache&&) = delete;
Cache& operator=(Cache&&) = delete;
};
int main() {
Cache cache;
Key key1 = "key1";
{
auto ptr = make_shared<Resource>();
cache.addResource(key1, ptr);
if(auto cached_ptr = cache.getResource(key1)) {
cached_ptr->doSomething();
}
} // ptr 离开作用域,Resource 应析构
if (auto cached_ptr = cache.getResource(key1)) {
cout << "获取成功" << endl;
} else {
cout << "获取失败:资源已不存在" << endl;
}
}
但却输出获取成功,原因是只要 Cache 对象存在,它里面存的所有 Resource 永远不会被释放,即使外部已经没有任何人使用该资源了,引用计数也至少是 1
- cache 应该持有 weak_ptr,只是 “观测” 资源,不干扰资源的生命周期
class Cache {
unordered_map<Key, std::weak_ptr<Resource>> m_cache; // 修改
public:
Cache() { std::cout << "Cache created\n"; }
void clear() { m_cache.clear(); }
~Cache() { clear(); std::cout << "Cache destroyed\n"; }
void addResource(Key key, std::shared_ptr<Resource> resource) {
m_cache[key] = resource;
}
std::shared_ptr<Resource> getResource(Key key) {
// ... Serach / create resource
return m_cache[key].lock(); // 修改
}
Cache(const Cache&) = delete;
Cache& operator=(const Cache&) = delete;
Cache(Cache&&) = delete;
Cache& operator=(Cache&&) = delete;
};
输出获取失败:资源已不存在,符合预期
双向链表
struct Node {
std::string name;
std::shared_ptr<Node> next; // next 持有 Node 的强引用
std::weak_ptr<Node> prev; // prev 持有 Node 的弱引用
Node(const std::string& n) : name(n) {}
~Node() {}
};
不多解释了,否则循环引用无法析构
观察者模式
如果是 “从属” 关系,从属者指向拥有者时,应该用 weak_ptr
class Teacher {
std::vector<std::shared_ptr<Student>> student_; // Teacher持有Student的强引用
};
class Student {
std::weak_ptr<Teacher> teacher_; // Student持有Teacher的弱引用
};
- Student 不 “控制” Teacher 的生命周期
5.6 shared_ptr 与 weak_ptr 的底层原理
// todo? 去看 ppt 的动画
标题:C++ STL高级
作者:Zwing
创建于:2026-03-20 23:35:21
更新于:2026-08-07 16:17:52
链接:https://zanytriumph.github.io/posts/CPP-STL高级.html
版权声明:本文章采用 CC BY-NC-SA 4.0 进行许可