一文带你了解智能指针(转载并结合总结)
参考博文
C++ RAII机制详解:https://blog.csdn.net/yyxyong/article/details/76099721
现代 C++:一文读懂智能指针:https://zhuanlan.zhihu.com/p/150555165
假设没有智能指针会怎么样
在介绍智能指针之前,先介绍C++11中的RAII机制
RAII 是 resource acquisition is initialization 的缩写,意为“资源获取即初始化”。它是 C++ 之父 Bjarne Stroustrup 提出的设计理念,其核心是把资源和对象的生命周期绑定,对象创建获取资源,对象销毁释放资源。在 RAII 的指导下,C++ 把底层的资源管理问题提升到了对象生命周期管理的更高层次。
RAII 机制?
使用 C++ 时,最让人头疼的便是内存管理,但却又正是对内存高度的可操作性给了 C++ 程序猿极大的自由与装逼资本。
当我们 new 出一块内存空间,在使用完之后,如果不使用 delete 来释放这块资源则将导致内存泄露,这在中大型项目中是极具破坏性的。但是人无完人,我们并不能保证每次都记得释放无法再次获取到且不再使用的内存,下面我给出一个例子,大家看看忘记释放资源而造成内存泄露是多么恐怖!!
#include <iostream>
#include <memory>
int main()
{
for (int i = 1; i <= 10000000; i++)
{
int32_t *ptr = new int32_t[3];
ptr[0] = 1;
ptr[1] = 2;
ptr[2] = 3;
//delete ptr; //假设忘记了释放内存
}
system("pause");
return 0;
}
运行程序,打开资源管理器,可以这么简单的一个程序竟然就已经占用了500 多MB的内存,所以大家应该祈祷千万不要犯这么低级的错误!
有什么方法可以保证资源的自动释放呢?就像Java一样,但是却又不失C++程序猿的面子
这个时候我们想到对象的析构是自动完成的,那么可不可以利用这个机制呢?答案很明确,可以。我们需要做的便是将资源托管给某个对象,或者说这个对象是资源的代理,在这个对象析构的时候完成资源的释放。于是我们可以将上例改成如下形式: 将代码修改如下
#include <iostream>
#include <memory>
//先创建一个模板类, 负责资源的释放, 当资源生命周期结束, 自动释放资源;
template<typename T>
class auto_release_ptr
{
public:
auto_release_ptr(T *t) :_t(t){};
~auto_release_ptr()
{
delete _t;
};
T * getPtr()
{
return _t;
}
private:
T *_t;
};
int main()
{
for (int i = 1; i <= 10000000; i++)
{
auto arp = auto_release_ptr<int32_t>(new int32_t[3]);
int32_t *ptr = arp.getPtr();
ptr[0] = 1;
ptr[1] = 2;
ptr[2] = 3;
}
system("pause");
return 0;
}
然后内存占用变成了这样: 只占用0.5M;
auto_release_ptr
有一个数据成员在构造时完成了初始化并指向new出来的空间,而在其析构函数中,我们使用delete来释放这块内存空间,于是我们new出来的资源便有了和auto_release_ptr
对象一样的生命周期,并且会在其托管的auto_release_ptr
对象声明周期结束时被释放。ptr
和auto_release_ptr
对象定义是在一块的,所以生命周期也相同,即使ptr被回收我们也不用担心ptr其指向的内存空间没有被释放了
小结:
RAII机制便是通过利用对象的自动销毁,使得资源也具有了生命周期,有了自动销毁(自动回收)的功能
接下来讨论智能指针
智能指针
C++11引入了3个智能指针类型:
std::unique_ptr
:独占资源所有权的指针std::shared_ptr
:共享资源所有权的指针std::weak_ptr
:共享资源的观察者,需要和std::shared_ptr
一起使用,不影响资源的生命周期
std::auto_ptr
已被废弃
std::unique_ptr
简单说,当我们独占资源的所有权的时候,可以使用std::unique_ptr
对资源进行管理——离开unique_ptr
对象的作用域时,会自动释放资源。这是很基本的RAll思想。
std::unique_ptr 的使用比较简单,也是用得比较多的智能指针。这里直接看例子。
- 使用裸指针时,要记得释放内存
{
int* p = new int(100);
// ...
delete p; // 要记得释放内容
}
- 使用std::unique_ptr自动管理内存
{
std::unique_ptr<int> uptr = std::make_unique<int>(200);
//...
// 离开 uptr 的作用域的时候自动释放内存
}
- std::unique_ptr是move-only的
{
std::unique_ptr<int>uptr = std::make_unique<int>(200);
std::unique_ptr<int>uptr1 = uptr; // ERROR!
std::unique_ptr<int> uptr2 = std::move(uptr); // TRUE!
assert(uptr == nullptr);
}
在uptr
经过std::move移动之后,可以知道此时uptr
是nullptr空指针
- std::unique_ptr可以指向一个数组
{
std::unique_ptr<int[]> uptr = std::make_unique<int[]>(10);
for (int i = 0; i < 10; i++) {
uptr[i] = i * i;
}
for (int i = 0; i < 10; i++) {
std::cout << uptr[i] << std::endl;
}
}
- 自定义deleter
{
struct FileCloser {
void operator()(FILE* fp) const {
if (fp != nullptr) {
fclose(fp);
}
}
};
std::unique_ptr<FILE, FileCloser> uptr(fopen("test_file.txt", "w"));
}
- 使用Lambda的deleter
{
std::unique_ptr<FILE, std::function<void(FILE*)>> uptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
fclose(fp);
});
}
std::shared_ptr
std::shared_ptr其实就是对资源做引用计数——当引用计数为0的时候,自动释放资源
{
std::shared_ptr<int> sptr = std::make_shared<int>(200);
assert(sptr.use_count() == 1); // 此时引用计数为 1
{
std::shared_ptr<int> sptr1 = sptr;
assert(sptr.get() == sptr1.get());
assert(sptr.use_count() == 2); // sptr 和 sptr1 共享资源,引用计数为 2
}
assert(sptr.use_count() == 1); // sptr1 已经释放
}
// use_count 为 0 时自动释放内存
和 unique_ptr 一样,shared_ptr 也可以指向数组和自定义 deleter。
{
// C++20 才支持 std::make_shared<int[]>
// std::shared_ptr<int[]> sptr = std::make_shared<int[]>(100);
std::shared_ptr<int[]> sptr(new int[10]);
for (int i = 0; i < 10; i++) {
sptr[i] = i * i;
}
for (int i = 0; i < 10; i++) {
std::cout << sptr[i] << std::endl;
}
}
{
std::shared_ptr<FILE> sptr(
fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});
}
std::shared_ptr的实现原理
一个shared_ptr对象的内存开销要比裸指针和无自定义deleter的unique_ptr对象略大
std::cout << sizeof(int*) << std::endl; // 输出 8
std::cout << sizeof(std::unique_ptr<int>) << std::endl; // 输出 8
std::cout << sizeof(std::unique_ptr<FILE, std::function<void(FILE*)>>)
<< std::endl; // 输出 40
std::cout << sizeof(std::shared_ptr<int>) << std::endl; // 输出 16
std::shared_ptr<FILE> sptr(fopen("test_file.txt", "w"), [](FILE* fp) {
std::cout << "close " << fp << std::endl;
fclose(fp);
});
std::cout << sizeof(sptr) << std::endl; // 输出 16
无自定义 deleter 的 unique_ptr 只需要将裸指针用 RAII 的手法封装好就行,无需保存其它信息,所以它的开销和裸指针是一样的。如果有自定义 deleter,还需要保存 deleter 的信息。
shared_ptr 需要维护的信息有两部分:
- 指向共享资源的指针
- 引用计数等共享资源的控制信息
所以,shared_ptr 对象需要保存两个指针。shared_ptr 的 的 deleter 是保存在控制信息中,所以,是否有自定义 deleter 不影响 shared_ptr 对象的大小。
当我们创建一个 shared_ptr 时,其实现一般如下:
std::shared_ptr<T> sptr1(new T);
复制一个 shared_ptr :
std::shared_ptr<T> sptr2 = sptr1;
为什么控制信息和每个 shared_ptr 对象都需要保存指向共享资源的指针?可不可以去掉 shared_ptr 对象中指向共享资源的指针,以节省内存开销?
答案是:不能。 因为 shared_ptr 对象中的指针指向的对象不一定和控制块中的指针指向的对象一样。
来看一个例子。
struct Fruit {
int juice;
};
struct Vegetable {
int fiber;
};
struct Tomato : public Fruit, Vegetable {
int sauce;
};
// 由于继承的存在,shared_ptr 可能指向基类对象
std::shared_ptr<Tomato> tomato = std::make_shared<Tomato>();
std::shared_ptr<Fruit> fruit = tomato;
std::shared_ptr<Vegetable> vegetable = tomato;
另外,std::shared_ptr 支持 aliasing constructor。
template< class Y >
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;
Aliasing constructor,简单说就是构造出来的 shared_ptr 对象和参数 r 指向同一个控制块(会影响 r 指向的资源的生命周期),但是指向共享资源的指针是参数 ptr。看下面这个例子。
using Vec = std::vector<int>;
std::shared_ptr<int> GetSPtr() {
auto elts = {0, 1, 2, 3, 4};
std::shared_ptr<Vec> pvec = std::make_shared<Vec>(elts);
// 函数原型, 第一个是与参数r指向同一个控制块,第二个是指向共享资源的指针
/*
template <class _Ty2>
shared_ptr(const shared_ptr<_Ty2>& _Right, element_type* _Px) noexcept {
// construct shared_ptr object that aliases _Right
this->_Alias_construct_from(_Right, _Px);
}
*/
return std::shared_ptr<int>(pvec, &(*pvec)[2]);
}
std::shared_ptr<int> sptr = GetSPtr();
for (int i = -2; i < 3; ++i) {
printf("%d\n", sptr.get()[i]);
}
看上面的例子,使用 std::shared_ptr 时,会涉及两次内存分配:一次分配共享资源对象;一次分配控制块。C++ 标准库提供了 std::make_shared 函数来创建一个 shared_ptr 对象,只需要一次内存分配。
这种情况下,不用通过控制块中的指针,我们也能知道共享资源的位置——这个指针也可以省略掉。
std::weak_ptr
std::weak_ptr 要与 std::shared_ptr 一起使用。 一个 std::weak_ptr 对象看做是 std::shared_ptr 对象管理的资源的观察者,它不影响共享资源的生命周期:
- 如果需要使用 weak_ptr 正在观察的资源,可以将 weak_ptr 提升为 shared_ptr。
- 当 shared_ptr 管理的资源被释放时,weak_ptr 会自动变成 nullptr。
void Observe(std::weak_ptr<int> wptr) {
if (auto sptr = wptr.lock()) {
std::cout << "value: " << *sptr << std::endl;
} else {
std::cout << "wptr lock fail" << std::endl;
}
}
std::weak_ptr<int> wptr;
{
auto sptr = std::make_shared<int>(111);
wptr = sptr;
Observe(wptr); // sptr 指向的资源没被释放,wptr 可以成功提升为 shared_ptr
}
Observe(wptr); // sptr 指向的资源已被释放,wptr 无法提升为 shared_ptr
可以看到shared_ptr指向的值被打印了出来
当 shared_ptr 析构并释放共享资源的时候,只要 weak_ptr 对象还存在,控制块就会保留,weak_ptr 可以通过控制块观察到对象是否存活。
enable_shared_from_this
class Foo {
public:
std::shared_ptr<Foo> GetSPtr() {
return std::shared_ptr<Foo>(this);
}
};
auto sptr1 = std::make_shared<Foo>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 1);
assert(sptr2.use_count() == 1);
上述代码编译运行后会报错
成员函数获取 this 的 shared_ptr 的正确的做法是继承 std::enable_shared_from_this。
// 此处要写public继承,不然也会报错
class Bar : public std::enable_shared_from_this<Bar> {
public:
std::shared_ptr<Bar> GetSPtr() {
return shared_from_this();
}
};
auto sptr1 = std::make_shared<Bar>();
assert(sptr1.use_count() == 1);
auto sptr2 = sptr1->GetSPtr();
assert(sptr1.use_count() == 2);
assert(sptr2.use_count() == 2);
一般情况下,继承了 std::enable_shared_from_this 的子类,成员变量中增加了一个指向 this 的 weak_ptr。这个 weak_ptr 在第一次创建 shared_ptr 的时候会被初始化,指向 this。
似乎继承了 std::enable_shared_from_this 的类都被强制必须通过 shared_ptr 进行管理。
auto b = new Bar;
auto sptr = b->shared_from_this();
小结
智能指针,本质上是对资源所有权和生命周期管理的抽象:
- 当资源是被独占时,使用
std::unique_ptr
对资源进行管理。 - 当资源会被共享时,使用
std::shared_ptr
对资源进行管理。 - 使用
std::weak_ptr
作为std::shared_ptr
管理对象的观察者。 - 通过继承
std::enable_shared_from_this
来获取 this 的std::shared_ptr
对象。