目录
1. 产生的原因-内存泄漏
2. 智能指针简介
3. auto_ptr
4. unique_ptr
5. shared_ptr
6. weak_ptr
参考链接:智能指针、auto_ptr 参考、shared_ptr参考
1. 产生的原因-内存泄漏
当我们当从堆中申请了内存后,如果不释放空间,就发生内存泄漏。内存泄漏的情景主要有几种:
* new和delete没有匹配。
* 没有正确清楚嵌套对象指针。
* 释放对象数组的时候没有使用方括号。
* 指向对象的指针数组不等同于对象数组(数组中的每个对象为指针)。
* 缺少拷贝构造函数或者没有重载赋值操作符,导致按值传递,两次释放相同的内存。
* 没有将基类的析构函数定义为虚函数。
然而,即便写出了清晰并且带有错误验证的代码,有时候仍会出现问题。比如和别人合作写代码的时候,合作者可能就会在完美的程序中增加一个提早返回的语句,导致申请的内存空间无法释放。针对以上原因,C++推出了智能指针。
这里顺便提一个概念,野指针。野指针表示指向被释放的或者访问受限内存的指针。使用野指针很容易导致内存泄漏。 在使用指针的时候除了避免内存泄漏,野指针也需要尽量避免的。产生原因主要有几个:
- 指针变量没有初始化。
- 指针被free或者delete后,没有置为NULL。
- 指针操作超过变量作用范围。
2. 智能指针简述
智能指针定义在memoery文件中,是一个RAII(Resource Acquisition is initialization)的类模型。智能指针类的构造函数中传入一个普通指针,析构函数中释放传入的指针。因为智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。
3. C++98 auto_ptr
auto_ptr的出现,主要是为了解决“有异常抛出时发生内存泄漏”的问题,如下所示。
int* p = new int(100);
try
{
doSomething();
cout << *p << endl;
delete p;
}
catch(){}
当doSomething();部分抛出异常,将导致指针p所指向的空间得不到释放而导致内存泄露,因而引入auto_ptr。
3.1 auto_ptr的使用
在使用auto_ptr的时候,我们实际上是创建一个auto_ptr类型的局部对象。该局部对象析构时,会将自身所拥有的指针空间释放,从这个角度避免了内存泄露。另外,auto_ptr重载了’*‘和’->’,运算符,可以像普通指针那样进行提领操作。
#include <iostream>
#include <string>
#include <memory>
class Test
{
public:
Test(int a = 0 ) : m_a(a){
std::cout << "Calling constructor" << std::endl;
}
~Test( )
{
std::cout << "Calling destructor" << std::endl;
}
public:
int m_a;
};
//抛出异常
void Fun(int a, int b, int &c)
{
if( a == 0 )
{
throw -1;
}
c = b / a;
return;
}
//测试
int main( )
{
try{
std::auto_ptr<Test> p(new Test(5));
int c = 0; Fun(0, 3, c);
std::cout << p->m_a << std::endl;//提领操作
}
catch(...){
std::cout << "catch()" << std::endl;
}
return 0;
}
/*
Output:
Calling constructor
Calling destructor
catch()
*/
使用auto_ptr的时候有几个问题需要注意:
(a) auto_ptr的构造函数为explicit,阻止了一般指针隐式类型转换为auto_ptr的构造,所以如下的创建方式是编译不过的。
int* p = new int(1);
auto_ptr<int> ap = p;//无法进行隐式变换
(b) 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时应避免像下例所示的多个auto_ptr对象管理同一个指针。
int* np = new int(1);
auto_ptr<int> p1(np);
auto_ptr<int> p2(np);
© auto_ptr的析构函数删除对象使用delete而不是delete[],所以auto_ptr不能用来管理数组指针。
int *p = new int[100];
auto_ptr<int> ap(p);//仅仅释放第一个元素空间,造成内存泄漏
(d) C++中对一个空指针NULL执行delete操作是安全的,所以在auto_ptr的析构函数中无须判断它所拥有指针是否为空。
3.2 auto_ptr的拷贝构造和赋值
auto_ptr要求对所拥有的指针完全占有,也就是说,一个一般指针不能同时被两个auto_ptr所拥有。这也意味着auto_ptr在拷贝构造和赋值运算符重载时要做特殊处理。具体的做法是对所有权进行了完全转移,在拷贝和赋值时,剥夺原auto_ptr拥有权(置空),赋予当前auto_ptr对指针的拥有权。
由于会修改原对象,所以auto_ptr的拷贝构造函数以及赋值运算符重重载函数的参数是引用而不是常(const)引用。这时候就需要注意以下几个问题:
(a) auto_ptr对象被拷贝或者被赋值后,已经失去了对原指针的所有权,此时,对这个auto_ptr的读取操作是不安全的。尤其是当auto_ptr作为函数参数按值传递,传入的实参auto_ptr对指针的所有权会转移到函数临时的auto_ptr对象上。临时auto_ptr在函数退出时被析构,则当函数调用结束时,原实参所指向的对象已经被删除了。
void func(auto_ptr<int> ap)
{
cout << *ap << endl;
}
auto_ptr<int> ap(new int(1));
func(ap);
cout << *ap1 << endl;//错误,函数调用结束后,ap1已经不再拥有任何对象了
因此,要避免使用auto_ptr对象作为函数参数按值传递,按引用传递在调用函数是不会发生所有权转移,但是无法预测函数体内的操作,有可能在函数体内进行了所有权的转移。因此按引用传递auto_ptr作为函数参数也是不安全的。如果不得不使用auto_ptr对象作为函数参数时,尽量使用const引用传递参数。
(b) auto_ptr支持所拥有的指针类型之间的隐式类型转换。
class base{};
class derived: public base{};
auto_ptr<base> apbase = auto_ptr<derived>(new derived);//auto_ptr<derived>隐式转换到auto_ptr<base>
© auto_ptr对象不能作为STL容器元素。C++的STL容器对于容器元素类型的要求是有值语义,即可以赋值和复制。但是auto_ptr在赋值和复制时都进行了特殊操作。
3.3 auto_ptr的相关操作
T* get(); //获得auto_ptr所拥有的指针。
T* release(); //释放auto_ptr的所有权,并将所有用指针返回。
void reset(T* ptr=0); // 接收所有权,接收之前拥有其它指针的话,必须先释放其空间。
4. C++11 unique_ptr
unique_ptr和auto_ptr类似,都是同一时刻只能有一个unique_ptr指向给定对象。但是它禁止拷贝语义,只支持移动语义。
4.1 unique_ptr的使用方法
unique_ptr不能拷贝,也不能赋值,只能通过move()转换所有权或者通过reset()重置所有权。另外,它还可以通过release方法释放所有权。
#include <iostream>
#include <memory>
class Test
{
public:
Test(int a = 0 ) : m_a(a){
std::cout << "Calling constructor" << std::endl;
}
~Test( )
{
std::cout << "Calling destructor" << std::endl;
}
public:
int m_a;
};
//测试
int main() {
std::unique_ptr<Test> uptr(new Test(10)); //绑定动态对象
//std::unique_ptr<Test> uptr2 = uptr; //不能赋值
//std::unique_ptr<Test> uptr2(uptr); //不能拷贝
std::cout << "uptr->ma: " << uptr->m_a << std::endl;
std::unique_ptr<Test> uptr2 = std::move(uptr); //转换所有权
//std::cout << "uptr->ma: " << uptr->m_a << std::endl;
std::cout << "uptr2->ma: " << uptr2->m_a << std::endl;
//uptr2.release(); //释放所有权
return 0;
}//离开作用域是自动析构
/*
Output:
Calling constructor
uptr->ma: 10
uptr2->ma: 10
Calling destructor
*/
上例需要注意的是,如果我们取消掉release()函数的注释,两个指针都失去了对象的所有权,这时候之前的Test对象就无法被销毁。
5. C++11 shared_ptr
shared_ptr 是一个标准的共享所有权的智能指针, 允许多个指针指向同一个对象,主要是为了解决auto_ptr和unique_ptr在对象所有权上的局限性。因为加入了计数机制,也就产生了额外的开销:
-
shared_ptr对象除了包括一个所拥有对象的指针外, 还必须包括一个引用计数代理对象的指针。
-
时间上的开销主要在初始化和拷贝操作上, *和->操作符重载的开销跟auto_ptr是一样。
5.1 何时需要shared_ptr?
-
程序不知道自己需要使用多少对象. 如使用窗口类, 使用 shared_ptr 为了让多个对象能共享相同的底层数据.
-
程序不知道所需对象的准确类型.
-
程序需要在多个对象间共享数据.
5.2 shared_ptr的使用方法
每一个shared_ptr的拷贝都指向相同的内存。每拷贝一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
#include <iostream>
#include <memory>
int main() {
{
int a = 10;
//使用make_shared函数初始化。
std::shared_ptr<int> ptra = std::make_shared<int>(a);
//智能指针是个模板类,可以指定类型,传入指针通过构造函数初始化。
std::shared_ptr<int> ptra2(ptra);
std::cout << ptra.use_count() << std::endl;
int b = 20;
int *pb = &a;
//std::shared_ptr<int> ptrb = pb; //error
std::shared_ptr<int> ptrb = std::make_shared<int>(b);
//ptra2原来指向的对象引用计数减1(如果为0, 释放内存), ptrb指向的对象的引用计数加1
ptra2 = ptrb; //assign,拷贝构造
pb = ptrb.get(); //获取原始指针
std::cout << ptra.use_count() << std::endl;
std::cout << ptrb.use_count() << std::endl;
}
}
//Output: 2 1 2
5.3 注意事项
a. 不能将指针直接赋值给一个智能指针,一个是类,一个是指针。
std::shared_ptr<int> p4 = new int(1);//ERROR
b. 不要用一个原始指针初始化多个shared_ptr,否则会造成二次释放同一内存。
c. shared_ptr作为对象成员变量时,应避免循环引用。
假设a对象中含有一个shared_ptr指向对象b, 对象b中含有一个shared_ptr 指向对象a, 并且 a, b 对象都是堆中分配的。当m_spa被销毁时, 对象a的use_count从2变为1; 对象b同理。至此,a和b失去联系,但是却无法继续销毁自身,如下所示。
#include <iostream>
#include <memory>
class CB;
class CA;
class CA{
public:
CA() { }
~CA() { std::cout << "~CA()" << std::endl;}
void Register(const std::shared_ptr<CB>& sp){
m_sp = sp;
}
private:
std::shared_ptr<CB> m_sp;
};
class CB{
public:
CB() { };
~CB() { std::cout << "~CB()" << std::endl;};
void Register(const std::shared_ptr<CA>& sp){
m_sp = sp;
}
private:
std::shared_ptr<CA> m_sp;
};
int main(){
std::shared_ptr<CA> spa(new CA());
std::shared_ptr<CB> spb(new CB());
spa->Register(spb);
spb->Register(spa);
std::cout << spb.use_count() << std::endl;//2
std::cout << spa.use_count() << std::endl;//2
}
//两个对象都没有调用析构函数(都没有被销毁)
解决此方法是使用 weak_ptr 替换 shared_ptr,在weak_ptr小节介绍。
d. shared_ptr 不支持数组, 如果使用数组, 需要自定义删除器。
// 下例是一个利用 lambda 实现的删除器
std::shared_ptr<int> sps(new int[10], [](int *p){delete[] p;});
//对于数组元素的访问, 需使要使用 get 方法取得内部元素的地址后, 再加上偏移量取得
for (size_t i = 0; i < 10; i++)
*((int*)sps.get() + i) = 10 - i;
6. C++11 weak_ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作。
weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
6.1 weak_ptr的使用
-
use_count():观测资源的引用计数
-
expired():等价于use_count()==0,表示被观测的资源(也就是shared_ptr的管理的资源)已经不复存在。
-
lock():从被观测的shared_ptr获得一个可用的shared_ptr对象,从而操作资源。
当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。
#include <iostream>
#include <memory>
int main() {
{
std::shared_ptr<int> sh_ptr = std::make_shared<int>(10);
std::cout << sh_ptr.use_count() << std::endl;//1
std::weak_ptr<int> wp(sh_ptr);
std::cout << wp.use_count() << std::endl;//1
if(!wp.expired()){//判断wp是否指向对象
std::shared_ptr<int> sh_ptr2 = wp.lock(); //get another shared_ptr
*sh_ptr = 100;
std::cout << wp.use_count() << std::endl;//2
}
}
//delete memory
}
6.2 利用weak_ptr规避循环引用
如果忘记了循环引用问题,可以到[shared_ptr[(#5)部分回顾,这里主要给出解决方案。
#include <iostream>
#include <memory>
class CB;
class CA;
class CA{
public:
CA() { }
~CA() { std::cout << "~CA()" << std::endl;}
void Register(const std::shared_ptr<CB>& sp){
m_sp = sp;
}
private:
std::weak_ptr<CB> m_sp;//将这里设置为weak_ptr
};
class CB{
public:
CB() { };
~CB() { std::cout << "~CB()" << std::endl;};
void Register(const std::shared_ptr<CA>& sp){
m_sp = sp;
}
private:
std::shared_ptr<CA> m_sp;
};
int main(){
std::shared_ptr<CA> spa(new CA());
std::shared_ptr<CB> spb(new CB());
spa->Register(spb);
spb->Register(spa);
std::cout << spb.use_count() << std::endl;//1
std::cout << spa.use_count() << std::endl;//2
}
/*
成功调用析构函数,销毁对象
*/