C++:四大智能指针
一、前言:
作为C++程序员,我们是否总new完对象,未delete而导致内存泄露问题,导致程序慢慢变卡直至崩溃。为了解决这个问题,C++11推出了智能指针,将new完的对象管理交给智能指针,它会在该释放的时候会自动帮我们释放资源。
手动释放造成程序内存泄露问题的小案例:
#include<iostream>
using namespace std;
class Test
{int a = 10;
};
void func()
{while (true){Test* t = new Test();//new完忘记delete/*..............程序运行相关代码*/}
}
int main()
{func();return 0;
}
二、四大智能指针:
1、auto_ptr
特点:
- 析构时自动释放资源
- 赋值或拷贝时会将原本的指针置空
应用场景:
- 具有致命缺陷:auto_ptr是最简易的智能指针,析构自动释放资源这是优点,但是赋值拷贝时导致原本的auto_ptr置空就可能会导致访问空指针的问题,引起程序崩溃,所以一般项目中不常用,但是具有初步学习了解智能指针原理的意义
简单实现:
#pragma once
#include <iostream>template <class T>
class auto_ptr
{
public:// 构造函数:接管原始指针的所有权auto_ptr(T *ptr = nullptr) : _ptr(ptr) {}// 析构函数:自动释放资源~auto_ptr(){if (_ptr){delete _ptr; // 析构时自动释放资源}}// 拷贝构造函数:转移所有权,源对象置空auto_ptr(auto_ptr<T>& rhs) : _ptr(rhs._ptr){rhs._ptr = nullptr; // 拷贝时将原本指向的指针置空}// 赋值运算符:先释放当前资源,再接管新资源auto_ptr<T>& operator=(auto_ptr<T>& rhs){if (&rhs != this) // 防止自赋值{if (_ptr)delete _ptr;_ptr = rhs._ptr;rhs._ptr = nullptr; // 赋值时将原本指向的指针置空}return *this;}// 解引用操作符:获取托管对象的引用T& operator*(){return *_ptr;}// 箭头操作符:访问托管对象的成员T* operator->(){return _ptr;}private:T *_ptr; // 托管的原始指针
};
2、unique_ptr
特点:
- 小巧高效:默认情况下,其大小与原始指针相同,几乎没有额外开销。
- 安全:不允许赋值或者拷贝,保证了其安全性
- 缺乏灵活性:不允许赋值或者拷贝,就导致了只有这一个指针可以指向,少了些灵活性
应用场景:
- 在不需要共享所有权的绝大多数情况下,都应使用 unique_ptr。
- 管理独占资源的对象:例如,在单例模式中,完全符合单例模式单一资源的要求
简单实现:
#pragma once
#include <iostream>template <class T>
class unique_ptr
{
public:// 构造函数:获取资源所有权unique_ptr(T *ptr = nullptr) : _ptr(ptr) {}// 析构函数:自动释放资源~unique_ptr(){if (_ptr){delete _ptr; // 析构时自动释放资源}}// 禁止拷贝构造:保证所有权唯一性unique_ptr(unique_ptr<T> &rhs) = delete;// 禁止拷贝赋值:保证所有权唯一性unique_ptr<T> &operator=(unique_ptr<T> &rhs) = delete;// 解引用操作符:获取托管对象的引用T &operator*(){return *_ptr;}// 箭头操作符:访问托管对象的成员T *operator->(){return _ptr;}private:T *_ptr; // 托管的原始指针
};
3、shared_ptr
特点:
- 共享所有权:多个 shared_ptr 可以共同拥有同一个对象。
- 引用计数:有计数器可以记录指向资源的指针个数,拷贝赋值时,会增加,析构时会--,当计数为0时就会释放资源
- 有额外开销:需要额外的资源来存储引用计数等信息,大小通常是原始指针的两倍。
- 循环引用问题:如果两个对象互相使用 shared_ptr 指向对方,会导致引用计数永远无法降为0,从而产生内存泄漏。
ps:循环引用问题由下图详解:
首先释放Node1,先释放ptr1引用计数仍为1,所以想要释放Node1,就要释放Node2中的_prev,要先释放Node2的_prev,就要先释放Node2,但是Node2有ptr2指针指向所以释放不了,就会因为这种相互引用,使得节点的引用计数无法减到 0,进而无法释放内存,造成内存泄漏。这就是循环引用问题。
应用场景:
- 需要共享所有权的场景:多个模块或对象需要同时访问和管理同一个资源,且无法确定哪个对象最后使用它。
简单实现:
#include <iostream>template <class T>
class shared_ptr
{
private:T *_ptr; // 托管的对象指针int *_cnt; // 引用计数器指针public:// 构造函数:创建新资源并初始化引用计数为1shared_ptr(T *ptr = nullptr) : _ptr(ptr), _cnt(new int(1)){std::cout << "shared_ptr构造函数" << std::endl;}// 析构函数:释放资源管理权~shared_ptr(){release();}// 拷贝构造函数:共享所有权,引用计数+1shared_ptr(const shared_ptr<T> &rhs) : _ptr(rhs._ptr), _cnt(rhs._cnt){(*_cnt)++; // 增加引用计数}// 赋值运算符:先释放旧资源,再共享新资源shared_ptr<T> &operator=(const shared_ptr<T> &rhs){if (&rhs != this) // 防止自赋值{release(); // 先释放管理的旧资源_cnt = rhs._cnt;_ptr = rhs._ptr;++(*_cnt); // 增加新资源的引用计数}return *this;}// 解引用操作符:获取托管对象的引用T &operator*(){return *_ptr;}// 箭头操作符:访问托管对象的成员T *operator->(){return _ptr;}// 获取原始指针T* get(){return _ptr;}// 获取当前引用计数int get_cnt(){return *_cnt;}// 重置智能指针:管理新资源void reset(T* ptr){release(); // 释放当前资源_cnt = new int(1); // 创建新的引用计数_ptr = ptr; // 接管新资源}private:// 释放资源:减少引用计数,必要时彻底释放void release(){if (_cnt) // 确保计数器存在{--(*_cnt); // 减少引用计数if (*_cnt == 0 && _ptr) // 如果计数为0且对象存在{std::cout << "资源被彻底释放" << std::endl;delete _ptr; // 释放托管对象delete _cnt; // 释放计数器_ptr = nullptr;_cnt = nullptr;}}}
};
4、weak_ptr
特点:
- 弱引用:它是对由 shared_ptr 管理的对象的非拥有性引用。
- 不增加引用计数:它的存在与否不会影响对象的生命周期。
- 解决循环引用:用于打破 shared_ptr 的循环引用问题。例如,在上文提到双向链表中节点的_prev和_next指针就可以是weak_ptr类型,这样指向对方时不会增加计数,很好地解决了shared_ptr中的循环引用问题
- 需要转换为 shared_ptr 才能访问对象:不能直接解引用访问数据。
应用场景:
- 需要解决了shared_ptr中的循环引用问题
简单实现:
#pragma once
#include <iostream>template<class T>class weak_ptr {public:// 默认构造函数weak_ptr() : _ptr(nullptr), _cnt(nullptr) {}// 从 shared_ptr 构造weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()), _cnt(sp.get_cnt_ptr()) {}// 拷贝构造函数weak_ptr(const weak_ptr<T>& other) : _ptr(other._ptr), _cnt(other._cnt) {}// 析构函数 - weak_ptr 析构不需要操作引用计数~weak_ptr() = default;// 赋值运算符weak_ptr& operator=(const weak_ptr<T>& other) {if (this != &other) {_ptr = other._ptr;_cnt = other._cnt;}return *this;}// 从 shared_ptr 赋值weak_ptr& operator=(const shared_ptr<T>& sp) {_ptr = sp.get();_cnt = sp.get_cnt_ptr(); // 同样需要这个辅助函数return *this;}// 检查对象是否已被释放bool expired() const {return _cnt == nullptr || *_cnt == 0;}// 获取引用计数int use_count() const {return (_cnt) ? *_cnt : 0;}// 重置 weak_ptrvoid reset() {_ptr = nullptr;_cnt = nullptr;}// 不允许有解引用相关操作// T &operator*() { return *_ptr; } // T *operator->() { return _ptr; } private:T* _ptr; // 观察的对象的指针int* _cnt; // 指向 shared_ptr 的引用计数器的指针// 声明为友元,以便 shared_ptr 可以访问私有成员friend class shared_ptr<T>;};
三、智能指针的优劣
这四大智能指针可以很好地满足C++程序员用指针的需求,要学会在合适的情况下运用正确的指针类型,这样可以保证程序减少出现内存泄露问题、使程序更安全等优势,下图的表格是对这四大指针总结,要学会合理使用它们
指针类型 | 特点 | 应用场景 |
auto_ptr | 赋值或拷贝时会将原本的指针置空 | 已经被摒弃,项目中几乎不用 |
unique_ptr | 不允许赋值或者拷贝,保证了其安全性 | 在不需要共享所有权,独占管理权的的情况下 |
shared_ptr | 共享所有权、引用计数 | 需要共享所有权的场景 |
weak_ptr | 弱引用、不增加引用计数 | 需要解决了shared_ptr中的循环引用问题 |
下图是我做的思维导图,供参考复习:
结语:
以上就是我分享的C++四大智能指针的全部内容了,希望对大家有些帮助,也希望与一样喜欢编程的朋友们共进步
谢谢观看
如果觉得还阔以的话,三连一下,以后会持续更新的,我会加油的
祝大家早安午安晚安