【C++进阶之路】C++11(中)

一、可变参数模板

1.基本概念

 想要了解C语言的可变参数列表的原理可见:可变参数列表

 这个跟C语言的可变参数列表有一定的关系,常用的printf与scanf的参数就包含可变参数列表。

 那么可变参数模板是什么呢?举个例子便一目了然。

template<class...Arg>
void ShowList(Arg... arg)
{
	//...
}
int main()
{
	ShowList();
	ShowList(1, 2, 3);
	ShowList('a', 'b', 'c');
	return 0;
}

 这就是可变参数模板,可以传多个参数,这里的Arg称为模板的可变参数包,这里的arg称为形参的可变参数包(包括0-N个参数)。

但是这里有一个问题即如何用这个参数包呢?

  1. 递归展开
void _ShowList()
{
	cout<< endl;
}

template<class T,class...Arg>
void _ShowList(T val, Arg... arg)
{
	cout << val << " ";
	_ShowList(arg...);
}

template<class...Arg>
void ShowList( Arg... arg)
{
	_ShowList(arg...);
}
int main()
{
	ShowList();
	ShowList(1, 2, 3);
	ShowList('a', 'b', 'c');
	return 0;
}
  • 编译器以类似递归的方式(调用的不是同一个函数,是来自一个模板实例化出的函数)自动推导出参数包的类型然后进行打印。

运行结果:
【C++进阶之路】C++11(中)_第1张图片

  1. 逗号表达式
template<class T>
int PrintArg(T val)
{
	cout << val << " ";
	return 0;
}

void ShowList()
{
	cout << endl;
}

template<class... Arg>
void ShowList(Arg... arg)
{
	int arr[] = { PrintArg(arg)... };
	//处理成类似与int arr[] = { PrintArg(arg), PrintArg(arg),PrintArg(arg)};这样的形式。
	//这是编译链接阶段处理的结果。
	cout << endl;
}

int main()
{
	ShowList();
	ShowList(1, 2, 3);
	ShowList('a', 'b', 'c');
	return 0;
}

运行结果:
【C++进阶之路】C++11(中)_第2张图片

2.应用场景

1.提升传参的灵活性

例:

class Date
{
public:
	Date(int year = 0, int month = 0, int day = 0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
public:
	int _year;
	int _month;
	int _day;
};

template<class ...Args>
Date* CreatDate(Args... args)
{
	Date* ret = new Date(args...);

	return ret;
}

int main()
{
	Date* date1 = CreatDate(1);
	Date* date2 = CreatDate(1,2);
	Date* date3 = CreatDate(1,2,3);
	Date* date4 = CreatDate(Date(1,2,3));
	return 0;
}

  • 不仅可以调用构造函数,也可调用默认的拷贝构造函数,进而提高了灵活性。

2.接口——emplace_back

参数:
【C++进阶之路】C++11(中)_第3张图片

  • 说明:这里在可变参数模板的基础上添加了万能引用。
int main()
{
	list<pair<int, MY_STL::string>>lt;
	//先调用pair的构造函数再调用右值的移动拷贝构造。
	lt.push_back(make_pair(1, "张三"));
	cout << endl;
	lt.push_back({ 2, "李四" });

	cout << endl << endl;
	
	//作为参数包(引用)传递下去,直接调用list的构造函数进行构造
	lt.emplace_back(3, "王五");
	cout << endl;

	lt.emplace_back(make_pair(4, "赵六"));//这里编译器进行了优化,也是直接调用构造函数进行初始化。

	return 0;
}
  • 可见push_back虽然多调用了两次构造但好在是移动构造消耗不大,emplace_back直接将参数包传下去,直接调用构造函数进行初始化,总而言之与push_back有区别但性能上相差不大。

  • 举例 : list的emplace_back

//list里面的实现接口
	template <class... Args>
	void push_back(Args&&... args)
	{
		insert(end(), args...);
	}
	template <class... Args>
	void emplace_back(Args&&... args)
	{
		push_back(args...);
	}
	template <class... Args>
	void insert(iterator pos, Args&&... args)
	{
		Node* newnode = new Node(args...);
		Node* cur = pos._node;
		Node* prev = cur->_prev;
	
		newnode->_next = cur;
		newnode->_prev = prev;
	
		cur->_prev = newnode;
		prev->_next = newnode;
		_size++;
	}
//Node的实现接口
	template<class ...Args>
	list_node(Args&& ...arg)
		:_val(arg...)
		, _next(nullptr)
		, _prev(nullptr)
	{}
//实现逻辑——不断传参数包,将参数包传给构造函数进行初始化。

二、包装器

1. 简单实现

 如何用外界控制某段执行逻辑?

  • C语言阶段我们可以通过传函数指针
  • C++阶段我们可以通过传仿函数或者lambda(底层是仿函数)

 这三者有没有什么共同的特性呢?就是可以像函数一样进行使用。如果实现的功能一样那么参数和返回值是一样的。

 根据这两个性质,我们可以将这三者抽象成一个模板,以实现减法举例:

template<class T1,class T2,class T3>
double fun(T1 f, const T2& x, const T3& y)
{
	return f(x, y);
}
double sub(int x, int y)
{
	return x - y;
}
struct Sub
{
	double operator()(int x, int y)
	{
		return x - y;
	}
};
int main()
{
	auto x = [](int x, int y)->double {return x - y;};//lambda表达式。
	cout << fun(x, 1, 2) << endl;
	cout << fun(sub, 4, 2) << endl;
	Sub s;
	cout << fun(s, 9, 2) << endl;
	return 0;
}

 这样的一个模板就将这三者进行泛化,都可以通过模板实例化使用,也就是实现了包装的作用,当然这只是最简单的用法,下面我们就进入库里面的包装器的使用。

2. 基本语法与使用

【C++进阶之路】C++11(中)_第4张图片

重点:class function<Ret()Args...>;
1. Ret 返回值
2. Args... 函数参数

简单使用:

double sub(int x, int y)
{
	return x - y;
}
struct Sub
{
	double operator()(int x, int y)
	{
		return x - y;
	}
};
int main()
{
	auto x = [](int x, int y)->double {return x - y;};
	function<double(int, int)> f1 = x;
	function<double(int, int)> f2 = s;
	function<double(int, int)> f3 = sub;
	cout << f1(2 , 3) << endl;
	cout << f2(2, 9) << endl;
	cout << f3(9, 9) << endl;
	return 0;
}

更为实际的使用:
比如:逆波兰表达式的求值

class Solution {
public:
    int evalRPN(vector<string>& tokens) 
    {
        map<string,function<int(int,int)>> s = {
            {"+",[](int x,int y){return x + y;}},
            {"-",[](int x,int y){return x - y;}},
            {"*",[](int x,int y){return x * y;}},
            {"/",[](int x,int y){return x / y;}}
        };
        stack<int> st;
        int size = tokens.size();
        for(int i = 0; i < size; i++)
        {
            if(tokens[i] == "+" || tokens[i] == "-"
            || tokens[i] == "*" || tokens[i] == "/")
            {
                int right = st.top();
                st.pop();
                int left = st.top();
                st.pop(); 
                st.push(s[tokens[i]](left,right));
            }
            else
            {
                st.push(stoi(tokens[i]));
            }
        }
        return st.top();
    }
};

 仔细品味这段代码,就会发现包装器的好用之处,其原因在于直接将一类功能类似的函数进行泛化,可以通过具体的反应进行直接调用,还有我们所写的一类代码统一的出现在一个地方这样便于维护

三、bind

 bind也是函数的封装,类似与包装器,不过更详细的是对参数的个数和传参顺序的封装,下面我们来看看如何具体使用。

1.基本语法

在这里插入图片描述

  • 将参数1(函数指针/ 仿函数/lambda),剩余函数参数传进去即可,具体要进行分类进讨论。

2.常见用法

1.函数指针

  • 改变参数顺序
double sub(int x, int y)
{
	return x - y;
}
int main()
{
	function<double(int, int)> f1 = bind(sub, placeholders::_1,\
	  placeholders::_2);
	cout << f1(1, 2) << endl;
	
	function<double(int, int)> f2 = bind(sub, placeholders::_2,\
	 placeholders::_1);
	cout << f2(1, 2) << endl;
	return 0;
}

逻辑图解:
【C++进阶之路】C++11(中)_第5张图片

  • 改变参数个数
double sub1(int x, double rate, int y)
{
	return (x - y) * rate;
}
int main()
{
	function<double(int, int)> ff1 = bind(sub1, placeholders::_1,\
	 1.5, placeholders::_2);
	 //在定义期间固定参数,类似与缺省值的效果。
	cout << ff1(2, 1) << endl;

	auto ff2 = bind(sub1, placeholders::_1, 1, placeholders::_2);
	cout << ff2(3, 1) << endl;
}

逻辑图解:
【C++进阶之路】C++11(中)_第6张图片

  • 说明:lambda与函数指针的用法相同。

2.仿函数

struct SubType
{
	static double sub(int x, int y)//没有this指针,不用进行传参
	{
		return x - y;
	}
	double ssub(int x, int y)
	{
		return x - y;
	}
};
int main()
{
	function<double(int, int)> pf1 = bind(SubType::sub,\
	 placeholders::_1, placeholders::_2);
	//sub为static函数,没有this指针,只需指定作用域即可。

	SubType st;		//语法规定普通成员函数传参需加&
	auto pf3 = bind(&SubType::ssub, &st, placeholders::_1,\
	 placeholders::_2);
	//ssub为普通成员函数,有this指针,因此需要指定作用域外加传this指针。

	auto pf2 = bind(&SubType::ssub, SubType(), placeholders::_1,\
	 placeholders::_2);
									//也可用对象进行调用。
	return 0;
}

 总之,bind可以改变参数的个数和顺序,改变参数的个数可以固定参数值和减少传参改变参数顺序,可以更加符合函数使用者的习惯(比如在strcpy一般第一个参数是dest,第二个参数为source,如果实现接口的程序员将参数写反了,我们可以通过包装更加符合我们的代码编写习惯。) 。

四、智能指针

 为了解决异常的执行流乱跳而导致内存泄漏的问题:

void func()
{
	//假设new int 可能会抛出bad_alloc的异常。
	int* ptr1 = new int(1);
	//当运行到此处ptr1出错时,我们只需抛出异常即可

	int* ptr2 = new int(2);
	//当运行到此处出ptr2错时,我们需要释放ptr1,然后抛出异常。

	int* ptr3 = new int(3);
	//当运行到此处ptr3出错时,我们需要释放ptr2,然后向外层抛出异常。
	
	delete ptr1;
	delete ptr2;
	delete ptr3;
}

 第一种方式我们可以通过try catch进行解决:

void func1()
{
	//假设new int 也可能会抛出异常。
	//根据异常的捕获与处理,我们可以这样解决。
	try
	{
		int* ptr1 = new int(1);
		//...
		try
		{
			int* ptr2 = new int(2);
			//...
			try
			{
				int* ptr3 = new int(3);
				//...
				delete ptr3;
			}
			catch (...)
			{
				//当运行到此处ptr3出错时,我们需要释放ptr2,然后向外层抛出异常。
				delete ptr2;
				throw;
			}
			delete ptr2;
		}
		catch (...)
		{
			//当运行到此处出ptr2错时,我们需要释放ptr1,然后抛出异常。
			delete ptr1;
			throw;
		}
		delete ptr1;
	}
	catch (...)
	{
		//当运行到此处ptr1出错时,我们只需抛出异常即可
		throw;
	}
}
int main()
{
	try
	{
		func1();
	}
	catch (const exception& e)
	{
		cout << e.what() << endl;
	}
	catch (...)
	{
		//...未知异常;
	}
	return 0;
}
  • 很明显,每开辟一次我们需要进行try catch进行手动对异常进行捕获,很麻烦,那有没有简单的方式呢?很明显是有的。

  • RAII思想,资源获取即初始化,也就是用局部对象对资源进行管理,出作用域时会自动调用析构函数对资源进行释放。

就像这样:

template<class T>
class smart_ptr
{
public:
	smart_ptr(T* ptr)
		:_ptr(ptr)
	{}
	~smart_ptr()
	{
		if (_ptr)
			delete _ptr;
	}
private:
	T* _ptr;
};
void func()
{
	//假设new int 可能会抛出bad_alloc的异常。
	smart_ptr<int> ptr1 (new int(1));
	//当运行到此处ptr1出错时,我们只需抛出异常即可

	smart_ptr<int> ptr2(new int(2));
	//当运行到此处ptr2出错时,出作用域自动释放ptr1,然后抛出异常。

	smart_ptr<int> ptr3(new int(3));
	//当运行到此处ptr3出错时,出作用域自动释放ptr1,ptr2,然后向外层抛出异常。
}

 这样就简单的使用了RAII思想解决问题,但是如果是赋值问题与拷贝该如何解决呢?在使用默认的成员函数的情况下,可能会出现内存泄漏和重复释放的风险。下面就让我们看看库里面是如何实现的吧!

1.auto_ptr

1.1基本使用

class A
{
public:
	A(int a)
		:_a(a)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 0;
};
int main()
{
	auto_ptr<A> ap1(new A(1));
	auto_ptr<A> ap2(new A(2));
	auto_ptr<A> ap3(ap1);//资源管理权的转移。
	ap1 = ap2;//调用析构函数先释放ap1的资源,再将ap2的资源转移给ap1。
	return 0;
}

监视逻辑:从ap3的初始化开始。

【C++进阶之路】C++11(中)_第7张图片

  • 赋值与拷贝的实现方式是对资源的管理权进行转移。

1.2基本实现

namespace MY_SMART_PTR
{
	template<class T>
	class auto_ptr
	{
	public:
		auto_ptr(T* ptr = nullptr)
			:_ptr(ptr)
		{}
		~auto_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		//既然是指针就得像指针一样进行访问。
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
		auto_ptr( auto_ptr<T>& ptr)
			:_ptr(ptr._ptr)
		{
			ptr._ptr = nullptr;
		}
		auto_ptr<T>& operator = (auto_ptr& ptr)
		{
			if(_ptr)
				delete _ptr;
			_ptr = ptr._ptr;
			ptr._ptr = nullptr;

			return *this;
		}
	private:
		T* _ptr;
	};
}

2.unique_ptr

1.1基本使用

  • 对资源的使用权进行转移。
class A
{
public:
	A(int a)
		:_a(a)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 0;
};
int main()
{
	unique_ptr<A> up1(new A(1));
	unique_ptr<A> up2(new A(2));
	unique_ptr<A> up3(new A(3));

	up1 = up2;//直接禁掉了。
	unique_ptr<A> up3(up1);//也是被删除了
	return 0;
}

会直接进行报错:
【C++进阶之路】C++11(中)_第8张图片

1.2基本实现

  • 不可进行共享资源
namespace MY_SMART_PTR
{
	template<class T>
	class unique_ptr
	{
	public:
		unique_ptr(T* ptr)
			:_ptr(ptr)
		{}
		~unique_ptr()
		{
			if (_ptr)
				delete _ptr;
		}
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
		//两种方式:
		//1.C++98之前构造函数私有+声明进行实现。
		//2.C++11沿用之前的default与纯虚函数 == 0的写法,设为delete进行删除。
		unique_ptr(const unique_ptr<T>& ptr) = delete;
		unique_ptr<T>& operator = (unique_ptr<T>& ptr) = delete;

	private:
		T* _ptr;
	};
}

3.shared_ptr

1.1基本使用

  • 可以进行共享资源。
class A
{
public:
	A(int a)
		:_a(a)
	{}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a = 0;
};
int main()
{
	shared_ptr<A> sp1(new A(1));
	shared_ptr<A> sp2(new A(2));

	shared_ptr<A> sp3 = sp1;//采取引用计数sp3与sp1共享资源,析构采取引用计数。
	sp1 = sp2;//实现需注意与自身的赋值
	return 0;
}

实现图解:
【C++进阶之路】C++11(中)_第9张图片

  • 当对象进行拷贝构造时,将对象进行浅拷贝,并进行引用计数。
  • 当对象进行赋值时,对被赋值的对象的引用计数进行检查,对引用计数减减,如果引用计数为0。则进行资源的清理。

1.2基本实现

  • 开辟一个count的资源对指向资源的对象进行计数。
namespace MY_SMART_PTR
{
	template<class T>
	class shared_ptr
	{
	public:
		shared_ptr(T* ptr )
			:_ptr(ptr)
			,_pcount(new int(1))
		{}
		~shared_ptr()
		{
			if (--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
		}
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
		shared_ptr(shared_ptr<T>& ptr)
			:_ptr(ptr._ptr)
			,_pcount(ptr._pcount)
		{
			if(_pcount)
				(*_pcount)++;
		}
		shared_ptr<T>& operator = (shared_ptr<T>& ptr)
		{
			//判断是否相等,包括资源指向空的情况
			if (ptr._pcount == _pcount)
				return *this;
			//*this的_pcount的内容进行减减,判断是否为0
			if (_pcount && --(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			//对ptr指向的pcount加加
			_ptr = ptr._ptr;
			_pcount = ptr._pcount;
			if(_pcount)
				++(*_pcount);
			return *this;
		}
		T* get()const
		{
			return _ptr;
		}
	private:
		T* _ptr;
		int* _pcount;
	};
}

shared_ptr看起来是完美的,那么有没有什么缺陷呢?

思考下面一段代码:

struct Node
{
public:
	~Node()
	{
		cout << "~Node()" << endl;
	}
	shared_ptr<Node> _prev;
	shared_ptr<Node> _next;
};
int main()
{
	//循环引用——关键与难点
	//画图分析
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;
	return 0;
}
  • 运行分析程序的结果:

【C++进阶之路】C++11(中)_第10张图片
 运行结果看似正确,其实隐含了内存泄漏,因为sp1和sp2的资源没有被释放,这是为什么呢?

我们画图进行分析:
【C++进阶之路】C++11(中)_第11张图片

  • 关键在于对象析构时sp1和sp2分别只调用了一次析构函数,使引用计数减为1,最后对资源没有进行释放,那么如何解决这个问题呢?就引出了weak_ptr。
  • 这种现象我们称之为循环引用。

4.weak_ptr

1.1基本使用

{
public:
	~Node()
	{
		cout << "~Node()" << endl;
	}
	weak_ptr<Node> _prev;
	weak_ptr<Node> _next;
};
int main()
{
	//循环引用——关键与难点
	//画图分析
	shared_ptr<Node> sp1(new Node);
	shared_ptr<Node> sp2(new Node);

	sp1->_next = sp2;
	sp2->_prev = sp1;
 	return 0;
}
  • 其实现原理也很简单,就是weak_ptr中没有引用计数,且不参与资源的析构。

图解:
【C++进阶之路】C++11(中)_第12张图片

  • 在析构时调用两次即可对资源进行释放。

1.2基本实现

  • 实现shared_ptr转weak_ptr的赋值与拷贝构造。
  • 没有引用计数且不需要写析构函数。
namespace MY_SMART_PTR
{

	template<class T>
	class weak_ptr
	{
	public:
		weak_ptr()
		{}
		T& operator *()
		{
			return *_ptr;
		}
		T* operator ->()
		{
			return _ptr;
		}
		weak_ptr(shared_ptr<T>& ptr)
			:_ptr(ptr.get())
		{}
		weak_ptr<T>& operator = (shared_ptr<T>& ptr)
		{
			_ptr = ptr.get();
			return *this;
		}
	private:
		T* _ptr = nullptr;
	};
}
  • 1.weak_ptr不是RAII的智能指针,是专门用来解决循环引用的问题。
    2.weak_ptr不增加引用计数,只参与资源使用,不参与资源的释放。

总结

 今天的分享就到这里了,如果有所帮助,不妨点个赞鼓励一下,我们下篇文章再见~

你可能感兴趣的:(C++进阶之路,包装器,C++11,bind,可变模板参数,智能指针)