内存问题一直是C/C++程序员面临的重大挑战,就语言层面而言,主要问题有:
- 野指针:一些内存单元已被释放,之前指向它的指针却还在被使用。这些内存有可能被系统重新分配给程序使用,从而导致了无法预测的错误。
- 重复释放:程序试图去释放已经被释放过的内存单元,或者释放已经被重新分配过的内存单元,导致重复释放错误。
- 内存泄漏:不再需要使用的内存单元如果没有被释放就会导致内存泄漏。如果程序不断地重复进行这类操作,将会导致内存占用剧增,甚至导致系统无可用内存,严重影响系统的运行。
随着多线程程序的出现和广泛使用,内存问题更加突出。为了让程序员从内存管理细节中解放出来,越来越多的程序及程序库采用了智能指针(smart pointer)。
智能指针
智能指针泛指一类原生指针(raw pointer)的封装,它的行为非常类似于原生指针,同时又可以自动化地实现资源管理(比如对象的自动析构)。智能指针使用广泛,几乎在所有的大型工程中都可以看到它的身影,比如boost、Android、WebKit、Chromium中都自行设计了智能指针。虽然这些开源项目中智能指针的实现各有不同,但其基本原理都是一样的:
- 在C++语言中,智能指针对象作为栈上分配的自动变量存在,比如局部变量、类的成员变量,在代码执行上下文退出其作用域时被自动析构。
- 智能指针的析构函数中一般包含封装指针对象的delete操作,从而间接实现了被封装对象的自动析构。
下面的代码展示智能指针的一般实现:
template <typename T>
class SmartPtr {
public:
typedef T ValueType;
typedef ValueType *PtrType;
SmartPtr() : m_ptr(NULL) {}
SmartPtr(PtrType ptr) : m_ptr(ptr) {}
~SmartPtr() { if (m_ptr) delete m_ptr; }
SmartPtr(const SmartPtr<T>& o);
template<typename U> SmartPtr(const SmartPtr<U>& o);
template<typename U> SmartPtr& operator=(const SmartPtr<U>& o);
// 指针运算
ValueType& operator*() const { return *m_ptr; }
PtrType operator->() const { return m_ptr; }
// 逻辑运算符重载
bool operator!() const { return !m_ptr; }
// 转换为raw ptr
operator PtrType() { return m_ptr; }
private:
PtrType m_ptr;
};
auto_ptr
在C++ 98中,智能指针通过一个模板类型auto_ptr来实现,程序员只需将new操作返回的指针作为auto_ptr的初始值即可,不用再显式的调用delete。比如:
auto_ptr(new int);
这在一定程度上避免了堆内存忘记释放而造成的内存泄漏。但是auto_ptr存在很明显的缺点,它采取了独占所有权模式,这样两个相同类型的指针不能同时指向相同的资源。比如下面的代码:
// C++ program to illustrate the use of auto_ptr
#include<iostream>
#include<memory>
using namespace std;
class A
{
public:
void show() { cout << "A::show()" << endl; }
};
int main()
{
// p1 is an auto_ptr of type A
auto_ptr<A> p1(new A);
p1 -> show();
// returns the memory address of p1
cout << p1.get() << endl;
// copy constructor called, this makes p1 empty.
auto_ptr <A> p2(p1);
p2 -> show();
// p1 is empty now
cout << p1.get() << endl;
// p1 gets copied in p2
cout<< p2.get() << endl;
return 0;
}
输出为:
A::show()
0x1b42c20
A::show()
0
0x1b42c20
形象的用图形表示如下:
auto_ptr的拷贝构造函数和赋值操作符实际上并不复制已存储的指针,而是传递它,使第一个auto_ptr对象为空。由于auto_ptr不支持拷贝语义,所以不能用于STL容器,它的另一个缺点就是不能调用delete[],而无法用于数组。
介于auto_ptr有上述的缺点,所以在C++11标准中被废弃了,取而代之的是unique_ptr, shared_ptr及weak_ptr。
unique_ptr
C++11中unique_ptr是用来取代auto_ptr的,就像其名字所表明的,它与所指对象的内存紧密绑定,不能与其他unique_ptr类型的指针对象共享所指对象的内存。比如下面的代码是无法编译通过的:
unique_ptr<int> up1(new int(11));
unique_ptr<int> up2 = up1; // 不能通过编译
相比auto_ptr,这可以避免程序中误用拷贝。当然,如果程序员确实希望使用auto_ptr那样的转移所有权操作,可以借助std::move来完成:
// Works, resource now stored in ptr2
unique_ptr<A> ptr2 = move(ptr1);
值得注意的是,如果unique_ptr作为函数的返回值,下面的代码会自动使用move语义而不会出现编译错误:
unique_ptr<A> fun()
{
unique_ptr<A> ptr(new A);
/* ...
... */
return ptr;
}
此外unique_ptr还增加了对数组的支持,所以在代码中应该使用unique_ptr而不应该使用废弃了的auto_ptr。
shared_ptr
在有的情形下,程序可能需要共享“拥有”同一个堆分配对象的内存,这个时候shared_ptr就可以派上用场。shared_ptr采用引用计数所有权模型,它与shared_ptr的所有副本合作维护其包含的指针的引用计数。每当一个新的指针指向资源时,计数器就会增加,当shared_ptr析构时,计数器就会递减。引用计数大于零时,shared_ptr包含的原始指针不会被销毁,直到引用计数递减到零才会释放。
所以,当我们要分配一个原始指针给多个所有者时,应该使用shared_ptr。
// C++ program to demonstrate shared_ptr
#include<iostream>
#include<memory>
using namespace std;
class A
{
public:
void show()
{
cout<<"A::show()"<<endl;
}
};
int main()
{
shared_ptr<A> p1 (new A);
cout << p1.get() << endl;
p1->show();
shared_ptr<A> p2 (p1);
p2->show();
cout << p1.get() << endl;
cout << p2.get() << endl;
// Returns the number of shared_ptr objects
//referring to the same managed object.
cout << p1.use_count() << endl;
cout << p2.use_count() << endl;
// Relinquishes ownership of p1 on the object
//and pointer becomes NULL
p1.reset();
cout << p1.get() << endl;
cout << p2.use_count() << endl;
cout << p2.get() << endl;
return 0;
}
输出:
0x1c41c20
A::show()
A::show()
0x1c41c20
0x1c41c20
2
2
0 // NULL
1
0x1c41c20
weak_ptr
在C++11标准中,除了unique_ptr和shared_ptr,智能指针还包括了weak_ptr这个类模板。weak_ptr的使用更为复杂一点,它可以指向shared_ptr指针指向的对象内存,却并不拥有该内存。使用weak_ptr成员函数lock,则可返回其指向内存的一个shared_ptr对象,且在所指对象内存已经无效时,返回空指针。
为什么会引入weak_ptr呢?它是为了解决shared_ptr循环依赖导致的内存问题而引入的。让我们考虑一个场景,有两个类A和B,都包含指向对方类的shared_ptr指针,这样A指向B,B指向A,引用计数永远不会达到零,两个对象也永远不会被删除。
引入weak_ptr后,声明weak_ptr的类不共享所有权,但是它可以通过lock方法访问这些对象。通过weak_ptr,可以打破A和B之间的循环依赖。
总结
虽然智能指针能帮助用户进行有效的堆内存管理,但是它还是需要程序员显式地声明智能指针。此外我们需要小心地使用shared_ptr,避免循环依赖导致内存无法释放。weak_ptr提供了解决循环依赖的途径,但决定何时使用shared_ptr,何时使用weak_ptr仍然是程序员的职责。
参考
- 深入理解C++11: C++11新特性解析与应用,p163 ~ p173
- 深入理解Android: WebKit卷, p40 ~ p45
- auto_ptr, unique_ptr, shared_ptr and weak_ptr