std::enable_shared_from_this

要理解为什么用 this 指针创建新的 shared_ptr 会导致"两个独立控制块管理同一个对象",关键在于 shared_ptr 如何识别"同一个对象"并共享控制块

核心原因:shared_ptr 无法通过原始指针识别已有控制块

std::shared_ptr 的控制块(存储引用计数的结构)与 "原始指针"和"shared_ptr 的创建方式" 强绑定,但 对象本身不存储"是否已有控制块"的信息

具体来说:

  • 当你第一次用原始指针(如 new MyClass())创建 shared_ptr 时(如 p1),shared_ptr 会为这个原始指针 新建一个控制块,并将引用计数初始化为 1。
  • shared_ptr 无法知道"这个原始指针是否已经被其他 shared_ptr 管理"(因为控制块是独立于对象的外部结构,对象本身不记录控制块的地址)。
  • 因此,如果你再用同一个原始指针(如 this)创建另一个 shared_ptr,新的 shared_ptr 会认为"这是一个全新的、未被管理的指针",从而 再新建一个独立的控制块

代码对比:正常共享 vs 错误共享

我们通过两段代码对比,看为什么"用 this 创建 shared_ptr 会导致独立控制块":

情况1:正确共享(通过拷贝 shared_ptr
cpp 复制代码
#include <memory>
#include <iostream>

class MyClass {
public:
    ~MyClass() {
        std::cout << "MyClass 被销毁" << std::endl;
    }
};

int main() {
    // 1. 第一次创建 shared_ptr,为 new 返回的原始指针创建控制块 A(引用计数=1)
    std::shared_ptr<MyClass> p1(new MyClass());
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1

    // 2. 通过拷贝 p1 创建 p2:共享控制块 A(引用计数=2)
    std::shared_ptr<MyClass> p2 = p1; 
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 2
    std::cout << "p2 引用计数: " << p2.use_count() << std::endl; // 输出 2

    // 离开作用域时:p2 先销毁(计数=1),p1 再销毁(计数=0 → 销毁对象)
    return 0;
}

为什么正确?
p2 是通过拷贝 p1 创建的,p1 会将自己的控制块地址传递给 p2,因此 p2 直接复用控制块 A,引用计数正确递增。

在上述代码中,p2可以理解为p1浅拷贝 ,但需要结合std::shared_ptr的特性来具体分析:

1. 浅拷贝的核心特征

浅拷贝的本质是:拷贝操作仅复制指针(或引用)本身,而不复制指针所指向的底层对象。拷贝后,原对象和拷贝对象会指向同一块内存空间,共享同一个底层实例。

2. shared_ptr的拷贝行为分析

在代码中,p2 = p1std::shared_ptr的拷贝操作,其行为完全符合浅拷贝的特征:

  • 未复制底层对象p1p2指向的是同一个MyClass实例(通过new MyClass()创建的对象),并未因为p2的拷贝而创建新的MyClass对象(从输出结果看,MyClass的构造函数只执行一次,析构函数也只执行一次,可证明这一点)。
  • 共享底层对象p1p2共同管理同一个MyClass实例,通过引用计数(use_count)跟踪共享关系。拷贝后引用计数从1增加到2,说明两者指向同一个对象。

3. 与原始指针浅拷贝的区别

需要注意的是,shared_ptr的浅拷贝与原始指针的浅拷贝效果不同但本质一致

  • 原始指针的浅拷贝可能导致"double free"问题(多个指针释放同一内存)。
  • shared_ptr通过引用计数机制避免了这个问题:只有当最后一个shared_ptr销毁时(引用计数减为0),才会释放底层MyClass对象。

但无论如何,两者的拷贝行为本质都是"复制指针本身,不复制底层对象",因此p2p1的拷贝属于浅拷贝。

结论

p2p1的浅拷贝。因为p2拷贝的是shared_ptr本身(而非其指向的MyClass对象),拷贝后p1p2指向同一个MyClass实例,共享该对象的所有权(通过引用计数管理),符合浅拷贝"只复制指针、不复制底层对象"的核心特征。

情况2:错误共享(通过 this 指针创建 shared_ptr
cpp 复制代码
#include <memory>
#include <iostream>

class MyClass {
public:
    // 用 this 指针创建新的 shared_ptr
    std::shared_ptr<MyClass> get_self() {
        return std::shared_ptr<MyClass>(this); // this 是原始指针
    }

    ~MyClass() {
        std::cout << "MyClass 被销毁" << std::endl;
    }
};

int main() {
    // 1. p1 为原始指针(new 返回)创建控制块 A(计数=1)
    std::shared_ptr<MyClass> p1(new MyClass());
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1

    // 2. p2 通过 this 指针创建:this 与 p1 指向同一个对象,但 p2 不知道控制块 A 的存在
    // 因此 p2 会为 this 指针新建控制块 B(计数=1)
    std::shared_ptr<MyClass> p2 = p1->get_self();
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1(仍用控制块 A)
    std::cout << "p2 引用计数: " << p2.use_count() << std::endl; // 输出 1(用控制块 B)

    // 离开作用域时:
    // p1 销毁 → 控制块 A 计数=0 → 销毁对象
    // p2 销毁 → 控制块 B 计数=0 → 再次销毁已被释放的对象(二次释放,未定义行为)
    return 0;
}

为什么错误?
p2 是通过 this 指针(原始指针)创建的,它无法知道 this 已经被 p1 的控制块 A 管理(因为对象本身不存储控制块信息)。因此 p2 会新建控制块 B,导致两个控制块同时管理同一个对象。

关键结论

  • shared_ptr 的控制块是 "跟着创建方式走" 的:通过已有 shared_ptr 拷贝时,共享控制块;通过原始指针(如 this)创建时,新建控制块。
  • 对象本身 不包含任何与控制块相关的信息 ,因此 shared_ptr 无法通过原始指针判断"这个对象是否已经被其他 shared_ptr 管理"。
  • std::enable_shared_from_this 的作用就是让对象内部能"记住"已有的控制块(通过内部的 weak_ptr),从而通过 shared_from_this() 创建的 shared_ptr 能共享已有控制块,避免重复创建。

要理解 std::enable_shared_from_this 的作用,我们需要先明确 std::shared_ptr 的核心工作机制,再分析直接使用 this 指针创建 shared_ptr 会导致的问题,最后看 std::enable_shared_from_this 如何解决这些问题。

1. std::shared_ptr 的核心机制:控制块与引用计数

std::shared_ptr 是 C++ 中用于管理动态内存的智能指针,其核心功能依赖两个关键组件:

  • 被管理的对象 :即我们通过 new 分配的内存(如 new MyClass())。
  • 控制块(control block) :一个独立的辅助结构,内部存储:
    • 引用计数(use_count):记录当前有多少个 shared_ptr 指向同一个对象。
    • 弱引用计数(weak_count):记录当前有多少个 weak_ptr 指向同一个对象。
    • 其他辅助信息(如对象的销毁器 deleter 等)。

当我们创建第一个 shared_ptr 时(如 std::shared_ptr<MyClass> p(new MyClass())),会同时创建一个控制块,此时引用计数为 1

当我们通过拷贝构造或赋值操作创建新的 shared_ptr 时(如 auto p2 = p),新的 shared_ptr共享同一个控制块 ,并将引用计数递增(此时 use_count 变为 2)。

当某个 shared_ptr 被销毁(如离开作用域)时,会将控制块的引用计数递减;当引用计数减为 0 时,控制块会负责销毁被管理的对象(调用 delete),并释放自身内存。

2. 直接用 this 指针创建 shared_ptr 的问题

假设我们有一个类 MyClass,其内部有一个成员函数需要返回指向自身的 shared_ptr。如果直接通过 this 指针创建 shared_ptr(如 return std::shared_ptr<MyClass>(this)),会导致多个独立的控制块,进而引发未定义行为。

问题代码示例:
cpp 复制代码
#include <memory>
#include <iostream>

class MyClass {
public:
    // 错误做法:通过 this 指针创建 shared_ptr
    std::shared_ptr<MyClass> get_self() {
        return std::shared_ptr<MyClass>(this); // 新创建一个控制块
    }

    ~MyClass() {
        std::cout << "MyClass 被销毁" << std::endl;
    }
};

int main() {
    // 创建第一个 shared_ptr,此时会创建控制块 A,引用计数 = 1
    std::shared_ptr<MyClass> p1(new MyClass());
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1

    // 调用 get_self(),通过 this 创建 p2,此时会创建控制块 B,引用计数 = 1
    std::shared_ptr<MyClass> p2 = p1->get_self();
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1(p1 仍用控制块 A)
    std::cout << "p2 引用计数: " << p2.use_count() << std::endl; // 输出 1(p2 用控制块 B)

    // 当 p1 和 p2 离开作用域时:
    // 1. p1 销毁:控制块 A 计数减为 0 → 销毁 MyClass 对象。
    // 2. p2 销毁:控制块 B 计数减为 0 → 再次尝试销毁 MyClass 对象(已被销毁)。
    // 结果:二次释放(double free),属于未定义行为(可能崩溃)。
    return 0;
}
问题分析:
  • p1 初始化时创建了控制块 A ,引用计数为 1(管理 MyClass 对象)。
  • p2 通过 this 指针创建,会新建控制块 B ,引用计数为 1(同样指向 MyClass 对象)。
  • 两个控制块完全独立,彼此不知道对方的存在。当 p1p2 都销毁时,会分别尝试释放同一个 MyClass 对象,导致二次释放(未定义行为)。

3. std::enable_shared_from_this 的解决方案

std::enable_shared_from_this 是一个模板基类,其作用是让被 shared_ptr 管理的对象能够安全地返回一个指向自身的 shared_ptr,且该 shared_ptr共享已有的控制块,避免创建新的控制块。

使用方法:

  1. 让类继承 std::enable_shared_from_this<MyClass>(注意模板参数是类自身)。
  2. 在成员函数中通过 shared_from_this() 方法获取指向自身的 shared_ptr,而非直接使用 this
正确代码示例:
cpp 复制代码
#include <memory>
#include <iostream>

// 让类继承 enable_shared_from_this<MyClass>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
    // 正确做法:通过 shared_from_this() 获取 shared_ptr
    std::shared_ptr<MyClass> get_self() {
        return shared_from_this(); // 共享已有的控制块
    }

    ~MyClass() {
        std::cout << "MyClass 被销毁" << std::endl;
    }
};

int main() {
    // 创建第一个 shared_ptr,创建控制块 A,引用计数 = 1
    std::shared_ptr<MyClass> p1(new MyClass());
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 1

    // 调用 get_self(),通过 shared_from_this() 创建 p2,共享控制块 A
    std::shared_ptr<MyClass> p2 = p1->get_self();
    std::cout << "p1 引用计数: " << p1.use_count() << std::endl; // 输出 2(p1 和 p2 共享控制块 A)
    std::cout << "p2 引用计数: " << p2.use_count() << std::endl; // 输出 2

    // 当 p1 和 p2 离开作用域时:
    // 1. p2 销毁:控制块 A 计数减为 1。
    // 2. p1 销毁:控制块 A 计数减为 0 → 销毁 MyClass 对象。
    // 结果:对象只被销毁一次,行为正确。
    return 0;
}
原理分析:
  • MyClass 继承 std::enable_shared_from_this<MyClass> 后,enable_shared_from_this 内部会存储一个 weak_ptr(弱引用),该 weak_ptr 会指向 MyClass 对象的控制块。
  • 当调用 shared_from_this() 时,会通过内部的 weak_ptr 升级为 shared_ptr,这个新的 shared_ptr共享已有的控制块 (即 p1 创建的控制块 A),因此引用计数会正确递增(从 1 变为 2)。
  • 最终 p1p2 销毁时,引用计数会依次递减,当减为 0 时,对象只会被销毁一次,避免了二次释放。

4. 注意事项

  • shared_from_this() 只能在对象已被 shared_ptr 管理时调用。如果对象未被 shared_ptr 管理(如直接在栈上创建,或通过 this 调用但无 shared_ptr 指向它),调用 shared_from_this() 会抛出 std::bad_weak_ptr 异常。

    错误示例:

    cpp 复制代码
    MyClass obj;
    auto p = obj.get_self(); // 错误:obj 未被 shared_ptr 管理,调用 shared_from_this() 会抛异常
  • 不要在构造函数中调用 shared_from_this()。因为此时对象尚未完全构造,enable_shared_from_this 内部的 weak_ptr 还未与控制块关联,调用会导致未定义行为。

总结

std::enable_shared_from_this 的核心作用是:让被 shared_ptr 管理的对象能够安全地返回指向自身的 shared_ptr,确保所有 shared_ptr 共享同一个控制块,从而保证引用计数正确,避免二次释放或对象提前析构等未定义行为

相关推荐
SNAKEpc121382 小时前
QML和Qt Quick
c++·qt
hansang_IR2 小时前
【题解】洛谷 P4286 [SHOI2008] 安全的航线 [递归分治]
c++·数学·算法·dfs·题解·向量·点积
GanGuaGua2 小时前
Linux系统:线程的互斥和安全
linux·运维·服务器·c语言·c++·安全
怀旧,3 小时前
【C++】18. 红⿊树实现
开发语言·c++
lsnm3 小时前
【LINUX网络】IP——网络层
linux·服务器·网络·c++·网络协议·tcp/ip
宁静致远20213 小时前
【C++设计模式】第三篇:观察者模式(别名:发布-订阅模式、模型-视图模式、源-监听器模式)
c++·观察者模式·设计模式
Cinema KI4 小时前
内存管理这一块
c++
1白天的黑夜14 小时前
哈希表-49.字母异位词分组-力扣(LeetCode)
c++·leetcode·哈希表
深耕AI5 小时前
【MFC典型类和函数:CString的字符串魔法与Afx全局函数的便利店】
c++·mfc