智能指针

为什么需要智能指针

智能指针主要解决以下问题:

  1. 内存泄漏:内存手动释放(malloc free; new delete ),使用智能指针可以自动释放
  2. 共享所有权指针的传播和释放,比如多线程使用同一个对象时析构问题

C++里面的四个智能指针: auto_ptr,shared_ptr,unique_ptr, weak_ptr 其中后三个是C++11支持,并且 第一个已经被C++11弃用。

智能指针的线程安全问题:

  • 引用计数是安全的,因为内部使用了原子计数技术
  • 指向对象数据,如果修改,是不安全的。如果要数据安全,需要自己加锁机制

几个指针的特点

  • unique_ptr独占对象的所有权,由于没有引用计数,因此性能较好。
  • shared_ptr共享对象的所有权,但性能略差。
  • weak_ptr配合shared_ptr,解决循环引用的问题。

shared_ptr内存模型

image-20240614095701245

shared_ptr 内部包含两个指针,一个指向对象,另一个指向控制块(control block),控制块中包含一个 引用计数(reference count), 一个弱计数(weak count)和其它一些数据。

shared_ptr 使用场景

  1. 使用智能指针可以自动释放占用的内存
    {
        shared_ptr<Buffer> buf = make_shared<Buffer>("auto free memory");  // Buffer对象分配在堆上,但能自动释放
       //对比
       Buffer *buf = new Buffer("auto free memory"); //Buffer对象分配在堆上,但需要手动 delete释放
    }
    
  2. 共享所有权指针的传播和释放

    image-20240614100809003

    示例代码

shared_ptr共享的内存

std::shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。 shared_ptr共享被管理对象,同一时刻可以有多个shared_ptr拥有对象的所有权,当最后一个 shared_ptr对象销毁时,被管理对象自动销毁。

简单来说,shared_ptr实现包含了两部分,

  • 一个指向堆上创建的对象的裸指针,raw_ptr
  • 一个指向内部隐藏的、共享的管理对象。share_count_object
  • image-20240614095756687

shared_ptr的基本用法和常用函数

1.初始化make_shared/reset

通过构造函数、std::shared_ptr辅助函数和reset方法来初始化shared_ptr

std::shared_ptr<int> p1(new int(1));
std::shared_ptr<int> p2 = p1;
std::shared_ptr<int> p3;
p3.reset(new int(1));

auto sp1 = make_shared<int>(100);   //我们应该优先使用make_shared来构造智能指针,因为他更高效
shared_ptr<int> sp1 = make_shared<int>(100);
 //相当于
shared_ptr<int> sp1(new int(100))

shared_ptr不能通过“直接将原始这种赋值”来初始化

std::shared_ptr<int> sp = new int(1);

reset函数的功能

  • reset( )不带参数时,若智能指针sp是唯一指向该对象的指针,则释放,并置空。若智能指针P不是唯 一指向该对象的指针,则引用计数减少1,同时将P置空。并且多次reset(),引用计数只会减少1次,而不是多次
  • reset( )带参数时,若智能指针s是唯一指向对象的指针,则释放并指向新的对象。若P不是唯一的指 针,则只减少引用计数,并指向新的对象。

智能指针可以通过重载的bool类型操作符来判断

std::shared_ptr<int> p1;
p1.reset(new int(1));
std::shared_ptr<int> p2 = p1;
if(!p1) {    //虽然普通指针也可以这样判断,但本质是不一样的
 cout << "p1 is empty\n";
}
if(!p2) {
 cout << "p2 is empty\n";
}

2.获取原始指针get

当需要获取原始指针时,可以通过get方法来返回原始指针

std::shared_ptr<int> ptr(new int(1));
int *p = ptr.get(); 

谨慎使用p.get()的返回值,如果你不知道其危险性则永远不要调用get()函数。

p.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生, 遵守以下几个约定:

  • 不要保存p.get()的返回值 ,无论是保存为裸指针还是shared_ptr都是错误的
  • 保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
  • 不要delete p.get()的返回值 ,会导致对一块内存delete两次的错误

3.指定删除器

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器。

void DeleteIntPtr(int *p) {
    cout << "call DeleteIntPtr" << endl;
    delete p;
}
int main()
{
    std::shared_ptr<int> p(new int(1), DeleteIntPtr);
    return 0;
}

当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达 式,上面的写法可以改为:

std::shared_ptr<int> p(new int(1), [](int *p) {
    cout << "call lambda delete p" << endl;
    delete p;});

当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对 象

std::shared_ptr<int> p3(new int[10], [](int *p) { delete [] p;});

使用shared_ptr需要注意的问题

1.不要用一个原始指针初始化多个shared_ptr

int *ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr); // 逻辑错误

这和shared_ptr的设计相悖,并且实际操作会造成ptr被delete两次

2.不要在函数实参中创建shared_ptr

function(shared_ptr<int>(new int), g()); //有缺陷

因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也 可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还 没有创建, 则int内存泄漏了,正确的写法应该是先创建智能指针

shared_ptr<int> p(new int);
function(p, g()); 

3.通过shared_from_this()返回this指针

#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
shared_ptr<A> GetSelf()
{
    return shared_ptr<A>(this); // 不要这么做
}
~A()
{
    cout << "Destructor A" << endl;
}
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf();
    return 0;
}

在这个例子中,由于用同一个指针(this)构造了两个智能指针sp1和sp2,类似于情况1。而他们之间是没有任何关系 的,在离开作用域之后this将会被构造的两个智能指针各自析构,导致重复析构的错误。

正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的 成员函数shared_from_this()来返回this的shared_ptr

class A: public std::enable_shared_from_this<A>
{
public:
shared_ptr<A>GetSelf()
{
    return shared_from_this(); // 
}
~A()
{
    cout << "Destructor A" << endl;
}
};
int main()
{
    shared_ptr<A> sp1(new A);
    shared_ptr<A> sp2 = sp1->GetSelf();  
    return 0;
}

在weak_ptr章节我们继续讲解使用shared_from_this()的原因。

4.避免循环引用

#include <iostream>
 #include <memory>
 using namespace std;
 class A;
 class B;
 class A {
 public:
 std::shared_ptr<B> bptr;
 ~A() {
 cout << "A is deleted" << endl;
 }
 };
 int main()
 {
 {
     std::shared_ptr<A> ap(new A);
     std::shared_ptr<B> bp(new B);
     ap->bptr = bp;
     bp->aptr = ap;
 }
 cout<< "main leave" << endl;  // 循环引用导致ap bp退出了作用域都没有析构
 return 0;
 }

先声明的在程序结束时后释放。循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,导 致两个指针都不会被析构,产生内存泄漏。

解决的办法是把A和B任何一个成员变量改为weak_ptr,具体方法见weak_ptr章节。

unique_ptr独占的智能指针

1.unique_ptr是一个独占型的智能指针,不能将其赋值给另一个unique_ptr

  1. unique_ptr可以指向一个数组
  2. unique_ptr需要确定删除器的类型

unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,不允许通过赋值将 一个unique_ptr赋值给另一个unique_ptr。

unique_ptr<T> my_ptr(new T);
unique_ptr<T> my_other_ptr = my_ptr;  // 报错,不能复制

unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其 他的unique_ptr,这样它本身就不再拥有原来指针的所有权了。

unique_ptr<T> my_ptr(new T); // 正确
unique_ptr<T> my_other_ptr = std::move(my_ptr); // 正确
unique_ptr<T> ptr = my_ptr;  // 报错,不能复制

std::make_shared是c++11的一部分,但std::make_unique不是。它是在c++14里加入标准库的。

auto upw1(std::make_unique<Widget>());     
// with make func
std::unique_ptr<Widget> upw2(new Widget); // without make func

使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的 一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀。

除了unique_ptr的独占性, unique_ptr和shared_ptr还有一些区别,比如

  • unique_ptr可以指向一个数组
std::unique_ptr<int []> ptr(new int[10]);
ptr[9] = 9;
std::shared_ptr<int []> ptr2(new int[10]);  // 这个是不合法的
  • unique_ptr指定删除器和shared_ptr有区别
std::shared_ptr<int> ptr3(new int(1), [](int *p){delete  p;}); // 正确
std::unique_ptr<int> ptr4(new int(1), [](int *p){delete  p;}); // 错误

unique_ptr需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,可以这样写:

std::unique_ptr<int, void(*)(int*)> ptr5(new int(1), [](int *p){delete  p;}); // 正确

关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择。 如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr,如果希望多个智能指针管理同一个 资源就用shared_ptr。

weak_ptr弱引用的智能指针

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内 存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。

weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从 一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。

基本用法

1.通过use_count()方法获取当前观察资源的引用计数

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
cout << wp.use_count() << endl;  //结果讲输出1

2.通过expired()方法判断所观察资源是否已经释放

shared_ptr<int> sp(new int(10));
weak_ptr<int> wp(sp);
if(wp.expired())
    cout << "weak_ptr无效,资源已释放";
else
    cout << "weak_ptr有效";

3.通过lock方法获取监视的shared_ptr

std::weak_ptr<int> gw;
void f()
{
 auto spt = gw.lock();
 if(gw.expired()) {
 cout << "gw无效,资源已释放";
 }
 else {
    cout << "gw有效, *spt = " << *spt << endl;
 }
}
int main()
{
 {
     auto sp  = std::make_shared<int>(42);
     gw = sp;
     f();
 }
f();
return 0;
}

先lock后检测expired

应用场景

1.weak_ptr返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生 std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针,原因是 std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针,调用 shared_from_this()方法是,会调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回,再看前面的范例

2.weak_ptr解决循环引用问题

在shared_ptr章节提到智能指针循环引用的问题,因为智能指针的循环引用会导致内存泄漏,可以通过 weak_ptr解决该问题,只要将A或B的任意一个成员变量改为weak_ptr

#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A {
public:
std::shared_ptr<B> bptr; 
~A() {
cout << "A is deleted" << endl;
}
};
class B {
public:
std::weak_ptr<A> aptr;   // 修改为weak_ptr
~B() {
cout << "B is deleted" << endl;
}
};
int main()
{
    {
    std::shared_ptr<A> ap(new A);
    std::shared_ptr<B> bp(new B);
    ap->bptr = bp;
    bp->aptr = ap;
    }
    cout<< "main leave" << endl;
    return 0;
}

这样在对B的成员赋值时,即执行bp->aptr=ap;时,由于aptr是weak_ptr,它并不会增加引用计数,所 以ap的引用计数仍然会是1,在离开作用域之后,ap的引用计数为减为0,A指针会被析构,析构后其内 部的bptr的引用计数会被减为1,然后在离开作用域后bp引用计数又从1减为0,B对象也被析构,不会发 生内存泄漏。

weak_ptr使用注意事项

weak_ptr在使用前需要检查合法性

weak_ptr<int> wp;
{
    shared_ptr<int>  sp(new int(1));  //sp.use_count()==1
    wp = sp; //wp不会改变引用计数,所以sp.use_count()==1
    shared_ptr<int> sp_ok = wp.lock(); //wp没有重载->操作符。只能这样取所指向的对象
}
shared_ptr<int> sp_null = wp.lock(); //sp_null .use_count()==0;

因为上述代码中sp和sp_ok离开了作用域,其容纳的对象已经被释放了。 得到了一个容纳NULL指针的sp_null对象。在使用wp前需要调用wp.expired()函数判断一下。 因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息。直到最后 一个weak_ptr对象被析构,这块“堆”存储块才能被回收。

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top