欢迎交流 QQ 2431173627 微信 ccc17862701790
本系列的上一篇文章讲了手动方式的内存管理 new delete
这种方式往往存在很多问题 这一节将讲通过智能指针来自动管理动态内存
智能指针的引入
智能指针的引入有以下几大原因
1.在动态内存管理中,如果new上一块空间,但是没有delete,就会产生内存泄露的问题。
2.但是有时候,我们new了,也delete了,但是还会出现问题。例如在new和delete之间调用了某个抛异常的函数,就有可能导致没有执行delete。
例如:fun2里面使用了new[],最后也使用了delete[],看着没有问题,但是在new和delete之间调用了fun1,而fun1里面抛了异常,但是在fun2的delete之前并没有捕获,就会导致delete没有执行,仍然会有内存泄露的问题。
void fun1()
{
throw int(11);
}
void fun2()
{
int* p = new int[10000];
fun1();
delete[] p;
}
int main()
{
try
{
fun2();
}
catch (int& e)
{
cout << "捕获" << endl;
}
system("pause");
return 0;
}
如果想要解决上面的问题,有一种方法:就是如果发现delete之前发现某个函数抛出了异常,就在delete之前捕获这个异常,并且在catch语句里面进行资源的释放,并且可以再将这个异常重新抛出。
void fun2()
{
int* p = new int[10000];
try
{
fun1();
}
catch(int& e)
{
delete[] p;
cout << "重新抛出" << endl;
throw;
}
delete[] p;
}
但是这种方法写着比较繁琐,而且如果代码很多,就有可能忘记哪个函数抛出了异常,会防不胜防。
3.还有一种场景就是内存空间是delete释放了以后 但是指针没有置NULL (delete是不会把指针自动置为null的) 这样是野指针
3.因此,只要有一种方法,能够在出了作用域之后能够自动释放掉申请的空间,就会让空间得到正确的释放。而一个对象出了作用域会自动调用自己的析构函数,只要在析构函数里能够释放自己开辟的空间,就能达到目的。
4.智能指针就有上面的作用,能够自动的管理指针所指向的动态资源的释放。它不仅有着RAII的思想还能够像指针一样。(RAII:分配资源即初始化,即构造函数分配资源和初始化资源,在析构函数清理资源。像指针一样:能够解引用)。
5.智能指针实质上就是一个模板类,成员变量包含一个任意类型的指针,构造函数获得资源并初始化,析构函数清理资源。
注意:智能指针只能管理动态开辟的空间。
智能指针的发展历史
C++98时代的智能指针
Boost库的智能指针

C++11时代的智能指针
unique_ptr
介绍
unique_ptr是auto_ptr的继承者,对于同一块内存只能有一个持有者,而unique_ptr和auto_ptr唯一区别就是unique_ptr不允许赋值操作,也就是不能放在等号的右边(函数的参数和返回值例外),这一定程度避免了一些误操作导致指针所有权转移,然而,unique_str依然有提供所有权转移的方法move,调用move后,原unique_ptr就会失效,再用其访问裸指针也会发生和auto_ptr相似的crash,如下面示例代码,所以,即使使用了unique_ptr,也要慎重使用move方法,防止指针所有权被转移。
unique_ptr<int> up(new int(5));
//auto up2 = up; // 编译错误
auto up2 = move(up);
cout << *up << endl; //crash,up已经失效,无法访问其裸指针
除了上述用法,unique_ptr还支持创建动态数组。在C++中,创建数组有很多方法,如下所示:
// 静态数组,在编译时决定了数组大小
int arr[10];
// 通过指针创建在堆上的数组,可在运行时动态指定数组大小,但需要手动释放内存
int *arr = new int[10];
// 通过std::vector容器创建动态数组,无需手动释放数组内存
vector<int> arr(10);
// 通过unique_ptr创建动态数组,也无需手动释放数组内存,比vector更轻量化
unique_ptr<int[]> arr(new int[10]);
这里需要注意的是,不管vector还是unique_ptr,虽然可以帮我们自动释放数组内存,但如果数组的元素是复杂数据类型时,我们还需要在其析构函数中正确释放内存。
使用
/***unique_ptr********/
unique_ptr<int> uni_p1;//unique_ptr默认初始化
unique_ptr<int> uni_p2(new int(42));//unique_ptr 普通初始化
unique_ptr<int> uni_p4(new int(51));//unique_ptr 普通初始化
unique_ptr<int> uni_p3(uni_p4.release());//unique_ptr 使用release初始化
//unique_ptr <int> uni_p2(uni_p1) 是错误的 不支持拷贝
//unique_ptr<int> uni_p2; uni_p2=unoi_p1是错误的 不支持赋值
*uni_p3=4;//unique_ptr解引用赋值
cout<<*uni_p3<<endl;//unique_ptr解引用取值
cout<<"value of uni_p2="<<*uni_p2<<endl;
swap(uni_p1,uni_p2);//unique_ptr交换两指针
uni_p2.swap(uni_p1);//unique_ptr交换两指针
uni_p2=nullptr;//unique_ptr 释放u指向的对象 将u置空
int *temp_p3=uni_p1.release();//unique_ptr放弃对内存对象的控制权 返回普通指针 将uni_p1置空
delete temp_p3;temp_p3=NULL;
uni_p3.reset(new int(66));//unique_ptr释放原来所指向的内存对象 让其指向新给定的对象
uni_p3.reset();//unique_ptr释放所指向的内存对象 将指针置空
shared_ptr
介绍
auto_ptr和unique_ptr都有或多或少的缺陷,因此C++11还推出了shared_ptr,这也是目前工程内使用最多最广泛的智能指针,他使用引用计数(感觉有参考Objective-C的嫌疑),实现对同一块内存可以有多个引用,在最后一个引用被释放时,指向的内存才释放,这也是和unique_ptr最大的区别。
另外,使用shared_ptr过程中有几点需要注意:
构造shared_ptr的方法,如下示例代码所示,我们尽量使用shared_ptr构造函数或者make_shared的方式创建shared_ptr,禁止使用裸指针赋值的方式,这样会shared_ptr难于管理指针的生命周期。
// 使用裸指针赋值构造,不推荐,裸指针被释放后,shared_ptr就野了,不能完全控制裸指针的生命周期,失去了智能指针价值
int *p = new int(10);
shared_ptr<int>sp = p;
delete p; // sp将成为野指针,使用sp将crash
// 将裸指针作为匿名指针传入构造函数,一般做法,让shared_ptr接管裸指针的生命周期,更安全
shared_ptr<int>sp1(new int(10));
// 使用make_shared,推荐做法,更符合工厂模式,可以连代码中的所有new,更高效;方法的参数是用来初始化模板类
shared_ptr<int>sp2 = make_shared<int>(10);
禁止使用指向shared_ptr的裸指针,也就是智能指针的指针,这听起来就很奇怪,但开发中我们还需要注意,使用shared_ptr的指针指向一个shared_ptr时,引用计数并不会加一,操作shared_ptr的指针很容易就发生野指针异常。
shared_ptr<int>sp = make_shared<int>(10);
cout << sp.use_count() << endl; //输出1
shared_ptr<int> *sp1 = &sp;
cout << (*sp1).use_count() << endl; //输出依然是1
(*sp1).reset(); //sp成为野指针
cout << *sp << endl; //crash
使用shared_ptr创建动态数组,在介绍unique_ptr时我们就讲过创建动态数组,而shared_ptr同样可以做到,不过稍微复杂一点,如下代码所示,除了要显示指定析构方法外(因为默认是T的析构函数,不是T[]),另外对外的数据类型依然是shared_ptr<T>,非常有迷惑性,看不出来是数组,最后不能直接使用下标读写数组,要先get()获取裸指针才可以使用下标。所以,不推荐使用shared_ptr来创建动态数组,尽量使用unique_ptr,这可是unique_ptr为数不多的优势了。
template <typename T>
shared_ptr<T> make_shared_array(size_t size) {
return shared_ptr<T>(new T[size], default_delete<T[]>());
}
shared_ptr<int>sp = make_shared_array(10); //看上去是shared<int>类型,实际上是数组
sp.get()[0] = 100; //不能直接使用下标读写数组元素,需要通过get()方法获取裸指针后再操作
用shared_ptr实现多态,在我们使用裸指针时,实现多态就免不了定义虚函数,那么用shared_ptr时也不例外,不过有一处是可以省下的,就是析构函数我们不需要定义为虚函数了,如下面代码所示:
class A {
public:
~A() {
cout << "dealloc A" << endl;
}
};
class B : public A {
public:
~B() {
cout << "dealloc B" << endl;
}
};
int main(int argc, const char * argv[]) {
A *a = new B();
delete a; //只打印dealloc A
shared_ptr<A>spa = make_shared<B>(); //析构spa是会先打印dealloc B,再打印dealloc A
return 0;
}
循环引用,笔者最先接触引用计数的语言就是Objective-C,而OC中最常出现的内存问题就是循环引用,如下面代码所示,A中引用B,B中引用A,spa和spb的强引用计数永远大于等于1,所以直到程序退出前都不会被退出,这种情况有时候在正常的业务逻辑中是不可避免的,而解决循环引用的方法最有效就是改用weak_ptr,具体可见下一章。
class A {
public:
shared_ptr<B> b;
};
class B {
public:
shared_ptr<A> a;
};
int main(int argc, const char * argv[]) {
shared_ptr<A> spa = make_shared<A>();
shared_ptr<B> spb = make_shared<B>();
spa->b = spb;
spb->a = spa;
return 0;
} //main函数退出后,spa和sp
在程序发生异常退出时,会销毁对象,如果使用new delete 程序在new delete之间发生异常退出
只会销毁指针对象 斌不会释放指针对应的内存
而如果用智能指针在销毁对象后会自动把引用计数减一 , 引用计数减到0的时候就销毁内存
使用
/******shared_ptr*******/
shared_ptr<int> p1;//shared_ptr默认初始化 保存为空指针
shared_ptr<int> p2=make_shared<int>(5);//shared_ptr使用make_shared初始化,make_shared返回一个shared_ptr
shared_ptr<int> p3(p2);//shared_ptr拷贝初始化
shared_ptr<int> p4(new int(6));//shared_ptr使用new 来初始化
int *p5=p4.get();//shared_ptr返回一个普通指针int * 注意后面不能delete p5 ,此处理解是p4和p5是独立创建的 各自引用技术为1,同时不能将两个shared_ptr绑定到一个get()返回的指针
p1=p2;//shared_ptr赋值
*p1=4;//shared_ptr解引用赋值 会同时影响别的共享在此对象上的指针
cout<<"value of p2="<<*p2<<endl;
swap(p1,p3);//shared_ptr交换两指针
p3.swap(p1);//shared_ptr交换两指针
p3.reset();//shared_ptr 释放一个shared_ptr
p1.reset(new int(6));//shared_ptr 令shared_ptr转移指向到内置指针
cout<<p1.use_count()<<endl;//shared_ptr返回当前有几个指针指向共享对象
cout<<p1.unique()<<endl;//shared_ptr当前若只有一个指针指向共享对象则返回true 否则false
weak_ptr
介绍
weak_ptr来解决循环引用的问题,weak_ptr叫弱指针,它主要是为了配合shared_ptr使用,用来解决循环引用的问题;
将会出现循环引用问题的指针用weak_ptr保存着,weak_ptr并不拥有这块空间,所以weak_ptr里面不增加shared_ptr的引用计数,也不会释放这块空间。(注意weak_ptr里面也有自己的引用计数)
正如上一章提到,使用shared_ptr过程中有可能会出现循环引用,关键原因是使用shared_ptr引用一个指针时会导致强引用计数+1,从此该指针的生命周期就会取决于该shared_ptr的生命周期,然而,有些情况我们一个类A里面只是想引用一下另外一个类B的对象,类B对象的创建不在类A,因此类A也无需管理类B对象的释放,这个时候weak_ptr就应运而生了,使用shared_ptr赋值给一个weak_ptr不会增加强引用计数(strong_count),取而代之的是增加一个弱引用计数(weak_count),而弱引用计数不会影响到指针的生命周期,这就解开了循环引用,上一章最后的代码使用weak_ptr可改造为如下代码。
class A {
public:
shared_ptr<B> b;
};
class B {
public:
weak_ptr<A> a;
};
int main(int argc, const char * argv[]) {
shared_ptr<A> spa = make_shared<A>();
shared_ptr<B> spb = make_shared<B>();
spa->b = spb; //spb强引用计数为2,弱引用计数为1
spb->a = spa; //spa强引用计数为1,弱引用计数为2
return 0;
} //main函数退出后,spa先释放,spb再释放,循环解开了
使用weak_ptr也有需要注意的点,因为既然weak_ptr不负责裸指针的生命周期,那么weak_ptr也无法直接操作裸指针,我们需要先转化为shared_ptr,这就和OC的Strong-Weak Dance有点像了,具体操作如下:
shared_ptr<int> spa = make_shared<int>(10);
weak_ptr<int> spb = spa; //weak_ptr无法直接使用裸指针创建
if (!spb.expired()) { //weak_ptr最好判断是否过期,使用expired或use_count方法,前者更快
*spb.lock() += 10; //调用weak_ptr转化为shared_ptr后再操作裸指针
}
cout << *spa << endl; //20
使用
/***unique_ptr********/
unique_ptr<int> uni_p1;//unique_ptr默认初始化
unique_ptr<int> uni_p2(new int(42));//unique_ptr 普通初始化
unique_ptr<int> uni_p4(new int(51));//unique_ptr 普通初始化
unique_ptr<int> uni_p3(uni_p4.release());//unique_ptr 使用release初始化
//unique_ptr <int> uni_p2(uni_p1) 是错误的 不支持拷贝
//unique_ptr<int> uni_p2; uni_p2=unoi_p1是错误的 不支持赋值
*uni_p3=4;//unique_ptr解引用赋值
cout<<*uni_p3<<endl;//unique_ptr解引用取值
cout<<"value of uni_p2="<<*uni_p2<<endl;
swap(uni_p1,uni_p2);//unique_ptr交换两指针
uni_p2.swap(uni_p1);//unique_ptr交换两指针
uni_p2=nullptr;//unique_ptr 释放u指向的对象 将u置空
int *temp_p3=uni_p1.release();//unique_ptr放弃对内存对象的控制权 返回普通指针 将uni_p1置空
delete temp_p3;temp_p3=NULL;
uni_p3.reset(new int(66));//unique_ptr释放原来所指向的内存对象 让其指向新给定的对象
uni_p3.reset();//unique_ptr释放所指向的内存对象 将指针置空
智能指针的底层原理
版权声明:本文为vjhghjghj原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。