智能指针其实是通过类对普通指针进行封装,但用法与普通指针类似。智能指针主要解决普通指针容易存在的内存泄漏和悬挂指针等问题。因为智能指针一般作为类对象存在,所以当智能指针离开自己的作用域时会自动析构,delete 掉其指向的内存,能够有效避免忘记 delete 而造成的内存泄漏问题。要注意智能指针在任意可能释放内存的操作之前必须保证其指向的是动态分配的内存,否则会因为 delete 失败而造成程序崩溃。
智能指针常用的有 unique_ptr, shared_ptr 和 weak_ptr,在 <memory> 头文件中。另外还有 auto_ptr,但其属于较老的标准,目前已经被新标准弃用。
1. auto_ptr
auto_ptr 属于一种独占智能指针,当一个 auto_ptr 赋值给另外一个 auto_ptr 时,前者将失去对其指向的内存的所有权。不过,直接将普通指针分别赋值给不同的 auto_ptr 是可以,这时不同的 auto_ptr 可以访问同一块内存。然而一般不建议这么做,因为这样很容易造成同一块内存被多次 delete 的情况。
typedef element_type _Ty;
typedef auto_ptr<_Ty> _Myt;
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
: _Myptr(_Ptr)
{
}
auto_ptr(_Myt& _Right) _THROW0()
: _Myptr(_Right.release())
{
}
_Ty *get() const _THROW0()
{
return (_Myptr);
}
_Ty *release() _THROW0()
{
_Ty *_Tmp = _Myptr;
_Myptr = 0;
return (_Tmp);
}
void reset(_Ty *_Ptr = 0)
{
if (_Ptr != _Myptr)
delete _Myptr;
_Myptr = _Ptr;
}
_Myt& operator=(_Myt& _Right) _THROW0()
{
reset(_Right.release());
return (*this);
}
auto_ptr<T> ap;
auto_ptr<T> ap(new T);
T *x = new T;
auto_ptr<T> ap1(x);
auto_ptr<T> ap2(x);
T x;
auto_ptr<T> ap(&x);
auto_ptr<T> ap1(new T(a));
auto_ptr<T> ap2(new T(b));
ap2 = ap1;
T *x = new T;
auto_ptr<T> ap1(x);
auto_ptr<T> ap2(x);
ap2 = ap1;
auto_ptr<T> ap(new T);
ap.release();
可以看到,auto_ptr 虽然在一定程度上减小了内存泄漏的风险,但其可能性依然存在。而且 auto_ptr 容易产生空指针,即把一个auto_ptr 赋值给另一个 auto_ptr 后,其本身会变为指向 NULL,所以如果这时不小心访问了该智能指针指向的内存即 NULL,则会引起程序崩溃。因为 auto_ptr 使用 delete 释放内存,所以不能指向 new[] 分配的动态数组,特别是数组元素为类对象时。
注意不要使用 STL 容器存储 auto_ptr,因为两个 auto_ptr 之间的拷贝构造或者赋值都会导致内存所有权的转移,使得被复制的 auto_ptr 变为 NULL。而容器中的很多操作如 sort, swap 等都可能存在临时的对象拷贝,且拷贝出来的对象不一定会拷贝回容器当中,从而导致元素丢失。STL 容器的规范是,其存储的元素必须具有拷贝构造和赋值函数,且进行复制后不改变被复制对象的值,两个对象在逻辑上应该是相等的。因此 auto_ptr 是明显与规范有冲突的,这种行为也被称为 COAPS(container of auto_ptrs)。
auto_ptr 仅用于对智能指针有个初步的认识,不建议在代码中使用 auto_ptr,因为其已经被标准弃用。
2. unique_ptr
为了解决 auto_ptr 容易产生空指针引起程序崩溃的隐患,C++11 引入了 unique_ptr。
unique_ptr<T> ap1(new T);
unique_ptr<T> ap1 = make_unique<T>();
unique_ptr<T> ap2 = ap1;
unique_ptr<T> ap3 = move(ap1);
unique_ptr<T[]> ap2(new T[n]);
unique_ptr 支持自定义的删除器,可以定义更复杂的内存释放操作。删除器是一个包含括号 () 重载函数的类或结构体,输入参数为对应类型的指针,即如 void Del::operator()(T *p); 假设删除器中是通过 delete []p; 来释放内存的,那么可通过 unique_ptr<T, Del> x(new T[n]); 来创建指向动态数组的 unique_ptr。不过,unique_ptr 对动态数组的创建进行了特化,你也可以直接通过 unique_ptr<T[]> x(new T[n]); 来创建指向动态数组的 unique_ptr,而无需自定义删除器。
unique_ptr 同样具有 get, release, reset 等方法,功能与 auto_ptr 基本一致。尽管 unique_ptr 之间不能直接复制,但是其所管理的指针依然可以通过 get 方法读出,并且可以自由地修改所指向的内存。因此我们可以看出,智能指针主要是为了解决所有权的问题,而不是完全代替普通指针。当普通指针不涉及所有权的变化时(比如仅用于内存的读写而不需负责内存释放),我们不一定要使用智能指针,毕竟智能指针经过类封装后必然会带来效率的损失。但总体上还是建议使用 unique_ptr,因为其大小与裸指针一样,访问内存所需时间复杂度也不算太高。
3. shared_ptr
shared_ptr 是比较常用的智能指针,因为其不是独占的,一个 shared_ptr 赋值给其他 shared_ptr 不会把前者置零,而是通过引用计数来记录有多少个 shared_ptr 指向了某块内存。当引用计数为零时,内存会被释放。
shared_ptr 可用于需要多个指针副本,但又不需要人为地管理内存生命期的场景。shared_ptr 没有 release 方法,因为很可能还有其他 shared_ptr 在使用这块内存,如果把裸指针交给外部变量处理很容易出问题。另外,reset 方法也不会释放之前指针的内存,而只是把引用计数减一。use_count 和 unique 方法可用于查看引用计数和唯一性。
shared_ptr<T> ap1(new T);
shared_ptr<T> ap2 = ap1;
{
shared_ptr<T> ap3 = ap2;
}
ap1.reset();
shared_ptr 也支持自定义的删除器,但与 unique_ptr 稍有不同,其调用形式为 shared_ptr x(new T[n], deleter); deleter 既可以为类似于 unique_ptr 的类或结构体对象,也可以为输入参数为指针的函数指针。shared_ptr 默认是不支持动态数组的释放的,所以分配动态数组时要手动设置删除器。
!!! shared_ptr 存在循环引用的问题。
class A
{
public:
shared_ptr<A> a;
~A() { cout << "~A" << endl; }
};
int main()
{
A *raw = new A;
{
shared_ptr<A> pa(raw);
pa->a = pa;
}
raw->a.~shared_ptr();
}
以上只是 shared_ptr 循环引用的一个例子,在实际编程的时候应该尽量避免这种循环引用的设计模式。但如果不可避免地用到,或者说想规避无意中可能出现的循环引用操作,可以使用 weak_ptr 来解决。
4. weak_ptr
weak_ptr 主要为了解决 shared_ptr 的循环引用问题,其本身不能直接由裸指针来初始化,也不能 get 出裸指针以及使用 * 和 ->。weak_ptr 通常由 shared_ptr 对象来初始化或赋值,但不会增加 shared_ptr 的引用计数,但 weak_ptr 会包含 shared_ptr 的引用计数,并且跟随 shared_ptr 变化,同样可以用 use_count() 读出。weak_ptr 之间的赋值或复制不会改变引用计数。当要使用 weak_ptr 的指针时,需要首先导出一个 shared_ptr 对象,导出的 shared_ptr 是跟之前导出 weak_ptr 的 shared_ptr 绑定的,即导出的 shared_ptr 会继承之前的 shared_ptr 的引用计数,并且 +1,就像是直接从原来的 shared_ptr 复制过来一样。
由于 weak_ptr 是和 shared_ptr 绑定的,当 shared_ptr 引用计数为 0 时,weak_ptr 自然也就为空了。shared_ptr 引用计数为 0 意味着作用域内所有的 shared_ptr 对象都已经被销毁,但 weak_ptr 对象本身的生存期是和 shared_ptr 独立的,weak_ptr 对象此时还可能存在。因此 weak_ptr 要导出 shared_ptr 前,需要查询是否还和某些 shared_ptr 绑定着。
weak_ptr 提供了 expired() 函数,当其不与任何 shared_ptr 绑定时返回 true,否则返回 false。lock() 函数首先会查看是否与 shared_ptr 绑定,有绑定则返回相应的 shared_ptr 的复制,引用计数 +1,否则返回一个空的 shared_ptr 对象。
class A
{
public:
weak_ptr<A> a;
~A() { cout << "~A" << endl; }
};
int main()
{
A *raw = new A;
{
shared_ptr<A> pa(raw);
pa->a = pa;
}
}
|