【C++】智能指针

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。
笔记借鉴了《c++11实用特性[c/c++项目开发必备技能]》课程



智能指针是存储指向动态分配(堆)对象指针的,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露

智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C++11中提供三种智能指针,使用这些智能指针时需要引用头文< memory >

  • std::shared_ptr:共享的智能指针
  • std::unique_ptr:独占的智能指针
  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

一、 shared_ptr共享智能指针

共享智能指针是指多个智能指针可以同时管理同一块有效的内存

1.1 shared_ptr的初始化

共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的堆内存.

如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:

// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;

1.1.1 通过构造函数初始化

// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);
#include <iostream>
#include <memory> // 包含头文件
using namespace std;

int main(){
    // 使用智能指针管理一块 int 型的堆内存
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; // 1
    
    // 使用智能指针管理一块字符数组对应的堆内存
    shared_ptr<char> ptr2(new char[12]);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; // 1
    
    // 创建智能指针对象, 不管理任何内存
    shared_ptr<int> ptr3;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; // 0
    
    // 创建智能指针对象, 初始化为空
    shared_ptr<int> ptr4(nullptr);
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl; // 0

    return 0;
}

Tip:不要使用一个原始指针初始化多个shared_ptr

int *p = new int;
shared_ptr<int> p1(p);
shared_ptr<int> p2(p);		// error, 编译不会报错, 运行会出错

1.1.2 通过拷贝和移动构造函数初始化

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

#include <iostream>
#include <memory>
using namespace std;

int main(){
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
   
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;  // 2
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;  // 3
   
    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;  // 3
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;  // 3
//如果使用移动的方式初始智能指针对象,只是转让了内存的所有权
//管理内存的对象并不会增加,因此内存的引用计数不会变化。
  
    return 0;
}

1.1.3 通过 reset方法初始化

共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:

void reset() noexcept;

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

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

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

  • ptr:指向要取得所有权的对象的指针
  • d:指向要取得所有权的对象的指针
  • aloc:内部存储所用的分配器
#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    shared_ptr<int> ptr2 = ptr1;
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; // 2
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl; // 2

    ptr2.reset(); // 当智能指针中有值的时候,调用reset会使引用计数减1。
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl; // 1

    shared_ptr<int> ptr3;
    ptr3.reset(new int(250)); // 对于一个未初始化的共享智能指针,可以通过reset方法来初始化,
    // reset初始化时,注意和定义时智能指针管理内存类型要一直
    // 比如这里定义时是int,那用reset时也要用int
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl; // 1

    return 0;
}

1.2 智能指针的使用

1.2.1 获取原始指针

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的get()方法,其函数原型如下:

T* get() const noexcept;

测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    
    // 得到指针的原始地址
    char* add = ptr.get(); //智能指针本质是一个类,因此用“.”调用get()
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    return 0;
}

1.2.2 直接使用

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> p(new int);
    *p = 100;  // 可以直接当普通指针使用
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

1.3 指定删除器

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。这是自带的删除器。

另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用是由智能指针完成的。

#include <iostream>
#include <memory>
using namespace std;

// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p) {
    delete p;
    cout << "int 型内存被释放了...";
}

int main() {
    shared_ptr<int> ptr(new int(250), deleteIntPtr); //第二个参数指定删除器函数
    return 0;
}

删除器函数也可以是lambda表达式,因此代码也可以写成下面这样:

int main() {
    shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
    // lambda表达式参数列表中的p接收自new int(250)
    return 0;
}

在C++11中使用shared_ptr管理动态数组时,需要指定删除器,因为std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:

int main(){
	// 删除器释放数组
    shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
    return 0;
}

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。
具体处理代码如下:

int main(){
    shared_ptr<int> ptr(new int[10], default_delete<int[]>());
    return 0;
}

另外,我们还可以自己封装一个make_shared_array方法来让shared_ptr支持数组,代码如下:

#include <iostream>
#include <memory>
using namespace std;

template <typename T>
shared_ptr<T> make_share_array(size_t size){
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main(){
	// 利用模板函数初始化shared_ptr
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

二、 unique_ptr独占指针

std::unique_ptr是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针。

2.1 初始化

通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr

// 通过构造函数初始化对象
unique_ptr<int> ptr1(new int(10));
// error, 不允许将一个unique_ptr赋值给另一个unique_ptr
unique_ptr<int> ptr2 = ptr1;

但是可以通过自定义函数返回给其他的std::unique_ptr,还可以通过std::move来转译给其他的std::unique_ptr,这样原始指针的所有权就被转移了,这个原始指针还是被独占的。

#include <iostream>
#include <memory>
using namespace std;

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

int main(){
    // 通过构造函数初始化
    unique_ptr<int> ptr1(new int(10));
    // 通过转移所有权的方式初始化
    unique_ptr<int> ptr2 = move(ptr1);
    unique_ptr<int> ptr3 = func();

    return 0;
}

unique_ptr独占智能指针类也有一个reset方法,函数原型如下:

void reset( pointer ptr = pointer() ) noexcept;

使用reset方法可以让unique_ptr解除对原始内存的管理,也可以用来初始化一个独占的智能指针。

int main(){
    unique_ptr<int> ptr1(new int(10));
    unique_ptr<int> ptr2 = move(ptr1);  // move转译

    ptr1.reset();  // 使ptr1解除管理
    ptr2.reset(new int(250)); // 重新指定智能指针管理的原始内存

    return 0;
}

2.2 删除器

unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,举例说明:

shared_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// ok
unique_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// error

int main(){
    using func_ptr = void(*)(int*);
    // 指定删除器类型,func_ptr的类型和lambda表达式的类型是一致的,所以用函数指针类型
    unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; });

    return 0;
}
//独占的智能指针可以管理数组类型的地址,能够自动释放,需要指定为数组类型[]
unique_ptr<Test[]>ptr5 (new Test [3]);

// 共享智能指针在C++11中不支持这种做法,需要指定删除器(因为默认删除器不会删除数组),
//但是在C++11以后的版本就支持共享智能指针管理数组了

三、 weak_ptr弱化智能指针

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

3.1 初始化

// 默认构造函数
constexpr weak_ptr() noexcept;

// 拷贝构造
weak_ptr (const weak_ptr& x) noexcept;
template <class U> 
weak_ptr (const weak_ptr<U>& x) noexcept;

// 通过shared_ptr对象构造
template <class U> 
weak_ptr (const shared_ptr<U>& x) noexcept;
#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp(new int); // 默认构造

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);  // 拷贝构造
    weak_ptr<int> wp3(sp);  // 通过shared_ptr对象构造
    weak_ptr<int> wp4;
    wp4 = sp;  // 通过一个shared_ptr对象构造了一个可用的weak_ptr实例对象(隐式类型转换)
    weak_ptr<int> wp5;
    wp5 = wp3;  // 通过一个weak_ptr对象构造了一个可用的weak_ptr实例对象
    
    return 0;
}

3.2 常用方法

3.2.1 use_count()获得当前所观测资源的引用计数

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

打印引用资源计数

#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp(new int);

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);
    weak_ptr<int> wp3(sp);
    weak_ptr<int> wp4;
    wp4 = sp;
    weak_ptr<int> wp5;
    wp5 = wp3;

    cout << "use_count: " << endl;
    cout << "wp1: " << wp1.use_count() << endl; // 0,因为没有指定shared_ptr
    cout << "wp2: " << wp2.use_count() << endl; // 0
    cout << "wp3: " << wp3.use_count() << endl; // 1,weak_ptr只是监测资源,并不管理资源
    cout << "wp4: " << wp4.use_count() << endl; // 1
    cout << "wp5: " << wp5.use_count() << endl; // 1
    return 0;
}

3.2.2 expired()判断观测的资源是否已经被释放

// 返回true表示资源已经被释放, 返回false表示资源没有被释放
bool expired() const noexcept;
#include <iostream>
#include <memory>
using namespace std;

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

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

    return 0;
}

weak_ptr监测的就是shared_ptr管理的资源,当共享智能指针调用shared.reset();之后管理的资源被释放,因此weak.expired()函数的结果返回true,表示监测的资源已经不存在了。

3.2.3 lock()获取管理所监测资源的shared_ptr对象

shared_ptr<element_type> lock() const noexcept;
#include <iostream>
#include <memory>
using namespace std;

int main(){
    shared_ptr<int> sp1, sp2;
    weak_ptr<int> wp;

    sp1 = std::make_shared<int>(520);
    wp = sp1;
    sp2 = wp.lock();
    // 通过调用lock()方法得到一个用于管理weak_ptr对象所监测的资源的共享智能指针对象
    // 使用这个对象初始化sp2,此时所监测资源的引用计数为2
    cout << "use_count: " << wp.use_count() << endl;  // 2

    sp1.reset();
    cout << "use_count: " << wp.use_count() << endl;  // 1

    sp1 = wp.lock();
    cout << "use_count: " << wp.use_count() << endl;  // 2

    cout << "*sp1: " << *sp1 << endl;
    cout << "*sp2: " << *sp2 << endl;

    return 0;
}

3.2.4 reset()清空对象

void reset() noexcept;
#include <iostream>
#include <memory>
using namespace std;

int main() {
    shared_ptr<int> sp(new int(10));
    weak_ptr<int> wp(sp);
    cout << "1. wp " << (wp.expired() ? "is" : "is not") << " expired" << endl; // 1. weak is not expired

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

    return 0;
}

3.3 返回管理this的shared_ptr

#include <iostream>
#include <memory>
using namespace std;

struct Test{
    shared_ptr<Test> getSharedPtr() {
        return shared_ptr<Test>(this);
    }
    
    ~Test() {
        cout << "class Test is disstruct ..." << endl;
    }

};

int main() {
    shared_ptr<Test> sp1(new Test);
    cout << "use_count: " << sp1.use_count() << endl;  // 1
    shared_ptr<Test> sp2 = sp1->getSharedPtr();
    cout << "use_count: " << sp1.use_count() << endl;  // 1
    return 0;
}

//------------------------------------
use_count: 1
use_count: 1
class Test is disstruct ...
class Test is disstruct ...

在这个例子中使用同一个指针this构造了两个智能指针对象sp1sp2,这二者之间是没有任何关系的,因为sp2并不是通过sp1初始化得到的实例对象。在离开作用域之后this将被构造的两个智能指针各自析构,导致重复析构的错误。

这个问题可以通过weak_ptr来解决,通过wek_ptr返回管理this资源的共享智能指针对象shared_ptr。C++11中为我们提供了一个模板类叫做std::enable_shared_from_this<T>,这个类中有一个方法叫做shared_from_this(),通过这个方法可以返回一个共享智能指针。
在函数的内部就是使用weak_ptr来监测this对象,并通过调用weak_ptrlock()方法返回一个shared_ptr对象。

#include <iostream>
#include <memory>
using namespace std;

struct Test : public enable_shared_from_this<Test>{ //继承相关类
    shared_ptr<Test> getSharedPtr(){
        return shared_from_this(); // 通过这个方法可以返回一个共享智能指针
    }
    ~Test() {
        cout << "class Test is disstruct ..." << endl;
    }
};

int main() {
    shared_ptr<Test> sp1(new Test);
    cout << "use_count: " << sp1.use_count() << endl;  // 1
    shared_ptr<Test> sp2 = sp1->getSharedPtr();
    cout << "use_count: " << sp1.use_count() << endl;  // 2
    return 0;
}

Tip:在调用enable_shared_from_this类的shared_from_this()方法之前,必须要先初始化函数内部weak_ptr对象,否则该函数无法返回一个有效的shared_ptr对象。
如上边代码,就是Test在继承类后,实例化Test时,发生了weak_ptr的初始化,指向了sp1

3.4 解决循环引用问题

智能指针如果循环引用会导致内存泄露,比如下面的例子:

#include <iostream>
#include <memory>
using namespace std;

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 << "TA object use_count: " << ap.use_count() << endl; // 1
    cout << "TB object use_count: " << bp.use_count() << endl; // 1

    ap->bptr = bp;
    bp->aptr = ap;
    cout << "TA object use_count: " << ap.use_count() << endl; // 2
    cout << "TB object use_count: " << bp.use_count() << endl; // 2
}

int main(){
    testPtr();
    return 0;
}

在测试程序中,共享智能指针ap、bpTA、TB实例对象的引用计数变为2,在共享智能指针离开作用域之后引用计数只能减为1,这种情况下不会去删除智能指针管理的内存,导致类TA、TB的实例对象不能被析构,最终造成内存泄露。
通过使用weak_ptr可以解决这个问题,只要将类TA或者TB的任意一个成员改为weak_ptr,修改之后的代码如下:

#include <iostream>
#include <memory>
using namespace std;

struct TA;
struct TB;

struct TA{
    weak_ptr<TB> bptr; //改为`weak_ptr`类型
    ~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 << "TA object use_count: " << ap.use_count() << endl;  // 1
    cout << "TB object use_count: " << bp.use_count() << endl;  // 1

    ap->bptr = bp;
    bp->aptr = ap;
    cout << "TA object use_count: " << ap.use_count() << endl;  // 2
    cout << "TB object use_count: " << bp.use_count() << endl;  // 1
}

int main(){
    testPtr();
    return 0;
}

上面程序中,在对类TA成员赋值时ap->bptr = bp;由于bptr是weak_ptr类型,这个赋值操作并不会增加引用计数,所以bp的引用计数仍然为1,在离开作用域之后bp的引用计数减为0,类TB的实例对象被析构。
在类TB的实例对象被析构的时候,内部的aptr也被析构,其对TA对象的管理解除,内存的引用计数减为1,当共享智能指针ap离开作用域之后,对TA对象的管理也解除了,内存的引用计数减为0,类TA的实例对象被析构。

归根结底就是打破循环引用,将共享智能指针类型在离开作用域时,引用计数都减为0,才会被析构,防止内存泄漏。

  • 36
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值