C++11新特性(续)

目录

一、包装器

1.1 包装器的介绍

1.2 包装器的绑定

二、智能指针

2.1 为什么需要智能指针?

2.2 智能指针的使用及原理

 2.3 智能指针的拷贝问题

2.3.1 auto_ptr

2.3.2 unique_ptr

2.3.3 shared_ptr

2.3.4 weak_ptr

2.4 智能指针的删除器                


一、包装器

1.1 包装器的介绍

template<class F, class T>
T useF(F f, T x)
{
	static int count = 0;
	cout << "count:" << ++count << endl;
	cout << "count:" << &count << endl;

	return f(x);
}

double f(double i)
{
	return i / 2;
}

struct Functor
{
	double operator()(double d)
	{
		return d / 3;
	}
};

int main()
{

	// 函数指针
	cout << useF(f, 11.11) << endl;

	// 函数对象
	cout << useF(Functor(), 11.11) << endl;

	// lambda表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
}

        这段代码的useF有三种可能,分别是:函数指针、函数对象、lambda表达式。可以看到运行后static变量都是1,说明开始的类模版会被实例化成三份。

        问题来了,如果我们想把这些可调用对象存储到容器中,如vector<>,那么里面的类型应该怎么写?

        如果我们把它们统一成一份,是不是就可以办到了呢。

        统一的工具便是包装器,它可以将函数指针、仿函数、lambda表达式包装,我们来看看它的使用:

#include<functional>        //头文件
int main()
{

	// 函数指针
	cout << useF(f, 11.11) << endl;

	// 函数对象
	cout << useF(Functor(), 11.11) << endl;

	// lambda表达式
	cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;

    //包装器
    function<double(double)> f1 = f; 
    function<double(double)> f2 = [](double d)->double { return d / 4; }; 
    function<double(double)> f3 = Functor(); 
    //vector<function<double(double)>> v = { f1,f2,f3 };

	vector<function<double(double)>> v = 
    { f,[](double d)->double { return d / 4; },Functor() };

	double n = 4;
	for (auto f : v)
	{
		cout << f(n++) << endl;
	}
    return 0;
}

  其命名格式为:function<Ret(Args...)>,Ret为返回类型,Args为参数列表的类型。

我们来验证一下:

int main()
{
	// 函数名
	std::function<double(double)> func1 = f;
	cout << useF(func1, 11.11) << endl;

	// 函数对象
	std::function<double(double)> func2 = Functor();
	cout << useF(func2, 11.11) << endl;

	// lambda表达式
	std::function<double(double)> func3 = [](double d)->double { return d / 4; };
	cout << useF(func3, 11.11) << endl;
	return 0;
}

        可以发现这三个调用对象都被实例化成为了一份,它们共享count变量。

1.2 包装器的绑定

bind

        std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

        我们可以使用它来进行函数中参数位置的调换:

        其中bind调用时首先是一个可调用对象Sub,placeholders是一个命名空间后面的数字对应的是参数的位置,在调用时我们就可以像图中箭头那样理解,那么如何调换参数位置即传参为10,5但让函数运行的是5-10呢?

        相信你也想到了,那就是将bind中placeholders的位置调换一下:

        这样10就对应到了第二个参数,5就对应到了第一个参数。

当然我们也可以对参数进行缺省,这样我们就绑定了第三个参数,Plus只需要传两个就可以。

需要注意,要绑定的参数是不会占参数位置的,我们直接忽略它即可,如:

        即使rate在中间,它也不会占参数的位置,a仍然对应的是1,按顺序b下来仍然是2,rate我们直接忽略不看。

        这里的调用对象都是全局的,那么对于局部的成员函数,我们应该如何绑定?

class SubType
{
public:
	static int sub(int a, int b)
	{
		return a - b;
	}
	int ssub(int a, int b, int rate)
	{
		return(a - b) * rate;
	}
};
 
int main()
{
	function<double(int, int)> Sub1 =     
    bind(&SubType::sub, placeholders::_1, placeholders::_2);    //&也可以不加
        
	SubType st;    
	function<double(int, int)> Sub2 = 
    bind(&SubType::ssub,&st, placeholders::_1, placeholders::_2,3);    //多一个参数
    //&必须加
    function<double(int, int)> Sub3 = 
    bind(&SubType::ssub,SubType(), placeholders::_1, placeholders::_2,3);    

	return 0;
}

        对于static成员函数,可以在没有创建类的实例的情况下直接调用,因此我们传参只需传它本身即可,但是要加类域,其前面的取地址符可以不加。但是对于非static成员函数,它的取地址符是一定要加的,并且还要再声明一个对象。

二、智能指针

2.1 为什么需要智能指针?

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}
void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}
int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

     如果在new与delete之间抛异常了,那么就会出现内存泄漏的问题。当然可以使用try-catch来解决问题,但如果new的对象很多,这麻烦就很大了,因此我们可以通过智能指针来解决这种问题。

2.2 智能指针的使用及原理

// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
public:
    SmartPtr(T* ptr = nullptr)
       : _ptr(ptr)
   {}
    ~SmartPtr()
   {
        if(_ptr)
            delete _ptr;
   }
    
private:
    T* _ptr;
};
int div()
{
     int a, b;
     cin >> a >> b;
     if (b == 0)
     throw invalid_argument("除0错误");
     return a / b;
}
void Func()
{
     SmartPtr<int> sp1(new int);
     SmartPtr<int> sp2(new int);
     cout << div() << endl;
}

int main()
{
    try {
        Func();
    }
    catch(const exception& e)
    {
        cout<<e.what()<<endl;
    }
    return 0;
}

        智能指针实则完美地运用了构造函数和析构函数的特性,这样就保证了初始化自动调用构造函数,出了作用域自动调用析构函数,这样就不需要我们手动delete了。其设计思想我们称为RAII,它是一种利用对象生命周期来控制程序资源的技术。

        上述的SmartPtr还不能将其称为智能指针,因为它还不具有指针的行为。指针可以解引用,也可以通过->去访问所指空间中的内容,因此我们在模板类中还得需要将* 、->重载下,才可以让其像指针一样去使用。

template<class T>
class SmartPtr
{
public:
	// RAII
	// 资源交给对象管理,对象生命周期内,资源有效,对象生命周期到了,释放资源
	// 1、RAII管控资源释放
	// 2、像指针一样
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
	}

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
};

 2.3 智能指针的拷贝问题

经过上面的学习,我们可以知道,所有的智能指针都具有以下特点:

  1. 都借助RAII的思想管理资源;
  2. 都像指针一样

因此有了这些基础我们再来认识几个智能指针并尝试自己写出它们。

2.3.1 auto_ptr

我们自定义一个类A,来试试看auto_ptr是什么效果:

class A
{
public:
	A(int a = 0 )
		:_a(a)
	{
		cout << "A(int a = 0)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	auto_ptr<A> ap1(new A(1));
	return 0;
}

        可以看到,正常的创建并释放了对象。我们再进行拷贝:

        可以看到,也正常的创建并释放了对象。

        我们再来看看底层的代码:

        可以发现在拷贝之后,代码实现了管理权转移,把ap1的管理权给了ap3,ap1悬空了,这样就导致了auto_ptr的一个缺陷:拷贝时会把被拷贝对象的资源管理权转移给拷贝对象,导致被拷贝对象悬空,访问就会出问题:

下面我们来尝试自己写一个auto_ptr

namespace lee 
{
	template<class T>
	class auto_ptr
	{
	public:
		//RAII
		auto_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//拷贝问题
		//管理权转移
		auto_ptr (auto_ptr<T>& ap)
			:_ptr(ap._ptr)
		{
			ap._ptr = nullptr;
		}

	private:
		T* _ptr;
	};
}

        前面的编写与上述智能指针的原理思想一样,要符合RAII和指针一样的原则,然后就是拷贝问题,拷贝其实就是把自己的指针传给要拷贝的对象的指针,然后将自己置为空。

2.3.2 unique_ptr

        知道了auto_ptr的缺陷,我们应该怎样解决呢?

        C++11标准库中有一个unique_ptr,它就很好的避免了auto_ptr的问题。我们换成unique_ptr继续尝试运行下拷贝的代码:

        可以发现这个智能指针直接禁止了拷贝。这就是它简单粗暴的解决方法。

我们也来自己模拟实现一下吧:

template<class T>
	class unique_ptr
	{
	public:
		//RAII
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			cout << "delete:" << _ptr << endl;
			delete _ptr;
		}
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		//拷贝问题
		//禁止拷贝
	    //private:
	    //	unique_ptr(unique_ptr<T>& up);

		unique_ptr(unique_ptr<T>& up)=delete;
		unique_ptr<T>& operator=(unique_ptr<T>& up)=delete;



	private:
		T* _ptr;
	};

前两块内容与上面代码一致,主要是拷贝问题,如何实现禁止拷贝,有两种方法:

  1. 只声明不实现,并且设为私有,目的是防止外部再对其进行实现
  2. 与C++11库内一致,直接将声明delete

        对赋值我们也是要实现一下禁用的,否则编译器会默认生成赋值,导致程序崩溃。

目的达成:

2.3.3 shared_ptr

        上述智能指针都是不让拷贝的,但有的场景是必须要拷贝的,这就需要用到shared_ptr来完成拷贝的任务了。

        sp1和sp3是共同管理一块空间的,那它在析构时不应该会析构两次吗?它底层的拷贝问题是值得我们来探究的:

        为了方式它不被析构两次,我们可以采用引用计数的方法来解决:多一个对象对其管理,引用计数就加1,再最后析构时不着急释放资源,而是 -- 计数,当计数为0时再释放资源

        这里对于计数count变量的声明我们要仔细考虑,由于它是多个对象共享的,那我们能不能可以将其设为类的static变量?

        答案是不行的,如图,假设sp1、sp3共同管理一块空间,sp2、sp4、sp5共同管理一块空间,如果是staic变量(所有对象都共享),就会有冲突了。

        我们期望的是一个资源伴随一个count,因此我们再添加一个指针,让它一个指针指向资源,另一个指针指向计数:

        因此在构造时我们就new一个计数,析构时对计数--,直到为0再delete:

//RAII
shared_ptr(T* ptr)
	:_ptr(ptr)
	:_pcount(new int(1))
{}
~shared_ptr()
{
	if (-- (*_pcount) == 0)
	{
		cout << "delete:" << _ptr << endl;
		delete _ptr;
		delete _pcount;
	}
}

        拷贝时就先把原先的计数和指针交给要拷贝的对象,最后计数再加1:

//拷贝问题
shared_ptr(const shared_ptr<T>* sp)
	:_ptr(sp._ptr)
	,_pcount(sp._pcount)
	{
		++(*_pcount);
	}

目标达成:

接下来继续写一个赋值:

我们要考虑到的点有:

  1. 如果赋值的两个对象指向的是不同的资源,在赋值前要将自己的count--,当它为0时释放资源,赋值到的新的对象的count++;
  2. 自己给自己赋值或者原本就指向同一资源的两个对象之间的赋值。
//赋值问题
//在赋值前要将自己的count--,当它为0时释放资源,赋值到的新的对象的count++
shared_ptr<T>& operator=(const shared_ptr<T>& sp)
{
	//资源相同直接返回
	if (_ptr == sp._ptr)
        return *this;

	if (--(*_pcount) == 0)
	{
		delete _ptr;
		delete _pcount;
	}
	_ptr = sp._ptr;
	_pcount = sp._pcount;
	++(*_pcount);
	return *this;
}

2.3.4 weak_ptr

        shared_ptr几乎是没有缺点的,但是对于下面的场景还是会有一些问题:

        可以看到,这里的两个shared_ptr节点并没有释放,内存泄漏了,这个问题我们叫做循环引用

sp2和sp1析构后释放的逻辑就死循环了:

要想解决循环引用的问题,就需要把这里的shared_ptr改为weak_ptr:

注意这里的shared_ptr并不是智能指针,它只是专门用来解决循环引用的指针。

代码实现:

template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}
		//没有释放

		weak_ptr(const shared_ptr<T>& sp) 
			:_ptr(sp._ptr)
		{}


		weak_ptr<T>& operator=(const shared_ptr<T>& sp)
		{
			_ptr = sp.get();
			return *this;
		}
		
		//像指针一样
		T& operator*()
		{
			return *_ptr;
		}
		T* operator->()
		{
			return _ptr;
		}
		
	private:
		T* _ptr;
	};

2.4 智能指针的删除器                

如果不是new出来的对象我们应该如何通过智能指针管理呢?

我们来看看库中是怎样解决的:

        可以看到在构造时,库中又加了一个新的参数,其实是将一个可调用对象作为删除器传入,因此我们可以用函数指针、仿函数、lambda指针的任意一种来做为删除器传给shared_ptr:

下面我们来尝试自己增加一个带删除器的智能指针:

首先遇到了问题:

       因为删除器可以是仿函数、函数指针、lambda表达式的任意一种,因此这里删除器我们用一个构造函数的类模版来声明它的类型,那在定义_del这个对象时应该用什么声明?

        这就需要我们用到上面讲知识:包装器来解决了。

         好啦,至此我们智能指针的版块就学习完成了,感谢观看(#^.^#)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值