当前位置: 首页 > news >正文

详解智能指针

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 为什么会出现智能指针?
  • 对于独占资源使用std::unique_ptr
  • 对于共享资源使用std::shared_ptr
  • 当std::shared_ptr可能悬空时使用std::weak_ptr
  • 优先考虑使用std::make_unique和std::make_shared而非new


为什么会出现智能指针?

原始指针在使用时的缺点:

  1. 它的声明不能指示所指到底是单个对象还是数组。
  2. 它的声明没有告诉你用完后是否应该销毁它,即指针是否拥有所指之物。
  3. 如果你决定你应该销毁指针所指对象,没人告诉你该用delete还是其他析构机制(比如将指针传给专门的销毁函数)。
  4. 如果你发现该用delete。 可能不知道该用单个对象形式(“delete”)还是数组形式(“delete[]”)。如果用错了结果是未定义的。
  5. 假设你确定了指针所指,知道销毁机制,也很难确定你在所有执行路径上都执行了恰为一次销毁操作(包括异常产生后的路径)。少一条路径就会产生资源泄漏,销毁多次还会导致未定义行为。
  6. 一般来说没有办法告诉你指针是否变成了悬空指针(dangling pointers),即内存中不再存在指针所指之物。在对象销毁后指针仍指向它们就会产生悬空指针。

智能指针(smart pointers)是解决这些问题的一种办法。
智能指针包裹原始指针,它们的行为看起来像被包裹的原始指针,但避免了原始指针的很多陷阱。所以我们应该更倾向于智能指针而不是原始指针。几乎原始指针能做的所有事情智能指针都能做,而且出错的机会更少。

在C++11中存在四种智能指针:std::auto_ptr,std::unique_ptr,std::shared_ptr, std::weak_ptr。都是被设计用来帮助管理动态对象的生命周期,在适当的时间通过适当的方式来销毁对象,以避免出现资源泄露或者异常行为。

对于独占资源使用std::unique_ptr

默认情况下,std::unique_ptr大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同。
std::unique_ptr体现了专有所有权语义。一个非空的std::unique_ptr始终拥有其指向的内容。
std::unique_ptr只允许移动,不允许拷贝

  • 移动std::unique_ptr会将所有权从源指针转移到目的指针。
  • 如果允许拷贝,则会有两个std::unique_ptr指向相同内容,导致重复销毁。
  • 析构时,非空std::unique_ptr会销毁指向的资源,默认通过对原始指针调用delete实现。
template<typename T, typename Deleter = std::default_delete<T>>
class unique_ptr {
private:T* ptr;           // 管理的原始指针Deleter deleter;  // 删除器,默认使用 std::default_delete<T>public:// 默认构造constexpr unique_ptr() noexcept : ptr(nullptr), deleter(Deleter()) {}// 构造:接收裸指针和删除器explicit unique_ptr(T* p) noexcept : ptr(p), deleter(Deleter()) {}unique_ptr(T* p, Deleter d) noexcept : ptr(p), deleter(d) {}// 禁止拷贝构造和拷贝赋值unique_ptr(const unique_ptr&) = delete;unique_ptr& operator=(const unique_ptr&) = delete;// 移动构造unique_ptr(unique_ptr&& other) noexcept: ptr(other.ptr), deleter(std::move(other.deleter)) {other.ptr = nullptr;}// 移动赋值unique_ptr& operator=(unique_ptr&& other) noexcept {if (this != &other) {reset(other.release());deleter = std::move(other.deleter);}return *this;}// 析构:释放资源~unique_ptr() {if (ptr) deleter(ptr);}// 释放资源所有权,返回裸指针T* release() noexcept {T* tmp = ptr;ptr = nullptr;return tmp;}// 重置,释放当前资源,并管理新资源void reset(T* p = nullptr) noexcept {if (ptr != p) {if (ptr) deleter(ptr);ptr = p;}}// 访问底层裸指针T* get() const noexcept { return ptr; }// 提供 operator* 和 operator-> 方便使用T& operator*() const noexcept { return *ptr; }T* operator->() const noexcept { return ptr; }// 检查是否为空explicit operator bool() const noexcept { return ptr != nullptr; }
};

在这里插入图片描述

默认情况下,销毁将通过delete进行,但是在构造过程中,std::unique_ptr对象可以被设置为使用自定义删除器:当资源需要销毁时可调用的任意函数(或者函数对象,包括lambda表达式)。

#include <iostream>
#include <memory>// 自定义删除器,使用 delete
struct myDeleter {void operator()(int* ptr) const {log<< "Using custom deleter with delete!" ;delete ptr;  }
};int main() {std::unique_ptr<int, myDeleter> ptr(new int(42));std::cout << "Value: " << *ptr << std::endl;return 0;
}

对于共享资源使用std::shared_ptr

std::shared_ptr通过引用计数(reference count)来确保它是否是最后一个指向某种资源的指针,引用计数关联资源并跟踪有多少std::shared_ptr指向该资源。
std::shared_ptr构造函数通常递增引用计数值,析构函数递减值。
如果std::shared_ptr在计数值递减后发现引用计数值为零,没有其他std::shared_ptr指向该资源,它就会销毁资源。
在这里插入图片描述

#include <iostream>
#include <atomic>template <typename T, typename Deleter = std::default_delete<T>>
struct ControlBlock {std::atomic<int> use_count;     // shared_ptr 引用计数std::atomic<int> weak_count;    // weak_ptr 引用计数T* ptr;                         // 指向被管理的资源Deleter deleter;                // 用户指定的删除器ControlBlock(T* p, Deleter d = Deleter()): use_count(1), weak_count(1), ptr(p), deleter(d) {}void release_shared() {if (--use_count == 0) {deleter(ptr);           // 调用自定义删除器release_weak();         // 尝试销毁控制块}}void release_weak() {if (--weak_count == 0) {delete this;            // 控制块自身使用默认 delete}}
};// shared_ptr 类
template <typename T>
class SharedPtr {
private:T* ptr;                          // 原始资源指针ControlBlock<T>* control_block;  // 控制块指针public:// 构造函数explicit SharedPtr(T* p = nullptr) : ptr(p) {if (ptr) {control_block = new ControlBlock<T>(ptr);} else {control_block = nullptr;}std::cout << "SharedPtr Constructor: " << ptr << std::endl;}// 拷贝构造函数SharedPtr(const SharedPtr& other) : ptr(other.ptr), control_block(other.control_block) {if (control_block) {++control_block->use_count; // 增加引用计数}std::cout << "SharedPtr Copy Constructor: " << ptr << std::endl;}//std::shared_ptr构造函数通常递增引用计数值。// 移动构造函数SharedPtr(SharedPtr&& other) noexcept : ptr(other.ptr), control_block(other.control_block) {other.ptr = nullptr;other.control_block = nullptr;std::cout << "SharedPtr Move Constructor" << std::endl;}// 析构函数~SharedPtr() {if (control_block && --control_block->use_count == 0) {std::cout << "SharedPtr Destructor: " << ptr << std::endl;delete control_block;  // 删除控制块}}// 重载赋值运算符SharedPtr& operator=(const SharedPtr& other) {if (this != &other) {if (control_block && --control_block->use_count == 0) {delete control_block;}ptr = other.ptr;control_block = other.control_block;if (control_block) {++control_block->use_count;  // 增加引用计数}}return *this;}// 访问资源T* get() const { return ptr; }// 解引用操作符T& operator*() const { return *ptr; }// 指针访问操作符T* operator->() const { return ptr; }// 获取引用计数int use_count() const { return control_block ? control_block->use_count : 0; }
};

引用计数暗示着性能问题:

  • std::shared_ptr大小是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还包含一个指向资源控制块原始指针。
  • 控制块的内存必须动态分配。shared_ptr 的引用计数(控制块)必须单独用 new 分配内存,因为它不能放在被管理的对象里。被管理的对象根本不知道自己被 shared_ptr 所管理了,更不知道引用计数在哪儿。所以,shared_ptr 只能自己额外分配一块内存来记录引用次数。
  • 递增递减引用计数必须是原子性的。在多线程程序中,同一个资源可能被多个 shared_ptr 管理,而这些 shared_ptr 可能同时被不同线程读写。比如:一个线程销毁了 shared_ptr,它会把引用计数减 1;另一个线程可能刚好拷贝了这个 shared_ptr,它会把引用计数加 1。如果这些加减操作不是原子的,引用计数就可能错乱。
#include <iostream>
#include <memory>
#include <chrono>
#include <vector>constexpr size_t N = 1'000'000; // 可调大小,防止内存爆炸struct MyObject {int value;MyObject(int v) : value(v) {}
};void test_raw_pointer() {std::vector<MyObject*> pointers;auto start = std::chrono::high_resolution_clock::now();for (size_t i = 0; i < N; ++i) {pointers.push_back(new MyObject(i));}auto mid = std::chrono::high_resolution_clock::now();long long sum = 0;for (auto ptr : pointers) {sum += ptr->value;}for (auto ptr : pointers) {delete ptr;}auto end = std::chrono::high_resolution_clock::now();size_t total_mem = N * sizeof(MyObject);std::cout << "[原始指针] \n分配时间: "<< std::chrono::duration<double>(mid - start).count() << " 秒, ";std::cout << "访问与释放时间: "<< std::chrono::duration<double>(end - mid).count() << " 秒, ";std::cout << "总内存估算: " << total_mem / (1024.0 * 1024) << " MB, ";std::cout << "求和值: " << sum << std::endl;std::cout << std::endl;
}void test_shared_ptr() {std::vector<std::shared_ptr<MyObject>> pointers;auto start = std::chrono::high_resolution_clock::now();for (size_t i = 0; i < N; ++i) {pointers.push_back(std::make_shared<MyObject>(i));}auto mid = std::chrono::high_resolution_clock::now();long long sum = 0;for (const auto& ptr : pointers) {sum += ptr->value;}pointers.clear();  // 引用计数归零auto end = std::chrono::high_resolution_clock::now();size_t control_block_size = 24; // 控制块估算size_t total_mem = N * (sizeof(MyObject) + control_block_size);std::cout << "[shared_ptr] \n分配时间: "<< std::chrono::duration<double>(mid - start).count() << " 秒, ";std::cout << "访问与释放时间: "<< std::chrono::duration<double>(end - mid).count() << " 秒, ";std::cout << "总内存估算: " << total_mem / (1024.0 * 1024) << " MB, ";std::cout << "求和值: " << sum << std::endl;
}int main() {test_raw_pointer();test_shared_ptr();system("pause");  return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
类似std::unique_ptr,std::shared_ptr使用delete作为资源的默认销毁机制,但是它也支持自定义的删除器。这种支持有别于std::unique_ptr。对于std::unique_ptr来说,删除器类型是智能指针类型的一部分。对于std::shared_ptr则不是:

在 std::unique_ptr 中,删除器是智能指针类型的一部分
std::unique_ptr<T, Deleter> ptr;在 shared_ptr中,删除器是控制块(ControlBlock)的一部分,而不是直接作为智能指针类型的一部分。
std::shared_ptr<T> ptr(p, Deleter{});auto loggingDel = [](Widget *pw)        //自定义删除器{                     makeLogEntry(pw);delete pw;};std::unique_ptr<                        //删除器类型是Widget, decltype(loggingDel)        //指针类型的一部分> upw(new Widget, loggingDel);std::shared_ptr<Widget>                 //删除器类型不是spw(new Widget, loggingDel);        //指针类型的一部分

这种设置使得std::shared_ptr的设计更为灵活
你可以创建多个 std::shared_ptr,它们指向相同类型 T 的对象。但使用不同的删除器,不会导致智能指针类型不同。
比如:考虑有两个std::shared_ptr,每个自带不同的删除器。
在这里插入图片描述

控制块创建需要注意的点:

  • std::make_shared总是创建一个控制块。

  • 当从独占指针(即std::unique_ptr)上构造出std::shared_ptr时会创建控制块。

    std::unique_ptr<MyObject> uptr = std::make_unique<MyObject>();
    std::shared_ptr<MyObject> sptr = std::move(uptr);   //此时创建了一个新的控制块
    
  • 当从原始指针上构造出std::shared_ptr时会创建控制块。

    auto pw = new Widget;                           //pw是原始指针
    std::shared_ptr<Widget> spw1(pw, loggingDel);   //为*pw创建控制块
    

    以下情况会出现未定义行为:

    auto pw = new Widget;                           //pw是原始指针
    std::shared_ptr<Widget> spw1(pw, loggingDel);   //为*pw创建控制块
    //将同样的原始指针传递给spw2的构造函数会再次为*pw创建一个控制块
    std::shared_ptr<Widget> spw2(pw, loggingDel);   //为*pw创建第二个控制块
    

    因此*pw有两个引用计数值,每一个最后都会变成零,然后最终导致*pw销毁两次。第二个销毁会产生未定义行为。

注意点1:
避免把原始指针传给 std::shared_ptr 构造函数,推荐使用std::make_shared。但是std::make_shared不接受删除器参数。

auto spw1 = std::make_shared<Widget>();  

注意点2:
如果必须传给std::shared_ptr构造函数原始指针,直接传new出来的结果,不要传指针变量。

std::shared_ptr<Widget> spw1(new Widget, loggingDel); //直接使用new的结果
std::shared_ptr<Widget> spw2(spw1);         //spw2使用spw1一样的控制块

当std::shared_ptr可能悬空时使用std::weak_ptr

std::weak_ptr通常从std::shared_ptr上创建。当从std::shared_ptr上创建std::weak_ptr时两者指向相同的对象,但是std::weak_ptr不会影响所指对象的引用计数:

auto spw = std::make_shared<Widget>();//spw创建之后,指向的Widget的引用计数为1。
std::weak_ptr<Widget> wpw(spw); //wpw指向与spw所指相同的Widget。RC仍为1
spw = nullptr;                  //RC变为0,Widget被销毁。//wpw现在悬空

悬空的std::weak_ptr被称作已经expired(过期)。
我们通常期望的是:检查std::weak_ptr是否已经过期,如果没有过期则访问其指向的对象,但是因为std::weak_ptr缺少解引用操作,没有办法写这样的代码。

if (!wp.expired()) {// ❌ 想这么写,但无法解引用 weak_ptr//std::weak_ptr 是一个非拥有型的观察者指针,它不控制资源的生命周期。wp->doSomething();  
}

需要一个原子操作(避免其他线程对指向这对象的std::shared_ptr重新赋值或者析构)检查std::weak_ptr是否已经过期,如果没有过期就访问所指对象。

因为std::weak_ptr缺少解引用操作, 那么怎么实现访问所指对象呢?
这可以通过从std::weak_ptr创建std::shared_ptr来实现

第一种形式:
std::weak_ptr::lock,它返回一个std::shared_ptr,如果std::weak_ptr过期这个std::shared_ptr为空:

std::shared_ptr<Widget> spw1 = wpw.lock();  //如果wpw过期,spw1就为空
struct ControlBlock {T* ptr;                    // 原始指针size_t shared_count;       // shared_ptr 的引用计数size_t weak_count;         // weak_ptr 的引用计数(含控制块自身)
};std::shared_ptr<T> lock() const {if (control_block->shared_count > 0)return std::shared_ptr<T>(control_block);elsereturn std::shared_ptr<T>();  // 空
}
std::shared_ptr<Widget> spw3(wpw);          //如果wpw过期,抛出std::bad_weak_ptr异常

另外一个使用std::weak_ptr的例子,考虑一个持有三个对象A、B、C的数据结构,A和C共享B的所有权,因此持有std::shared_ptr:
在这里插入图片描述

假定从B指向A的指针也很有用。应该使用哪种指针?
[图片]

有三种选择:

  • 原始指针。使用这种方法,如果A被销毁,但是C继续指向B,B就会有一个指向A的悬空指针。而且B不知道指针已经悬空,所以B可能会继续访问,就会导致未定义行为。
  • std::shared_ptr。这种设计,A和B都互相持有对方的std::shared_ptr,导致的std::shared_ptr环状结构(A指向B,B指向A)阻止A和B的销毁。A和B都被泄漏:程序无法访问它们,但是资源并没有被回收。
  • std::weak_ptr。这避免了上述两个问题。如果A被销毁,B指向它的指针悬空,但是B可以检测到这件事。尤其是,尽管A和B互相指向对方,B的指针不会影响A的引用计数,因此在没有std::shared_ptr指向A时不会导致A无法被销毁。

优先考虑使用std::make_unique和std::make_shared而非new

std::make_shared是C++11标准的一部分,但std::make_unique不是。它从C++14开始加入标准库。如果在使用C++11,一个基础版本的std::make_unique是很容易自己写出,如下:

template<typename T, typename... Ts>
std::unique_ptr<T> make_unique(Ts&&... params)
{return std::unique_ptr<T>(new T(std::forward<Ts>(params)...));
}

make_unique只是将它的参数完美转发到所要创建的对象的构造函数,从new产生的原始指针里面构造出std::unique_ptr,并返回这个std::unique_ptr。

auto upw1(std::make_unique<Widget>());      //使用make函数
std::unique_ptr<Widget> upw2(new Widget);   //不使用make函数auto spw1(std::make_shared<Widget>());      //使用make函数
std::shared_ptr<Widget> spw2(new Widget);   //不使用make函数

使用make函数的原因和异常安全有关。假设我们有个函数按照某种优先级处理Widget:

void processWidget(std::shared_ptr<Widget> spw, int priority);

假设我们有一个函数来计算相关的优先级

int computePriority();

我们在调用processWidget时使用了new而不是std::make_shared:

processWidget(std::shared_ptr<Widget>(new Widget),  //潜在的资源泄漏!computePriority());

这段代码可能在new一个Widget时发生泄漏。
调用的代码和被调用的函数都用std::shared_ptr,且std::shared_ptr就是设计出来防止泄漏的。它们会在最后一个std::shared_ptr销毁时自动释放所指向的内存,这段代码怎么会泄漏呢?

答案和编译器将源码转换为目标代码有关。在运行时,一个函数的实参必须先被计算,这个函数再被调用,所以在调用processWidget之前,必须执行以下操作,processWidget才开始执行:

  • 表达式“new Widget”必须计算,例如,一个Widget对象必须在堆上被创建
  • 负责管理new出来指针的std::shared_ptr构造函数必须被执行
  • computePriority必须运行

编译器不需要按照执行顺序生成代码。“new Widget”必须在std::shared_ptr的构造函数被调用前执行,因为new出来的结果作为构造函数的实参,但computePriority可能在这之前,之后,或者之间执行。也就是说,编译器可能按照这个执行顺序生成代码:

  1. 执行“new Widget”
  2. 执行computePriority
  3. 运行std::shared_ptr构造函数

如果按照这样生成代码,并且在运行时computePriority产生了异常,那么第一步动态分配的Widget就会泄漏。因为它永远都不会被第三步的std::shared_ptr所管理了。

使用std::make_shared可以防止这种问题。

processWidget(std::make_shared<Widget>(),   //没有潜在的资源泄漏computePriority());

在运行时,std::make_shared和computePriority其中一个会先被调用。

  • 如果是std::make_shared先被调用,在computePriority调用前,动态分配Widget的原始指针会安全的保存在作为返回值的std::shared_ptr中。如果computePriority产生一个异常,那么std::shared_ptr析构函数将确保管理的Widget被销毁。
  • 如果首先调用computePriority并产生一个异常,那么std::make_shared将不会被调用,因此也就不需要担心动态分配Widget。

std::make_shared的另一个特性(与直接使用new相比)是效率提升。使用std::make_shared允许编译器生成更小,更快的代码,并使用更简洁的数据结构。考虑以下对new的直接使用:

std::shared_ptr<Widget> spw(new Widget);

这段代码需要进行内存分配,但它实际上执行了两次:直接使用new需要为Widget进行一次内存分配,为控制块再进行一次内存分配。
[图片]

如果使用std::make_shared代替:

auto spw = std::make_shared<Widget>();

一次分配足矣。这是因为std::make_shared分配一块内存,同时容纳了Widget对象和控制块。这种优化减少了程序的静态大小,因为代码只包含一个内存分配调用,并且它提高了可执行代码的速度,因为内存只分配一次。

相关文章:

  • 会计 - 财务报告
  • IO之详解cin(c++IO关键理解)
  • Java基础复习之static
  • 【数据集成与ETL 04】dbt实战指南:现代化数据转换与SQL代码管理最佳实践
  • 修改Typora快捷键
  • XCTF-misc-Test-flag-please-ignore
  • 【redis——缓存雪崩(Cache Avalanche)】
  • 实习记录1
  • wpa_supplicant:无线网络连接的“智能管家”
  • cpu微码大全 微码添加工具 八九代cpu针脚屏蔽图
  • 17_Flask部署到网络服务器
  • Vue3中v-bind=“$attrs“用法讲解
  • 人工智能学习25-BP代价函数
  • 计网复习知识(16)传输层及其协议功能
  • SCADE Suite / Scade 6 官方参考材料介绍
  • 无监督的预训练和有监督任务的微调
  • PH热榜 | 2025-06-14
  • 附录:对于头结点单向链表的优化方法
  • 关于钉钉的三方登录
  • Trino权威指南
  • 渭南市建网站/b站视频推广的方法有哪些
  • 物业网站模板下载/我想做地推怎么找渠道
  • 做毛绒玩具在什么网站上找客户/百家号seo怎么做
  • 建立网站ftp/全国最好网络优化公司
  • 哪些网站是discuz做/百度关键词排名原理
  • 邢台做wap网站的公司/福州360手机端seo