C/C++ 智能指针
前言
不可否认的,我们每一个人在编写代码的时候,都遇见过程序异常跳转的情况:
比如,在程序1中,正常我们的流程是main->func()->func中的动态开辟空间->函数体中依次执行(函数体有正常情况和异常情况的throw)->函数内动态内存回收给操作系统->函数回收->main异常捕获->main执行结束;
在上面的例子中,就一般我们需要的就是正常情况的发生,但是始终会有意外的发生,出现了问题:当执行函数体出现异常,程序会从throw处跳转,然后跳到main中的异常捕获,也就是跳过了 函数内动态内存回收给操作系统->函数回收 这一步骤,导致没有手动回收内存,将内存还给操作系统,造成内存泄漏。那我们的C++自C++11后就提供了智能指针来解决这一问题
内存泄漏:在程序运行时,动态分配的内存没有被正确释放,导致这些内存无法再被程序访问,也无法由操作系统回收,从而占用了系统的内存资源。随着时间的推移,内存泄漏可能导致程序占用的内存越来越多,最终可能导致程序崩溃或系统性能严重下降。
C/C++内存错误
在实际的 C/C++ 开发中,我们经常会遇到诸如 coredump、segmentfault 之类的内存问题,使用指针也会出现各种问题,比如:
Core Dump通常发生在程序遇到无法处理的错误时,例如:
- 访问非法内存(如空指针解引用、越界访问等)
- 使用无效的指针或数组
- 程序执行非法操作(例如除以零、无限递归等)
Segmentation Fault (Segfault):程序在试图访问非法内存区域时发生的一种错误。操作系统通过访问控制机制(如虚拟内存、内存保护)来防止程序访问它不应该访问的内存区域。如果程序尝试访问没有分配的内存,或者试图写入只读内存,操作系统会终止程序并发送一个 "段错误" 信号。
原因:
- 空指针解引用:程序尝试访问一个指针所指向的内存,而该指针为 NULL 或没有被正确初始化。
- 数组越界访问:程序访问了数组或缓冲区外的内存,可能会覆盖其他数据或导致崩溃。
- 非法内存访问:程序访问了没有权限访问的内存区域,例如读取未初始化的内存,或者访问已经被释放的内存(悬空指针)。
- 栈溢出:递归函数没有正确的终止条件,导致调用栈空间耗尽,进而引发段错误。
野指针:未初始化或已经被释放的指针被称为野指针
空指针:指向空地址的指针被称为空指针
内存泄漏:如果在使用完动态分配的内存后忘记释放,就会造成内存泄漏,长时间运行的程序可能会消耗大量内存。
悬空指针:指向已经释放的内存的指针被称为悬空指针
内存泄漏和悬空指针的混合:在一些情况下,由于内存泄漏和悬空指针共同存在,程序可能会出现异常行为。
智能指针
智能指针 是 C++ 提供的一种对象,它包装了普通指针,并通过自动管理内存和资源,帮助开发者避免手动管理内存的复杂性。智能指针的主要目的是自动管理内存的分配和释放,避免内存泄漏和悬空指针等问题。
根据官方文档,C++11之后官方大肆传播,鼓励使用智能指针,由于智能指针利用 RAII(Resource Acquisition Is Initialization)机制来管理资源,确保资源在对象生命周期结束时自动释放。
RAII(资源获取即初始化)
RAII 是 C++ 中非常重要的编程理念。它的核心思想是:资源的获取和释放应当与对象的生命周期绑定。当对象被创建时,资源(如内存、文件句柄、网络连接等)被获取;当对象销毁时,资源自动释放。这种方式保证了资源的自动管理,从而减少了手动释放资源时可能出现的错误,如内存泄漏、资源泄漏、悬空指针等。
智能指针正是 RAII 的一个经典应用,通过将动态分配的内存或其他资源封装在智能指针对象中,利用智能指针的析构函数来自动释放资源。这样,程序员不需要显式地调用 delete 或 close 等操作,避免了忘记释放资源或提前释放的风险。
智能指针的设计理念
智能指针的设计本质上是让对象的行为更像指针,但同时又具备自动管理资源的特性。传统的指针是原始的内存地址,而智能指针是封装了原始指针并且能够自动处理资源的对象。
具体来说,智能指针的目标是提供类似于原始指针的操作(如指向某个对象、解引用、判断指针有效性等),但它在内存管理上比普通指针更安全。智能指针通过其特殊的析构机制来确保所管理的对象在指针离开作用域时得到正确释放,避免了内存泄漏和双重释放等问题。
智能指针的基本原理
- 对象的生命周期管理:智能指针通过其析构函数管理对象的销毁。例如,std::unique_ptr 和 std::shared_ptr 都会在智能指针离开作用域时自动释放它们所指向的内存。
- 资源管理的封装:智能指针通常将资源的释放封装在析构函数中。例如,std::unique_ptr 和 std::shared_ptr 会在其生命周期结束时自动调用 delete 或 delete[] 来释放内存。
- 指针操作的接口:智能指针通过重载 *(解引用操作符)和 ->(成员访问操作符)使得使用它的方式接近原始指针。例如,std::unique_ptr<int> 可以像普通指针一样访问其管理的对象。
- 引用计数机制:std::shared_ptr 使用引用计数来管理共享对象的生命周期。每次创建一个新的 shared_ptr 对象并指向相同的资源时,引用计数会增加;当引用计数变为 0 时,所管理的资源会自动释放。
如图,就是一个智能指针一个很好的简单示例,真正的智能指针比这复杂的多
我们来查阅一下官方文档
这些都是我们的智能指针
我们依次来看看,auto_ptr,unique_ptr,shared_ptr,weak_ptr。
auto_ptr【官方文档】
auto_ptr 是 C++98 和 C++03 中的一个智能指针类,用于管理动态分配的内存,并在对象生命周期结束时自动释放内存。它的目的是帮助开发者避免手动调用 delete 来释放内存,从而减少内存泄漏的风险。
然而, auto_ptr 在 C++11 中已被弃用,并且被 unique_ptr 所取代,因为 auto_ptr 存在一些缺陷,尤其是在处理对象所有权转移时存在问题。
下面让我们来详细了解一下auto_ptr的隐患。
我们先来看看auto_ptr的工作原理:
std::auto_ptr 是一个模板类,它将动态分配的内存封装起来,并在 auto_ptr 对象销毁时自动释放内存。它的行为类似于 C++ 中的智能指针。
特点:
- 自动释放资源:auto_ptr 会在作用域结束时自动释放它所管理的对象。
- 不可复制:auto_ptr 的一个非常重要的特点是它采用了“传递所有权”的方式,这意味着当你将 auto_ptr 赋值给另一个 auto_ptr 时,原 auto_ptr 不再拥有那个资源。这一行为是通过重载拷贝构造函数和赋值操作符实现的。
std::auto_ptr 的问题
- 所有权转移的隐式行为:auto_ptr 的所有权转移在复制和赋值操作中隐式发生,这使得代码的意图不明确,容易出错。
例如,赋值操作不会进行深拷贝,而是转移所有权,原对象变为空指针,这可能导致意外的错误。相比之下,std::unique_ptr 的所有权转移是显式的,使用 std::move 来清楚地表达所有权转移。
- 不支持与标准容器的配合使用:由于 auto_ptr 会在赋值时转移所有权,它不适合在标准容器(如 std::vector)中使用,因为容器在复制或重新分配时可能会引发意外的所有权转移。
关键问题:所有权转移
std::auto_ptr 采用的是所有权转移(Move Semantics),即当你将一个 auto_ptr 赋值给另一个 auto_ptr 时,资源的所有权会从原 auto_ptr 转移到新 auto_ptr。这意味着原 auto_ptr 将变为空指针,不再管理原始对象。这种行为在某些情况下可能导致意外错误,例如,两个 auto_ptr 试图同时销毁同一个对象。
我们用代码来看看示例:
1.auto_ptr的使用
#include <iostream>
#include <memory> // 引入 auto_ptr
class MyClass {
public:
MyClass() {
std::cout << "MyClass 构造\n";
}
~MyClass() {
std::cout << "MyClass 析构\n";
}
};
int main() {
// 使用 auto_ptr
std::auto_ptr<MyClass> ptr1(new MyClass); // ptr1 拥有 MyClass 对象
// ptr1 的所有权转移给 ptr2,ptr1 不再拥有 MyClass 对象
std::auto_ptr<MyClass> ptr2 = ptr1;
// 这里不再需要手动 delete,ptr2 被销毁时会自动调用 MyClass 的析构函数
}
2.所有权转移的示例
#include <iostream>
#include <memory> // 引入 auto_ptr
class MyClass {
public:
MyClass() {
std::cout << "MyClass 构造\n";
}
~MyClass() {
std::cout << "MyClass 析构\n";
}
};
int main() {
std::auto_ptr<MyClass> ptr1(new MyClass); // ptr1 拥有 MyClass 对象
std::auto_ptr<MyClass> ptr2 = ptr1; // ptr1 的所有权转移给 ptr2,ptr1 不再拥有对象
// ptr1 在这里已经变为空指针,不能再访问它
return 0; // ptr2 离开作用域时销毁对象
}
总结
std::auto_ptr 是 C++98 和 C++03 中提供的一种智能指针,但由于其在资源管理和所有权转移方面的缺陷,它在 C++11 中被弃用,并在 C++17 中被移除。现代 C++ 推荐使用 std::unique_ptr 和 std::shared_ptr 来替代 std::auto_ptr,这些类提供了更清晰、更安全的资源管理方式,符合现代 C++ 的移动语义和所有权模型。
所以auto_ptr的特性:抢占式的智能指针,对资源唯一占有。
unique_ptr【官方文档】
unique_ptr实际上是为了填补auto_ptr的缺点所诞生的,其与auto_ptr的区别是,unique_ptr不允许进行拷贝、赋值等操作,因为其内部限制了构造函数。其它部分的特性与auto_ptr相似。
unique_ptr示例:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructed\n"; }
~MyClass() { std::cout << "MyClass destructed\n"; }
};
void process(std::unique_ptr<MyClass> ptr) {
std::cout << "Processing\n";
// ptr 进入作用域,超出作用域时会自动释放资源
}
int main() {
std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();
// std::unique_ptr<MyClass> ptr2 = ptr1; // 错误:禁止拷贝
std::unique_ptr<MyClass> ptr2 = std::move(ptr1); // 正确:所有权转移
process(std::move(ptr2)); // 传递所有权
// ptr2 在这里已经不再拥有资源,资源已经在 process 函数中释放
return 0;
}
shared_ptr【官方文档】
std::shared_ptr 允许多个智能指针共同管理同一个资源,并通过引用计数来自动释放资源。
std::shared_ptr 的基本原理
std::shared_ptr 通过引用计数机制管理动态分配的内存。当多个 shared_ptr 实例指向同一个对象时,每个 shared_ptr 会增加该对象的引用计数;当一个 shared_ptr 被销毁或重新指向其他对象时,它会减少该对象的引用计数。只有当引用计数归零时,资源才会被释放。
shared_ptr多个对象可以同时指向同一块同台开辟的内存空间,指向同一动态内存空间的shared_ptr共享一个计数,当额外有1个shared_ptr对象指向其时,计数加1,当解除1次指向时,计数减1。当计数为1并且在解除一次引用时,调用析构函数来释放资源。
这就造成了很多问题,如果 shared_ptr 之间存在循环引用(例如,两个 shared_ptr 对象互相持有对方),引用计数永远不会归零,也就永远不会被操作系统回收从而导致内存泄漏。
示例:
#include<iostream>
#include<memory>
using namespace std;
class ListNode
{
public:
shared_ptr<ListNode> next;
shared_ptr<ListNode> prev;
int val;
ListNode(int val = 0) : val(val){}
~ListNode() {
cout << "ListNode 析构" << endl;
}
};
int main() {
shared_ptr<ListNode> p1(new ListNode(10));
shared_ptr<ListNode> p2(new ListNode(20));
p1->next = p2;
p2->prev = p1;
/*这里暴露了问题,shared_ptr 只有在引用计数为1并且再次解除引用时才会释放资源,
在这里,程序会释放p1,p2,使得我们无法访问new出来的ListNode结点,也就不能完成
计数为1并且再次解除引用这一操作 ,就导致了内存泄漏
*/
return 0;
}
上述代码是一个简单的链表结点的定义,对于正常的数据我们知道在程序结束后自动进行释放回收
我们画图来看看:
显而易见,当循环引用的时候,shared_ptr就有其自己的问题了,造成内存泄漏
weak_ptr【官方文档】
因为shared_ptr存在的问题,就迎来了weak_ptr的出现,解决了shared_ptr的问题,作为一个”观察者”当weak_ptr指向shared_ptr已经指向过的位置时,不会增加其计数,weak_ptr在解除指向资源时,也不会减少计数。
std::weak_ptr与 std::shared_ptr 和 std::unique_ptr 配合使用,主要用于解决 循环引用 问题。它可以在不增加引用计数的情况下,观察一个由 std::shared_ptr 管理的对象。
1. std::weak_ptr 的基本概念
std::weak_ptr 允许你持有一个对象的“弱引用”,它不会增加该对象的引用计数。这样,std::weak_ptr 就不会影响对象的生命周期,也不会导致循环引用(比如两个对象相互持有 shared_ptr)。
std::shared_ptr 的问题:
std::shared_ptr 使用引用计数来管理对象的生命周期,多个 shared_ptr 可以共享同一个对象。当所有 shared_ptr 都销毁时,资源被释放。然而,如果两个 shared_ptr 互相持有对方,形成一个循环引用,那么它们都永远不会被销毁,导致内存泄漏。
std::weak_ptr 解决了这个问题,因为它不会改变引用计数。
用法示例:
#include<iostream>
#include<memory>
using namespace std;
class MyClass
{
public:
MyClass() {
cout << "构造" << endl;
}
~MyClass() {
cout << "析构" << endl;
}
void hello() {
cout << "hello";
}
};
int main() {
shared_ptr<MyClass> sp1 = make_shared<MyClass>();
weak_ptr<MyClass> wp1 = sp1; // wp1 是一个弱引用
// wp1 并不增加引用计数
cout << "sp1 use_count: " << sp1.use_count() << endl;
// 检查 wp1 是否指向有效对象
if (auto sp2 = wp1.lock()) { // lock() 尝试获取 shared_ptr
sp2->hello();
}
else {
cout << "Object is expired!" << endl;
}
return 0;
}
深入理解引用计数
对于shared_ptr和weak_ptr,我们都知道引用计数,但是实现引用计数的细节,那就模糊不清了…
其实引用计数本身是使用指针实现的,也就是将计数变量存储在堆上,所以共享指针的shared_ptr 就存储一个指向堆内存的指针
shared_ptr的double free问题
double free 问题就是一块内存空间或者资源被释放两次。
double free 可能是下面这些原因造成的:
- 直接使用原始指针创建多个 shared_ptr,而没有使用 shared_ptr 的 make_shared 工厂函数,从而导致多个独立的引用计数。
- 循环引用,即两个或多个 shared_ptr 互相引用,导致引用计数永远无法降为零,从而无法释放内存。
如何解决 double free
方法:
- 使用make_shared 函数来创建share_ptr对象,而不是使用原始指针,这样可以确保所有shared_ptr对象共享相同的引用计数。
- 对于可能产生循环引用的情况,使用 weak_ptr。weak_ptr 是一种不控制对象生命周期的智能指针,它只观察对象,而不增加引用计数。这可以避免循环引用导致的内存泄漏问题。
但是使用 weak_ptr 也有几点注意事项:
- 如果需要访问 weak_ptr 所指向的对象,需要将std::weak_ptr 通过 weak_ptr::lock() 临时转换为std::shared_ptr.
- 在使用lock()方法之前,应当检查使用 std::weak_ptr::expired() 检查 std::weak_ptr是否有效,即它所指向的对象是否仍然存在。
#include <iostream>
#include <memory>
class B; // 前向声明
class A {
public:
std::shared_ptr<B> b_ptr;
~A() {
std::cout << "A destructor called" << std::endl;
}
};
class B {
public:
std::weak_ptr<A> a_ptr; // 使用 weak_ptr 替代 shared_ptr
~B() {
std::cout << "B destructor called" << std::endl;
}
};
int main() {
{
std::shared_ptr<A> a = std::make_shared<A>();
std::shared_ptr<B> b = std::make_shared<B>();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 对 A 使用 weak_ptr
} // a 和 b 离开作用域,它们的析构函数会被正确调用
std::cout << "End of main" << std::endl;
return 0;
}
现在让我们来细看一下引用计数共享机制
在底层,std::shared_ptr 会使用一个 控制块 来管理对象的生命周期,包括:
- 引用计数 (Reference Count):用于追踪当前有多少个 shared_ptr 正在指向这个对象。引用计数有两个主要的部分:
- 强引用计数 (strong reference count):管理持有对象的 shared_ptr 的数量。
- 弱引用计数 (weak reference count):如果使用了 std::weak_ptr,则此计数用于追踪有多少个 weak_ptr 引用对象。
- 对象的指针:存储实际对象的内存地址,shared_ptr 会通过这个指针来访问对象。
std::make_shared 的作用
std::make_shared 用来创建对象,并同时返回一个 shared_ptr。它的核心优点是将对象的内存分配和引用计数的控制块分配在同一块内存区域,这样做可以提高性能和节省内存。
控制块结构
控制块通常包括以下内容:
- 对象指针:指向实际的对象。
- 引用计数:
- 强引用计数 (strong reference count):用于跟踪有多少个 shared_ptr 引用这个对象。
- 弱引用计数 (weak reference count):用于跟踪有多少个 weak_ptr 引用这个对象。
- 删除器(如果有的话):通常用于自定义的删除策略。
内存布局
当我们使用 std::make_shared 时,底层会为 对象 和 控制块 分配一个连续的内存块。具体来说,内存布局大致如下:
- 控制块:包含引用计数、对象指针、删除器等信息。
- 对象内存:实际对象的内存空间。
这段代码中
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass constructor\n"; }
~MyClass() { std::cout << "MyClass destructor\n"; }
void hello() { std::cout << "Hello, world!" << std::endl; }
};
int main() {
// 使用 make_shared 创建 shared_ptr
std::shared_ptr<MyClass> sp1 = std::make_shared<MyClass>();
sp1->hello();
// 共享引用
std::shared_ptr<MyClass> sp2 = sp1; // sp2 与 sp1 共享引用计数
std::cout << "sp1 use_count: " << sp1.use_count() << std::endl;
std::cout << "sp2 use_count: " << sp2.use_count() << std::endl;
return 0;
}
在底层,std::make_shared 会:
- 为 MyClass 对象和引用计数分配一块内存。
- 在控制块中初始化强引用计数为 1。
- 返回一个 shared_ptr,它指向这个控制块,并管理对象的生命周期。
底层内存结构:
------------------------------------------------------
| 控制块 (Control Block) |
------------------------------------------------------
| 强引用计数 (strong_ref_count) | 弱引用计数 (weak_ref_count) |
------------------------------------------------------
| 对象指针 (MyClass*) |
------------------------------------------------------
| 对象 (MyClass) |
- 控制块 存储对象的指针和引用计数,管理 shared_ptr 的生命周期。
- 对象 部分存储实际的 MyClass 对象。
引用计数的管理
- 当你创建 shared_ptr 时,引用计数从 1 开始。
- 当一个新的 shared_ptr 被复制到另一个时(比如 sp2 = sp1),引用计数增加。
- 当一个 shared_ptr 被销毁时,引用计数减少。
- 当引用计数降为 0 时,对象会被销毁并释放内存。
手撕shared_ptr
我们要实现简化版本的shared_ptr,关键在于引用计数
我们要考虑这些:
在智能指针类中存储原始指针和引用计数
构造函数中就开辟空间,为原始指针和引用计数分配内存
拷贝构造以及赋值符重载中要更新引用计数
析构函数中要对引用计数进行递减操作,当计数为0,要删除对象和计数,回收内存到操作系统
我们来看代码
简化版1:
#pragma once
#include<iostream>
using namespace std;
template<typename T>
class MyShared_ptr
{
public:
// 构造函数
explicit MyShared_ptr(T* ptr = nullptr) : ptr_(ptr),count_(ptr ? new size_t(1) : nullptr){}
// 析构
~MyShared_ptr() {
// 销毁函数
destroy();
}
// 拷贝构造
MyShared_ptr(const MyShared_ptr& other) :ptr_(other.ptr_), count_(other.count_) {
if (count_) {
++(*count_);
}
}
// 赋值
MyShared_ptr& operator=(const MyShared_ptr& other) {
if (this != &other) {
destroy();
ptr_ = other.ptr_;
count_ = other.count_;
if (count_) {
++(*count_);
}
}
return *this;
}
// 运算符重载
T& operator*() const {
return *ptr_;
}
T* operator->() const {
return ptr_;
}
T* get()const {
return ptr_;
}
size_t use_count() const {
return count_ ? *count_ : 0;
}
private:
// 销毁函数
void destroy() {
if (count_ && --(*count_) == 0) {
delete ptr_;
delete count_;
}
}
T* ptr_;
size_t* count_;
};
简化版2:
#pragma once
#include<iostream>
#include<atomic>
using namespace std;
// 引用计数控制块
template<typename T>
struct ControlBlock
{
T* ptr; // 对象指针
size_t count_; // 引用计数
// 初始化
ControlBlock(T* p) : ptr(p), count_(1) {}
~ControlBlock() {
delete ptr;
}
};
// 自定义 shared_ptr
template<typename T>
class MySharedPtr
{
public:
// 默认构造
MySharedPtr() : controlBlock(nullptr) {}
// 接受对象构造
explicit MySharedPtr(T* ptr) : controlBlock(new ControlBlock<T>(ptr)) {}
// 拷贝构造
MySharedPtr(const MySharedPtr& other) : controlBlock(other.controlBlock) {
if (controlBlock) {
controlBlock->count_++;
}
}
// 移动构造
MySharedPtr(MySharedPtr&& other) noexcept : controlBlock(other.controlBlock) {
other.controlBlock = nullptr;
}
// 析构
~MySharedPtr() {
if (controlBlock && --controlBlock->count_ == 0) {
delete controlBlock;
}
}
// 赋值
MySharedPtr& operator=(const MySharedPtr& other) {
if (this != &other) {
// 减少当前控制块的引用计数
if (controlBlock && --controlBlock->count_ == 0) {
delete controlBlock;
}
// 复制其他对象的控制块
controlBlock = other.controlBlock;
if (controlBlock) {
controlBlock->count_++;
}
}
return *this;
}
// 移动赋值
MySharedPtr& operator=(MySharedPtr&& other) noexcept {
if (this != &other) {
// 释放当前控制块
if (controlBlock && --controlBlock->count_ == 0) {
delete controlBlock;
}
// 转移所有权
controlBlock = other.controlBlock;
other.controlBlock = nullptr;
}
return *this;
}
// 运算符重载
T& operator*() const {
return *controlBlock->ptr;
}
T* operator->() const {
return controlBlock->ptr;
}
T* get()const {
return controlBlock->ptr;
}
size_t use_count() const {
return controlBlock ? controlBlock->count_ : 0;
}
private:
ControlBlock<T>* controlBlock; // 控制块指针
};
我们分别对两种简化版本的shared_ptr进行测试,让我们来看看
测试1:
class MyClass {
public:
MyClass() { std::cout << "MyClass 构造函数\n"; }
~MyClass() { std::cout << "MyClass 析构函数\n"; }
void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};
void Test_shared_ptr() {
{
MyShared_ptr<MyClass> ptr1(new MyClass());
{
MyShared_ptr<MyClass> ptr2 = ptr1;
ptr1->do_something();
ptr2->do_something();
std::cout << "引用计数: " << ptr1.use_count() << std::endl;
}
std::cout << "引用计数: " << ptr1.use_count() << std::endl;
}
}
运行结果:
测试2:
class MyClass2 {
public:
MyClass2(int val) : value(val) {
std::cout << "MyClass 构造函数,值 = " << value << std::endl;
}
~MyClass2() {
std::cout << "MyClass 析构函数,值 = " << value << std::endl;
}
void print() {
std::cout << "MyClass 值: " << value << std::endl;
}
private:
int value;
};
void Test_myshared_ptr() {
// 创建一个 MyClass2 对象并通过 MySharedPtr 管理
MySharedPtr<MyClass2> ptr1(new MyClass2(10));
std::cout << "ptr1 创建后引用计数: " << ptr1.use_count() << std::endl;
{
MySharedPtr<MyClass2> ptr2 = ptr1; // 拷贝构造,引用计数增加
std::cout << "ptr2 拷贝后引用计数: " << ptr1.use_count() << std::endl;
ptr2->print(); // 通过 ptr2 访问 MyClass2 对象
} // ptr2 离开作用域,引用计数减 1
std::cout << "ptr2 离开作用域后引用计数: " << ptr1.use_count() << std::endl;
// 移动语义
MySharedPtr<MyClass2> ptr3 = std::move(ptr1); // 移动构造
std::cout << "ptr3 移动后引用计数: " << ptr3.use_count() << std::endl;
// 直接访问 MyClass2 对象
ptr3->print();
}
运行结果:
对于控制块对引用计数的控制要深入理解一下!
weak_ptr的深入理解
许多人的理解只是停留在避免std::shared_ptr出现相互引用,导致对象无法析构,内存无法释放的问题。
当然,并不是说这种用法有什么不对,恰恰相反,它是一个非常经典的使用场景。
但是,它的用途不仅仅只是解决循环引用
std::weak_ptr从概念上,它是一个智能指针,相对于std::shared_ptr,它对于引用的对象是“弱引用”的关系。
简单来说,它并不“拥有”对象本身。
std::weak_ptr并不拥有对象,在另外一个std::shared_ptr想要拥有对象的时候,它并不能做决定,需要转化到一个std::shared_ptr后才能使用对象。所以std::weak_ptr只是一个“引路人”而已。
说了这么多,那么std::weak_ptr除了解决相互引用的问题,还能做什么?
答案是:一切应该不具有对象所有权,又想安全访问对象的情况。