C++智能指针(后)
C++智能指针(后)
前面一节我们已经大概智能指针是什么东西了,但是可能大家还是会有点懵,我也觉得说的还没很全面,这节就作为上一节的补充继续讲讲。
回顾
我们先回顾一下智能指针是啥?
首先,智能指针的出现主要是为了能更好的管理我们手动申请的内存空间。因为不管是我们人为的忘记释放,亦或者是程序设计不当((内存空间还没释放之前就return返回了)或者(内存空间还没释放,前方的代码就抛异常了,跳过了释放内存空间的步骤))。
这些行为都会导致内存泄露的问题,除此之外还会有悬空指针的问题存在,这些问题我们在上一节已经说的比较详细了,这一节我们主要从智能指针的发展历史来逐步学习智能指针。
std::auto_ptr
在开始之前,有一个极其重要的警告必须放在最前面:
std::auto_ptr 已在 C++11 中被弃用 (deprecated),并在 C++17 中从标准中彻底移除。绝对不应该在新代码中使用它。
学习 std::auto_ptr 的主要目的,是理解它的设计缺陷、为什么它会被淘汰,以及如何将遗留代码中的 auto_ptr 正确地迁移到现代智能指针(主要是 std::unique_ptr)。这是一个了解C++语言演进和设计哲学的反面教材。
第一部分:std::auto_ptr 是什么?它当初要解决什么问题?
std::auto_ptr 是 C++98 标准中引入的第一个尝试实现“自动资源管理”的智能指针。它的核心思想与后来的智能指针一致:基于 RAII 原则,在析构函数中自动 delete 其管理的指针,以解决原生指针导致的内存泄漏问题。
一个简单的例子:
#include <iostream>
#include <memory> // 在C++98中,auto_ptr定义在<memory>中
class MyClass {
public:
MyClass() { std::cout << "Constructed\n"; }
~MyClass() { std::cout << "Destroyed\n"; }
void doSomething() { std::cout << "Doing something\n"; }
};
int main() {
std::cout << "Start of main\n";
{
std::auto_ptr<MyClass> myPtr(new MyClass());
myPtr->doSomething();
} // myPtr 离开作用域,其析构函数会自动调用 delete
std::cout << "End of main\n";
return 0;
}
输出:
Start of main
Constructed
Doing something
Destroyed
End of main
从上面的例子看,它似乎完美地解决了问题。那么,它的问题到底出在哪里呢?
第二部分:std::auto_ptr 的致命设计缺陷
std::auto_ptr 的核心缺陷在于其**“复制”语义**。为了模拟“独占所有权”,它的拷贝构造函数和拷贝赋值运算符进行的不是真正的复制,而是所有权的转移。
缺陷示例1:令人困惑的所有权转移
#include <iostream>
#include <memory>
int main() {
std::auto_ptr<int> ap1(new int(42));
std::cout << "ap1 points to: " << *ap1 << "\n"; // 输出 42
// “复制”构造函数:所有权从 ap1 转移到 ap2
std::auto_ptr<int> ap2(ap1);
// 此时,ap1 已经变为空指针!
// 这样会导致ap1变成了悬空指针。如果再次访问ap1就会报错。
std::cout << "ap1 is now: " << (ap1.get() ? "not null" : "null") << "\n"; // 输出 null
if (ap1.get() == nullptr) {
std::cout << "ap1 has lost ownership!\n";
}
// ap2 现在拥有那个整数
std::cout << "ap2 points to: " << *ap2 << "\n"; // 输出 42
return 0;
}
发生了什么?
std::auto_ptr<int> ap2(ap1); 这行代码看起来像是在复制,但实际上它调用了 auto_ptr 的拷贝构造函数,这个构造函数的实现类似于:
auto_ptr(auto_ptr& other) { // 注意:参数是非常量引用
ptr = other.ptr; // 偷走指针
other.ptr = nullptr; // 并将源指针置空
}
这种隐性的、不可控的所有权转移是反直觉和极其危险的根源。
缺陷示例2:在STL容器中使用会导致灾难
STL容器(如 std::vector, std::map)在其内部操作(如排序、插入、重新分配内存)中,严重依赖于元素的可复制且复制后等价的语义。auto_ptr 的“复制”行为会破坏容器的不变量。
#include <iostream>
#include <memory>
#include <vector>
int main() {
std::vector<std::auto_ptr<int>> vec;
vec.push_back(std::auto_ptr<int>(new int(1)));
vec.push_back(std::auto_ptr<int>(new int(2)));
vec.push_back(std::auto_ptr<int>(new int(3)));
// 尝试对vector进行排序或任何可能导致内部复制的操作...
// std::sort(vec.begin(), vec.end()); // 这行代码如果取消注释,会导致未定义行为!
// 即使只是访问,也可能因为所有权不明而崩溃
for (const auto& ptr : vec) { // C++11范围for循环
// 但此时,你能确定vec里的每个auto_ptr还拥有对象吗?
// 在之前的push_back过程中,可能已经发生了所有权转移
// std::cout << *ptr << std::endl; // 极有可能在此处崩溃!
}
return 0;
}
为什么? push_back 操作可能会触发 vector 的扩容,需要将旧元素“复制”到新内存中。这个“复制”过程会转移旧内存中 auto_ptr 的所有权,导致旧位置的 auto_ptr 变为 nullptr。之后任何对它们的访问都会导致崩溃。
第三部分:为什么 std::unique_ptr 是完美的替代品?
std::unique_ptr 从设计上就根除了 auto_ptr 的所有问题:
-
明确的语义 (Explicit Semantics):
unique_ptr禁止拷贝。任何拷贝的尝试都会导致编译错误,从根本上杜绝了隐性的所有权转移。- 所有权的转移必须通过
std::move()显式地进行,这让代码的意图非常清晰。
std::unique_ptr<int> up1(new int(42)); // std::unique_ptr<int> up2 = up1; // 错误!编译不通过! std::unique_ptr<int> up2 = std::move(up1); // 正确!显式转移所有权,语义清晰 -
与STL容器完美兼容:
- 因为
unique_ptr提供了移动构造函数和移动赋值运算符,它可以安全地存放在容器中。容器在内部调整时使用的是移动语义,而非拷贝语义,这不会导致所有权意外丢失。
std::vector<std::unique_ptr<int>> vec; vec.push_back(std::make_unique<int>(1)); // 成功 vec.push_back(std::make_unique<int>(2)); // 成功,可能会移动之前的元素,但这是安全的 auto another_vec = std::move(vec); // 甚至可以移动整个vector! - 因为
-
更小的开销和更多的功能:
- 默认情况下,
unique_ptr和auto_ptr一样,几乎没有空间开销。 - 它支持自定义删除器,可以管理不是
new分配的资源(如malloc, 文件句柄等)。 - 对数组有特化版本(
std::unique_ptr<T[]>),会正确地调用delete[]。
- 默认情况下,
| 特性 | std::auto_ptr (已废弃) | std::unique_ptr (现代替代) |
|---|---|---|
| 复制语义 | 隐式所有权转移(极其危险) | 禁止拷贝(编译错误) |
| 移动语义 | C++98 没有移动语义,用它模拟 | 显式所有权转移(std::move) |
| STL兼容性 | 不兼容(导致未定义行为) | 完全兼容(使用移动语义) |
| 安全性 | 低(运行时潜在崩溃) | 高(很多错误在编译时发现) |
| 清晰度 | 低(代码行为令人困惑) | 高(代码意图一目了然) |
第四部分:简单实现
虽然它十分不堪,但是我们还是得知道它大概是什么样子的:
Smart.h
#pragma once
#include<iostream>
using namespace std;
template<class T>
class AutoPtr
{
private:
T* _ptr;
public:
//基本的智能指针的需求
//自动管理资源
AutoPtr(T* ptr)
:_ptr(ptr)
{}
~AutoPtr()
{
if (_ptr != nullptr)
{
delete _ptr;
_ptr = nullptr;
}
}
//拷贝构造
//这是auto_ptr有问题的地方
AutoPtr(AutoPtr<T>& atp)
:_ptr(atp._ptr)
{
//管理权转移
atp._ptr = nullptr;
}
//赋值重载
AutoPtr<T>& operator=(AutoPtr<T>& atp)
{
if (this != &atp)
{
//先释放当前对象中的资源
if (_ptr != nullptr)
{
delete _ptr;
}
//然后将atp中的资源转移到当前对象中
_ptr = atp._ptr;
atp._ptr = nullptr;
}
return *this;
}
//指针的基本作用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
std::unique_ptr
第一部分:核心思想与“为什么是它?”
std::unique_ptr 是一个智能指针,它独占(拥有)其所指对象的所有权。
-
独占所有权 (Exclusive Ownership):
- 同一时间内,只有一个
unique_ptr可以拥有一个给定的对象。 - 当这个
unique_ptr被销毁时(例如,离开作用域),它所拥有的对象也会被自动销毁。
- 同一时间内,只有一个
-
移动语义 (Move Semantics):
unique_ptr不能被复制(拷贝构造和拷贝赋值被禁用)。这保证了所有权的唯一性。- 所有权可以从一个
unique_ptr转移(移动)到另一个。这是通过C++11的移动语义实现的。转移后,源unique_ptr变为nullptr,目标unique_ptr获得资源。
-
为何取代
auto_ptr:auto_ptr试图通过“拷贝即转移”的诡异语义来实现独占所有权,这在代码中极易引发误解和错误。unique_ptr通过禁止拷贝、强制显式移动(std::move)来明确表达所有权的转移,从语言层面杜绝了auto_ptr的隐患,并且与STL容器完美兼容。
第二部分:基本用法与代码示例
1. 创建与基本操作
首选方式:使用 std::make_unique (C++14引入)
#include <iostream>
#include <memory> // 必须包含的头文件
class MyClass {
public:
MyClass(int value) : data(value) { std::cout << "Constructed: " << data << "\n"; }
~MyClass() { std::cout << "Destroyed: " << data << "\n"; }
void print() const { std::cout << "Value: " << data << "\n"; }
int data;
};
int main() {
// 1. 创建 - 最推荐的方式,安全且高效
auto uPtr1 = std::make_unique<MyClass>(42); // 创建一个MyClass对象,初始值为42
uPtr1->print(); // 使用 -> 操作符访问成员
(*uPtr1).print(); // 使用 * 操作符解引用
// 2. 检查是否拥有对象 (since C++17 之前需用 uPtr1.get() != nullptr 判断)
if (uPtr1) { // 重载了 bool 操作符,可直接在条件中判断
std::cout << "uPtr1 owns an object.\n";
}
// 3. 释放所有权 (release)
MyClass* rawPtr = uPtr1.release(); // uPtr1 放弃所有权,返回裸指针并将其置为空
if (!uPtr1) {
std::cout << "uPtr1 released ownership. Now it's null.\n";
}
// 注意:现在你必须手动管理 rawPtr!
delete rawPtr;
// 4. 重置 (reset): 销毁当前对象并接管新对象(或置空)
auto uPtr2 = std::make_unique<MyClass>(100);
uPtr2.reset(); // 销毁对象,uPtr2变为nullptr
// uPtr2.reset(new MyClass(200)); // 销毁旧对象,并接管新分配的MyClass(200)
// 5. 获取底层裸指针 (get): 当你需要传递指针但不转移所有权时使用
auto uPtr3 = std::make_unique<MyClass>(300);
someLegacyFunction(uPtr3.get()); // 函数签名: void someLegacyFunction(MyClass* ptr);
return 0;
} // uPtr3 离开作用域,管理的对象(300)被自动销毁
输出:
Constructed: 42
Value: 42
Value: 42
uPtr1 owns an object.
uPtr1 released ownership. Now it's null.
Constructed: 100
Destroyed: 100
Constructed: 300
Destroyed: 300
2. 所有权的转移
这是 unique_ptr 的核心操作。
void ownershipTransfer() {
std::cout << "--- Ownership Transfer ---\n";
std::unique_ptr<MyClass> source = std::make_unique<MyClass>(999);
// 错误!!拷贝构造是被禁止的
// std::unique_ptr<MyClass> copy = source;
// 正确:通过移动构造函数转移所有权
std::unique_ptr<MyClass> destination = std::move(source);
if (!source) {
std::cout << "Source is now empty after move.\n";
}
if (destination) {
std::cout << "Destination now owns the object: " << destination->data << "\n";
}
// 同样,移动赋值也可以
source = std::move(destination); // 所有权又转移回 source
// 此时 destination 变为空
}
输出:
Constructed: 999
Source is now empty after move.
Destination now owns the object: 999
3. 在函数中使用
unique_ptr 明确了函数参数的语义。
// 1. sink 函数:接受所有权
// 按值传递。调用者必须使用 std::move,明确表示所有权被此函数夺取。
void takeOwnership(std::unique_ptr<MyClass> ptr) {
std::cout << "Sink function took ownership of: " << ptr->data << "\n";
} // ptr 离开作用域,对象被销毁
// 2. 源函数:返回所有权
// 工厂函数的经典用法,将新创建的对象的所有权返回给调用者。
std::unique_ptr<MyClass> createObject(int value) {
return std::make_unique<MyClass>(value); // 无需std::move,编译器会优化(RVO/NRVO)
}
// 3. 借用:不需要所有权,只需要访问
// 使用 const & 或裸指针(get()),避免不必要的所有权转移开销。
void useObject(const std::unique_ptr<MyClass>& ptr) { // 常量引用
if (ptr) {
ptr->print();
}
}
// 或者更推荐的方式:直接使用裸指针或引用,语义更清晰
void useObjectBetter(const MyClass* ptr) {
if (ptr) {
ptr->print();
}
}
void useObjectBest(const MyClass& obj) {
obj.print();
}
int main() {
// 使用 sink 函数
auto ptr = createObject(123); // 从工厂获取所有权
takeOwnership(std::move(ptr)); // 明确转移所有权给函数
// 使用借用函数
auto ptr2 = createObject(456);
useObject(ptr2); // 只是借用,所有权还在main函数的ptr2中
useObjectBetter(ptr2.get()); // 更好,明确表示只是用指针
useObjectBest(*ptr2); // 最好,明确表示只是用对象引用
return 0;
}
第三部分:高级特性与底层原理
1. 自定义删除器 (Custom Deleter)
默认情况下,unique_ptr 使用 delete 或 delete[] 来释放资源。但你可以指定一个自定义的删除器来处理特殊资源(如文件句柄、C库的 malloc/free 等)。
#include <cstdio>
// 1. 函数指针形式的删除器
void FileDeleter(std::FILE* filePtr) {
if (filePtr) {
std::fclose(filePtr);
std::cout << "File closed using function pointer.\n";
}
}
// 2. 仿函数(Functor)形式的删除器
struct FileCloser {
void operator()(std::FILE* filePtr) const {
if (filePtr) {
std::fclose(filePtr);
std::cout << "File closed using functor.\n";
}
}
};
// 3. Lambda 表达式形式的删除器
auto lambdaDeleter = [](std::FILE* filePtr) {
if (filePtr) {
std::fclose(filePtr);
std::cout << "File closed using lambda.\n";
}
};
int main() {
// 使用自定义删除器,模板参数需要指明类型和删除器类型
std::unique_ptr<std::FILE, decltype(&FileDeleter)> file1(std::fopen("test.txt", "w"), &FileDeleter);
std::unique_ptr<std::FILE, FileCloser> file2(std::fopen("test.txt", "w"), FileCloser());
std::unique_ptr<std::FILE, decltype(lambdaDeleter)> file3(std::fopen("test.txt", "w"), lambdaDeleter);
// 对于有状态的删除器(如捕获变量的lambda),unique_ptr的大小会增加
// 无状态的删除器(如函数指针、空仿函数、无捕获lambda)不会增加unique_ptr的大小
return 0;
} // 离开作用域时,自定义删除器会被调用,文件被关闭
2. 管理数组 (Array Specialization)
unique_ptr 对数组进行了特化,使用 delete[] 进行释放。
// 方法一:使用 unique_ptr<T[]> 特化版本
std::unique_ptr<int[]> arrayPtr(new int[10]); // 注意模板类型中的[]
for (int i = 0; i < 10; ++i) {
arrayPtr[i] = i; // 可以使用下标操作符
}
// 离开作用域时自动调用 delete[]
// 方法二:使用默认的 unique_ptr<T> 但提供自定义删除器(效果相同,但更繁琐)
std::unique_ptr<int, void(*)(int*)> arrayPtr2(new int[10], [](int* p) { delete[] p; });
// 方法三(最佳):优先使用 std::vector 或 std::array,除非有特殊需求。
3. 底层实现简析
unique_ptr 的本质是一个包装了原生指针的类模板,其简化实现如下:
template<class T>
class UniquePtr
{
private:
T* _ptr;
public:
//基本的智能指针的需求
//自动管理资源
UniquePtr(T* ptr = nullptr)
:_ptr(ptr)
{}
~UniquePtr()
{
if (_ptr != nullptr)
{
delete _ptr;
_ptr = nullptr;
}
}
//拷贝构造
UniquePtr(const UniquePtr<T>& up) = delete;
//赋值重载
UniquePtr<T>& operator=(const UniquePtr<T>& up) = delete;
//移动构造函数:转移资源管理权
UniquePtr(UniquePtr<T>&& up)
:_ptr(up._ptr)
{
up._ptr = nullptr;
}
//移动赋值重载
UniquePtr<T>& operator=(UniquePtr<T>&& up)
{
if (this != &up)
{
delete _ptr;
_ptr = up._ptr;
up._ptr = nullptr;
}
return *this;
}
//指针的基本作用
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
};
第四部分:实践
- 默认选择:当你需要智能指针时,应优先考虑
std::unique_ptr。它开销最小,语义最清晰。 - 使用
std::make_unique:创建unique_ptr的首选方法。它更安全(异常安全)、更高效(减少代码生成)。 - 明确所有权转移:使用
std::move来显式转移所有权,让代码意图一目了然。 - 函数传参:
- 传递所有权:按值传递
std::unique_ptr。 - 不传递所有权,仅访问:传递裸指针 (
ptr.get()) 或引用 (*ptr或const &)。避免使用const std::unique_ptr<T>&作为参数,这通常是一种设计异味。
- 传递所有权:按值传递
- 管理数组:使用
std::unique_ptr<T[]>,但更优先考虑std::vector。 - 管理非内存资源:使用自定义删除器,可以安全地管理文件、套接字、互斥锁等任何需要“释放”操作的资源。
std::shared_ptr
它是 C++11 中引入的用于管理共享所有权模型资源的智能指针,与独占所有权的 std::unique_ptr 形成互补。
第一部分:核心思想与解决的问题
std::shared_ptr 的核心是共享所有权。多个 shared_ptr 可以指向同一个对象,系统会通过引用计数来跟踪有多少个“所有者”。只有当最后一个指向该对象的 shared_ptr 被销毁或重置时,该对象才会被销毁。
它解决了什么问题?
想象一下这些场景:
- 你有一个对象,需要被多个其他对象使用,但你无法预知哪个对象会是最后一个使用它的。
- 你想将对象放入多个容器中(如多个
vector或map)。 - 你需要通过多个路径(例如多个线程)访问同一个对象,并希望在其不再被需要时自动释放。
在这些情况下,所有权是共享的、不明确的。std::unique_ptr 因其独占性无法胜任,而原生指针又无法自动管理生命周期。std::shared_ptr 正是为此而生。
一个简单的例子:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructed\n"; }
~MyClass() { std::cout << "MyClass Destroyed\n"; }
};
int main() {
std::cout << "Start of main\n";
// 创建第一个 shared_ptr,引用计数为 1
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();
{
// 复制构造,共享所有权,引用计数增加为 2
std::shared_ptr<MyClass> ptr2 = ptr1;
std::cout << "Inside inner scope. Use count: " << ptr1.use_count() << "\n"; // 输出 2
} // ptr2 离开作用域,析构,引用计数减为 1
std::cout << "Outside inner scope. Use count: " << ptr1.use_count() << "\n"; // 输出 1
// ptr1 离开作用域,引用计数减为 0,对象被销毁
std::cout << "End of main\n";
return 0;
}
输出:
Start of main
MyClass Constructed
Inside inner scope. Use count: 2
Outside inner scope. Use count: 1
MyClass Destroyed
End of main
第二部分:基本用法与代码示例
1. 创建与基本操作
首选方式:使用 std::make_shared
#include <iostream>
#include <memory>
class MyClass {
public:
int value;
MyClass(int v) : value(v) { std::cout << "Constructor: " << value << "\n"; }
~MyClass() { std::cout << "Destructor: " << value << "\n"; }
};
int main() {
// 1. 创建 - 最推荐的方式,高效且安全
std::shared_ptr<MyClass> shPtr1 = std::make_shared<MyClass>(10);
std::cout << "shPtr1 use_count: " << shPtr1.use_count() << "\n"; // 输出 1
// 2. 拷贝 - 共享所有权,引用计数增加
std::shared_ptr<MyClass> shPtr2 = shPtr1; // 拷贝构造
std::shared_ptr<MyClass> shPtr3;
shPtr3 = shPtr2; // 拷贝赋值
std::cout << "After copies, use_count: " << shPtr1.use_count() << "\n"; // 输出 3
// 3. 使用 -> 和 * 操作符访问对象
shPtr1->value = 20;
std::cout << "Value via shPtr3: " << shPtr3->value << "\n"; // 输出 20
// 4. 获取原始指针(不推荐,慎用!仅用于与不支持智能指针的API交互)
MyClass* rawPtr = shPtr1.get();
// 注意:绝对不要用这个 rawPtr 去创建另一个智能指针!
// 5. 重置 (reset): 停止共享当前对象,指向新对象或变为空
shPtr2.reset(); // 停止共享,引用计数减1
std::cout << "After shPtr2 reset, use_count: " << shPtr1.use_count() << "\n"; // 输出 2
shPtr3.reset(new MyClass(30)); // shPtr3 停止共享旧对象(10),开始拥有新对象(30)
std::cout << "After shPtr3 reset with new obj, use_count for old: " << shPtr1.use_count() << "\n"; // 输出 1
std::cout << "shPtr3 use_count for new obj: " << shPtr3.use_count() << "\n"; // 输出 1
// 6. 交换 (swap)
std::shared_ptr<MyClass> shPtr4 = std::make_shared<MyClass>(40);
shPtr1.swap(shPtr4); // shPtr1 现在指向 40, shPtr4 指向 10
return 0;
} // 所有 shared_ptr 离开作用域,各自管理的对象被正确销毁
2. 在函数中的使用
// 1. 按值传递:表示函数参与共享所有权(函数内部会增加引用计数)
void processByValue(std::shared_ptr<MyClass> ptr) {
std::cout << "In processByValue, use_count: " << ptr.use_count() << "\n";
} // ptr 析构,引用计数减少
// 2. 按引用传递:表示函数只使用对象,但不参与所有权管理,不会增加引用计数
void processByReference(const std::shared_ptr<MyClass>& ptr) {
if (ptr) { // 使用前最好检查
std::cout << "In processByReference, value: " << ptr->value << "\n";
}
// use_count 保持不变
}
// 3. 工厂函数:返回 shared_ptr
std::shared_ptr<MyClass> createMyClass(int value) {
return std::make_shared<MyClass>(value); // 返回值优化,很高效
}
int main() {
auto ptr = createMyClass(50);
processByValue(ptr); // 内部use_count变为2,函数返回后变回1
processByReference(ptr); // use_count 始终为1
return 0;
}
第三部分:原理与底层机制
std::shared_ptr 的实现比 unique_ptr 复杂得多。它通常包含两个指针:
- 指向被管理对象的指针。
- 指向控制块 (Control Block) 的指针。
控制块是一个动态分配的内含元数据的结构,它包括:
- 引用计数 (Use Count):记录有多少个
shared_ptr正拥有这个对象。 - 弱计数 (Weak Count):记录有多少个
weak_ptr正指向这个控制块(详见下一部分关于weak_ptr的讲解)。 - 删除器 (Deleter):通常是
std::default_delete,但可以被自定义。 - 分配器 (Allocator):用于分配控制块和对象内存,通常不使用。
std::make_shared 的优势(非常重要!)
std::make_shared<MyClass>(args...) 通常会执行一次单一的内存分配,这块内存同时容纳了控制块和 MyClass 对象本身。
auto p = std::make_shared<MyClass>(); // 一次分配
而直接使用 std::shared_ptr<MyClass>(new MyClass) 会执行两次内存分配:一次 new 分配对象,一次 new 分配控制块。
std::shared_ptr<MyClass> p(new MyClass); // 两次分配
因此,std::make_shared 不仅更安全(避免了内存泄漏的潜在风险),而且更高效(更少的内存分配、更好的局部性)。
简单实现
这里我们的控制块就简单些,就只包含引用计数。
那么我们要怎么设计这个智能指针才能达到我们想要的效果呢?

我们得这样写:
template<class T>
class SharedPtr
{
private:
T* _ptr = nullptr;
//如果想详细实现控制块的话,就创建一个控制块结构体
//这里换成结构体指针
int* _count = nullptr;
public:
//构造函数
SharedPtr(T* ptr)
:_ptr(ptr)
,_count(new int(1))
{
//简单的初始化
}
//这个函数是方便我们外部调用
//当其中一个智能指针对象不再指向它原来管理的资源后,我们就将它们共用的*_count--
//当*_count--后,如果*_count值为0,就释放它管理的空间以及_count指向的空间
//因为*_count为0,以为着最后一个管理该资源的智能指针对象也不再管理了,所以就要释放空间
void release()
{
if (_count != nullptr && --(*_count) == 0)
{
delete _ptr;
delete _count;
_ptr = nullptr;
_count = nullptr;
}
}
~SharedPtr()
{
release();
}
//拷贝构造
SharedPtr(const SharedPtr<T>& sp)
{
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
//赋值重载
SharedPtr<T>& operator=(const SharedPtr<T>& sp)
{
//避免管理同一块资源的智能指针对象互相赋值
//我们并不希望管理同一块资源的智能指针对象互相赋值
//而且这种判断也包含了自己给自己赋值的情况
if (_ptr != sp._ptr)
{
//因为现在要接管新的资源,所以要先将原来管理资源的计数*_count解决一下
//也就是把*_count--
//如果*_count==0就释放原资源
release();
_ptr = sp._ptr;
_count = sp._count;
++(*_count);
}
return *this;
}
// 移动构造函数
SharedPtr(SharedPtr<T>&& sp) noexcept {
_ptr = sp._ptr;
_count = sp._count;
sp._ptr = nullptr;
sp._count = nullptr;
}
// 移动赋值运算符
SharedPtr<T>& operator=(SharedPtr<T>&& sp) noexcept {
if (_ptr != sp._ptr) {
release();
_ptr = sp._ptr;
_count = sp._count;
sp._ptr = nullptr;
sp._count = nullptr;
}
return *this;
}
//简单的获取计数
int use_count()
{
return *_count;
}
//获取管理的资源的地址
T* get() const
{
return _ptr;
}
//指针的基本作用
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
//这里我顺便把WeakPtr也简单写了一下,大家接着往下看就ok了
template<class T>
class WeakPtr
{
public:
// RAII
WeakPtr()
:_ptr(nullptr)
{}
WeakPtr(const shared_ptr<T>& sp)
{
_ptr = sp.get();
}
WeakPtr<T>& operator=(const shared_ptr<T>& sp)
{
_ptr = sp.get();
return *this;
}
// 像指针一样
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
private:
T* _ptr;
};
第四部分:陷阱、挑战与 std::weak_ptr
1. 循环引用问题
这是 shared_ptr 最著名的陷阱。如果两个由 shared_ptr 管理的对象互相持有对方的 shared_ptr,它们的引用计数永远无法降到零,导致内存泄漏。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> bPtr; // A 持有 B 的 shared_ptr
~A() { std::cout << "A destroyed\n"; }
};
class B {
public:
std::shared_ptr<A> aPtr; // B 也持有 A 的 shared_ptr -> 循环引用!
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->bPtr = b; // A 引用 B,B的引用计数变为2
b->aPtr = a; // B 引用 A,A的引用计数变为2
std::cout << "a use_count: " << a.use_count() << "\n"; // 2
std::cout << "b use_count: " << b.use_count() << "\n"; // 2
return 0;
} // main 结束时,a 和 b 的引用计数都只减为1,不会为0!
// A 和 B 的对象都不会被销毁!内存泄漏!
输出中不会有 “A destroyed” 和 “B destroyed”。


2. 解决方案:std::weak_ptr
std::weak_ptr 是一种不控制对象生命周期的智能指针,它指向一个由 shared_ptr 管理的对象,但不增加其引用计数。它是专门为解决循环引用而设计的。
我们将上面例子中 class A 的成员改为 weak_ptr:
class A {
public:
std::weak_ptr<B> bPtr; // 改为 weak_ptr!打破循环
~A() { std::cout << "A destroyed\n"; }
};
// Class B 保持不变
class B {
public:
std::shared_ptr<A> aPtr;
~B() { std::cout << "B destroyed\n"; }
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->bPtr = b; // weak_ptr 的赋值不会增加B的引用计数(B的计数仍为1)
b->aPtr = a; // A的引用计数变为2
std::cout << "a use_count: " << a.use_count() << "\n"; // 2
std::cout << "b use_count: " << b.use_count() << "\n"; // 1 (关键!)
return 0;
} // main 结束时:
// 1. b 的引用计数从1减为0 -> B对象被销毁 -> B的析构函数被调用,输出 "B destroyed"
// 2. B被销毁导致其成员 aPtr 被销毁 -> A的引用计数从2减为1
// 3. a 的引用计数从1减为0 -> A对象被销毁 -> 输出 "A destroyed"
输出:
B destroyed
A destroyed
循环引用被成功打破!weak_ptr 的详细用法我们在下面。
第五部分:总结与最佳实践
- 适用场景:仅在需要共享所有权时使用
std::shared_ptr。默认情况下应优先考虑std::unique_ptr。 - 性能开销:
shared_ptr大小通常是原生指针的两倍(两个指针),且控制块的分配和原子引用计数的操作都有开销。不要滥用。 - 优先使用
std::make_shared:除非需要指定自定义删除器,否则总是使用std::make_shared来创建shared_ptr。它更安全、更快速。 - 警惕循环引用:在设计对象关系时,如果存在互相持有的情况,果断将其中一方改为
std::weak_ptr。 - 避免从原始指针创建多个
shared_ptr:MyClass* rawPtr = new MyClass; std::shared_ptr<MyClass> p1(rawPtr); // std::shared_ptr<MyClass> p2(rawPtr); // 灾难!两个独立的控制块会尝试删除同一个对象! - 线程安全:
shared_ptr的引用计数操作是原子的、线程安全的。但对其指向的对象的读写则需要额外的同步机制(如互斥锁)。
std::weak_ptr
好的,我们来深入探讨 std::weak_ptr。它是C++智能指针家族中比较特殊但非常重要的成员,专门设计用来与 std::shared_ptr 协同工作。
第一部分:std::weak_ptr 是什么?为什么需要它?
std::weak_ptr 是一种不控制所指向对象生命周期的智能指针。它指向一个由 std::shared_ptr 管理的对象,但不会增加该对象的引用计数。
它解决的两个核心问题:
-
打破
std::shared_ptr的循环引用
这是weak_ptr最主要的设计目的。当两个或多个shared_ptr相互引用时,会形成循环引用,导致引用计数永远无法归零,从而引发内存泄漏。weak_ptr通过不增加引用计数来打破这种循环。 -
临时访问共享资源而不影响其生命周期
当你需要观察一个可能已被释放的对象,但又不想阻止它被释放时,weak_ptr是理想的选择。它就像一个"安全的观察者"。
第二部分:基本特性与工作原理
1. 关键特性:
- 不拥有对象:不参与引用计数,不影响所指向对象的生命周期。
- 必须从
shared_ptr创建:不能直接指向对象,总是作为shared_ptr的"伴侣"存在。 - 检查对象是否存在:可以检查其观察的对象是否仍然有效。
- 临时获取所有权:可以通过
lock()方法获取一个临时的shared_ptr来安全地使用对象。
2. 底层原理:
weak_ptr 与 shared_ptr 共享同一个控制块。控制块中除了有强引用计数(use_count,由 shared_ptr 管理),还有一个弱引用计数(weak_count,由 weak_ptr 管理)。
- 当最后一个
shared_ptr被销毁时(use_count变为 0),对象被销毁,但控制块仍然存在(直到weak_count也变为 0)。 weak_ptr通过检查控制块中的信息来判断对象是否还存在。
第三部分:基本用法与代码示例
1. 创建与基本操作
weak_ptr 必须从 shared_ptr 创建或赋值。
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int v) : value(v) { std::cout << "Constructed: " << value << "\n"; }
~MyClass() { std::cout << "Destroyed: " << value << "\n"; }
void doSomething() { std::cout << "Doing something with " << value << "\n"; }
int value;
};
int main() {
// 1. 创建 shared_ptr
std::shared_ptr<MyClass> shPtr = std::make_shared<MyClass>(42);
// 2. 从 shared_ptr 创建 weak_ptr
std::weak_ptr<MyClass> weakPtr = shPtr;
std::cout << "shPtr use_count: " << shPtr.use_count() << "\n"; // 输出 1
// weak_ptr 不会增加引用计数!
// 3. 使用 lock() 获取临时的 shared_ptr 来访问对象
if (auto tempShared = weakPtr.lock()) {
// 如果对象还存在,lock() 返回一个有效的 shared_ptr
tempShared->doSomething();
std::cout << "Temp use_count: " << tempShared.use_count() << "\n"; // 输出 2
} // tempShared 离开作用域,引用计数减回 1
// 4. 释放 shared_ptr
shPtr.reset();
std::cout << "shPtr reset. Object should be destroyed.\n";
// 5. 再次尝试访问
if (auto tempShared = weakPtr.lock()) {
// 这里不会执行,因为对象已经不存在了
std::cout << "This won't be printed.\n";
} else {
std::cout << "Object is no longer available.\n";
}
return 0;
}
输出:
Constructed: 42
shPtr use_count: 1
Doing something with 42
Temp use_count: 2
Destroyed: 42
shPtr reset. Object should be destroyed.
Object is no longer available.
2. 解决循环引用问题(核心用途)
这是 weak_ptr 最重要的应用场景。
#include <iostream>
#include <memory>
class Controller; // 前向声明
class Device {
public:
std::string name;
// 使用 weak_ptr 而不是 shared_ptr 来打破循环引用
std::weak_ptr<Controller> controller;
Device(const std::string& n) : name(n) {
std::cout << "Device " << name << " created\n";
}
~Device() {
std::cout << "Device " << name << " destroyed\n";
}
void checkController() {
if (auto ctrl = controller.lock()) {
std::cout << name << ": Controller is still alive\n";
} else {
std::cout << name << ": Controller has been destroyed\n";
}
}
};
class Controller {
public:
std::string name;
std::shared_ptr<Device> device;
Controller(const std::string& n) : name(n) {
std::cout << "Controller " << name << " created\n";
}
~Controller() {
std::cout << "Controller " << name << " destroyed\n";
}
};
int main() {
// 创建相互引用的对象
auto ctrl = std::make_shared<Controller>("MainCtrl");
auto dev = std::make_shared<Device>("Sensor1");
// 建立双向关联
ctrl->device = dev; // shared_ptr 拷贝,引用计数增加
dev->controller = ctrl; // weak_ptr 赋值,不会增加引用计数
std::cout << "Ctrl use_count: " << ctrl.use_count() << "\n"; // 2 (main + device)
std::cout << "Dev use_count: " << dev.use_count() << "\n"; // 1 (只有ctrl->device)
// 检查控制器状态
dev->checkController();
// 释放控制器
ctrl.reset();
std::cout << "Controller released\n";
// 再次检查
dev->checkController();
// 现在只有 device 还有一个引用(来自 main 函数的 dev 变量)
std::cout << "Dev use_count: " << dev.use_count() << "\n"; // 1
return 0;
} // dev 离开作用域,引用计数归零,Device 也被正确销毁
输出:
Controller MainCtrl created
Device Sensor1 created
Ctrl use_count: 2
Dev use_count: 1
Sensor1: Controller is still alive
Controller released
Controller MainCtrl destroyed
Sensor1: Controller has been destroyed
Dev use_count: 1
Device Sensor1 destroyed
注意:循环引用被成功打破!两个对象都能被正确销毁。
第四部分:其他重要方法与用法
1. expired() 方法
快速检查对象是否已被释放,比 lock() 更轻量。
std::weak_ptr<MyClass> weakPtr;
// ... 一些操作后 ...
if (!weakPtr.expired()) {
// 对象还存在
auto obj = weakPtr.lock();
obj->doSomething();
} else {
// 对象已被释放
std::cout << "Object no longer exists\n";
}
2. 构造函数与赋值
std::shared_ptr<MyClass> shPtr = std::make_shared<MyClass>(100);
// 各种创建 weak_ptr 的方式
std::weak_ptr<MyClass> w1(shPtr); // 从 shared_ptr 构造
std::weak_ptr<MyClass> w2 = shPtr; // 从 shared_ptr 赋值
std::weak_ptr<MyClass> w3 = w2; // 从另一个 weak_ptr 拷贝
std::weak_ptr<MyClass> w4;
w4 = shPtr; // 从 shared_ptr 赋值
w4 = w3; // 从另一个 weak_ptr 赋值
3. 重置与交换
std::weak_ptr<MyClass> weakPtr;
// 重置为空的 weak_ptr
weakPtr.reset();
// 交换两个 weak_ptr
std::weak_ptr<MyClass> anotherWeakPtr;
weakPtr.swap(anotherWeakPtr);
第五部分:使用场景与最佳实践
1. 主要使用场景:
- 打破循环引用:如上所示,是主要用途。
- 缓存实现:缓存可以使用
weak_ptr来持有对象。如果内存紧张,对象可以被释放,当再次需要时可以从源重新加载。 - 观察者模式:观察者可以使用
weak_ptr来观察主题,避免影响主题的生命周期。 - 避免悬挂指针:比使用原始指针更安全地引用可能已被释放的对象。
2. 缓存示例:
class Cache {
private:
std::unordered_map<int, std::weak_ptr<LargeObject>> cache;
public:
std::shared_ptr<LargeObject> getObject(int id) {
auto it = cache.find(id);
if (it != cache.end()) {
// 尝试从缓存中获取
if (auto cachedObj = it->second.lock()) {
std::cout << "Cache hit for id " << id << "\n";
return cachedObj;
} else {
// 对象已被释放,从缓存中移除
cache.erase(it);
}
}
// 缓存未命中,创建新对象
std::cout << "Cache miss for id " << id << ", creating new object\n";
auto newObj = std::make_shared<LargeObject>(id);
cache[id] = newObj; // 存储 weak_ptr
return newObj;
}
};
3. 最佳实践:
- 总是检查:在使用
weak_ptr前,总是使用lock()或expired()检查对象是否还存在。 - 优先使用
lock():lock()是原子操作,能保证线程安全地获取shared_ptr。 - 不要直接解引用:不能直接使用
operator*或operator->,必须先转换为shared_ptr。 - 理解性能:
weak_ptr的操作(特别是lock())比原始指针访问稍慢,但提供了安全性。 - 与
shared_ptr配合使用:weak_ptr总是与shared_ptr成对出现,单独使用没有意义。
总结
std::weak_ptr 是C++智能指针系统中解决特定问题的精巧工具:
| 特性 | 描述 |
|---|---|
| 目的 | 打破 shared_ptr 的循环引用;安全地观察共享资源 |
| 所有权 | 不拥有对象,不影响生命周期 |
| 创建方式 | 必须从 shared_ptr 或另一个 weak_ptr 创建 |
| 核心方法 | lock():获取临时的 shared_ptrexpired():检查对象是否已被释放 |
| 性能 | 比原始指针稍慢,但提供了内存安全性 |
| 使用场景 | 循环引用、缓存、观察者模式等 |
更多推荐
所有评论(0)