转载地址: https://blog.csdn.net/qq_43287763/article/details/144238420

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

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

#include
#include

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

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

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

}
AI构建项目
cpp
运行

    在上面的例子中,ptr 和 ptr2 都指向同一块内存,它们的引用计数为 2。shared_ptr 的引用计数会在它们被销毁时自动减少,最终引用计数变为 0,内存就会被释放。
  1. 什么是循环引用?
    循环引用是指两个或多个对象互相持有对方的 std::shared_ptr,形成一个闭环。这个闭环导致它们的引用计数永远不为 0,从而内存无法被释放。即当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏。以下是循环引用的情况的示例:

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

#include
#include

class B; // 前向声明

class A {
public:
std::shared_ptr ptrB;
~A() { std::cout << “A destroyed\n”; }
};

class B {
public:
std::shared_ptr ptrA;
~B() { std::cout << “B destroyed\n”; }
};

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

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

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

}
AI构建项目
cpp
运行

在上面的代码中:

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
#include

class B; // 前向声明

class A {
public:
std::shared_ptr ptrB;
~A() { std::cout << “A destroyed\n”; }
};

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

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

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

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

}
AI构建项目
cpp
运行

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

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

指向实际对象的指针:也就是 shared_ptr 管理的对象。
控制块:包含了引用计数、弱引用计数等信息。
控制块通常包括以下内容:

强引用计数(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 再指向该控制块,控制块本身也会被销毁。

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

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

std::shared_ptr p1 = std::make_shared(10);
// 这里,shared_ptr 实际上指向一个 int 类型的资源对象,这个对象的值是 10。
// 这个资源对象是通过 std::make_shared 在堆上动态分配的。
AI构建项目
cpp
运行
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 p1 = std::make_shared(10); // 引用计数为 1
std::shared_ptr p2 = p1; // 引用计数增加到 2
AI构建项目
cpp
运行
在这个例子中:

p1 和 p2 都是 shared_ptr 类型的智能指针。
它们管理的资源对象是一个堆上分配的 int 类型对象,值为 10。
资源对象的引用计数会从 1 增加到 2,因为 p1 和 p2 都指向同一个资源对象(即 int 类型的值为 10 的对象)。
当你复制一个 shared_ptr 或赋值时,引用计数的增加是针对资源对象的,而不是 shared_ptr 本身。每个 shared_ptr 的生命周期和它所指向的资源对象的生命周期是关联的。当资源对象的引用计数降到 0 时,资源对象会被销毁。

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

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

在这个过程中:

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

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

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

std::shared_ptr p1 = std::make_shared(10); // 创建一个 shared_ptr,引用计数为 1
std::shared_ptr p2 = p1; // p2 复制 p1,引用计数增加到 2
AI构建项目
cpp
运行
shared_ptr 的析构
当一个 shared_ptr 被销毁时,它的强引用计数减少 1。如果该引用计数降到 0,那么这个 shared_ptr 管理的对象会被销毁。

{
std::shared_ptr p1 = std::make_shared(10); // 引用计数为 1
{
std::shared_ptr p2 = p1; // 引用计数增加到 2
} // p2 被销毁,引用计数减少到 1
} // p1 被销毁,引用计数减少到 0,对象被销毁
AI构建项目
cpp
运行
在上面的代码中,p2 在内层作用域结束时被销毁,引用计数从 2 减少到 1。当 p1 被销毁时,引用计数变为 0,对象才会被删除。

std::shared_ptr 的复制(copy)操作
当一个 shared_ptr 被复制时,复制会导致控制块中的强引用计数加 1。

std::shared_ptr p1 = std::make_shared(10); // 引用计数为 1
std::shared_ptr p2 = p1; // 引用计数增加到 2
AI构建项目
cpp
运行
在这里,p2 是从 p1 复制出来的,p1 和 p2 都指向相同的对象,所以引用计数增加了。

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

{
std::shared_ptr p1 = std::make_shared(10); // 引用计数为 1
std::shared_ptr p2 = p1; // 引用计数增加到 2
} // p1 和 p2 都超出作用域,引用计数减少到 0,内存被释放
AI构建项目
cpp
运行
在这个例子中,p1 和 p2 都超出作用域时,它们的引用计数会减少到 0,导致内存被释放。

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

std::shared_ptr p1 = std::make_shared(10); // 引用计数为 1
std::weak_ptr w1 = p1; // weak_ptr 不增加引用计数
AI构建项目
cpp
运行
在这个例子中,w1 只是观察 p1 指向的对象,并不会影响引用计数。

  1. 什么情况下,引用计数会增加?
    std::shared_ptr 的引用计数会在以下几种情况中增加:

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

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

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

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

创建 shared_ptr 时
std::shared_ptr p1 = std::make_shared(10);

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

// 为什么引用计数是 1 呢?
// 因为此时只有 p1 一个 shared_ptr 指向该对象,所以强引用计数是 1。
AI构建项目
cpp
运行
复制 shared_ptr 时
当你通过复制一个已有的 shared_ptr 时,引用计数会增加 1。

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

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

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

// 在这段代码中,p2 是通过赋值将 p1 的 shared_ptr 赋值给它的:
// 赋值时,p2 会指向与 p1 相同的对象,并且它会共享相同的控制块。
// 赋值会导致控制块中的强引用计数增加 1,所以最终引用计数为 2。
AI构建项目
cpp
运行
8. 引用计数的实现细节?
std::shared_ptr 内部是如何知道何时加 1 的呢?这实际上是由 C++ 标准库实现中的智能指针设计决定的。它通过 控制块(control block) 来管理引用计数和资源的生命周期。每次创建、复制或赋值 shared_ptr 时,控制块都会更新其引用计数。

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

强引用计数(strong reference count):记录有多少个 shared_ptr 指向该对象。
弱引用计数(weak reference count):记录有多少个 weak_ptr 观察该对象。
指向实际对象的指针:即 shared_ptr 管理的对象的地址。
控制块是由 shared_ptr 对象的构造函数、复制构造函数、赋值操作符等触发的。例如:

复制构造函数:
shared_ptr(const shared_ptr& other) : ptr_(other.ptr_), count_(other.count_) {
++(*count_); // 增加引用计数
}
AI构建项目
cpp
运行
每当 shared_ptr 被复制时,它的构造函数会拷贝另一个 shared_ptr 的控制块指针,并且将控制块中的引用计数加 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;
}
AI构建项目
cpp
运行

    赋值操作符会减少当前 shared_ptr 的引用计数,并在必要时释放资源。然后它将控制块指针更新为 other 的控制块,并增加引用计数。
  1. 引用计数存储存储在什么空间?
    关于 控制块 存储的空间位置,它通常是存储在 堆(heap)内存 中。这是因为 shared_ptr 和它管理的资源对象通常都是在堆上动态分配的。

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

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

强引用计数(strong_count),即有多少个 shared_ptr 指向这个资源对象。
弱引用计数(weak_count),即有多少个 weak_ptr 引用该资源对象。
指向资源对象的指针(ptr),即指向实际的资源对象。
控制块和资源对象通常是分开存储的,它们有各自独立的内存地址。控制块会由 shared_ptr 的内部实现负责分配,并且通常与资源对象一起动态分配在堆内存中。

  1. 为什么控制块存储在堆上?
    动态分配:shared_ptr 需要在运行时根据实际需要动态分配资源对象及其控制块,因此它们通常存储在堆内存中。而堆内存具有灵活性,可以在程序运行时动态分配和释放,适合这种情况。
    生命周期管理:因为 shared_ptr 本身可以有多个实例(即多个 shared_ptr 对象可以指向同一个资源),它需要一种机制来管理这些实例之间的引用计数,控制块便是实现这个管理机制的地方。由于控制块需要根据引用计数的变化在多个 shared_ptr 实例之间共享,堆内存是理想的存储位置。
    在 std::make_shared 和类似函数的帮助下,控制块和资源对象往往会一起分配。你可以将它们视为一个结构体,其中控制块是一个附加的信息块,紧跟在资源对象的后面,或者前面。这些内存都会被动态分配在堆上,并在资源的生命周期结束时自动释放。

假设你有以下代码:

std::shared_ptr p1 = std::make_shared(10);

/*
在这个过程中,p1 的实现会做以下几件事:

  1. 分配内存给 int 对象:在堆上分配内存并创建 int 对象,值为 10。
  2. 创建控制块:在堆上分配一个控制块,它包含:
    a. 强引用计数,初始值为 1。
    b. 弱引用计数,初始值为 0。
    c. 指向 int 对象的指针(即 ptr)。
    */
    AI构建项目
    cpp
    运行

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


| Control Block | Resource Object (int value) |

| strong_count=1 | ptr -> 10 |
| weak_count=0 | |

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

    具体而言,make_shared 会分配一个足够大的内存块,既存储资源对象(比如 int)也存储控制块。它通过内存偏移来组织资源对象和控制块。
  1. 总结:
    循环引用:两个或多个 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)来一起分配的,从而优化了内存管理。
    ————————————————
    版权声明:本文为CSDN博主「阳光开朗_大男孩儿」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
    原文链接:https://blog.csdn.net/qq_43287763/article/details/144238420
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐