智能指针(smart pointers)是一种比原始指针(raw pointers)更健全、更安全的指针对象,用于管理指向动态分配内存(堆内存)的指针。它们有自动内存管理的功能,因此可以自动执行一些任务,例如在适当的时候释放内存资源、处理异常等。智能指针还能提高代码的可读性和健壮性,降低内存泄漏的风险,以及减少手动内存管理所导致的错误。
C++ 标准库中提供了以下几种智能指针:
std::unique_ptr
:std::unique_ptr
指针具有独占所有权语义,即每个动态分配的对象在某个时刻只能由一个 std::unique_ptr
实例拥有。当 std::unique_ptr
离开作用域或者被重新赋值时,它会自动释放原先管理的堆内存。这种智能指针尤其有助于避免异常触发的内存泄漏。
std::shared_ptr
:std::shared_ptr
允许多个指针共享同一个对象的所有权。它采用引用计数的方式来跟踪指向资源的指针数量。当引用计数变为0,资源自动被释放。这个智能指针在管理需要在多个对象间共享的资源时很有价值。
std::weak_ptr
:
std::weak_ptr 是⽤来解决 shared_ptr 相互引⽤时的死锁问题,如果说两个 shared_ptr 相互引⽤,那么这两个指针的引⽤计数永远不可能下降为0,也就是资源永远不会释放。它是对对象的⼀种弱引⽤,不会增加对象的引⽤计数,和 shared_ptr 之间可以相互转化shared_ptr 可以直接赋值给它,它可以通过调⽤ lock 函数来获得shared_ptr
使用智能指针而非原始指针,是现代 C++ 编程推荐的一种做法(RAII:资源获取即初始化)。它可以让我们更专注于解决业务逻辑问题,而不是在处理内存管理细节上花费太多精力。
如何来让指针知道还有其他指针的存在呢?这个时候我们该引入引用计数的概念了。引用计数是这样一个技巧,它允许有多个相同值的对象共享这个值的实现。引用计数的使用常有两个目的:
简化跟踪堆中(也即C++中new出来的)的对象的过程。一旦一个对象通过调用new被分配出来,记录谁拥有这个对象是很重要的,因为其所有者要负责对它进行delete。但是对象所有者可以有多个,且所有权能够被传递,这就使得内存跟踪变得困难。引用计数可以跟踪对象所有权,并能够自动销毁对象。可以说引用计数是个简单的垃圾回收体系
节省内存,提高程序运行效率。如何很多对象有相同的值,为这多个相同的值存储多个副本是很浪费空间的,所以最好做法是让左右对象都共享同一个值的实现。C++标准库中string类采取一种称为”写时复制“的技术,使得只有当字符串被修改的时候才创建各自的拷贝,否则可能(标准库允许使用但没强制要求)采用引用计数技术来管理共享对象的多个对象。
控制块(Control Block)是一个在C++智能指针(如shared_ptr
和weak_ptr
)实现过程中用到的内部数据结构。它用于存储一些与智能指针关联的状态信息,例如引用计数器和弱引用计数器。控制块的主要作用是跟踪资源的生命周期,确保资源能够安全地在合适的时机进行释放。通常,每个资源都与一个独立的控制块关联。
在 shared_ptr
和 weak_ptr
的情况下,控制块至少包含以下两种信息:
shared_ptr
对象共享相同的资源。当计数器为零时(即没有 shared_ptr
持有资源),资源会被释放。weak_ptr
对象引用资源(即使它们不会增加共享引用计数器)。当弱引用计数器和共享计数器都为零时,控制块才会被释放。控制块起到了协调智能指针和指向的资源的关系的作用。通过在控制块中管理引用计数和弱引用计数,智能指针可以确保资源按预期地分配和销毁,从而提供了正确的内存管理和简化了手动内存分配/释放带来的复杂性。
shared_ptr的底层原理:可以看看你真的了解智能指针shared_ptr吗
在 C++ 中,智能指针(如 std::shared_ptr
)的引用计数是存储在堆上的。这是因为智能指针对象本身可以在栈上或堆上创建,但引用计数用于共享同一底层资源的多个智能指针对象之间,因此它必须位于一个独立于栈或具体智能指针实例的位置。
实际上,std::shared_ptr
会使用一个控制块来存储引用计数以及底层资源。每当一个 std::shared_ptr
指向同一个资源时,根据需要执行拷贝构建或赋值操作,它们都会共享同一个控制块,从而实现引用计数。
这里有一个实例:
#include
#include
int main() {
std::shared_ptr ptr1(new int(10));
std::cout << "使用前引用计数: " << ptr1.use_count() << std::endl;
std::shared_ptr ptr2(ptr1); // 让 ptr2 与 ptr1 指向同一块内存
std::cout << "共享后引用计数: " << ptr1.use_count() << std::endl;
ptr2.reset(); // 释放 ptr2 对内存的引用
std::cout << "释放一个引用后引用计数: " << ptr1.use_count() << std::endl;
ptr1.reset(); // 释放 ptr1 对内存的引用,此时引用计数变成0,内存被释放
}
输出结果:
使用前引用计数: 1
共享后引用计数: 2
释放一个引用后引用计数: 1
shared_ptr 的一个关键特性是可以共享所有权,即多个 shared_ptr 可以同时指向并拥有同一个对象。
当最后一个拥有该对象的 shared_ptr 被销毁或者释放该对象的所有权时,对象会自动被删除。
这种行为通过引用计数实现,即 shared_ptr 有一个成员变量记录有多少个 shared_ptr 共享同一个对象。
shared_ptr 的简单用法示例:
#include
#include
class MyClass {
public:
MyClass() { std::cout << "MyClass 构造函数\n"; }
~MyClass() { std::cout << "MyClass 析构函数\n"; }
void do_something() { std::cout << "MyClass::do_something() 被调用\n"; }
};
int main() {
{
std::shared_ptr ptr1 = std::make_shared();
{
std::shared_ptr ptr2 = ptr1; // 这里共享 MyClass 对象的所有权
ptr1->do_something();
ptr2->do_something();
std::cout << "ptr1 和 ptr2 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
} // 这里 ptr2 被销毁,但是 MyClass 对象不会被删除,因为 ptr1 仍然拥有它的所有权
std::cout << "ptr1 作用域结束前的引用计数: " << ptr1.use_count() << std::endl;
} // 这里 ptr1 被销毁,同时 MyClass 对象也会被删除,因为它是最后一个拥有对象所有权的 shared_ptr
return 0;
}
输出结果:
MyClass 构造函数
MyClass::do_something() 被调用
MyClass::do_something() 被调用
ptr1 和 ptr2 作用域结束前的引用计数: 2
ptr1 作用域结束前的引用计数: 1
MyClass 析构函数
使用std::enable_shared_from_this ,使用方法很简单,只需要让SomeData继承std::enable_shared_from_this,然后调用shared_from_this,例如:
#include
struct SomeData;
void SomeAPI(const std::shared_ptr& d) {}
struct SomeData:std::enable_shared_from_this {
static std::shared_ptr Create() {
return std::shared_ptr(new SomeData);
}
void NeedCallSomeAPI() {
SomeAPI(shared_from_this());
}
private:
SomeData() {}
};
int main()
{
auto d{ SomeData::Create() };
d->NeedCallSomeAPI();
}
关于 enable_shared_from_this 的更多原理可以看一下这篇博客: https://0cch.com/2020/08/05/something-about-enable_shared_from_this/
std::shared_ptr 提供了许多有用的 API,以下是一些常用的 API:
shared_ptr
std::shared_ptr ptr;
make_shared
std::shared_ptr ptr = std::make_shared(42);
reset():释放当前 shared_ptr 的所有权,将其设置为 nullptr。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除对象。
ptr.reset();
reset(T*):释放当前 shared_ptr 的所有权,并使其指向新的对象。如果当前 shared_ptr 是最后一个拥有对象所有权的智能指针,则会删除原对象。
ptr.reset(new int(42));
get():返回指向的对象的裸指针。注意,这个裸指针的生命周期由 shared_ptr 管理,你不应该使用它来创建另一个智能指针。
int* raw_ptr = ptr.get();
operator* 和 operator->:访问指向的对象。
int value = *ptr;
std::shared_ptr> vec_ptr = std::make_shared>();
vec_ptr->push_back(42);
use_count():返回当前 shared_ptr 的引用计数,即有多少个 shared_ptr 共享同一个对象。注意,use_count() 通常用于调试,不应该用于程序逻辑。
size_t count = ptr.use_count();
unique():检查当前 shared_ptr 是否是唯一拥有对象所有权的智能指针。等价于 use_count() == 1。
bool is_unique = ptr.unique();
swap(shared_ptr&):交换两个 shared_ptr 的内容。
std::shared_ptr ptr1 = std::make_shared(42);
std::shared_ptr ptr2 = std::make_shared(24);
ptr1.swap(ptr2);
operator bool():将 shared_ptr 隐式转换为 bool 类型,用于检查其是否为空。
if (ptr) {
std::cout << "ptr 不为空" << std::endl;
} else {
std::cout << "ptr 为空" << std::endl;
}
std::weak_ptr是C++11引入的一种智能指针,主要与std::shared_ptr配合使用。
它的主要作用是解决循环引用问题、观察std::shared_ptr对象而不影响引用计数,以及在需要时提供对底层资源的访问。
解决循环引用问题:当两个或多个std::shared_ptr对象互相引用时,会导致循环引用。这种情况下,这些对象的引用计数永远不会变为0,从而导致内存泄漏。
#include
#include
class B; // 前向声明
class A {
public:
std::shared_ptr b_ptr;
~A() {
std::cout << "A destructor called" << std::endl;
}
};
class B {
public:
std::shared_ptr a_ptr;
~B() {
std::cout << "B destructor called" << std::endl;
}
};
int main() {
{
std::shared_ptr a = std::make_shared();
std::shared_ptr b = std::make_shared();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 指向 A
} // a 和 b 离开作用域,但由于循环引用,它们的析构函数不会被调用
std::cout << "End of main" << std::endl;
return 0;
}
std::weak_ptr可以打破这种循环引用,因为它不会增加引用计数。只需要将其中一个对象的std::shared_ptr替换为std::weak_ptr,即可解决循环引用问题。
观察std::shared_ptr对象:std::weak_ptr可以用作观察者,监视std::shared_ptr对象的生命周期。它不会增加引用计数,因此不会影响资源的释放。
#include
#include
class B; // 前向声明
class A {
public:
std::shared_ptr b_ptr;
~A() {
std::cout << "A destructor called" << std::endl;
}
};
class B {
public:
std::weak_ptr a_ptr; // 使用 weak_ptr 替代 shared_ptr
~B() {
std::cout << "B destructor called" << std::endl;
}
};
int main() {
{
std::shared_ptr a = std::make_shared();
std::shared_ptr b = std::make_shared();
a->b_ptr = b; // A 指向 B
b->a_ptr = a; // B 对 A 使用 weak_ptr
} // a 和 b 离开作用域,它们的析构函数会被正确调用
std::cout << "End of main" << std::endl;
return 0;
}