C++中的智能指针

本文介绍了智能指针如何通过RAII原则管理内存,重点讲解了C++中的auto_ptr,unique_ptr和shared_ptr,以及它们在避免内存泄漏和环状引用问题中的应用。
摘要由CSDN通过智能技术生成

目录

背景

裸指针

智能指针

原理

智能指针

auto_ptr

unique_ptr

       1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)

          1.1 C++提供了标准库函数move()

         1.2.如果unique_ptr是一个临时右值

       2. unique_ptr可用于数组 

shared_ptr

     

      环状引用问题

weak_ptr

    注意:

总结


背景

  • 裸指针

如果编程中直接使用裸指针,就需要程序员手动的去allocate和release资源了。假设有这样一段代码:

void f(){
  int* a = new int(100);  //申请资源
   ....
  delete a; //release a所指向的对象
}

这段程序乍看起来没有什么问题,new和delete有成对地出现,应该不会发生内存泄漏的情况吧。那如果程序员忘记写delete了;或者是像上面一样有加delete,但这new和delete中间的"...."区域内是否有可能出现过早的return语句呢,亦或是这块"...."区域的语句抛出了异常,无论是哪一种原因,最后其实都没有做delete,那这种情况下内存泄露就发生了。

那怎么避免这种问题呢?最直接的想法可能就是“既然前面说的问题是因为程序可能会提前退出导致的,那就在程序每一个可能会退出的地方,都加上一个delete,以进行全面的防守”。这样子做当然没有问题,但是越到后面,可能就越难维护了,假设后面的人,在这段程序里又加了一些内容,使得程序可能又新增了一个会提前退出的地方,但是后面的人不知道这个函数里还有着对资源管理的事情要做,而忘记了加delete,还是会引入问题。所以单纯地依赖在f()里总是会手动地执行其delete语句不是一个很好的做法。

  • 智能指针

那前面说的问题还有其他更好的解决办法吗?答案是:有。这个方法就是采用智能指针(smart pointer). 

智能指针能够解决前面说的“因为忘记delete或者是提前退出导致的内存泄漏问题”,在介绍智能指针是如何使用之前,还是需要了解一下智能指针是怎么解决掉前面说的那些问题的,它能解决这个问题的原理是什么。

  • 原理

智能指针是基于这样一个思想:以对象来管理资源。简单来说就是,把资源放进一个对象里面,将资源释放的动作放在析构函数里,我们便可以依赖C++的析构函数自动调用机制确保资源被释放。

“以对象来管理资源”的观念常被称为“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization; RAII)”,这里面有两个关键的想法:

  • 获得资源后立刻放进管理对象内:每一笔资源在获得的同时立刻被放进管理对象当中
  • 管理对象运用析构函数确保资源被释放:不论流程上如何控制,一旦管理对象被销毁,其析构函数自然会被调用,其所管理的资源也就会被释放掉

没错,智能指针正是一个类(模板类,因为指针类型很多),它所管理的资源就是指针,可以简单地理解为就是在这个类的析构函数里有做一个delete的动作。智能指针这个模板类重载了一些指针相关的运算操作符(比如“*”,“->”),让程序员在操作这个对象时就像是操作普通指针一样。所以智能指针不仅使得人们可以像使用普通指针那样去使用它,还可以防止对内存的不当使用。

这里再多说几句:以对象来管理资源,其实不仅仅是内存是资源,其他的比如:互斥锁(Mutex)、文件描述器(File descriptor) 等其实也是资源,一旦你用了它,将来就必须要还给系统,如果不这样,就会发生各种事故。资源交给对象来管理,只是不同的资源所对应的管理对象不一样,比如互斥锁的管理对象一般使用的是lock_guard、unique_lock这些类的对象(C++ 多线程 (mutex & conition_variable篇)),但是基本原理是一样的,就是利用了C++对象的构造函数和析构函数的自动调用机制,防止资源泄漏发生。

智能指针

前面介绍了智能指针的大概原理,接下来看看C++里提供的智能指针有哪些,以及用法。

C++提供的智能指针有如下几种:auto_ptr(C++11中已经摒弃)、shared_ptr、weak_ptr和unique_ptr,就像前面说的,它们都是模板类。关于这些智能指针:

  1. 要创建智能指针对象,必须包含头文件<memory>;
  2. 所有的指针类都有一个explicit构造函数,该函数将指针作为参数
  3. 可以通过智能指针类提供的get()方法获取智能指针所指向的原始资源
  4. 使用通常的模板语法来实例化所需类型的指针(weak_ptr的构造方法稍微特殊点,后面会介绍),比如:
auto_ptr<int> a(new int(100));
unique_ptr<int> a(new int(100));
shared_ptr<int> a(new int(100));
weak_ptr<int> b = a; 

为什么会有多种不同类型的智能指针呢?接下来介绍一下这些不同类型的智能指针的差异

auto_ptr

auto_ptr是C++ 98提出来的,C++11中已经摒弃了。它的用法如下:

void f(){
 auto_ptr<int> a(new int(100));
 ....
//后面就不用再手动去加delete了
}

当通过copy构造函数或者时赋值运算符复制它们,它们会变成NULL,而复制所得的指针将取得资源的唯一拥有权

void f(){
  auto_ptr<int> a(new int(100)); //a指向new int返回的地址
  auto_ptr<int> b(a); //现在b指向对象,a被置为NULL
  a = b; //现在a指向对象,b置为NULL
}

这意味着受auto_ptr管理的资源没有一个以上的auto_ptr可以同时指向它。但是在auto_ptr放弃了对象的所有权后,这样就留下了一个悬挂指针,使用者后面还有可能使用它来访问该对象(比如上面的例子中,最后再做一个“*b”的操作),但显然这会导致问题.

unique_ptr

独占式指针,即同一时刻只能有一个指针指向同一个对象。如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。

 这一点看起来和auto_ptr是一样的,都是对于一个资源来说,只有一个指针指向它。但是它们之间有如下差别:

       1. unique_ptr禁止拷贝构造(copy constructor)和赋值运算(=)

对于unique_ptr来说,编译器会认为下面#3这一步是非法操作,直接编译阶段就会报错,这样就避免了a不再指向有效数据的问题,所以从这一点来讲,unique_ptr比auto_ptr更安全。

void f_unique() {
	unique_ptr<int> a(new int(100)); //#1
	unique_ptr<int> b;               //#2
	b = a;                           //#3 not allowed
}

但如果真的想把unique_ptr赋给另一个的话,其实也不是不行,如下两种情况可以做到将一个unique_ptr赋给另一个: 

          1.1 C++提供了标准库函数move()

比如想将上面的a赋给b,则可以使用move()

void f_unique() {
	unique_ptr<int> a(new int(100));
	cout << "Address: " << a.get() << "; *a = " << *a << endl;
	unique_ptr<int> b;
	b = move(a);                          //not allowed
	cout << "Address: " << b.get() << "; *b = " << *b << endl;

	cout << "Address: " << a.get() << endl;
}

 5966f9f9e47e42c4bdf98d21d5d56a18.png

         1.2.如果unique_ptr是一个临时右值

如果unique_ptr是一个临时右值,编译器允许这样做;如果unique_ptr将存在一段时间,编译器将禁止这样做。

如下面这段示例,demo返回的是一个临时的unique_ptr,然后a接管了原本返回的unique_ptr所拥有的对象,而返回的unique_ptr被销毁。这样的话,没有机会使用返回的临时unique_ptr对象来访问无效的数据,这样的赋值行为想象上也应该要是合理的

unique_ptr<int> demo(int a) {
	unique_ptr<int> res(new int(a));
	return res;
}

void testDemo() {
	unique_ptr<int> a;
	a = demo(10);
	cout << "Address: " << a.get() << "; *a = " << *a << endl;
}

       2. unique_ptr可用于数组 

  unique_ptr<int []> a(new int(100));

但是auto_ptr不可以,因为auto_ptr里使用的是delete而没有使用delete [],所以只能与new一起使用,无法与delete []一起使用,所以无法用于数组。而unique_ptr里有使用new []和delete []的版本。

shared_ptr

shared_ptr是计数型智能指针,同一时刻可以有多个指针指向同一个对象,并在无人指向它时自动删除该资源。它的大概原理是:shared_ptr内部有一个计数器,每当新增一个指向同一个对象的指针时,那么这个计数器会加1;反之,每有一个指针被释放,计数器减1,如果计数器为0时,shared_ptr就会释放指向的对象。

所以可见shared_ptr和前面所说的auto_ptr、unique_ptr最明显的不同就是:shared_ptr允许多个指针同时指向一个对象,而auto_ptr、unique_ptr都只允许同一时刻只能有一个指针指向同一个对象。

如果程序要使用多个指向同一个对象的指针,则应该选择shared_ptr。如下这段程序所示, a,b,c这三个shared_ptr都同时指向了同一块内存

void f() {
	shared_ptr<int> a(new int(100));
	cout << "Adress: " <<a.get() << "; *a = " << *a << endl;

	shared_ptr<int> b(a);
	cout << "Adress: " << b.get() << "; *b = " << *b << endl;

	shared_ptr<int> c = a;
	cout << "Adress: " << c.get() << "; *c = " << *c << endl;
	return;
}


80fa8776eac84648b253899c4c3ed2ec.png

     

      环状引用问题

环状引用是指两个或多个对象之间相互引用,形成一个闭环的情况。在面对环状引用时,shared_ptr无法打破这种情况,会引起内存泄漏。

假设有这样一段代码:

class A;
class B;

class A {
  public:
	shared_ptr<B> b_ptr;
};

class B {
  public:
	shared_ptr<A> a_ptr;
};

void testCycles() {
	shared_ptr<A> a = make_shared<A>(); //#1
	shared_ptr<B> b = make_shared<B>(); //#2

	a->b_ptr = b;  //#3
	b->a_ptr = a;  //#4

	return;
}

1.经过上面的#1和#2后,指向a和b的shared_ptr引用计数各自为1,即当前各有一个shared_ptr有分别指向a和b

2. 经过#3和#4后,a->b_ptr指向了b,   b->a_ptr指向a, 因为a和b有互指,则各自的引用计数变为2,如下图所示:

b24ee3b221494350b4f45ac13737cfe8.png

3. 当a和b都脱离作用域,各自调用自己的析构函数后,a和b各自的引用计数减为1,但是a和b的资源还没有release,a还有指向b,b也还有指向a.

4. a释放,就需要b的a_ptr释放,b释放,就需要a的b_ptr释放。因此a和b互相约束着对方的析构,最后都没法析构,导致内存泄漏。

这有点儿“死锁”的味道了。所以环状引用会导致对象的引用计数无法减为0,从而导致内存泄漏。为了解决环状引用的问题,可以使用weak_ptr来打破环状引用。weak_ptr是一种弱引用,它不会增加对象的引用计数,也不会阻止对象的销毁。下面介绍一下weak_ptr。

weak_ptr

weak_ptr是用来解决shared_ptr相互引用导致的内存泄漏问题,是shared_ptr的辅助类。具体来说,就是在环状引用中,将其中一个对象的指针使用weak_ptr来引用另一个对象,而不是使用shared_ptr。这样,在两个对象相互引用时,当所有的shared_ptr都释放了所指向的对象,即使还有weak_ptr指向该对象,对象也会被销毁,不会导致引用计数无法降为0,从而避免了内存泄漏的问题

因此,上面那段示例代码可以将A or B里的某一方改为weak_ptr:

class A;
class B;

class A {

public:
	shared_ptr<B> b_ptr;
};

class B {

public:
	weak_ptr<A> a_ptr;
};

void testCycles() {
	shared_ptr<A> a = make_shared<A>();
	shared_ptr<B> b = make_shared<B>();

	a->b_ptr = b;
	b->a_ptr = a;

	return;
}

    注意:

    1. weak_ptr不参与资源的管理和释放,可以使用shared_ptr对象来构造weak_ptr对象,但是不能直接使用指针来构造weak_ptr对象,如下的语法是非法的

weak_ptr<int> a(new int(100));

   2. weak_ptr没有operator*函数和operator->成员函数,不具有一般指针的行为。

总结

在实际中,应尽量避免直接使用裸指针,而应优先选择智能指针,它们利用对象的生命周期来管理资源,避免了资源泄漏这些问题。并且针对不同的需求选用合适类型的智能指针。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值