C++、智能指针

智能指针其实是通过类对普通指针进行封装,但用法与普通指针类似。智能指针主要解决普通指针容易存在的内存泄漏和悬挂指针等问题。因为智能指针一般作为类对象存在,所以当智能指针离开自己的作用域时会自动析构,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 的情况。

/* 以下为auto_ptr的常用成员函数 */
typedef element_type _Ty;
typedef auto_ptr<_Ty> _Myt;
explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
	: _Myptr(_Ptr)
{
    
      // construct from object pointer
}  // 构造函数直接接受普通指针作为参数输入
  
auto_ptr(_Myt& _Right) _THROW0()
	: _Myptr(_Right.release())
{
    
      // construct by assuming pointer from _Right auto_ptr
}  // 构造函数从其他的auto_ptr对象复制其指向的内存地址,注意被复制的auto_ptr对象指向的内存地址将会被置为NULL,
   // 至少在这里保证了只有一个auto_ptr指向一块内存,即独占

_Ty *get() const _THROW0()
{
    
    	// return wrapped pointer
	return (_Myptr);
}   // 返回auto_ptr对象指向的内存的普通指针

_Ty *release() _THROW0()
{
    
    	// return wrapped pointer and give up ownership
	_Ty *_Tmp = _Myptr;
	_Myptr = 0;
	return (_Tmp);
}   // 将auto_ptr对象指向的内存置为NULL,但原本指向的内存并没有被释放掉,而是将其地址作为返回值,所以要注意内存泄漏

void reset(_Ty *_Ptr = 0)
{
    
    	// destroy designated object and store new pointer
	if (_Ptr != _Myptr)
		delete _Myptr;
	_Myptr = _Ptr;
}   // 将auto_ptr指向的内存更改为给定的地址,如果新的内存地址不一样,原本的内存将会被释放掉,而不是像release那样返回地址。
    // 因为delete NULL是合法的,所以对于一个空智能指针也可以调用reset函数

_Myt& operator=(_Myt& _Right) _THROW0()
{
    
    	// assign compatible _Right (assume pointer)
	reset(_Right.release());
	return (*this);
}   // 可以看到,auto_ptr对象之间的赋值会使得等号右侧的对象指向的内存置为NULL,而如果两者指向的内存地址不一样,
    // 左侧对象指向的原始内存将会被释放掉

/* 以下是一些用例 */
auto_ptr<T> ap;         // 创建一个空的auto_ptr,T为相应的数据类型。注意ap是一个类对象,空智能指针不需要手动初始化为NULL。
auto_ptr<T> ap(new T);  // 标准的auto_ptr创建方式,即动态内存不显式地赋值给某个普通指针,
                        // 而是通过构造函数直接由ap来管理,避免多个指针指向同一块内存区域

T *x = new T;
auto_ptr<T> ap1(x); // 当然显式地将动态内存赋值给某个普通指针再赋值给auto_ptr也是可以的,
                    // 但是这相当于多出了一个指针变量,容易造成错误。
auto_ptr<T> ap2(x); // 通过直接使用普通指针赋值的方法,可以使得不同的auto_ptr指向同一块内存,
                    // 注意这种情况十分危险,最好的做法是不要使用任何显式的普通指针

T x;
auto_ptr<T> ap(&x); // 甚至将栈内存赋值给auto_ptr也是可以的,但这时也十分危险,
                    // 在ap析构或者显式地释放内存时,x栈内存的属性会使得程序崩溃。

auto_ptr<T> ap1(new T(a));
auto_ptr<T> ap2(new T(b));
ap2 = ap1;     // 这时ap2会释放掉原本指向的内存,并且改为指向ap1原本指向的内存,而ap1则不再指向任何内存

T *x = new T;
auto_ptr<T> ap1(x);
auto_ptr<T> ap2(x);
ap2 = ap1;    // 根据前面reset函数的定义可知,因为两者指向同一块内存,所以虽然ap1被置零,
              // 但ap2还是指向原来的内存,并且没有内存被释放掉

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>(); // c++14,括号内为构造函数所需实参
unique_ptr<T> ap2 = ap1;       // 这条语句是无法通过编译的,这就是unique_ptr与auto_ptr的区别,从而避免无意的复制操作
unique_ptr<T> ap3 = move(ap1); // 但是通过move函数可以达到与auto_ptr相同的目的,这时ap1会成为空指针
unique_ptr<T[]> ap2(new T[n]); // unique_ptr支持动态数组指针的释放

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 方法可用于查看引用计数和唯一性。

扫描二维码关注公众号,回复: 17160367 查看本文章
shared_ptr<T> ap1(new T);   // 引用计数1
shared_ptr<T> ap2 = ap1;    // 引用计数+1
{
    
    
   shared_ptr<T> ap3 = ap2; // 引用计数+1
} // 离开局部作用域,ap3被销毁,引用计数-1
ap1.reset();   // 引用计数-1,通过ap2.use_count()可以查看,ap1此时指向空指针

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相当于一个A类型指针
   ~A() {
    
     cout << "~A" << endl; }
};

int main() 
{
    
    
   A *raw = new A;   // 仅作示例,不建议使用裸指针创建对象
   
   {
    
     // 局部作用域
      shared_ptr<A> pa(raw);   // 从裸指针构造共享指针,内存引用计数为1
      pa->a = pa;              // 共享指针赋值,内存引用计数+1,即2
   }
   // 离开局部作用域时,局部共享指针pa被销毁,raw指向的内存的引用计数-1,raw->a这个共享指针还没被销毁,
   // 因为只有当raw这块内存被销毁时,raw->a才会被自动销毁。然而只要raw->a没被销毁或析构,引用计数就始终不会为0,
   // 那么raw这块内存也就不会被自动销毁,这就是共享指针循环引用所导致的内存泄漏问题。

   // 如果要解决以上的内存泄漏问题,就需要手动地调用共享指针raw->a的析构函数,使得raw内存的引用计数变为0,
   // 这时候raw这个对象的内存就会被自动销毁。注意,调用了raw->a的析构函数后,raw->a的指针变为空指针,
   // 所以当自动销毁raw的内存时,raw->a这个共享指针对象也会被销毁,空指针使得它不会导致任何其他内存被销毁。
   raw->a.~shared_ptr();

   // 警告,不要使用 delete raw; 来代替以上语句。因为直接手动销毁raw这个对象会导致共享指针raw->a首先被销毁,
   // raw->a被销毁前,内存引用计数为1,且指向的是raw这块内存本身,那么raw->a被销毁时,由于内存计数变为0,
   // 就会自动地把raw这块内存给释放掉,这些操作是在 delete raw; 语句释放内存之前发生的。
   // 当 delate raw; 把raw->a销毁后再去销毁raw本身时,raw其实早就被销毁了,就会导致重复释放一块内存的问题。
}

以上只是 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);   // 从裸指针构造共享指针,内存引用计数为1
      pa->a = pa;              // 弱指针赋值,内存引用没有变化,还是为1
   }
   // 注意此时离开局部作用域后,由于局部shared_ptr对象pa被析构销毁,raw内存的引用计数-1,即为0,
   // 因此在pa被销毁之前,raw对象也会被析构然后销毁。weak_ptr析构并不会影响到所绑定的shared_ptr。
   // 因此这时候不会发生内存泄漏问题,也就不需要手动地释放raw的内存,解决了shared_ptr循环引用的问题。
}

猜你喜欢

转载自blog.csdn.net/qq_33552519/article/details/124125327