文章目录
为什么要引入智能指针
普通指针的问题
普通指针总是牵扯到许多问题,例如指针所指对象的生命周期,悬空指针和内存泄露等;
悬空指针:悬空指针:指针最初指向的内存已经被释放的一种指针:
#include <stdlib.h>
#include <stdio.h>
int main()
{
int *p1 = (int*)malloc(sizeof(int));
int *p2 = p1;
*p1 = 1;
printf("p2 = %p\n",p2);
//释放内存空间
free(p1);p1 = NULL;
//p2指向的地址不变,p2成为了悬空指针,*p2却变成了垃圾值
printf("p2 = %p\n",p2);
printf("*p2 = %d\n",*p2);
return 0;
}
内存泄漏:从堆中申请了内存却不释放回去,就会发生内存泄露。例如在new和delete之间不小心加了一个提早return的语句,或者是有些错误的语句使得程序抛出异常(例如除以0,访问无效的地址等),那么内存就泄露了。
1)下面一段代码演示了函数提前返回造成的内存泄漏:
void Foo( )
{
int* iPtr = new int;
...
if(...)
return;
...
delete iPtr;
}
2)下面一段代码演示了程序抛出异常而导致的内存泄漏:
// auto_ptr/normal_ptr_2.cpp
#include <iostream>
#include <memory>
using namespace std;
class Test
{
public:
Test(int a = 0 ) : m_a(a){}
~Test( ){cout<<"~Test()"<<endl;}
int m_a;
};
//除法函数
double Fun(double a,double b )
{
if( b == 0 )
{
throw "Invalid divisor";
}
return a/b;
}
int main( )
{
try
{
Test* p = new Test(5);
Fun(1,0);
//离开前不会调用~Test(),内存泄露
}
catch(...)
{
cout<<"Something has gone wrong"<<endl;
}
}
auto_ptr
智能指针是一个RAII
机制类模型,用来动态的分配内存,它提供所有普通指针的接口,却很少发生异常。它会自动释放已经分配的内存,避免内存泄露,还可以避免悬空指针的问题。智能指针使程序员能从手动管理动态内存的繁杂任务中解放出来。C++98引入了第一个智能指针auto_ptr。
auto_ptr解决的问题
避免内存泄露
1)利用智能指针从堆上申请空间,离开作用域时会自动释放,可以避免因为忘记释放而导致的内存泄露:
// auto_ptr/auto_ptr_1.cpp
//class Test的定义
...
int main()
{
auto_ptr<Test> p(new Test(1));
cout << p->m_a << endl;
return 0;
//函数离开作用域时自动释放智能指针p
}
2)下面例子中,尽管异常被抛出,但是指针仍然正确地被释放了:
// auto_ptr/auto_ptr_2.cpp
//class Test的定义
...
//除法函数
double Fun(double a,double b )
{
if( b == 0 )
{
throw "Invalid divisor";
}
return a/b;
}
int main( )
{
try
{
std::auto_ptr<Test> p( new Test(5) );
Fun(1,0);
//离开前会调用~Test()
}
catch(...)
{
cout<<"Something has gone wrong"<<endl;
}
}
auto_ptr存在的问题
1.所有权转移问题:当把一个auto_ptr赋值给另外一个auto_ptr时,它的所有权也转移了,这种场景在函数传参时经常出现,导致野指针的出现:
// auto_ptr/auto_ptr_3.cpp
// auto_ptr所有权转移问题
//class Test的定义
...
//auto_ptr用作形参
void Fun(auto_ptr<Test> p1)
{
cout << p1->m_a << endl;
}
int main( )
{
auto_ptr<Test> p (new Test(1));
Fun(p);
//p成为野指针了,这里很容易出错
cout << p->m_a << endl; //编译能通过,但是运行时出现段错误
}
2.析构数组问题:auto_ptr不能指向一组对象,因为auto_ptr只能调用delete来析构,因此下面的代码会c产生内存泄漏:
void Fun()
{
auto_ptr<Test> p (new Test[5]);
//离开p的作用域时,会调用delete进行析构,也就是只会析构一次
}
3. auto_ptr不能和标准容器放到一起使用;
4. 任一时刻只能有一个auto_ptr指针拥有所有权。
shared_ptr
C++11提供了一组新的智能指针,废弃了auto_ptr指针,这组新的智能指针包括:shared_ptr、unique_ptr和shared_ptr。下面先介绍shared_ptr:
shared_ptr介绍
特点
shared_ptr的设计目的在于:多个shared_ptr指针指向同一个对象,当最后一个shared_ptr离开作用域时,内存才被释放,这样就解决了悬空指针的问题。
创建
shared _ptr<int> sptr1(new int);
//使用make_shared宏来加速创建
shared_ptr<int> sptr2 = make_shared<int>(100);
引用计数use_count()
// shared_ptr/shared_ptr_use_count.cpp
#include <iostream>
#include <memory>
using namespace std;
int main()
{
shared_ptr<int> sp1(new int);
shared_ptr<int> sp2(sp1);
shared_ptr<int> sp3;
sp3 = sp1;
cout << sp1.use_count() << endl; //3
cout << sp2.use_count() << endl; //3
cout << sp3.use_count() << endl; //3
}
管理对象数组
shared_ptr默认调用delete释放关联的资源。如果用户采用一个不一样的析构策略时,他可以自由指定构造这个shared_ptr的策略。用户可以构造自己的析构函数(例如一个lamba表达式),实现对象数组的析构(delete []):
shared_ptr<Test> sp1(new Test[5], [ ](Test* p){delete[] p});
其他一些接口
除了能像普通指针一样使用shared_ptr,还提供了下面的一些接口:
- get():获取shared_ptr所绑定的资源;
- reset():释放关联内存的所有权,如果是最后一个拥有该资源的shared_ptr,就释放这块内存;
- unique():判断是否是唯一指向当前内存的shared_ptr。
shared_ptr存在的问题
1 不要用一个普通指针创建shared_ptr
// /shared_ptr/shared_ptr_normal_new.cpp
int main()
{
Test * p = new Test;
shared_ptr<Test> sp1(p); //use_count() = 1
shared_ptr<Test> sp2(p); //use_count() = 1
//假设sp1先离开作用域,那么sp1会调用析构函数并释放内存,此时sp2.use_count还是1
//sp2离开作用域时,也尝试调用析构函数并释放内存,产生段错误
}
2 循环引用问题
下面例子中:会产生循环引用,原因见代码注释:
// /shared_ptr/circle_refer.cpp
#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
A():m_spB(NULL) {};
~A() {cout << "~A()" << endl;}
shared_ptr<B> m_spB;
};
class B
{
public:
B():m_spA(NULL) {}
~B() {cout << "~B()" << endl;}
shared_ptr<A> m_spA;
};
int main()
{
shared_ptr<A> spA( new A);
shared_ptr<B> spB( new B);
spA->m_spB = spB; //spB.use_count = 2
spB->m_spA = spA; //spA.use_count = 2
return 0;
//注意,只有当spA指向的对象被析构之后,spA->m_spB才“离开作用域”,其对应的引用计数才会减一
//同理,只有当spB指向的对象被析构之后,spB->m_spA才“离开作用域”,其对应的引用计数才会减一
//spA离开作用域时,spA.use_count()减为1,因此,不会调用析构函数
//同理,spB离开作用域时,spB.use_count()减为1,因此,也不会调用析构函数
//这样一来就形成了引用循环,离开作用域时不会调用任何析构函数,内存泄露
}
weak_ptr
针对上面shared_ptr的引用循环问题,引入weak_ptr;
介绍
weak_ptr拥有共享语义和不包含语义
,weak_ptr
可以共享shared_ptr
持有的资源,但是却不能拥有shared_ptr的资源
(也就是不会增加use_count);
强引用与弱引用
前面我们所说的use_count其实就是强引用
的计数,当一个weak_ptr共享shared_ptr的资源时,会增加它的弱引用
计数,而弱引用计数对析构没有影响。
// /weak_ptr/use_weak_ptr.cpp
...
//class Test的定义
int main()
{
shared_ptr<Test> sp(new Test); //强引用计数=1,弱引用计数=0
weak_ptr<Test> wp1(sp); //强引用计数=1,弱引用计数=1
weak_ptr<Test> wp2 = sp; //强引用计数=1,弱引用计数=2
}
判断weak_ptr的有效性
如果shared_ptr的use_count减为0,那么指向它的weak_ptr会过期(expired),因此每次使用weak_ptr前必须要判断其有效性,有两种方法判断weak_ptr是否指向有效的资源:
- 调用
use-count()
去获取引用计数,该方法只返回强引用计数,并不返回弱引用计数。 - 调用
expired()
方法。比调用use_count()方法速度更快。
从weak_ptr转化到shared_ptr
默认情况下,weak_ptr不能访问所指向的资源,因此需要临时转化成shared_ptr来进行访问,方法是lock()
// /weak_ptr/use_weak_ptr.cpp
...
//class Test的定义
int main()
{
shared_ptr<Test> sp(new Test(1));
weak_ptr<Test> wp(sp);
if(!wp.expired())
{
//错误,需要lock()才能访问
//cout << wp->m_a << endl;
//lock()再访问才合法
cout << wp.lock()->m_a << endl;
//使用完毕后use_count自动减一
cout << sp.use_count() << endl; //输出为1
}
}
weak_ptr解决循环引用问题
// /weak_ptr/non_circle_refer.cpp
//解决shared_ptr存在的引用循环问题
class B;
class A
{
public:
A() : m_a(5) { };
~A() {cout<<" A is destroyed"<<endl;}
void PrintSpB( );
weak_ptr<B> m_sptrB;
int m_a;
};
class B
{
public:
B() : m_b(10) { };
~B(){cout<<" B is destroyed"<<endl;}
weak_ptr<A> m_sptrA;
int m_b;
};
void A::PrintSpB( )
{
if( !m_sptrB.expired() )
{
cout<< m_sptrB.lock( )->m_b<<endl;
}
}
int main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
sptrA->PrintSpB();
//能够正常析构
}
unique_ptr
unique_ptr是对auto_ptr的替换。unique_ptr遵循着独占语义。在任何时间点,资源只能唯一地被一个unique_ptr占有。当unique_ptr离开作用域,所包含的资源被释放。
创建
基本的创建方法与shared_ptr一样:
unique_ptr<int> up(new int);
创建对象数组时,unique_ptr提供了创建对象数组的特殊方法,会调用delete[]
进行析构:
unique_ptr<int []> up(new int[5]);
所有权转移
auto_ptr允许赋值语句,容易产生野指针,unique不允许赋值语义,只支持移动语义
:
1)reset:用于重置所有权:
void reset (nullptr_t p) noexcept;
// /unique_ptr/reset.cpp
#include <iostream>
#include <memory>
using namespace std;
class Test
{
public:
Test(int a = 0 ) : m_a(a){}
~Test( ){cout << m_a << " ~Test()"<<endl;}
int m_a;
};
int main()
{
unique_ptr<Test> up(new Test(5));
up.reset(); //5 ~Test()
unique_ptr<Test> up1(new Test(10));
unique_ptr<Test> up2;
//以下操作不被允许,如果所有权需要转移,请使用swap
//up1.reset(up2);
//up1原本的内存被释放,并且它会取得new Test(20)的所有权
up1.reset(new Test(20)); //10 ~Test()
cout << up1->m_a << endl; //20
return 0;
//20 ~Test()
}
2)swap:用于交换所有权:
void swap (unique_ptr& x) noexcept;
// /unique_ptr/swap.cpp
#include <iostream>
#include <memory>
int main () {
std::unique_ptr<int> foo (new int(10));
std::unique_ptr<int> bar (new int(20));
foo.swap(bar);
std::cout << "foo: " << *foo << '\n'; //20
std::cout << "bar: " << *bar << '\n'; //10
}
3)release:与reset不同,release仅仅释放所有权但不释放资源,因此为了防止内存泄漏,往往需要其他指针来负责将资源释放,也就是“接管”资源,具体做法是通过返回值来“接管”:
pointer release() noexcept;
// /unique_ptr/release.cpp
#include <iostream>
#include <memory>
using namespace std;
int main ()
{
unique_ptr<int> up1(new int);
int * take_over_ptr; //接管指针可以是普通指针
*up1 = 10;
//release返回其指向的资源,take_over_ptr会接管
take_over_ptr = up1.release();
cout << *take_over_ptr << endl; //10
if(up1==NULL) //条件成立,up1调用release后就是NULL了
cout << "up1 is NULL now" << endl;
//接管之后要记得负责释放
delete take_over_ptr;
}
// /unique_ptr/release2.cpp
//与release.cpp基本上一样,不过这里讨论接管指针是unique_ptr时该如何接管
#include <iostream>
#include <memory>
using namespace std;
int main ()
{
unique_ptr<int> up1(new int);
unique_ptr<int> take_over_ptr; //接管指针是unique_ptr
*up1 = 10;
//接管失败,原因是unique_ptr不支持赋值语句
//take_over_ptr = up1.release();
//因此,需要使用reset进行接管
take_over_ptr.reset(up1.release());
cout << *take_over_ptr << endl; //10
}