@TOC
C++智能指针
为什么要使用智能指针?
<<C++ Primer>> p400
虽然使用动态内存有时是必要的,但众所周知,正确地管理动态内存是非常棘手的。
为了更容易(同时也更安全的)地使用动态内存,新的标准库提供了两种智能指针,来管理动态对象。智能指针的行为类似于常规指针,重要的区别是它负责自动释放所指向的对象。
shared_ptr允许多个指针指向同一个对象,unique_ptr是“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memeory头文件中。
原理: 将我们分配的动态内存都交给有生命周期的对象来处理,当对象过期时,让它的析构函数删除指向的内存。
- C++98 提供了 auto_ptr模板的解决方案
- C++11 增加了 unique_ptr、shared_ptr、weak_ptr
(就是一个类模板,里面有析构函数,能够自动释放这个对象开辟的内存。)
auto_ptr
C++98的智能指针模板,其定义了管理指针的对象,可以将new获得(直接或间接获得)的地址赋值给这种对象。当对象过期时,其析构函数会用delete来释放内存。
就是一个类模板,自动调用析构函数释放。
用法
#include<memory>
用法: auto_ptr<类型>变量名(new 类型)
#include<iostream>
#include<memory>
using namespace std;
class Test{
public:
Test()
{
cout << "Test is construct" << endl;
}
~Test()
{
cout << "Test is destruct" << endl;
}
};
int main(void)
{
auto_ptr<Test>test(new Test());
return 0;
}
方法:
test.get();//得到new出来的指针,一般不会这么用
//get出来的指针不用手动清理掉指向的内存,new出来的那块儿内存还是归智能指针管理。
test.release();//取消智能指针对动态内存的托管,之前分配的内存需要手动释放。
test.reset();//重置指针托管的内存地址,如果地址不一样,原来的会被析构掉,
例如:test.reset(new Test());//创建一个新的,代替原来的,并将原来的释放掉。
补充——new 一个对象加不加括号-链接
建议
1.尽可能不要将auto_ptr 变量定义为全局变量或指针,程序结束之后释放,没有意义。
2.不要定义指向智能指针的指针。不会自动释放指针的指针。
3.除非自己知道后果,不要把auto_ptr 智能指针赋值给同类型的另外一个智能指针,解释如下。
4.C++11 后auto_ptr 已经被“抛弃”,已使用unique_ptr替代!
unique_ptr
auto_ptr弊端
auto_ptr是用于C++11之前的智能指针。由于auto_ptr基于排他所有权模式,两个之怎不能指向同一个资源,复制或赋值都会改变资源的所有权。auto_ptr主要问题如下:
- 复制和赋值会改变资源的所有权,不符合人的直觉。
- 在 STL 容器中使用auto_ptr存在重大风险,因为容器内的元素必需支持可复制(copy constructable)和可赋值(assignable)。
- 不支持对象数组的操作。
弊端1. auto_ptr 被C++11 抛弃的主要理由 p1= p2 ,复制或赋值都会改变资源的所有权
auto_ptr<string> p1(new string("I 'm martin."));
auto_ptr<string> p2(new string("I 'm rock."));
p1 = p2;
//p1先释放自己的动态内存然后接收p2的,p2再将自己置空
智能指针的内存管理陷阱(不只是它的缺陷,unique_ptr也有)
auto_ptr<string>p2;
string* ptr = new string("智能指针的内存管理陷阱");
p2.reset(str);
{
auto_ptr<string>p1;
p1.reset(str);
}//访问越界
弊端2.在 STL 容器中使用auto_ptr存在重大风险
vector<auto_ptr<string>>va;
auto_ptr<string>p1(new string("我是p1"));
auto_ptr<string>p2(new string("我是p2"));
va.push_back(std::move p1);
va.push_back(std::move p2);
//风险来啦
va[0] = va[1];//效果同上
弊端3.不支持对象数组的内存管理
auto_ptr<int[]>ary(new int[5]);//不行
总上所述,C++11用了更严谨的unique_ptr取代了aoto_ptr;
特性
- 基于排他所有权模式:两个指针不能指向同一个资源。
- 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值。std::move。把右值转换为左值。
- 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
- 在容器中保存指针是安全的。不支持直接复制v[0] = v[1]不行。
- 支持对象数组的内存管理,自动调用delete释放。
构造函数
unique_ptr<T> up ; //空的unique_ptr,可以指向类型为T的对象
unique_ptr<T> up(new T()) ;//定义unique_ptr,同时指向类型为T的对象
unique_ptr<T[]> up ; //空的unique_ptr,可以指向类型为T的数组对象
unique_ptr<T[]> up(new T[]) ;//定义unique_ptr,同时指向类型为T的数组对象
unique_ptr<T,D> up(); //空的unique_ptr,接受一个D类型的删除器D,使用D释放内存
unique_ptr<T,D> up(new T()); //定义unique_ptr,同时指向类型为T的对象,接受一个D类型的删除器d,使用删除器d来释放内存
删除器
利用一个仿函数实现一个删除器
class DestructTest
{
public:
void operator()(Test* pt)
{
//删除前做了一些操作
pt->dosomething();
delete pt;
}
};
int main(void)
{
//使用自定义的删除器
unique_ptr<Test, DestructTest>up(new Test());
return 0;
}
赋值
(接管所有权)一定要使用移动语义
(可以对比理解一下类中的深浅拷贝)
unique_ptr<int> s1(new int(1));
unique_ptr<int> s2(new int(2));
s1 = std::move(s2);
//s1将自己的所指向的动态内存释放,然后接管s2所指向的动态内存,s2将自己置空。
主动释放对象
unique_prt<int> s3(new int(3));
//如果你想早点释放早点用这块内存,或者说某个分支...那么你就主动释放它。
//多数情况下没必要,都主动释放了,那么我们使用智能指针的意义在哪呢?
s3 = NULL;
or
s3 = nullptr;
or
s3.reset();
放弃对象控制权
s3.release();//放弃对象的控制权,返回指针,然后将s3重为空
<<C++ Primer>>p418
调用release会切断unique_prt和它原来管理对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。在本例中,管理内存的责任简单地从一个指针转给了另一个。但是如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放。
p2.release();//错误,p2不会释放内存,而且我们丢失了指针。
auto p = p2.release();//正确,记得delete p
交换
s3.swap(s4);//将智能指针s3和s4所管控的对象进行交换。
重置
s3.reset();//参数可以为空、内置指针,先将up所指向的对象释放,然后重置up的值,将up指向新的玩意儿。放一个地址进去指向这个地址对应的东西。
s3.reset(p);//如果提供了内置指针q,令s3指向p, 注意p得是动态开辟的东西(返回的指针)。
s3.reset(s2.release());//将s2所指向内存的所有权交给了s3
(指针指向的是变量,存的是该变量的地址。)
share_ptr
熟悉了unique_ptr 后,其实我们发现unique_ptr 这种排他型的内存管理并不能适应所有情况,有很大的局限!如果需要多个指针变量共享怎么办?
如果有一种方式,可以记录引用特定内存对象的智能指针数量,当复制或拷贝时,引用计数加1,当智能指针析构时,引用计数减1,如果计数为零,代表已经没有指针指向这块内存,那么我们就释放它!这就是 shared_ptr 采用的策略!
构造函数
shared_ptr<T> sp ; //空的shared_ptr,可以指向类型为T的对象
shared_ptr<T> sp1(new T()) ;//定义shared_ptr,同时指向类型为T的对象
shared_ptr<T[]> sp2 ; //空的shared_ptr,可以指向类型为T[的数组对象 C++17后支持
shared_ptr<T[]> sp3(new T[]{...}) ;//指向类型为T的数组对象 C++17后支持
shared_ptr<T> sp4(NULL, D()); //空的shared_ptr,接受一个D类型的删除器,使用D释放内存
shared_ptr<T> sp5(new T(), D()); //定义shared_ptr,指向类型为T的对象,接受一个D类型的删除器,使用D删除器来释放内存
数组对象的管理:
shared_ptr<Person[]>sp1(new Person[5](3,4,5,6,7));
//创建对象数组并传递参数-顺序创建,反向析构
初始化
方式1:构造函数
shared_ptrr<int> up1(new int(10)); //int(10) 的引用计数为1
shared_ptrr<int> up2(up1); //使用智能指针up1构造up2, 此时int(10) 引用计数为2
方式2:使用make_shared初始化对象,分配内存效率更高
make_shared函数的主要功能是在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr;
(make_shared不算引用计数)
用法:
make_shared<类型>(构造类型对象需要的参数列表);
shared_ptr<int> p4 = make_shared<int>(2); //多个参数以逗号','隔开,最多接受十个
shared_ptr<string> p4 = make_shared<string>("字符串");
赋值
shared_ptrr<int> up1(new int(10)); //int(10) 的引用计数为1
shared_ptr<int> up2(new int(11)); //int(11) 的引用计数为1
up1 = up2;//int(10) 的引用计数减1,计数归零内存释放,up2共享int(11)给up1, int(11)的引用计数为2
主动释放对象
shared_prt<int>up(new int(10));
up = nullptr;//int(10)的引用计数-1,计数归零内存释放。
or
up = NULL;//作用同上
重置
up.reset(); //将up重置为空指针,所管理对象引用计数 减1
up.reset(p1); //将up重置为p1(的值),up 管控的对象计数减1,p接管对p1指针的管控
up.reset(p1,d); //将up重置为p1(的值),up管控的对象计数减1并使用d作为删除器
交换
std::swap(p1,p2); //交换p1 和p2 管理的对象,原对象的引用计数不变
p1.swap(p2); //同上
使用陷阱
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源。
#include<iostream>
#include<memory>
using namespace std;
class Girl;
class Boy
{
public:
Boy()
{
cout << "boy construct" << endl;
}
~Boy()
{
cout << "boy destruct" << endl;
}
void set_girl_friend(shared_ptr<Girl>& g)
{
girl_friend = g;
}
private:
shared_ptr<Girl>girl_friend;
};
class Girl
{
public:
Girl()
{
cout << "girl construct" << endl;
}
~Girl()
{
cout << "girl destruct" << endl;
}
void set_boy_friend(shared_ptr<Boy>& b)
{
boy_friend = b;
}
private:
shared_ptr<Boy>boy_friend;
};
void use_trap()
{
shared_ptr<Girl>sp_girl(new Girl());
shared_ptr<Boy>sp_boy(new Boy());
sp_girl->set_boy_friend(sp_boy);
sp_boy->set_girl_friend(sp_girl);
//对象引用计数都为2
//定义的指针和类中私有属性中的指针,都指向new出来的这个对象
//boy-firend sp_boy girl-firend sp_girl
cout << sp_girl.use_count() << endl;//2
cout << sp_boy.use_count() << endl;//2
}
int main(void)
{
use_trap();
return 0;
}
断开其中的一条链接之后,两个指针即可正常的释放
weak_ptr
为了解决shared_ptr交叉循环引用无法释放的问题。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少. *同时weak_ptr 没有重载和->但可以使用 lock 获得一个可用的 shared_ptr 对象。**
为了解决上面shared_ptr所出现的问题。提出weak_ptr,不影响引用计数。
在必要的时候可以转换成shared_ptr
.lock();
完美解决。
类中弱指针,用shared指针构造weak指针,用的时候,将weak指针转成shared指针来调用成员函数。
#include<iostream>
#include<memory>
using namespace std;
class Girl;
class Boy
{
public:
Boy()
{
cout << "boy construct" << endl;
}
~Boy()
{
cout << "boy destruct" << endl;
}
void set_girl_friend(weak_ptr<Girl>& g)
{
girl_friend = g;
}
private:
weak_ptr<Girl>girl_friend;
};
class Girl
{
public:
Girl()
{
cout << "girl construct" << endl;
}
~Girl()
{
cout << "girl destruct" << endl;
}
void set_boy_friend(weak_ptr<Boy>& b)
{
boy_friend = b;
}
private:
weak_ptr<Boy>boy_friend;
};
void use_trap()
{
shared_ptr<Girl>sp_girl(new Girl());
shared_ptr<Boy>sp_boy(new Boy());
weak_ptr<Girl>w_girl(sp_girl);
weak_ptr<Boy>w_boy(sp_boy);
shared_ptr<Girl>sp_girl2 = w_girl.lock();
shared_ptr<Boy>sp_boy2 = w_boy.lock();
sp_girl2->set_boy_friend(w_boy);
sp_boy2->set_girl_friend(w_girl);
}
int main(void)
{
use_trap();
return 0;
}
智能指针注意要点
- 不要把一个原生指针给多个智能指针管理
int *x = new int(10);
unique_ptr<int> up1(x);
unique_ptr<int> up2(x);
//警告! 以上代码使up1 up2指向同一个内存,非常危险或以下形式:up1.reset(x);up2.reset(x);
- 记得使用u.release()的返回值
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了
- 禁止delete 智能指针get 函数返回的指针
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
- 禁止用任何类型智能指针get 函数返回的指针去初始化另外一个智能指针!
shared_ptr sp1(new int(10));
一个典型的错误用法
shared_ptr sp4(sp1.get());
评论区