详解RAI I、C++中的智能指针以及一些常见的面试题

RAII

      RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。

用法

      RAII 的一般做法是这样的:在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此,我们实际上把管理一份资源的责任托管给了一个对象。主要应用智能指针!!!

这种做法有两大好处:
不需要显式地释放资源。
采用这种方式,对象所需的资源在其生命期内始终保持有效。

总结
RAII的本质内容是用对象代表资源,把管理资源的任务转化为管理对象的任务,将资源的获取和释放与对象的构造和析构对应起来,从而确保在对象的生存期内资源始终有效,对象销毁时资源一定会被释放。
说白了,就是拥有了对象,就拥有了资源,对象在,资源则在。所以,RAII机制是进行资源管理的有力武器,C++程序员依靠RAII写出的代码不仅简洁优雅,而且做到了异常安全。在以后的编程实际中,可以使用RAII机制,让自己的代码更漂亮。


智能指针

出现原因

由于 C++ 语言没有自动内存回收机制,程序员每次 new 出来的内存都要手动 delete,比如流程太复杂,最终导致没有 delete,异常导致程序过早退出,没有执行 delete 的情况并不罕见,并造成内存泄露。如此c++引入智能指针 。

解释

智能指针(smart pointer)其实不是一个指针。它就是用来帮助我们管理指针,维护其生命周期的类。c++引入 智能指针 ,智能指针即是C++ RAII的一种应用,可用于动态资源管理,资源即对象的管理策略。 智能指针在 头文件的 std 命名空间中定义。

作用
(1)方便管理堆内存。
(2)使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

智能指针分类

c++ 智能指针主要包括:unique_ptr,shared_ptr, weak_ptr, 这三种,其中auto_ptr 已被遗弃。
unique_ptr

只允许基础指针的一个所有者。 可以移到新所有者(具有移动语义),但不会复制或共享(即我们无法得到指向同一个对象的两个unique_ptr)。 替换已弃用的 auto_ptr。 相较于 boost::scoped_ptr。 unique_ptr 小巧高效;大小等同于一个指针,支持 rvalue 引用,从而可实现快速插入和对 STL 集合的检索。 头文件:。

功能:
1、为动态申请的内存提供异常安全。
2、将动态申请内存的所有权传递给某个函数。
3、从某个函数返回动态申请内存的所有权。
4、在容器中保存指针。
5、所有auto_ptr应该具有的(但无法在C++ 03中实现的)功能。
如下代码所示:

class A;
// 如果程序执行过程中抛出了异常,unique_ptr就会释放它所指向的对象
// 传统的new 则不行
unique_ptr<A> fun1()
{
	unique_ptr p(new A);
	//do something
	return p;
}

void fun2()
{   // unique_ptr具有移动语义
	unique_ptr<A> p = f();// 使用移动构造函数
	// do something
}// 在函数退出的时候,p以及它所指向的对象都被删除释放

shared_ptr

采用引用计数的智能指针。 shared_ptr基于“引用计数”模型实现,多个shared_ptr可指向同一个动态对象,并维护了一个共享的引用计数器,记录了引用同一对象的shared_ptr实例的数量。当最后一个指向动态对象的shared_ptr销毁时,会自动销毁其所指对象(通过delete操作符)。shared_ptr的默认能力是管理动态内存,但支持自定义的Deleter以实现个性化的资源释放动作。头文件:< memory>。

基本操作:
shared_ptr的创建、拷贝、绑定对象的变更(reset)、shared_ptr的销毁(手动赋值为nullptr或离开作用域)、指定deleter等操作。
创建
一,使用函数make_shared(会根据传递的参数调用动态对象的构造函数);
二,使用构造函数(可从原生指针、unique_ptr、另一个shared_ptr创建)
shared_ptr p1 = make_shared(1);// 通过make_shared函数
shared_ptr p2(new int(2));// 通过原生指针构造

shared_ptr<int> p1 = make_shared<int>(1);// 通过make_shared函数
shared_ptr<int> p2(new int(2));// 通过原生指针构造

此外智能指针若为“空“,即不指向任何对象,则为false,否则为true,可作为条件判断。
可以通过两种方式指定deleter,一是构造shared_ptr时,二是使用reset方法时。
可以重载的operator->, operator *,以及其他辅助操作如unique()、use_count(), get()等成员方法。

weak_ptr

结合 shared_ptr 使用的特例智能指针。 weak_ptr 提供对一个或多个 shared_ptr 实例所属对象的访问,但是,不参与引用计数。 如果您想要观察对象但不需要其保持活动状态,请使用该实例。 在某些情况下需要断开 shared_ptr 实例间的循环引用。 头文件:< memory>。

用法:
weak_ptr用于配合shared_ptr使用,并不影响动态对象的生命周期,即其存在与否并不影响对象的引用计数器。weak_ptr并没有重载operator->和operator *操作符,因此不可直接通过weak_ptr使用对象。提供了expired()与lock()成员函数,前者用于判断weak_ptr指向的对象是否已被销毁,后者返回其所指对象的shared_ptr智能指针(对象销毁时返回”空“shared_ptr)。循环引用的场景:如二叉树中父节点与子节点的循环引用,容器与元素之间的循环引用等。

智能指针的循环引用

循环引用问题可以参考这个链接上的问题理解,“循环引用”简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用。导致引用计数失效。
下面给段代码来说明循环引用:

#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:// 为了省去一些步骤这里 数据成员也声明为public   
	//weak_ptr<B> pb; 
	shared_ptr<B> pb;
	void doSomthing()
	{
		// if(pb.lock())
		// {
		//
		// } 
	}

	~A()
	{
		cout << "kill A\n";
	}
};
class B
{
public:
	//weak_ptr<A> pa; 
	shared_ptr<A> pa;
	~B()
	{
		cout << "kill B\n";
	}
};
int main(int argc, char** argv)
{
	shared_ptr<A> sa(new A());
	shared_ptr<B> sb(new B());
	if (sa && sb)
	{
		sa->pb = sb;
		sb->pa = sa;
	}
	cout << "sa use count:" << sa.use_count() << endl;
	return 0;
}

上面的代码运行结果为:sa use count:2, 注意此时sa,sb都没有释放,产生了内存泄露问题!!!

即A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。
一般来讲,解除这种循环引用有下面有三种可行的方法(参考):1. 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。2. 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。3. 使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。
强引用和弱引用
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr就是强引用。相对而言,弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不修改该对象的引用计数,这意味这弱引用它并不对对象的内存进行管理,在功能上类似于普通指针,然而一个比较大的区别是,弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。
使用weak_ptr来打破循环引用
代码如下:

#include <iostream>
#include <memory>
using namespace std;
class B;
class A
{
public:
	// 为了省去一些步骤这里 数据成员也声明为public 
	weak_ptr<B> pb;
	//shared_ptr<B> pb; 
	void doSomthing()
	{
		shared_ptr<B> pp = pb.lock();
		if (pp)//通过lock()方法来判断它所管理的资源是否被释放 
		{
			cout << "sb use count:" << pp.use_count() << endl;
		}
	}
	~A()
	{
		cout << "kill A\n";
	}
};
class B
{
public:
	//weak_ptr<A> pa; 
	shared_ptr<A> pa;
	~B()
	{
		cout << "kill B\n";
	}
};
int main(int argc, char** argv)
{
	shared_ptr<A> sa(new A());
	shared_ptr<B> sb(new B());
	if (sa && sb)
	{
		sa->pb = sb;
		sb->pa = sa;
	}
	sa->doSomthing();
	cout << "sb use count:" << sb.use_count() << endl;
	return 0;
}

几种智能指针的区别

  1. std::auto_ptr,有很多问题。 不支持复制(拷贝构造函数)和赋值(operator =),但复制或赋值的时候不会提示出错。因为不能被复制,所以不能被放入容器中。
  2. C++11引入的unique_ptr, 也不支持复制和赋值,但比auto_ptr好,直接赋值会编译出错。实在想赋值的话,需要使用:std::move。
    例如:
    std::unique_ptr p1(new int(5));
    std::unique_ptr p2 = p1; // 编译会出错
    std::unique_ptr p3 = std::move(p1); // 转移所有权, 现在那块内存归p3所有, p1成为无效的指针.
  3. C++11或boost的shared_ptr,基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。
  4. C++11或boost的weak_ptr,弱引用。 引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

一些常见的面试题


1.答:智能指针的类对象是栈上的,所以当函数(或程序)结束时会自动被释放


左边代码执行Process§的时候,传参的时候会进行默认的赋值构造函数,形参的p和实参的p指向同一块内存,Process函数运行结束回到main函数后会释放函数内部的p, while单层循环结束后仍然会释放p,p会进行两次的释放,造成异常。

目前我们有两个问题需要解决:

  1. 如何对指针引用的内存进行计数?
  2. 如何改进SmartPointer类使得它能够动态维护引用计数





    C++库中的智能指针:

    auto_ptr是一个独占类型的智能指针,同一时刻只能被一个对象拥有。



    导致结果:
    A内部有指向B,B内部有指向A,这样对于A,B必定是在A析构后B才析构,对于B,A必定是在B析构后才析构A,这就是循环引用问题,违反常规,导致内存泄露。

一般来讲,解除这种循环引用有下面有三种可行的方法( 参考 ):
1 . 当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
2 . 当A的生存期超过B的生存期的时候,B改为使用一个普通指针指向A。
3 . 使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。我们一般使用第三种方法:弱引用的智能指针weak_ptr。

使用weak_ptr时,并不知道引用计数是多少,一般会有一个强指针来帮助其判断内存是否已经被释放掉或者是否合法。weak_ptr不会去修改引用计数的大小,相当于把循环打破了,所以会用weak_ptr去解决循环引用问题。

强引用和弱引用
一个强引用当被引用的对象活着的话,这个引用也存在(就是说,当至少有一个强引用,那么这个对象就不能被释放)。share_ptr就是强引用。
弱引用当引用的对象活着的时候不一定存在。仅仅是当它存在的时候的一个引用。弱引用并不知道引用计数是多少,也不修改该对象的引用计数,这意味着弱引用它并不对对象的内存进行管理,在功能上类似于普通指针
区别: 弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

全部评论

相关推荐

1 2 评论
分享
牛客网
牛客企业服务