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 实际上是包含两个部分的:
- 指向实际对象的指针:也就是 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 再指向该控制块,控制块本身也会被销毁。
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 的引用计数会在以下几种情况中增加:
-
创建 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<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 对象的构造函数、复制构造函数、赋值操作符等触发的。例如:
- 复制构造函数:
shared_ptr(const shared_ptr& other) : ptr_(other.ptr_), count_(other.count_) {
++(*count_); // 增加引用计数
}
每当 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;
}
赋值操作符会减少当前 shared_ptr 的引用计数,并在必要时释放资源。然后它将控制块指针更新为 other 的控制块,并增加引用计数。
9. 引用计数存储存储在什么空间?
关于 控制块 存储的空间位置,它通常是存储在 堆(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 的内部实现负责分配,并且通常与资源对象一起动态分配在堆内存中。
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)来一起分配的,从而优化了内存管理。
173

被折叠的 条评论
为什么被折叠?



