【C++】智能指针的使用与手撕实现

本文介绍了C++11中的三种智能指针:unique_ptr、shared_ptr和weak_ptr,通过实例展示了它们的使用、特性以及线程安全优化。重点讲解了unique_ptr的独占所有权、shared_ptr的引用计数机制和自定义删除器,以及如何确保线程安全。
摘要由CSDN通过智能技术生成

智能指针的三种写法

茴字有四样写法,你知道么?

为了避免内存泄漏,C++11中提出了智能指针(参考开源库boost),智能指针不是一个指针,而是一个模板类。
其基本思想都是用引用计数的方式,用对象的生命周期控制内存的生命周期。智能指针有以下三种。

unique_ptr 独占对象
shared_ptr 允许多个shared_ptr 实例指向同一个对象
weak_ptr 是shared_ptr 的辅助类
下面将结合具体的例子来分析这三个智能指针。

unique_ptr

unique_ptr对象包装一个原始指针,并负责其生命周期,unique_ptr始终是关联的原始指针的唯一所有者,无法复制unique_ptr对象。
下面是一个unique_ptr的使用测试:

	unique_ptr<int> ptr1;
	unique_ptr<int> ptr2(new int(6));
	//检查是否为空
	if (ptr1 == nullptr) cout << "ptr1空" << endl;
	if (ptr2 == nullptr) cout << "ptr2空" << endl;
	//获取原始指针
	int* orign = ptr2.get();
	cout << *orign << endl;
	//unique_ptr只能移动,不能复制
	ptr1 = move(ptr2);
	if (ptr1 == nullptr) cout << "ptr1空" << endl;
	if (ptr2 == nullptr) cout << "ptr2空" << endl;
	cout << *ptr1 << endl;
	//释放裸指针
	int* orign2 = ptr1.release();
	cout << *orign << ' ' << *orign2 << endl;
	if (ptr1 == nullptr) cout << "ptr1空" << endl;
	//测试reset命令
	auto ptr3 = make_unique<int>(7);
	orign = ptr3.get();
	cout << orign << endl;
	ptr3.reset();
	cout << orign << endl;
	if (ptr3 == nullptr) cout << "ptr3空" << endl;
	if (orign == nullptr) cout << "裸指针空" << endl;
	cout << *orign << endl;
	ptr3.reset(orign2);
	cout << *ptr3 << endl;

下面是测试结果:

ptr1空
6
ptr2空
6
6 6
ptr1空
00D5BAF0
00D5BAF0
ptr3空
-572662307
6

上面测试了unique_ptr的两种初始化方式,移动方式,重置方式,且比较了releas()和reset()的区别,要注意到使用reset()时,原先获得的裸指针所指向内容已被释放,但是该指针仍然可以使用,可能引发错误。

shared_ptr

shared_ptr的核心是引用计数技术。在每个shared_ptr对象中,都有一个指向所管理对象的指针和一个整型计数器。shared_ptr在引用计数归零时,释放原有内存。shared_ptr支持赋值和等号运算符重载。

	auto ptr1 = make_shared<int>(10);
	cout << "ptr1值: " << *ptr1 << endl;
	cout << "ptr1引用计数:" << ptr1.use_count() << endl;
	//改变引用计数测试
	auto ptr2 = ptr1; //可以直接赋值,与unique_ptr不同
	cout << "ptr1引用计数:" << ptr1.use_count() << endl;
	cout << "ptr2引用计数:" << ptr2.use_count() << endl;
	ptr2.reset();
	cout << "ptr1引用计数:" << ptr1.use_count() << endl;
	cout << "ptr2引用计数:" << ptr2.use_count() << endl;

测试结果如下

ptr1值: 10
ptr1引用计数:1
ptr1引用计数:2
ptr2引用计数:2
ptr1引用计数:1
ptr2引用计数:0

自定义删除器

使用shared_ptr删除数组时,需要指定删除器。

//使用默认删除器
std::shared_ptr<int> ptr2(new int[10], std::default_delete<int[]>()); 
///使用lambda函数
std::shared_ptr<int> ptr1(new int[10], [](int* p){delete [] p;});           

也可以自定义函数来实现。
需要注意,make_shared没有方法指定自己的删除器。

两种声明方式辨析

//智能指针的两种声明方式
auto ptr1 = make_shared<int>(new int(5));
shared_ptr<int> ptr2(new int(6));

使用make函数更好,相比于直接使用new表达式,make系列函数消除了重复代码(避免new两次),改进了异常安全性。

详细可以参考下面这篇博客:优先选择make_unique和make_shared,而非直接使用new

手撕智能指针

这里以shared_ptr的实现作为例子,在上述分析中,了解到智能指针的实现依赖类内封装的引用计数,在计数器归零时,执行析构函数释放内存。并且对智能指针的使用和普通指针相同。
这里我们实现一个简单的共现智能指针实现,需要完成以下函数

  • 构造函数:有参,无参,左值,右值
  • 运算符重载:* & =
  • 获取裸指针和引用计数的函数
  • 析构函数
    函数模板定义如下:
template<typename T>
class mshared_ptr {
private:
    T* _ptr;
    int* _cnt;
public:
    //构造和析构函数
    mshared_ptr() 
    mshared_ptr(T* ptr)
    mshared_ptr(const mshared_ptr& lsh)
    mshared_ptr(mshared_ptr&& lsh)
    ~mshared_ptr() 
    
    //运算符重载
    T* operator->()
    T& operator*()
    mshared_ptr<T>& operator=(const mshared_ptr<T>& other) 
    
    //获取引用计数
    int count()
    //获得裸指针
    T* get() 
};

share_unique实现

template<typename T>
class mshared_ptr {
private:
    T* _ptr;
    int* _cnt;
public:
    //构造和析构函数
    mshared_ptr() :_ptr(nullptr), _cnt(new int(0)) {
        cout << "无参构造" << endl;
    };
    mshared_ptr(T* ptr) :_ptr(ptr), _cnt(new int(1)) {
        cout << "含参构造" << endl;
    };
    mshared_ptr(const mshared_ptr& lsh):_ptr(lsh._ptr),_cnt(lsh._cnt){
        if(_cnt) (*_cnt)++;
        cout << "左值构造" << endl;
    }; //左值引用
    mshared_ptr(mshared_ptr&& lsh):_ptr(lsh._ptr), _cnt(lsh._cnt) {
        lsh._ptr = nullptr;
        lsh._cnt = nullptr;
        cout << "右值构造" << endl;
    };   //右值引用
    ~mshared_ptr() {
            if (_cnt&& --(*_cnt) == 0) {
                delete _ptr;
                delete _cnt;
                cout << "析构" << endl;
            }
    };

    //运算符重载
    T* operator->() { return _ptr; };
    T& operator*() { return *_ptr; };
    mshared_ptr<T>& operator=(const mshared_ptr<T>& other) {
        if (this != &other) {
            // 释放当前资源
            if (_cnt && --(*_cnt) == 0) {
                delete _ptr;
                delete _cnt;
            }
            _ptr = other._ptr;
            _cnt = other._cnt;
            if (_cnt) (*_cnt)++;
        }
        return *this;
    }

    //获取引用计数
    int count() {
        return _cnt?*_cnt:0;
    };
    //获得裸指针
    T* get() {
        return _ptr;
    };
};

对功能进行试验:

mshared_ptr<int> ptr1;
    mshared_ptr<int> ptr2(new int(5));
    cout << ptr1.count() << ' ' << ptr2.count() << endl;
    mshared_ptr<int> ptr3(move(ptr2));
    cout << ptr2.count() << ' ' << ptr3.count() << endl;
    ptr1 = ptr3;
    cout << ptr3.count() << ' ' << ptr1.count() << endl;
    mshared_ptr<int> ptr4(ptr3);
    int* ori = ptr4.get();
    cout << *ori << ' ' << ptr4.count()<<endl;

得到结果为

无参构造
含参构造
0 1
右值构造
0 1
2 2
左值构造
5 3
析构

可以看到我们四种构造函数都能正常使用,在引用计数计算时也表现正常。因为左值构造和等号赋值都是对一个智能指针进行操作,程序结束时仅析构一次。

线程安全优化

对于shared_ptr而言:

  • 同一个shared_ptr被多个线程读,是线程安全的
  • 同一个shared_ptr被多个线程写,不是线程安全的
  • 共享引用计数的不同的shared_ptr被多个线程写,是线程安全的
    可见,shared_ptr内部实现了线程安全,但是具体使用时还需要程序员注意。shared_ptr内部对引用计数的改变可能导致线程不安全,现在来对其进行优化。
    参考C++的多线程安全,我们可以有以下两种操作:
  • 使用atomic原子操作,声明引用计数为 atomic<int>。
  • 使用互斥量进行lock操作,或者使用lock_guard 自动加锁、解锁
  • 25
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值