智能指针循环引用

1. 什么是std::shared_ptr?

        std::shared_ptr 是 C++ 中的一种智能指针,目的是用来自动管理动态分配的内存。当一个 std::shared_ptr 对象被销毁时,它会自动释放它指向的内存。每个 std::shared_ptr 都有一个引用计数,用于记录有多少个 shared_ptr 指向同一块内存。引用计数的值会随着 shared_ptr 被复制、销毁而增加或减少。

        当引用计数变为 0 时,表示没有任何 shared_ptr 再指向这个对象,它会自动释放内存。以下是代码举例:

#include <iostream>
#include <memory>

int main() {
    auto ptr = std::make_shared<int>(10); // 创建一个 shared_ptr,指向值为 10 的整数
    std::cout << *ptr << std::endl;        // 输出 10

    auto ptr2 = ptr;                      // ptr2 也指向同一个整数,引用计数加 1
    std::cout << *ptr2 << std::endl;       // 输出 10

    // 当 ptr 和 ptr2 都超出作用域时,内存会被自动释放
}

        在上面的例子中,ptr 和 ptr2 都指向同一块内存,它们的引用计数为 2。shared_ptr 的引用计数会在它们被销毁时自动减少,最终引用计数变为 0,内存就会被释放。

2. 什么是循环引用?

        循环引用是指两个或多个对象互相持有对方的 std::shared_ptr,形成一个闭环。这个闭环导致它们的引用计数永远不为 0,从而内存无法被释放。即当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。以下是循环引用的情况的示例:

        假设有两个类 A 和 B,它们互相持有对方的 std::shared_ptr。这样就会形成一个循环引用。

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> ptrB;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::shared_ptr<A> ptrA;
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();  // 创建 A 的 shared_ptr
    auto b = std::make_shared<B>();  // 创建 B 的 shared_ptr

    a->ptrB = b;  // A 持有 B 的 shared_ptr
    b->ptrA = a;  // B 持有 A 的 shared_ptr

    // 结束时 a 和 b 的引用计数都不会变成 0,导致内存泄漏
}

在上面的代码中:

  • a 持有一个指向 B 的 std::shared_ptr,而 b 持有一个指向 A 的 std::shared_ptr。
  • 这就形成了一个环:A 需要 B,B 需要 A,导致这两个对象的引用计数都不会降到 0。
  • 这样,当 main() 函数结束时,a 和 b 都不会被销毁,它们指向的内存永远无法释放,从而发生内存泄漏。

3. 如何解决循环引用?

        为了打破循环引用,我们需要用一个不增加引用计数的指针。这里我们可以使用 std::weak_ptr。

        std::weak_ptr 是一种不增加引用计数的智能指针。它可以观察一个由 std::shared_ptr 管理的对象,但不会影响引用计数。使用 std::weak_ptr 可以打破循环引用,从而避免内存泄漏。以下是解决循环引用的代码

#include <iostream>
#include <memory>

class B; // 前向声明

class A {
public:
    std::shared_ptr<B> ptrB;
    ~A() { std::cout << "A destroyed\n"; }
};

class B {
public:
    std::weak_ptr<A> ptrA; // 使用 weak_ptr
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();  // 创建 A 的 shared_ptr
    auto b = std::make_shared<B>();  // 创建 B 的 shared_ptr

    a->ptrB = b;  // A 持有 B 的 shared_ptr
    b->ptrA = a;  // B 持有 A 的 weak_ptr

    // 结束时,A 和 B 的引用计数可以正确归 0
    // A 和 B 都会被销毁
}

  在这个版本的代码中:
        - B 类中的 ptrA 成员变成了 std::weak_ptr<A>,而不是 std::shared_ptr<A>。
        - 由于 std::weak_ptr 不会增加引用计数,它不会干扰 A 和 B 的引用计数。
        - 当 main 函数结束时,a 和 b 的引用计数会正确归零,导致它们的内存被释放。

4. 引用计数的原理?

        std::shared_ptr 的引用计数(reference count)是通过内部的控制块(control block)来管理的,每个 shared_ptr 实际上是包含两个部分的:

  1. 指向实际对象的指针:也就是 shared_ptr 管理的对象。
  2. 控制块:包含了引用计数、弱引用计数等信息。

控制块通常包括以下内容:

  • 强引用计数(strong reference count):一个整数,记录有多少个 shared_ptr 指向该对象。
  • 弱引用计数(weak reference count):一个整数,记录有多少个 weak_ptr 指向该对象。
  • 指向实际对象的指针:即 shared_ptr 管理的对象的地址。

        每个 shared_ptr 都会指向一个控制块,而控制块的生命周期由 shared_ptr 来管理。控制块的引用计数与 shared_ptr 的生命周期绑定在一起。当 shared_ptr 被销毁时,它会减少控制块中的引用计数。如果控制块的引用计数变为 0,说明没有 shared_ptr 和 weak_ptr 再指向该控制块,控制块本身也会被销毁。

5. 对象的说明?

        后面的例子,所说的“对象”指的是 shared_ptr 管理的 资源对象,而不是 shared_ptr 本身(例如 p1 和 p2)。让我详细解释一下这个概念:

  • 资源对象(管理的对象):这是 shared_ptr 指向的动态分配的对象。(例如通过 std::make_shared(10) 创建的 int 类型对象)才是引用计数真正增加的目标。当 shared_ptr 被复制或者赋值时,它们的控制块中的引用计数会增加,指向同一个资源对象时,引用计数就会增加。举个例子,假设你有以下代码:

std::shared_ptr<int> p1 = std::make_shared<int>(10);
// 这里,shared_ptr<int> 实际上指向一个 int 类型的资源对象,这个对象的值是 10。
// 这个资源对象是通过 std::make_shared 在堆上动态分配的。
  • shared_ptr 对象:shared_ptr 自身是一个智能指针,它是一个管理资源(对象)的工具。它们自己并不会直接参与引用计数的增加和减少;是它们所管理的资源对象的引用计数增加或减少。比如 p1 和 p2,它们是 shared_ptr 类型的对象,用来管理资源对象。shared_ptr 本身并不直接存储实际的数据,而是存储指向资源对象的指针。

        当我们说引用计数增加时,指的是“资源对象”的引用计数增加,而不是 p1 或 p2 这样的 shared_ptr 对象的计数。shared_ptr 本身是一个容器,它包含了指向资源对象的指针,并通过控制块(control block)来管理引用计数。

  • 资源对象是 shared_ptr 管理的实际数据,它的引用计数会在 shared_ptr 被复制或赋值时增加。

  • shared_ptr 对象本身只是一个智能指针容器,它本身没有引用计数,引用计数仅针对它所管理的资源对象。

举个例子来明确

std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
std::shared_ptr<int> p2 = p1;                         // 引用计数增加到 2

在这个例子中:

  • p1 和 p2 都是 shared_ptr 类型的智能指针。
  • 它们管理的资源对象是一个堆上分配的 int 类型对象,值为 10。
  • 资源对象的引用计数会从 1 增加到 2,因为 p1 和 p2 都指向同一个资源对象(即 int 类型的值为 10 的对象)。

        当你复制一个 shared_ptr 或赋值时,引用计数的增加是针对资源对象的,而不是 shared_ptr 本身。每个 shared_ptr 的生命周期和它所指向的资源对象的生命周期是关联的。当资源对象的引用计数降到 0 时,资源对象会被销毁。

6. 如何控制应用计数的增加与减少?

        每当一个新的 shared_ptr 被创建时,它的引用计数增加;当一个 shared_ptr 被销毁时,引用计数减少。引用计数的具体增加和减少过程如下:

  • shared_ptr 的创建

        当一个新的 shared_ptr 被创建时(无论是直接初始化还是通过复制),它会将控制块中的强引用计数加 1。

在这个过程中:

  • p1 是 shared_ptr 对象,它指向一个动态分配的整数对象。

  • p2 是一个新的 shared_ptr,它通过复制 p1,所以它也指向同一个整数对象。

  • p1 和 p2 都指向同一个对象,因此它们的强引用计数都增加了。

std::shared_ptr<int> p1 = std::make_shared<int>(10); // 创建一个 shared_ptr,引用计数为 1
std::shared_ptr<int> p2 = p1; // p2 复制 p1,引用计数增加到 2
  • shared_ptr 的析构

        当一个 shared_ptr 被销毁时,它的强引用计数减少 1。如果该引用计数降到 0,那么这个 shared_ptr 管理的对象会被销毁。

{
    std::shared_ptr<int> p1 = std::make_shared<int>(10); // 引用计数为 1
    {
        std::shared_ptr<int> p2 = p1; // 引用计数增加到 2
    } // p2 被销毁,引用计数减少到 1
} // p1 被销毁,引用计数减少到 0,对象被销毁

        在上面的代码中,p2 在内层作用域结束时被销毁,引用计数从 2 减少到 1。当 p1 被销毁时,引用计数变为 0,对象才会被删除。

  • std::shared_ptr 的复制(copy)操作

        当一个 shared_ptr 被复制时,复制会导致控制块中的强引用计数加 1。

std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
std::shared_ptr<int> p2 = p1;                         // 引用计数增加到 2

        在这里,p2 是从 p1 复制出来的,p1 和 p2 都指向相同的对象,所以引用计数增加了。

  • shared_ptr 被销毁时减少引用计数

        当 shared_ptr 被销毁时,它的引用计数减少 1。如果引用计数降到 0,那么它指向的对象会被释放(调用 delete 或 delete[])。

{
    std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
    std::shared_ptr<int> p2 = p1;                         // 引用计数增加到 2
} // p1 和 p2 都超出作用域,引用计数减少到 0,内存被释放

        在这个例子中,p1 和 p2 都超出作用域时,它们的引用计数会减少到 0,导致内存被释放。

  • weak_ptr 不增加引用计数

        std::weak_ptr 不会增加 shared_ptr 的引用计数,它只会指向 shared_ptr 管理的对象,但不会影响对象的销毁。

std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
std::weak_ptr<int> w1 = p1;                           // weak_ptr 不增加引用计数

        在这个例子中,w1 只是观察 p1 指向的对象,并不会影响引用计数。

7. 什么情况下,引用计数会增加?

        std::shared_ptr 的引用计数会在以下几种情况中增加:

  1. 创建 shared_ptr 时:当你用 std::make_shared 或 shared_ptr 构造函数创建一个新的 shared_ptr 时,它会创建一个新的对象,并且控制块的强引用计数会设为 1。

  2. 复制构造时:当你将一个 shared_ptr 赋值给另一个 shared_ptr 或通过复制构造函数创建新 shared_ptr 时,它会增加控制块中的强引用计数。

  3. 赋值操作时:当你将一个已有的 shared_ptr 赋值给另一个 shared_ptr 时,引用计数同样会增加。

        关键点:引用计数的增加是由 shared_ptr 的复制构造函数和赋值操作符自动触发的,每当一个 shared_ptr 关联到同一个资源时,引用计数会增加。

  • 创建 shared_ptr 时
std::shared_ptr<int> p1 = std::make_shared<int>(10);

// 在这行代码中,std::make_shared<int>(10) 做了以下几步:
// 它在堆上分配了一个 int 类型的对象,并赋值为 10。
// 它会为这个对象创建一个控制块,并将控制块中的强引用计数设为 1。
// 这个控制块中还会存储指向对象的指针。

// 为什么引用计数是 1 呢?
// 因为此时只有 p1 一个 shared_ptr 指向该对象,所以强引用计数是 1。
  • 复制 shared_ptr 时

        当你通过复制一个已有的 shared_ptr 时,引用计数会增加 1。

std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
std::shared_ptr<int> p2 = p1;                         // 引用计数增加到 2

// 在上面的代码中:
// p1 初始化时引用计数为 1。
// 当我们创建 p2 并将其初始化为 p1 时,实际上 p2 只是对 p1 所指向的控制块进行了引用。
// 此时,p1 和 p2 都指向同一个对象(以及它的控制块)。
// 在 shared_ptr 的复制构造函数中,它会增加控制块中的强引用计数,所以引用计数变为 2。
  • 赋值操作时

        同样地,引用计数也会在进行赋值操作时增加。

std::shared_ptr<int> p1 = std::make_shared<int>(10);  // 引用计数为 1
std::shared_ptr<int> p2;                              // p2 初始化为空
p2 = p1;  // 引用计数增加到 2

// 在这段代码中,p2 是通过赋值将 p1 的 shared_ptr 赋值给它的:
// 赋值时,p2 会指向与 p1 相同的对象,并且它会共享相同的控制块。
// 赋值会导致控制块中的强引用计数增加 1,所以最终引用计数为 2。

8. 引用计数的实现细节?

        std::shared_ptr 内部是如何知道何时加 1 的呢?这实际上是由 C++ 标准库实现中的智能指针设计决定的。它通过 控制块(control block) 来管理引用计数和资源的生命周期。每次创建、复制或赋值 shared_ptr 时,控制块都会更新其引用计数。

正如之前所说的,控制块中通常会包含以下信息:

  • 强引用计数(strong reference count):记录有多少个 shared_ptr 指向该对象。
  • 弱引用计数(weak reference count):记录有多少个 weak_ptr 观察该对象。
  • 指向实际对象的指针:即 shared_ptr 管理的对象的地址。

        控制块是由 shared_ptr 对象的构造函数、复制构造函数、赋值操作符等触发的。例如:

  1. 复制构造函数:
shared_ptr(const shared_ptr& other) : ptr_(other.ptr_), count_(other.count_) {
    ++(*count_); // 增加引用计数
}

        每当 shared_ptr 被复制时,它的构造函数会拷贝另一个 shared_ptr 的控制块指针,并且将控制块中的引用计数加 1。

  1. 赋值操作符:
shared_ptr& operator=(const shared_ptr& other) {
    if (this != &other) {
        if (--(*count_) == 0) {
            delete ptr_;
            delete count_;
        }
        ptr_ = other.ptr_;
        count_ = other.count_;
        ++(*count_);
    }
    return *this;
}

        赋值操作符会减少当前 shared_ptr 的引用计数,并在必要时释放资源。然后它将控制块指针更新为 other 的控制块,并增加引用计数。

9. 引用计数存储存储在什么空间?

        关于 控制块 存储的空间位置,它通常是存储在 堆(heap)内存 中。这是因为 shared_ptr 和它管理的资源对象通常都是在堆上动态分配的。

  • 资源对象存储在堆内存中:

        shared_ptr 管理的资源对象,像 int、std::string 等,通常是在堆上分配的。例如,通过 std::make_shared 创建一个对象时,它会在堆上分配内存给这个对象。

  • 控制块存储在堆内存中:

        除了资源对象之外,shared_ptr 还会在堆上分配一个控制块(control block),控制块包含了:

  1. 强引用计数(strong_count),即有多少个 shared_ptr 指向这个资源对象。
  2. 弱引用计数(weak_count),即有多少个 weak_ptr 引用该资源对象。
  3. 指向资源对象的指针(ptr),即指向实际的资源对象。

        控制块和资源对象通常是分开存储的,它们有各自独立的内存地址。控制块会由 shared_ptr 的内部实现负责分配,并且通常与资源对象一起动态分配在堆内存中。

10. 为什么控制块存储在堆上?

  • 动态分配:shared_ptr 需要在运行时根据实际需要动态分配资源对象及其控制块,因此它们通常存储在堆内存中。而堆内存具有灵活性,可以在程序运行时动态分配和释放,适合这种情况。
  • 生命周期管理:因为 shared_ptr 本身可以有多个实例(即多个 shared_ptr 对象可以指向同一个资源),它需要一种机制来管理这些实例之间的引用计数,控制块便是实现这个管理机制的地方。由于控制块需要根据引用计数的变化在多个 shared_ptr 实例之间共享,堆内存是理想的存储位置。

        在 std::make_shared 和类似函数的帮助下,控制块和资源对象往往会一起分配。你可以将它们视为一个结构体,其中控制块是一个附加的信息块,紧跟在资源对象的后面,或者前面。这些内存都会被动态分配在堆上,并在资源的生命周期结束时自动释放。

假设你有以下代码:

std::shared_ptr<int> p1 = std::make_shared<int>(10);

/* 

在这个过程中,p1 的实现会做以下几件事:
1. 分配内存给 int 对象:在堆上分配内存并创建 int 对象,值为 10。
2. 创建控制块:在堆上分配一个控制块,它包含:
    a. 强引用计数,初始值为 1。
    b. 弱引用计数,初始值为 0。
    c. 指向 int 对象的指针(即 ptr)。
*/

具体内存布局可能是这样的(简化版):

---------------------------------------------------
| Control Block  |   Resource Object (int value)  |
---------------------------------------------------
| strong_count=1 |   ptr -> 10                   |
| weak_count=0   |                               |
---------------------------------------------------

        在 C++ 中,std::make_shared 函数实现了控制块和资源对象的联合分配,这种方式有一个好处:它能够减少内存分配的次数(减少了两次 new 操作),并且通过统一的内存块管理资源对象和控制块。这有助于提高内存效率,并减少碎片化。

        具体而言,make_shared 会分配一个足够大的内存块,既存储资源对象(比如 int)也存储控制块。它通过内存偏移来组织资源对象和控制块。

11. 总结:

  • 循环引用:两个或多个 std::shared_ptr 互相引用,导致引用计数无法归零,造成内存泄漏。
  • 循环引用解决方法:使用 std::weak_ptr 打破循环引用,因为 std::weak_ptr 不会增加引用计数。
  • std::shared_ptr 会增加引用计数,直到计数为 0 时释放内存。
  • std::weak_ptr 不增加引用计数,用来观察其他 shared_ptr 指向的对象,避免引用计数循环增大。
  • std::shared_ptr 的引用计数是通过控制块来实现的。
  • 每当一个 shared_ptr 被创建时,引用计数增加;每当 shared_ptr 被销毁时,引用计数减少。
  • 只要有 shared_ptr 对象存在指向某个资源,资源的引用计数就不会归零,直到最后一个 shared_ptr 被销毁,资源才会被释放。通过这种机制,shared_ptr 可以自动管理内存,避免手动释放内存时可能出现的错误。
  • 资源对象是 shared_ptr 管理的实际数据,它的引用计数会在 shared_ptr 被复制或赋值时增加。
  • shared_ptr 对象本身只是一个智能指针容器,它本身没有引用计数,引用计数仅针对它所管理的资源对象。
  • shared_ptr 本质上是一个智能指针,它指向实际的资源对象(如 int 类型的对象),但多了一个控制块来管理引用计数和资源的生命周期。
  • 引用计数是存储在控制块中的,而不是直接存储在 shared_ptr 对象本身。
  • 控制块帮助管理资源对象的生命周期,确保当没有 shared_ptr 再指向资源对象时,资源对象能被正确释放。
  • 资源对象和控制块都存储在堆内存中。堆内存是动态分配的,适合用于管理 shared_ptr 这种具有动态生命周期的对象。
  • shared_ptr 本身持有指向控制块的指针,控制块中包含资源对象指针和引用计数信息。
  • 控制块和资源对象的内存通常是通过一次联合分配(如 std::make_shared)来一起分配的,从而优化了内存管理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值