智能指针、共享指针

智能指针

**问题:**在实际的 C++ 开发中,我们经常会遇到诸如程序运行中突然崩溃、程序运行所用内存越来越多最终不得不重启等问题,这些问题往往都是内存资源管理不当造成的。比如:

  • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
  • 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
  • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

**解决方法:**在 C++ 中,智能指针是一种用于管理动态分配的对象内存的工具,它可以帮助自动管理内存的分配和释放,从而减少内存泄漏的风险。C++ 标准库提供了几种不同类型的智能指针,其中最常用的有以下几种:


  1. std::shared_ptr:共享指针是一种允许多个智能指针共享同一个对象所有权的指针。当最后一个共享指针离开作用域时,对象会被自动销毁。

基本原理:就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。

std::shared_ptr<T> ptr;//ptr 的意义就相当于一个 NULL 指针。在 C++中,<T> 用于指定所持有对象的类型。

//make_shared辅助函数创建
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);//make_shared 是一个方便的函数,它会分配内存并返回一个指向新对象的 shared_ptr。
std::shared_ptr<int> ptr2 = ptr1; // 共享所有权

 ptr1.reset();  // 原来所指的对象会被销毁
  1. std::unique_ptr:唯一指针是一种独占对象所有权的指针,不允许多个 unique_ptr 指向同一个对象。当 unique_ptr 离开作用域时,对象会被自动销毁。
std::unique_ptr<int> ptr = std::make_unique<int>(42);
  1. std::weak_ptr:弱指针不会增加对象的引用计数,因此不会阻止对象的销毁。它的作用是为了解决循环引用(circular reference)问题,弱指针可以从 shared_ptr 创建,从而避免内存泄漏。
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;

这些智能指针在不同场景下具有不同的用途。std::shared_ptr 用于多个指针共享对象所有权的情况,std::unique_ptr 用于独占对象所有权的情况,std::weak_ptr 用于解决循环引用问题。选择适当的智能指针类型可以提高代码的安全性和可维护性。除了上述三种,还有其他的智能指针如 std::auto_ptr(已在 C++11 中废弃)、std::scoped_ptr 等,但在现代 C++ 中推荐使用 std::shared_ptrstd::unique_ptr

针对std::shared_ptr
  • make_shared辅助函数创建
std::shared_ptr<int> foo = std::make_shared<int> (10);
  • 自定义所指堆内存的释放规则

当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

对于申请的动态数组,释放规则可以

  1. 使用 C++11 标准中提供的 default_delete 模板类
  2. 可以自定义释放规则
//指定 default_delete 作为释放规则
//这段代码的作用是创建一个共享指针 p6,它将管理一个包含 10 个整数的数组,并在 p6 被销毁时,使用 std::default_delete<int[]>() 来释放数组内存。
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
//自定义释放规则
void deleteInt(int*p) {
    delete[] p;//delete[] 操作符来释放该数组的内存,以防止内存泄漏。
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);
  • shared_ptr常用函数

get()函数,表示返回当前存储的指针(就是被shared_ptr所管理的指针) 。但是不建议使用get()函数获取 shared_ptr 关联的原始指针,因为如果在 shared_ptr 析构之前手动调用了delete函数,会导致错误。

shared_ptr<T> ptr(new T());
T *p = ptr.get(); // 获得传统 C 指针

use_count()函数,表示当前引用计数

shared_ptr<T> a(new T());
a.use_count(); //获取当前的引用计数

reset()函数,表示重置当前存储的指针。

shared_ptr<T> a(new T());
a.reset(); // 此后 a 原先所指的对象会被销毁,并且 a 会变成 NULL

operator*:表示返回对存储指针指向的对象的引用。它相当于:* get()。
operator->,表示返回指向存储指针所指向的对象的指针,以便访问其中一个成员。跟get函数一样的效果。


示例1:shared_ptr的基础应用:
#include <iostream>
#include  <memory> // 共享指针必须要包含的头文件
using namespace std;
int main()
{
	// 最好使用make_shared创建共享指针,
	shared_ptr<int> p1 = make_shared<int>();//make_shared 创建空对象,
	*p1 = 10;
	cout << "p1 = " << *p1 << endl; // 输出10
// 打印引用个数:1
cout << "p1 count = " << p1.use_count() << endl;
// 第2个 shared_ptr 对象指向同一个指针
std::shared_ptr<int> p2(p1);

// 输出2
cout << "p2 count = " << p2.use_count() << sendl;
cout << "p1 count = " << p1.use_count() << endl;

// 比较智能指针,p1 等于 p2
if (p1 == p2) {
	std::cout<< "p1 and p2 are pointing to same pointer\n";
}

p1.reset();// 无参数调用reset,无关联指针,引用个数为0
cout << "p1 Count = " << p1.use_count() << endl;

p1.reset(new int(11));// 带参数调用reset,引用个数为1
cout << "p1 Count = " << p1.use_count() << endl;

p1 = nullptr;// 把对象重置为NULL,引用计数为0
cout << "p1  Reference Count = " << p1.use_count() << endl;
if (!p1) {
	cout << "p1 is NULL" << endl; // 输出
}
return 0;
}
示例2:shared_ptr作返回值
shared_ptr<string> factory(const char* p) 
{
    shared_ptr<string> p1 = make_shared<string>(p);
    return p1;
}

void use_factory() 
{
    shared_ptr<string> p = factory("helloworld");
    int num1 = p.use_count();
    cout << *p << endl;//!离开作用域时,p引用的对象被销毁。
}
shared_ptr<string> return_share_ptr()
{
    shared_ptr<string> p = factory("helloworld");
    cout << *p << endl;
    return p; //!返回p时,引用计数进行了递增操作。 
} //!p离开了作用域,但他指向的内存不会被释放掉。 

int main()
{
    use_factory();
    auto p = return_share_ptr();
    cout << p.use_count() << endl;
    system("pause");
    return 0;
}
//可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。
//例如,当用一个shared_ptr去初始化另一个shared_ptr;当我们给shared_ptr赋予一个新的值或者是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。
//一旦一个shared_ptr的计数器变为0,他就会自动释放自己所管理的对象。
示例3:容器中的shared_ptr-记得用erease节省内存

对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,他就不会被释放掉。由于这个特性,保证shared_ptr在不用之后不再保留就非常重要了,通常这个过程能够自动执行而不需要人工干预,有一种例外就是我们将shared_ptr放在了容器中。所以永远不要忘记erease不用的shared_ptr。

#include <iostream>
using namespace std;
int main()
{
    list<shared_ptr<string>>pstrList;
    pstrList.push_back(make_shared<string>("1111"));
    pstrList.push_back(make_shared<string>("2222"));
    pstrList.push_back(make_shared<string>("3333"));
    pstrList.push_back(make_shared<string>("4444"));
    for(auto p:pstrList)
    {
        if(*p == "3333");
        {
            /*do some thing!*/
        }
        cout<<*p<<endl;
    }
    /*包含"3333"的数据我们已经使用完了!*/
for(list<shared_ptr<string>>::iterator itr = pstrList.begin();itr!=pstrList.end();++itr)
{
    if(**itr == "3333"){
        cout<<**itr<<endl;
        pstrList.erase(itr);
    }
}
cout<<"-------------after remove------------"<<endl;
for(auto p:pstrList)
{
    cout<<*p<<endl;
}
  while(1)
  {
    /*do somthing other works!*/
    /*遍历 pstrList*/    //!这样不仅节约了大量内存,也为容器的使用增加了效率  
  }
 }
示例4:shared_ptr:对象共享相同状态

使用shared_ptr在一个常见的原因是允许多个多个对象共享相同的状态,而非多个对象独立的拷贝!

#include <iostream>
using namespace std;
void copyCase()
{
    list<string> v1({"1","b","d"});
    list<string> v2 = v1;        //!v1==v2占用两段内存

    v1.push_back("cc");            //!v1!=v2
    
    for(auto &p:v1){
        cout<<p<<endl;
    }
    cout<<"--------void copyCase()---------"<<endl;
    for(auto &p:v2){
        cout<<p<<endl;
    }

} //v1和v2分属两个不同的对象,一个改变不会影响的状态。
void shareCase()
{
    shared_ptr<list<string>> v1 = make_shared<list<string>>(2,"bb");
    shared_ptr<list<string>> v2 = v1;
(*v1).push_back("c2c");
for(auto &p:*v1){
    cout<<p<<endl;
}
cout<<"----------shareCase()--------"<<endl;
for(auto &p:*v2){
    cout<<p<<endl;
}
} //v1和v2属于一个对象的两个引用,有引用计数为证,其内容的改变是统一的。

int main()
{
    copyCase();
    cout<<"++++++++++++++++"<<endl;
    shareCase();
}
示例5:shared_ptr管理动态数组

默认情况下,shared_ptr指向的动态的内存是使用delete来删除的。这和我们手动去调用delete然后调用对象内部的析构函数是一样的。与unique_ptr不同,shared_ptr不直接管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自定义的删除器来替代delete 。

#include <iostream>
using namespace std;

class DelTest
{
public:
    DelTest(){
        j= 0;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
  static int i,j;
};
int DelTest::i = 0;
int DelTest::j = 0;
void noDefine()
{
    cout<<"no_define start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10]);

}
void slefDefine()
{
    cout<<"slefDefine start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});
}                     //!传入lambada表达式代替delete操作。
int main()
{
    noDefine();   //!构造10次,析构1次。内存泄漏。
    cout<<"----------------------"<<endl;
    slefDefine();  //!构造次数==析构次数 无内存泄漏
}
Warning 常见错误(注意以下代码全是错误代码)

1.不能使用原始指针初始化多个shared_ptr。

int* p11 = new int;
std::shared_ptr<int> p12(p11);
std::shared_ptr<int> p13(p11);
// 由于p1和p2是两个不同对象,但是管理的是同一个指针,这样容易造成空悬指针,
//比如p1已经将aa delete了,这时候p2里边的aa就是空悬指针了

2.不允许以暴露裸漏的指针进行赋值

//带有参数的 shared_ptr 构造函数是 explicit 类型的,所以不能像这样
std::shared_ptr<int> p1 = new int();//不能隐式转换,类型不匹配

隐式调用它构造函数

3.不要用栈中的指针构造 shared_ptr 对象

int x = 12;
std::shared_ptr<int> ptr(&x);

shared_ptr 默认的构造函数中使用的是delete来删除关联的指针,所以构造的时候也必须使用new出来的堆空间的指针。当 shared_ptr 对象超出作用域调用析构函数delete 指针&x时会出错。

4.不要使用shared_ptr的get()初始化另一个shared_ptr

Base *a = new Base();
std::shared_ptr<Base> p1(a);
std::shared_ptr<Base> p2(p1.get());
//p1、p2各自保留了对一段内存的引用计数,其中有一个引用计数耗尽,资源也就释放了,会出现同一块内存重复释放的问题

5.多线程中使用 shared_ptr
shared_ptr的引用计数本身是安全且无锁的,但对象的读写则不是,因为shared_ptr 有两个数据成员,读写操作不能原子化。shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

  • 一个 shared_ptr 对象实体可被多个线程同时读取
  • 两个 shared_ptr 对象实体可以被两个线程同时写入,“析构”算写操作
  • 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值