智能指针

注意智能指针管理的是动态分配的内存,即堆区的数据,栈上的会有问题

#include <memory>  // 智能指针的公用头文件

shared_ptr 共享

共享指针(是指可以多个 shared_ptr 指向同一块内存,进行管理)

好处:

  • 方便调用,重要的是防止内存泄漏
  • 因为我们自己定义指针,new 出空间,就得手动释放,但是智能指针是一个模板类,封装好了的,当不需要的时候,会在智能指针生命周期结束后自动释放内存
  • 比如定义在函数里,函数结束,它就结束,自动释放内存,还比如它不再指向内存空间了,就自动释放内存,指向空

初始化

默认构造函数

// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);
shared_ptr<int> ptr1(new int(520));

std::make_shared

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
shared_ptr<int> ptr1 = make_shared<int>(520);

class Person{
public:
    Person(string name, int age):m_name(name),m_age(age){}
    string m_name;
    int m_age;
};
shared_ptr<Person> ptr2 = make_shared<Person>("zhangsan", 1);
  • T:模板参数的数据类型
  • Args&&... args :要初始化的数据,如果是通过 make_shared 创建对象,需按照构造函数的参数列表指定

reset

void reset() noexcept;

template<class T>
void reset(T* ptr);

template<class T, class Deleter>
void reset(T* ptr, Deleter d);

template<class T, class Deleter, class Alloc>
void reset(T* ptr, Deleter d, Alloc alloc);

ptr5.reset(new int(250));
  • ptr:指向要取得所有权的对象的指针
  • d:指定删除器,当不再使用这个共享指针的时候,就调用该删除器销毁对象
  • alloc:内部存储所用的分配器

==综合示例==

class Test {
public:
    Test() {}
    Test(int x) {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test() {}
};

int main()
{
    shared_ptr<int> ptr1(new int(520));
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    shared_ptr<int> ptr3 = ptr1;

    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    shared_ptr<int> ptr5 = std::move(ptr2);

    // make_shared 初始化
    shared_ptr<int> ptr6 = make_shared<int>(520);
    shared_ptr<Test> ptr7 = make_shared<Test>("zhangsan");
    shared_ptr<Test> ptr8 = make_shared<Test>(520);

    // reset 初始化
    ptr4.reset();     // 这样之后这个指针释放空间,不指向任何
    ptr5.reset(new int(250));	
    ptr8.reset(new int(250)); // 错误,类型不一致,一个为 Test 类,一个为 int
}

内部函数

// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;
ptr.use_count;  // 表示这个内存被多少个 shared_ptr 指针管理
// 获取原始指针(最开始的指针,指向空间的),也就是在智能指针管理之前的管理内存的指针
T* get() const noexcept;

shared_ptr<int> ptr1(new int(520));
int *p = ptr1.get();
Test *t = ptr.get();
t->setvalue(100);   // 假设 Test 类具有这个 setvalue
  • 上面 reset 和内部函数方法使用 . ,而这里使用 -> 本质是内部重载了这些符号,这是个类模板,这些方法是内部成员函数,使用 .
  • 而指针的使用是->,是为了方便我们把他就当成指针运算,方便记忆和使用,所以内部重载了->,使其和指针一样

注意点

int *a = new int(10);  
shared_ptr<int>ptr(a);  
*ptr = 100;  
cout << *a << endl;       // 输出 100
cout << ptr.use_count();  // 输出 1
  • 说明这个只是统计智能指针的数量,使用智能指针修改数据,实际也可改变,说明指针确实在管理内存

指定删除器

智能指针不指定删除器,会自动调用内部的删除器,将其内存释放,但也可指定

shared_ptr<int> ptr(new int(250), [](int *t){
    delete t;
});
// 这样直接结束后,调用析构函数,就会调用这个 lambda 构成的函数

但是数组不具有默认的删除器,因此需指定(C++11 不会,C++11 以后可以了)

shared_ptr<int> ptr(new int[10], [](int* p) {
    delete []p;
});
  • 同时,编译器也提供了删除器的模板函数
std::default_delete<T>();

shared_ptr<int> ptr(new int[10], default_delete<int []>());

unique_ptr 独占

只允许一个智能指针管理内存,但是不包括普通指针不能管理

初始化

unique_ptr<int> func() {
    return unique_ptr<int>(new int(520));
}

int main()
{
    // 通过构造函数初始化
    unique_ptr<int> ptr1(new int(10));
    // 通过转移所有权的方式初始化
    unique_ptr<int> ptr2 = std::move(ptr1);
    // 即将释放的独占智能指针
    unique_ptr<int> ptr3 = func();
    // reset
    ptr1.reset();
    ptr2.reset(new int(250));
}
// 同样支持 get 方法

注意

int main()  
{  
    int *a = new int(10);  
    unique_ptr<int>ptr(a);    // 这个定义是正确的
    *ptr = 100;  
    cout << *a << endl;     // 输出 100
}
// 说明普通指针也能指向,独占智能指针也在管理内存

指定删除器

语法和共享的类似

展示区别

shared_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// 正确
unique_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// 错误

int main()
{
    // 这样才正确,需要指定删除器函数类型,这里就是函数指针类型
    using func_ptr = void(*)(int*);
    unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; });
}
  • 如果上述的 lambda 表达式中 [] 里面传参数,就又不对了,捕捉了外部变量,此时 lambda 就不能看作是函数指针了,此时是一个仿函数
  • lambda 作为仿函数类型 我们不方便指定其类型
  • 使用包装器解决或者不使用 lambda 表达式,就使用一个函数,然后就能指定类型
void deleteInt(int* p) { 
    delete p; 
}

int main()
{
    // 错误演示
    using func_ptr = void(*)(int*);
    unique_ptr<int, func_ptr> ptr1(new int(10), [=](int*p) {delete p; });
    
    // 正确,使用包装器解决
    unique_ptr<int, function<void(int*)>> ptr1(new int(10), [&](int*p) {delete p; });
    // 正确,使用定义的函数解决
    using func_ptr = void(*)(int*);
    unique_ptr<int, func_ptr> ptr1(new int(10), deleteInt); 
}
// 对捕捉变量后的 lambda 表达式不是函数指针疑惑的,详情见 lambda 表达式章节
// 独占的 C++11 就支持对数组调用默认删除器
// 而共享的 C++11 后支持
unique_ptr<int[]> ptr(new int [3]);

weak_ptr 弱引用

它不共享指针,不能操作资源,是用来监视 shared_ptr

  • 弱引用智能指针 std::weak_ptr 可以看做是 shared_ptr 的助手,它不管理 shared_ptr 内部的指针
  • std::weak_ptr 没有重载操作符 *->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数(指向当前内存空间的智能指针数量),析构也不会减少引用计数,它的主要作用就是作为一个旁观者监视 shared_ptr 中管理的资源是否存在

初始化

语法:

// 默认构造函数
constexpr weak_ptr() noexcept;
// 拷贝构造
weak_ptr (const weak_ptr& x) noexcept;
template <class T> weak_ptr (const weak_ptr<T>& x) noexcept;
// 通过shared_ptr对象构造
template <class T> weak_ptr (const shared_ptr<T>& x) noexcept;
shared_ptr<int> sp(new int (10));

weak_ptr<int> wp1 = new int (11);    // 错误,需要在 shared_ptr 基础上创建,监视作用
weak_ptr<int> wp2(sp);
weak_ptr<int> wp3(wp2);
weak_ptr<int> wp4 = sp;
weak_ptr<int> wp5 = wp3;

内部函数

use_count()

// 函数返回所监测的资源的引用计数
long int use_count() const noexcept;

weak_ptr 只是监测资源,并不管理资源,所以不会导致计数加一

expired()

// 返回 true 表示 shared_ptr 指向的资源已经被释放, 返回 false 表示资源没有被释放
bool expired() const noexcept;

示例

shared_ptr<int> shared(new int(10));
weak_ptr<int> weak(shared);
cout << "1. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

shared.reset();
cout << "2. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

执行结果:

1. weak is not expired
2. weak is expired

lock()

// 获取所监测资源的 shared_ptr 对象
shared_ptr<element_type> lock() const noexcept;

reset()

// 清空对象,使其不检测任何资源
void reset() noexcept;

解决 shared_ptr 遗留的问题

不能使用一个原始地址初始化多个共享智能指针

struct Test {  
    Test(){};  
    ~Test() {  
        cout << "class Test is disstruct ..." << endl;  
    }  
};  
  
int main()  
{  
    Test *t = new Test;  
    shared_ptr<Test> ptr1(t);  
    cout << "use_count: " << ptr1.use_count() << endl;  
    shared_ptr<Test> ptr2(t);  
    cout << "use_count: " << ptr2.use_count() << endl;  
    return 0;  
}

执行结果:

use_count: 1
use_count: 1
析构了
析构了
  • 调用了两次析构,一个是 ptr1 ,一个是 ptr2
  • 这就有问题,一块空间释放了两次
  • 这里的错误是使用了原始地址 t 去初始化了多个共享智能指针,正确写法是 shared_ptr<Test> ptr2(ptr1);
  • 还有人可能疑惑第一行 Test *t = new Test 为啥没有析构函数调用
Test *t = new Test;  
delete t;        
// 因为是创建在堆区,需要手动释放,写 delete 就会调用析构函数

Test t; 
// 这样写,生命周期结束后就会自动调用析构函数

函数不能返回管理了 this 的共享智能指针

需求是通过成员函数返回管理当前对象的共享智能指针

  • 错误实现
struct Test {
    shared_ptr<Test> getSharedPtr() {
        return shared_ptr<Test>(this);
    }
    
    ~Test() {
        cout << "析构函数调用" << endl;
    }
};

int main() 
{
    shared_ptr<Test> ptr1(new Test);
    cout << "use_count: " << ptr1.use_count() << endl;
    shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
    cout << "use_count: " << ptr2.use_count() << endl;
    return 0;
}
use_count: 1
use_count: 1
析构函数调用
析构函数调用
  • 这本质和上面那个 错误一致,都是使用了原始地址初始化了两次智能指针,导致出错
  • 因为返回 shared_ptr ,在成员函数里使用的是 this 初始化 shared_ptr,再赋值给 ptr2,而 this 就是 ptr1 初始化的 new Test,所以就导致 new Test 的地址分配给 ptr1ptr2
  • 就和第一个一样的问题了

解决方案就是通过 weak_ptr 来解决,通过 wek_ptr 返回管理 this 资源的共享智能指针对象 shared_ptr

std::enable_shared_from_this<T>   // 模板类
shared_from_this()   // 所调用的成员方法
  • 它的本质实现是使用了 weak_ptr 里的 lock() 方法实现,返回 shared_ptr
struct Test : public enable_shared_from_this<Test> {
    shared_ptr<Test> getSharedPtr() {
        return shared_from_this();
    }
    
    ~Test() {
        cout << "析构函数调用" << endl;
    }
};

int main() 
{
    shared_ptr<Test> ptr1(new Test);
    cout << "use_count: " << ptr1.use_count() << endl;
    shared_ptr<Test> ptr2 = ptr1->getSharedPtr();
    cout << "use_count: " << ptr2.use_count() << endl;
    return 0;
}

执行结果:

use_count: 1
use_count: 2
析构函数调用
  • 在调用 enable_shared_from_this 类的 shared_from_this () 方法之前,必须要先初始化函数内部 weak_ptr 对象,否则该函数无法返回一个有效的 shared_ptr 对象

weak_ptr 是如何隐式创建的?

  • 当你使用 shared_ptr 创建对象并且该对象的类继承了 enable_shared_from_this,在对象创建时,enable_shared_from_this 内部会生成一个指向该对象的 weak_ptr。这个 weak_ptr 用来跟踪当前对象,确保 shared_from_this() 返回的 shared_ptr 是安全的。

解决循环引用问题

struct TA;
struct TB;

struct TA {
    shared_ptr<TB> bptr;
    ~TA() {
        cout << "class TA is disstruct ..." << endl;
    }
};

struct TB {
    shared_ptr<TA> aptr;
    ~TB() {
        cout << "class TB is disstruct ..." << endl;
    }
};

void testPtr()
{
    shared_ptr<TA> ap(new TA);
    shared_ptr<TB> bp(new TB);
    cout << "TA1 use_count: " << ap.use_count() << endl;
    cout << "TB1 use_count: " << bp.use_count() << endl;

    ap->bptr = bp;
    bp->aptr = ap;
    cout << "TA2 use_count: " << ap.use_count() << endl;
    cout << "TB2 use_count: " << bp.use_count() << endl;
}

int main() {
    testPtr();
}
TA1 use_count: 1
TB1 use_count: 1
TA2 use_count: 2
TB2 use_count: 2
  • 会发现没有析构函数的调用,但正确的应该有两个
  • 这个原因是循环了,最终的计数都为 2,但是当 testPtr() 函数执行完后,apbp 对象生命周期结束,那么它们所指向的内存空间智能指针计数都减一,最终空间计数都为 1
  • 此时不会析构,因为智能指针析构的判断依据是当前的内存空间指针指针计数为 0,此时就会发生内存泄漏

解决办法就是让类里面的其中一个 shared_ptr 定义为 weak_ptr,这样就打破了环的局面

记住 weak_ptr 不会让智能指针计数增加

struct TA {
    weak_ptr<TB> bptr;
    ~TA() {
        cout << "class TA is disstruct ..." << endl;
    }
};
....  // 其余代码和上面一样
TA1 use_count: 1
TB1 use_count: 1
TA2 use_count: 2
TB2 use_count: 1
class TB is disstruct ...
class TA is disstruct ...

分析这个如何打破的

  • weak_ptr 不会让智能指针计数增加,所以最终结果就是执行结果所示
  • testPtr() 函数执行完毕,apbp 对象生命周期结束,对应的内存空间计数减一,此时 bp 所指向的空间智能指针计数为 0,所以那块空间被释放,调用了析构函数,所以输出了 class TB is disstruct ...
  • 由于 TB 被析构,那么内部成员 aptr 也被析构,那么所指向的 TA 的空间的智能指针计数再减一,此时为 0,就调用了 TA 的析构函数,空间释放,输出 class TA is disstruct ...

可能还有小伙伴考虑为啥不直接删除其中一个 shared_ptr 成员变量,我也考虑到了,hh 还没想明白,但是需要知道 shared_ptr 是有循环引用风险的这里需注意,weak_ptr 是可以打破的 然后还能作为旁观者,应该是有它妙处的,以后遇到就明白了,不深究了


说明:本文是在 https://subingwen.cn/ 学习过程中的总结,这个 up 主 B 站讲得很好 !!

只管努力,剩下的交给天意