首页 > 代码库 > C++智能指针梳理
C++智能指针梳理
C++智能指针梳理
参考:
- https://en.wikipedia.org/wiki/Memory_leak (维基百科,内存泄漏)
- https://en.wikipedia.org/wiki/Resource_leak (维基百科,资源泄漏)
- http://blog.csdn.net/dangercheng/article/details/12618161(内存泄露和野指针的概念)
- http://blog.csdn.net/na_he/article/details/7429171 (内存泄漏以及常见的解决方法)
- http://blog.csdn.net/skiing_886/article/details/7937907 (C++中为什么需要智能指针)
- http://www.codeproject.com/Articles/541067/Cplusplus-Smart-Pointers (C++11 Smart Pointers)
- http://www.umich.edu/~eecs381/handouts/C++11_smart_ptrs.pdf
- http://blog.csdn.net/pi9nc/article/details/12227887 (C++11智能指针之unique_ptr)
- http://blog.csdn.net/u013696062/article/details/39665247 (C++之shared_ptr总结)
- http://blog.csdn.net/yang_lang/article/details/6725041 (C++括号()操作符的重载)
- http://blog.csdn.net/mmzsyx/article/details/8090849 (智能指针 weak_ptr)
- 《C++ Primer 第四版13.5.1节 定义智能指针类》
一、 为何需要智能指针
在C/C++指针引发的错误中有如下两种:内存泄漏和指针悬挂。使用智能指针可以较好地解决这两个问题。
1.1 内存泄漏
内存泄漏的含义可以由以下几个解释中获知:
解释1:In computer science, a memory leak is a type of resource leak that occurs when a computer program incorrectly manages memory allocations in such a way that memory which is no longer needed is not released.
解释2:指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。
解释3:用动态存储分配函数(如malloc)动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元,直到程序结束。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。
综合以上的解释,可以看到理解内存泄漏的关键点是“不再使用的内存没有得到释放”。
注意,内存泄漏是指堆内存泄漏(Heap leak)。
补充:关于资源泄漏
In computer science, a resource leak is a particular type of resource consumption by a computer program where the program does not release resources it has acquired. This condition is normally the result of a bug in a program. Typical resource leaks include memory leak and handle leak, particularly file handle leaks.
可见内存泄漏是资源泄漏中的一种,资源泄漏的另外一种是句柄泄漏,例如文件读写,socket操作等都有可能导致句柄的泄漏。
1.2 悬挂指针
悬挂指针也叫野指针,是未初始化或未清零的指针。与空指针(NULL)不同,悬挂指针无法通过简单地判断是否为 NULL避免,而只能通过养成良好的编程习惯来尽力减少。
悬挂指针的成因主要有两种:
- 指针变量没有被初始化
任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会乱指一气。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。 - 指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针。
别看free和delete的名字恶狠狠的(尤其是delete),它们只是把指针所指的内存给释放掉,但并没有把指针本身干掉。
1.3 实例
1) 忘记释放导致内存泄漏
/*
* 忘记释放导致内存泄漏
*
* 析构函数未得到执行
*/
#include <iostream>
using namespace std;
class Test
{
public:
Test(int a = 0 )
: m_a(a)
{}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
int main()
{
Test *t1 = new Test(3);
cout << t1->m_a << endl;
// delete t1;
return 0;
}
2) 异常导致内存泄漏
/*
* 异常导致内存泄漏
*
* 析构函数未得到执行
*/
#include <iostream>
using namespace std;
class Test
{
public:
Test(int a = 0 )
: m_a(a)
{}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
int main()
{
try
{
Test *t1 = new Test(3);
cout << t1->m_a << endl;
throw("an exception");
delete t1;
}
catch(...)
{
cout << "Something has gone wrong" << endl;
}
return 0;
}
3) 悬挂指针
/*
* 浅copy导致的指针悬挂问题
*
* 输出类似如下:
* destructor ---> Book
* destructor ---> 葺葺葺葺
*
*/
#include <iostream>
using namespace std;
class HasPtr
{
public:
HasPtr(char *s);
~HasPtr();
private:
char *ptr;
};
HasPtr::HasPtr(char *s)
{
if (s == nullptr)
{
ptr = new char[1];
*ptr = ‘\0‘;
}
else
{
ptr = new char[strlen(s) +1];
strcpy(ptr, s);
}
}
HasPtr::~HasPtr()
{
cout << "destructor ---> " << ptr << endl;
delete ptr;
}
int main()
{
HasPtr p1("Book");
HasPtr p2("Music");
p2 = p1;
return 0;
}
二、 自定义智能指针类
首先通过引入使用计数类来实现自己的智能指针类,以此加深对智能指针的理解,并解决上面例子中悬挂指针问题。
这部分具体参考《C++ Primer 第四版13.5.1节 定义智能指针类》,代码如下:
/*
*自定义智能指针类修复-->浅copy导致的指针悬挂问题
*
* 定义计数类:U_ptr
*
* 智能指针类中需要:
* 构造函数、析构函数、copy构造函数、赋值运算符"="重载
*
* 输出:
* destructor ---> Book
*
*/
#include <iostream>
using namespace std;
class HasPtr;
class U_ptr
{
private:
friend class HasPtr;
U_ptr(char *s)
: use(1)
{
if (s == nullptr)
{
sptr = new char[1];
sptr = ‘\0‘;
}
else
{
sptr = new char[strlen(s) +1];
strcpy(sptr, s);
}
}
~U_ptr()
{
delete sptr;
}
char *sptr;
int use;
};
class HasPtr
{
public:
HasPtr(char *s);
HasPtr(const HasPtr &other);
HasPtr &operator=(const HasPtr &other);
~HasPtr();
private:
U_ptr *ptr;
};
HasPtr::HasPtr(char *s)
{
ptr = new U_ptr(s);
}
HasPtr::~HasPtr()
{
if (--ptr->use == 0)
{
cout << "destructor ---> " << ptr->sptr << endl;
delete ptr;
}
}
HasPtr::HasPtr(const HasPtr &other)
{
ptr->sptr = other.ptr->sptr;
ptr->use ++;
}
HasPtr &HasPtr::operator=(const HasPtr &other)
{
++ other.ptr->use;
if (-- ptr->use == 0)
{
delete ptr;
}
ptr = other.ptr;
return *this;
}
int main()
{
HasPtr p1("Book");
HasPtr p2("Music");
p2 = p1;
return 0;
}
三、 auto_ptr
auto_ptr是一个模版类,是最早出现的智能指针,在C++98标准中已经存在。
智能指针的原理都是RAII(Resource Acquisition Is Initialization),即在构造的时候获取资源,在析构的时候释放资源。
另外,为了使用智能指针,需要引入头文件 #include <memory>
3.1 使用auto_ptr
首先使用它来解决1.3节中实例的问题。
1) 使用auto_ptr修复–>内存泄漏问题
/*
* 使用auto_ptr修复-->内存泄漏
*
* 析构函数可以执行到
*/
#include <iostream>
#include <memory>
using namespace std;
class Test
{
public:
Test(int a = 0 )
: m_a(a)
{}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
int main()
{
auto_ptr<Test> t1(new Test(3));
cout << t1->m_a <<endl;
return 0;
}
2) 使用auto_ptr修复–>浅copy导致的指针悬挂问题
/*
* 使用auto_ptr修复-->使用auto_ptr修复-->浅copy导致的指针悬挂问题
*
* 输出:
* destructor ---> Book
*
*/
#include <iostream>
#include <memory>
using namespace std;
class HasPtr
{
public:
HasPtr(char *s);
~HasPtr();
private:
char *ptr;
};
HasPtr::HasPtr(char *s)
{
if (s == nullptr)
{
ptr = new char[1];
*ptr = ‘\0‘;
}
else
{
ptr = new char[strlen(s) +1];
strcpy(ptr, s);
}
}
HasPtr::~HasPtr()
{
cout << "destructor ---> " << ptr << endl;
delete ptr;
}
int main()
{
auto_ptr<HasPtr>p1(new HasPtr("Book"));
auto_ptr<HasPtr>p2(p1); // 所有权转移到p2,p1变为empty
return 0;
}
3.2 auto_ptr的问题
1) 所有权转移(ownership transfer)
auto_ptr transfers the ownership when it is assigned to another auto_ptr. This is really an issue while passing the auto_ptr between the functions. Say, I have an auto_ptr in Foo( ) and this pointer is passed another function say Fun( ) from Foo. Now once Fun( ) completes its execution, the ownership is not returned back to Foo.
为了说明ownership transfer,参加如下代码
/*
* auto_ptr’s ownership transfer
*
* 该程序运行会崩溃
* 执行完Fun(t1),t1变成empty
*/
#include <iostream>
#include <memory>
using namespace std;
class Test
{
public:
Test(int a = 0 )
: m_a(a)
{}
~Test( )
{
cout<<"Calling destructor"<<endl;
}
public:
int m_a;
};
void Fun(auto_ptr<Test> p1 )
{
cout << p1->m_a << endl;
}
int main()
{
auto_ptr<Test> t1(new Test(3));
Fun(t1);
cout << t1->m_a <<endl;
return 0;
}
调试显示执行完Fun(t1)的情况:
2) 多个auto_ptr不能同时拥有同一个对象
//error code
Test *t1 = new Test(3);
auto_ptr<Test> ptr1(t1);
auto_ptr<Test> ptr2(t1);
这里ptr1与ptr2都认为指针t1是归它管的,在析构时都试图删除t1,这样就造成了重复释放问题。程序中释放已经不属于自己的空间,而是非常危险的,比起内存泄露,重复释放是一个更加严重的问题。有可能你第二次释放的空间已经被别的程序所使用,所以C/C++中视这种错误为致命错误,也就是说,我容许你局部的浪费,但绝对不容许你释放(使用)别人的东西。
3) 不能用auto_ptr管理数组指针
因为auto_ptr的析构函数中删除指针用的是delete,而不是delete [],所以我们不应该用auto_ptr来管理一个数组指针。如下的代码虽然可以编译通过,但是不要这样使用。
//bad code
auto_ptr<Test> p(new Test[5]);
4) auto_ptr不可做为容器(vector, list, map)元素
//error code
vector<auto_ptr<int> > vec;
auto_ptr<int> ptr1(new int(3));
vec.push_back(ptr1);
四、 unique_ptr
从第三节的内容可以看出auto_ptr具有比较多的缺陷,使用时容易出错。在C++ 11标准中出现了新的智能指针unique_ptr、 shared_ptr与weak_ptr等,这里首先介绍unique_ptr,可以将unique_ptr看成是auto_ptr的升级替代品。
4.1 基本操作
unique_ptr类中有get()、reset()、release()等函数。
get(): 获得原生指针
reset():重置,显式释放资源
reset(new XX):重置,重新指定对象
release():释放所有权到某一原生指针上
另外可以通过std::move将所有权由一个unique_ptr对象转移到另一个unique_ptr对象上,具体见下例。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
//1. unique_ptr的创建
//1.1)创建空的,然后利用reset指定对象
unique_ptr<int> up1;
up1.reset(new int(3));
//1.2)通过构造函数在创建时指定动态对象
unique_ptr<int> up2(new int(4));
//2. 获得原生指针(Getting raw pointer )
int* p = up1.get();
//3.所有权的变化
//3.1)释放所有权,执行后变为empty
int *p1 = up1.release();
//3.2)转移所有权,执行后变为empty
unique_ptr<int> up3 = std::move(up2);
//4.显式释放资源
up3.reset();
return 0;
}
4.2 禁止赋值和复制
unique_ptr禁止赋值和复制,“唯一”地拥有其所指对象,同一时刻只能有一个unique_ptr实例指向给定对象。也就是说模板类unique_ptr的copy构造函数以及等号(“=”)操作符是无法使用的。
通过禁止复制和赋值可以较好的改善auto_ptr的所有权转移问题。下面是其禁止复制和赋值的例子:
#include <iostream>
#include <memory>
using namespace std;
void Fun1( unique_ptr<int> up )
{
}
int main()
{
unique_ptr<int> up1 = unique_ptr<int>(new int(10));
//不允许复制(Copy construction is not allowed),所以以下三个均错误
unique_ptr<int> up2 = up1; // error
unique_ptr<int> up3(up1); // error
Fun1(up1); // error
//不允许赋值(‘=‘),所以下面错误
unique_ptr<int> up4;
up4 = up1; // error
return 0;
}
4.3 针对auto_ptr缺陷的改善
1) 管理数组指针
因为unique_ptr有unique_ptr< X[ ] >重载版本,销毁动态对象时调用delete[],所以可以用unique_ptr来管理数组指针。
unique_ptr< Test[ ] > uptr1(new Test[3]);
//注意 unique_ptr<Test> uptr3(new Test[3]);是不对的
unique_ptr<int[]> uptr2(new int[5]);
2) 做容器(vector, list, map)元素
vector<unique_ptr<int> > vec;
unique_ptr<int> ptr1(new int(3));
vec.push_back(std::move(ptr1));
//vec.push_back(ptr1); //由于禁止复制这样不行
五、 shared_ptr
shared_ptr has the notion called shared ownership. The goal of shared_ptr is very simple: Multiple shared pointers can refer to a single object and when the last shared pointer goes out of scope, memory is released automatically.
从上面这段英文可以看出,shared_ptr是共享所有权的,其内部有一个计数机制,类似于第二节中我们自定义的智能指针类。
5.1 使用shared_ptr
shared_ptr类中有get()、reset()、unique()、swap()等函数。
get(): 获得原生指针
reset():重置,显式释放资源
reset(new XX):重新指定对象
unique ():检测对象管理者是否只有一个shared_ptr实例
在shared_ptr的RAII实现机制中,默认使用delete实现资源释放,也可以定义自己的函数来释放,比如当其管理数组指针时,需要delete[],这时就需要自定义释放函数。自定义释放的方法有两种:lambda表达式和括号操作符的重载。
另外可以通过dynamic_pointer_cast实现继承中的转换,具体见下例。
#include <memory>
#include <iostream>
using namespace std;
class Dealloc
{
public:
Dealloc()
{}
//括号()操作符的重载
void operator() (int* p )
{
if (p != nullptr)
{
//Do the custom deallocation job
cout << "Dealloc called to release the resource " << p
<< " whose value is " << *p<<endl;
delete p;
p = nullptr;
}
}
};
class Base
{
public:
Base() {}
// 虚函数保证Base的多态性,以便在dynamic_pointer_cast中使用
virtual void Foo() {}
};
class Derived : public Base
{
public:
Derived() {}
};
int main()
{
//1. 创建
shared_ptr<int> sp1 = shared_ptr<int>(new int(100));
shared_ptr<int> sp2 = make_shared<int>(int(10));
auto sp3 = shared_ptr<int>(nullptr);
if( sp3 == nullptr )
{
cout<<"Null pointer" << endl;
}
//2. 自定义资源释放函数
{
// lamdba表达式
auto sp4 = shared_ptr<int>(new int[5], [ ](int* p){
cout<<"In lambda releasing array of objects..."<<endl;
delete[ ] p;});
}
{
// 括号()操作符的重载
auto sp5 = shared_ptr<int>(new int(1000), Dealloc() );
}
//3. 复制
auto sp6(sp1);
auto sp7 = sp1;
//4. Getting raw pointer
int* pRaw = sp2.get( );
//5. Get how many shared pointers sharing the resource
long nCount1 = sp1.use_count();
long nCount2 = sp2.use_count();
//6. Is this only shared pointer sharing the resource
bool b1 = sp1.unique();
bool b2 = sp2.unique();
//7. swap
sp1.swap(sp2);
//8. reset
sp1.reset();
sp1.reset(new int(20));
//9.Using dynamic_cast_pointer on shared pointer
auto sp10 = shared_ptr<Derived>(new Derived( ));
shared_ptr<Base> sp11 = dynamic_pointer_cast<Base>(sp10);
if (sp11.get( ) != nullptr )
{
cout << "Dynamic casting from sp10 to sp11 succeeds...." << endl;
}
auto sp12 = shared_ptr<Base>(new Base());
shared_ptr<Derived> sp13 = dynamic_pointer_cast<Derived>(sp12);
if (sp13 != nullptr)
{
cout << "Dynamic casting from 12 to 13 succeeds...." << endl;
}
else
{
cout << "Dynamic casting from sp12 to sp13 failed ...." << endl;
}
return 0;
}
另外,shared_ptr对象的引用是强引用(stong_ref),如下图:
5.2 shared_ptr的三个问题
1) 多个独立的shared_ptr实例不能共享一个对象
类似于3.2节中auto_ptr的问题
int* p = new int;
shared_ptr<int> sptr1( p);
//right
shared_ptr<int> sptr2(sptr1);
//wrong
//shared_ptr<int> sptr2(p);
2) 循环引用问题
循环引用例子:
/*
* shared_ptr的循环引用(Cyclic Reference)问题
*
* 类A,B的析构函数没有得到执行
* 资源没得到释放
*/
#include <memory>
#include <iostream>
using namespace std;
class B;
class A
{
public:
A( ) : m_sptrB(nullptr) { };
~A( )
{
cout<<" A is destroyed"<<endl;
}
shared_ptr<B> m_sptrB;
};
class B
{
public:
B( ) : m_sptrA(nullptr) { };
~B( )
{
cout<<" B is destroyed"<<endl;
}
shared_ptr<A> m_sptrA;
};
int main( )
{
shared_ptr<B> sptrB( new B );
shared_ptr<A> sptrA( new A );
sptrB->m_sptrA = sptrA;
sptrA->m_sptrB = sptrB;
return 0;
}
过程分析:
为了解决shared_ptr的循环引用问题,需要用到weak_ptr
3) shared_ptr的this问题
假设如下情况,类Thing的一个成员函数需要传递其this指针到一个普通函数,如果不用智能指针,没有问题,如下:
#include <iostream>
using namespace std;
class Thing;
void UseThingThisPointer(Thing *);
class Thing
{
public:
void foo()
{
//注意这里this
UseThingThisPointer(this);
}
void print()
{
cout << "class Thing" << endl;
}
};
void UseThingThisPointer(Thing * ptr)
{
ptr->print();
}
int main()
{
Thing * t1 = new Thing;
t1->foo();
delete t1;
}
此时,我们需要使用shared_ptr智能指针来自动地管理Thing内存,如何使用呢?若我们简单的认为将所有的Thing *用shared_ptr<Thing>替换,代码如下,这时虽然可以编译通过,但是代码存在较大问题。
//...
class Thing
{
public:
void foo()
{
//注意这里this
shared_ptr<Thing> sp_for_this(this); // danger! a second manager object!
UseThingThisPointer(sp_for_this);
}
//...
};
void UseThingThisPointer(shared_ptr<Thing> ptr)
{
ptr->print();
}
int main()
{
shared_ptr<Thing> t1(new Thing);
t1->foo();
return 0;
}
通过调试可以发现,在执行shared_ptr t1(new Thing)后创建了类Thing的一个强引用(管理对象),在foo函数中使用shared_ptr sp_for_this(this)也在原生指针上创建了一个强用(管理对象),这样共有两个shared_ptr管理同一个对象。但是在foo()执行完成时,sp_for_this超出范围,其管理的Thing对象会被释放,留下t1指向一个不存在的对象。最后程序运行结束,释放t1管理的对象时便出现问题。此现象引发的问题和shared_ptr的第一个问题(如下代码)一样。
int* p = new int;
shared_ptr<int> sptr1( p);
shared_ptr<int> sptr2( p );
为了解决shared_ptr的this问题问题,也需要用到weak_ptr。
六、 weak_Ptr
weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它没有重载operator*和->,故而不具有普通指针的行为。它的最大作用在于协助shared_ptr工作。
weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。
6.1 使用weak_ptr
weak_ptr类中有use_count()、expired()等函数。
use_count():观测资源的引用计数
expired():等价于use_count()==0,但更快
lock():获取shared_ptr,当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr
具体见下例
/*
* weak_ptr的使用
*/
#include <iostream>
#include <memory>
using namespace std;
class Test
{
public:
Test(int a = 0) : m_a(a) { }
~Test( )
{
cout<<"Test object is destroyed whose value is = "<<m_a<<endl;
}
public:
int m_a;
};
int main( )
{
//1. Create a shared pointer
shared_ptr<Test> sp1(new Test(5) );
//2. Create a weak pointer from the shared pointer
weak_ptr<Test> wp1 = sp1;//(sp1);// = sp1;
//3. Create a weak pointer from another weak pointer
weak_ptr<Test> wp2(wp1);// = wp1;
//4. Get the reference count of the shared pointer
int nShared = sp1.use_count();
int nWeak = wp1.use_count();
//5.lock
shared_ptr<Test> sp2 = wp1.lock();
nShared = sp1.use_count();
nWeak = wp1.use_count();
//6. expired()
{
shared_ptr<Test> sp3 = shared_ptr<Test> (new Test(100));
wp1 = sp3;
}
if (wp1.expired())
{
cout << "expired" << endl;
}
return 0;
}
weak_ptr对象的引用是弱引用(weak ref),如下图:
6.2 解决shared_ptr的问题
1) 解决shared_ptr循环引用问题
/*
* 使用weak_ptr修复-->shared?_ptr循环引用问题
*
* 析构函数可以执行到
*
*/
#include <iostream>
#include <memory>
using namespace std;
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( );
return 0;
}
2) 解决shared_ptr的this问题
/*
* 使用weak_ptr修复--> shared_ptr的this问题
*
* 1) 继承模板类enable_shared_from_this
* 2) 用shared_from_this()替换this
*
*/
#include <iostream>
#include <memory>
using namespace std;
class Thing;
void UseThingThisPointer(shared_ptr<Thing> ptr);
class Thing : public enable_shared_from_this<Thing>
{
public:
void foo()
{
// get a shared_ptr from the weak_ptr in this object
UseThingThisPointer(shared_from_this());
}
void print()
{
cout << "class Thing" << endl;
}
};
void UseThingThisPointer(shared_ptr<Thing> ptr)
{
ptr->print();
}
int main()
{
shared_ptr<Thing> t1(new Thing);
t1->foo();
return 0;
}
C++智能指针梳理