前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++11特性大杂烩

C++11特性大杂烩

原创
作者头像
梨_萍
发布2023-05-11 15:30:38
8640
发布2023-05-11 15:30:38
举报
文章被收录于专栏:梨+苹的C++梨+苹的C++

C++11大杂烩

位图 (4)
位图 (4)
image-20230510192109430
image-20230510192109430

TOC

介绍

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。

相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。基于此,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。

C++11增加的语法特性非常篇幅非常多,这里没办法一 一展开,所以本篇幅主要介绍实际中比较实用的语法。若要深层次去了解,可以去C++官方库查询学习:point_right:C++11官方库

语法

统一的列表初始化:{}初始化

在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。

数组或者结构体对象后面接着{},{}里是要初始化的参数

image-20230430192207541
image-20230430192207541

{}初始化同样适用于new表达式

代码语言:c++
复制
	int* ptr1 = new int[4]{ 1,2,3,4 };

创建对象时也可以使用列表初始化方式调用构造函数初始化

代码语言:c++
复制
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date(int year, int month, int day)" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2002, 12, 20);//()传参构造函数初始化
	Date d2{ 2001,3,28 };//通过{}列表初始化的方式构造函数初始化
    Date d3 = { 2000,11,24 };//也可以带=
	return 0;
}

initializer_list

有时候我们会看到这样的初始化

代码语言:c++
复制
	vector<int> vt{ 1,2,3,4,5 };
	list<int> lt{ 3,4,5,6,7 };

根据图中所示,由于Date是有多个参数的构造函数,所以可以通过传多个参数去初始化对象;但是vector和list是单个参数的构造函数,并且没有多个参数的构造函数,那是怎么做到下面的初始化呢?

image-20230501161214818
image-20230501161214818

使用初始化列表也只能进行固定参数的初始化,如果想要做到和 STL 一样有任意长度初始化的能力,可以使用 std::initializer_list 这个轻量级的类模板来实现。

initializer_list底层是一个常量数组,存放在常量区(不能随意修改),initializer_list就是对这个常量数组进行封装。

image-20230501154734209
image-20230501154734209

:point_right: initializer_list

initializer_list的特性

  1. 参数的类型必须相同
image-20230501161428328
image-20230501161428328
  1. 只有三个成员函数,begin(),end()和size(),意味着支持迭代器遍历和获取容器中元素个数
  2. initializer_list本质上就是一个大括号括起来的一个列表,若用auto去接收这个列表的类型,可以通过typeid().name()看到变量的类型就是initializer_list
image-20230501161947902
image-20230501161947902
  1. initializer_list没有提供对应的增删查改的接口,意味着initializer_list并不是专门来存储数据的容器,而是给其他容器支持列表初始化

就像刚开始那样的初始化那样

代码语言:c++
复制
vector<int> vt{ 1,2,3,4,5 };
list<int> lt{ 3,4,5,6,7 };

其其他容器支持列表初始化的原因是因为这些容器支持initializer_list类的构造

image-20230501163724292
image-20230501163724292

比如在vector这里,若vector没有实现initializer_list的构造,就不能实现列表初始化,并且识别为类型转化(是否是initializer_list->xxx的转换?)

image-20230501164923867
image-20230501164923867

实现了initializer_list构造函数就能用了

image-20230501165835986
image-20230501165835986
  • 使用迭代器方式遍历时,需要在迭代器类型前面加上typename关键字,指明这是一个类型名字。因为这个迭代器类型由一个类模板来定义,在该类模板未被实例化之前编译器是无法识别这个类型

最好也增加一个用initializer_list为参数的赋值运算符重载函数,来支持对列表对象进行赋值。但实际上不需要。如果没有实现,那么编译器会走initializer_list构造函数

image-20230501181921654
image-20230501181921654

vector支持initializer_list初始化和赋值的简易代码如下

代码语言:c++
复制
	template<typename T>
	class vector
	{
	public:
		typedef T* iterator;
		
		vector()//构造函数
			//初始化列表-必要用的情况:const成员;没有默认构造的自定义类型成员
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr){}
			
				vector(const  initializer_list<T>& il)//initializer_list构造函数
			:_start(nullptr)
			, _finish(nullptr)
			, _endofstorage(nullptr)
		{
			reserve(il.size());
		/*	typename initializer_list<T>::iterator ilt = il.begin();
			while (ilt != il.end())
			{
				push_back(*ilt);
				ilt++;
			}*/
			for (auto& e : il)
			{
				push_back(e);
			}
		}
		
        		vector<T>& operator=(const initializer_list<T>& il)
		{
			vector<T> tmp(il);
			std::swap(_start, tmp._start);
			std::swap(_finish, tmp._finish);
			std::swap(_endofstorage, tmp._endofstorage);
			return *this;
		}
        
		size_t size()const
		{
			return _finish - _start;
		}
		size_t capacity() const//容量
		{
			return _endofstorage - _start;
		}

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}
		
			void reserve(size_t n)//扩容
		{
			
			if (n > capacity())
			{
				size_t oldsize = size();
				T* tmp = new T[n];//构造一个tmp
				//memcpy-浅拷贝
				//if (_start )//需要拷贝---有数据才要拷贝
				//{
				//	//memcpy(tmp, _start, sizeof(T) * oldsize);浅拷贝·1
				//	tmp = _start;
				//	delete[] _start;
				//}
				if (_start)
				{
					for (size_t i = 0; i < oldsize; i++)
					{
						tmp[i] = _start[i];
						
					}
					delete[] _start;
				}
				

				_start = tmp;
				//operator=-深拷贝
				_finish = _start + oldsize;
				_endofstorage = _start + n;
			}

		}
		
		
		void push_back(const T& val)//尾插
		{
			if (_finish == _endofstorage)//容量为0或者满了都要扩容
			{
				size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
				reserve(newcapacity);
			}
			*_finish = val;
			++_finish;
		}
				void swap(vector<T>& v)//交换
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);

		}
		
			private:
		iterator _start;//0的位置
		iterator _finish;//最后一个成员变量的下一个位置
		iterator _endofstorage;//

	};

	void testinlist1()
	{
		vector<int> vt1{ 1,2,3,4,5 };
		for (auto e : vt1)
		{
			cout << e << " ";
		}
		cout << endl;
		vt1 = { 10,20,30,40,50 };
		for (auto e : vt1)
		{
			cout << e << " ";
		}
		cout << endl;
	}

简化声明的方式

auto

C++11之前(C++98)auto的用法为声明变量为自动变量,自动变量Automatic Variable)指的是局部作用域变量,具体来说即是在控制流进入变量作用域时系统自动为其分配存储空间并在离开作用域时释放空间的一类变量,以上这个过程称为自动生命周期。

但变量默认拥有自动生命周期,所以这个用法是多余的。

代码语言:c++
复制
   //C++98auto的用法
	int a = 10;//自动生命周期
	auto int b = 10;//自动生命周期

所以在C++11的时候摒弃了之前的用法,取而代之的是auto用来推演变量类型:通过返回值的类型自动推演变量的类型

代码语言:c++
复制
	auto dou = 1.34;//返回值类型为double,auto推导变量dou的类型也为double
	auto str = "jinitaimei";//返回值类型为string,auto推导变量str的类型也为string
	list<int> lt;
	lt.push_back(11);
	lt.push_back(12);
	lt.push_back(13);
	lt.push_back(14);
	list<int>::iterator it1 = lt.begin();//这里迭代器的类型是list<int>::iterator
	cout << *it1 << endl;
	auto it2 = lt.begin();//这里=的右边的返回值类型是list<int>::iterator,所以auto在等号在=左边自动推导变量it2的类型为list<int>::iterator
	cout << *it2 << endl;
image-20230430213300213
image-20230430213300213
typeid().name():获取类型名

typeid(变量).name()获取变量的类型名;对于非引用类型,是在编译期间识别;对于引用类型,是在运行时识别

代码语言:c++
复制
	int a = 10;
	cout << typeid(a).name() << endl;   //变量a的类型:int
	cout << typeid(typeid(a).name()).name() << endl;   //变量名int的类型:const char *
	const int b = 7;
	cout << typeid(b).name() << endl;   //加const不改变类型:int
	int& c = a;
	cout << typeid(c).name() << endl;   //加引用&不改变类型:int
	const int& f = a;  
	cout << typeid(f).name() << endl;   //int
	int* d;
	cout << typeid(d).name() << endl;   //int *
	const int* e;  
	cout << typeid(e).name() << endl;   //const int *
image-20230430225035374
image-20230430225035374

然而auto在有些场景就不能推导类型了,比如没有返回值接收的话,就不能自动推导变量的类型了;而typeid().name()只能用来打印类型名,这时就需要另外一个关键字来处理这些场景

decltype

关键字decltype将变量的类型声明为表达式指定的类型

用法:decltype(表达式)变量:把变量的类型推导为括号里表达式的类型

代码语言:c++
复制
template<class T1,class T2>
void multis(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;//这里ret没有接收返回值,所以不能用auto来推导ret的类型;但可以通过decltype来推导
	cout <<"ret的类型为:" << typeid(ret).name() << endl;
}

int main()
{
	int x = 6;
	double y = 2.66;
	multis(x, y);
    return 0;
}
image-20230430230325941
image-20230430230325941

nullptr

由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。

代码语言:c++
复制
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

范围for循环

模型:

底层是迭代器for(元素类型 元素对象:容器对象)

{

  循环体

}

  1. 如果循环体由单条语句或者单个结构块组成,可以省略花括号
  2. 用元素对象依次结合容器对象中的每一个元素,每结合一个元素,执行依次循环体,直至容器内的所有元素都被结合完为止.
  3. 不依赖于下标元素,(通用于stl库的容器)
  4. 不需要访问迭代器,透明
  5. 不需要定义处理函数,简洁
image-20230501111819527
image-20230501111819527

stl库中的一些变化

根据C++官网可以查到容器在C++11上的改动

image-20230501000122546
image-20230501000122546
array

array即数组,跟C++11之前的数组(c语言)最重要的区别:C++11之前的数组对于越界访问是抽查,而C++11的array对于越界访问更严格。但相比于vector的各自功能,array还是稍稍逊色

image-20230501113115187
image-20230501113115187
forward_list

forward_list是单链表,带哨兵位的单向的单链表,每个节点只有一个指针next指向后一个节点,因此只能单向遍历|官网:point_right: forward_list

功能有如下图

image-20230501200031088
image-20230501200031088
  1. 在这里可以看到只有头插头删,没有尾插尾删,一是尾插尾删要找尾,需要从头开始遍历效率低;二是就算实现了一个尾指针能找到尾,满足了尾插,但是尾删还要找尾的前一个结点,尾指针实属是爱莫能助阿。
  2. 插入删除的结点对象都是在当前结点的下一个结点,因为找后一个结点好找,而找前一个结点不好找。

大概实现可以看我这篇 C语言实现单向链表

unordered_map和unordered_set以及unordered_multimap和unordered_multiset在之前的博客有介绍过::point_right: unordered_map、unordered_set 。也可以去C++官方库看:point_right: unordered_set , unordered_map

final和override

final修饰的类不能被继承;final修饰的虚函数不能被重写;override用来判断虚函数是否完成了重写,在之前的篇幅中有提到过,这里就不细嗦

右值引用和移动语义

左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们

之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

左值是什么呢?左值有以下特性:

  1. 左值是一个表示数据的表达式(如变量名或者解引用的指针)
  2. 可以<font size=4 color="red">获取左值的地址,并且可以对它赋值</font>;(最重要的性质)
  3. <font size=4 color="red">左值可以出现在赋值符号(=)的左边,也可以出现在赋值符号的右边</font>
  4. 定义时const修饰符后的左值(const value),不能给他赋值,但是可以取它的地址
  5. 左值引用就是给左值取别名
代码语言:c++
复制
    int* p = new int(1);
	int*& rp = p;//对p的引用
	int& p_value = *p;//对p解引用后的数据的引用
	int x = 10;
	int& rx = x;//对x的引用
	const int y = 20;
	const int& ry = y;//对y的引用

右值是什么?右值有以下特性

  1. 右值是一个表示数据的表达式(这点跟左值没差阿),比如:字面常量,表达式返回值,函数返回值、临时变量、匿名对象等等
  2. 右值不能出现在赋值符号的左边,但可以出现在赋值符号的右边
  3. <font size=4 color="red">右值不能被取地址</font>
  4. 右值引用就是给右值取别名
  5. <font size=4 color="blue">右值引用的用法:将左值引用的&换成&&</font>
  6. <font size=4 color="green">右值分为纯右值(内置类型表达式的值)和将亡值(自定义类型表达式的值)</font>
代码语言:c++
复制
	int x = 10;
	int y = 90;
	//以下是常见的右值
	10//字面常量
	x + y;//表达式返回值
	min(x, y);//函数返回值
int &&pxy=x+y;//对x+y右值引用

那么左值引用和右值引用有什么关联呢?

  1. 左值引用能引用左值,不能引用右值
  2. const左值引用能引用右值;(通俗理解:一些情况下右值不能被修改,const左值引用不能修改参数,性质符合这一点;其次是右值被const左值引用,权限被平移或者缩小了)
  3. <font size=4 color="red">右值引用能引用右值,不能引用左值</font>
  4. <font size=4 color="red">右值引用能引用move之后的左值</font>(move将一个左值强制转化为右值)
代码语言:c++
复制
	int mini(int x, int y)
{
	return x < y ? x : y;
}

	int x = 10;
	int y = 90;
	//以下是右值
10;
x+y;
mini(x,y);
//////////////左值引用右值
//int&lret=	x + y;//不能引用
const int&lret=	x + y;//可以引用
const int& lmin = mini(x, y);//可以引用
//右值引用左值
//int&& rx1 = x;//不能引用
int&& rx2 = move(x);//move之后的左值可以右值引用

那好端端的有了引用(左值引用),为什么还要在C++11提出右值引用呢?

无论左值引用还是右值引用,功能之一都是减少拷贝,减少消耗

左值引用在以下场景不适用

代码语言:c++
复制
template<class T>
const  T& fun3(const T& object)
{
	T ret;
	ret.resize(object.size());
	for (size_t i = 0; i < ret.size(); i++)
	{
		ret[i] = i;
	}
	//...... 这里对ret进行操作
	return ret;//函数结束,参数ret的生命周期结束销毁了,一是参数ret传不出去,二是传引用返回给外面那层,此时再访问到参数ret算是越界访问了!
}

int main()
{
	vector<int>v(10,0);
 vector<int> ret3=fun3(v);
 	}
  1. 如上面fun3函数,参数ret出了函数作用域,函数内的参数也随之销毁了一是此时该参数传不出去,二是传引用返回给外面那层,此时再访问到参数ret算是越界访问了!虽然这样写语法上没有报错,但是最好不要这样用!
image-20230502104110708
image-20230502104110708
image-20230502104121465
image-20230502104121465

解决参数出了作用域传不出去这样的问题的办法有很多种,这里我罗列几种

  1. 配备输出型参数

通过输出型参数ret3,在函数fun3内参数ret赋值给retu,成功把参数ret传出来

代码语言:c++
复制
template<class T>
void fun3(const T& object,T&retu)
{
	T ret;
	ret.resize(object.size());
	for (size_t i = 0; i < ret.size(); i++)
	{
		ret[i] = i;
	}
	//...... 这里对ret进行操作
	retu = ret;
}

int main()
{
	vector<int>v(10,0);
  vector<int> ret3;
 	fun3(v,ret3);
 	}
image-20230502104451131
image-20230502104451131

这种做法是C++11之前的普遍做法

  1. 传值返回

这种做法至少进行一次拷贝构造(老旧的编译器会进行两次拷贝构造),针对内置类型消耗还算小,但针对自定义类型或者容器类参数比如vector<vector < vector > >>这种,消耗非常大,不推荐这样用。

代码语言:c++
复制
template<class T>
 T fun2( T& object)
{
	T ret;
	ret.resize(object.size());
	for (size_t i = 0; i < ret.size(); i++)
	{
		ret[i] = i;
	}
	//...... 这里对ret进行操作
	return ret;//传值返回
}
int main()
{
 vector<int>v(10,0);
vector<int> ret2 = fun2(v);
    
}
  1. 可以定义到类里的函数,那么可以将参数定义成一个类成员,但是这里还得牵扯到构造函数、析构函数、拷贝构造函数等等。

针对上面的提到的参数出了函数作用域被销毁了,参数传不出去的问题,右值引用可以解决。

下面介绍右值引用的几大作用。

这里用到一个string类来介绍左值引用和值返回的不足之处。

代码语言:c++
复制
namespace pjl
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str="")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

	pjl::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}
}
int main()
{
    	pjl::string s1{ "xxxxx" };//左值
	pjl::string ret1(s1);//s1为左值,用到拷贝构造
	pjl::string s2{ "aaaaa" };
	s2 = s1;//s1为左值,用到赋值重载
	pjl::string s3(move(s1));//move之后的左值为右值
    s3 = "bbbbb";//右值
    return 0;
}

一般情况下,先是返回值拷贝构造一个临时对象,然后临时对象拷贝构造参数对象,这里一共拷贝构造了两次。这是老旧的编译器所做的

image-20230504101733118
image-20230504101733118

现在的编译器优化了效率,会跳过返回值拷贝临时对象这一步,直接用返回值拷贝构造参数对象,即一次拷贝构造

image-20230504101927223
image-20230504101927223

老旧的编译器会先用返回值拷贝构造临时对象,然后临时对象再赋值给参数对象

image-20230504102431321
image-20230504102431321

但现在的编译器优化了效率,跳过返回值拷贝构造临时对象这步,直接用返回值赋值给参数对象,这里的赋值重载也是一次拷贝构造(这里显示两次拷贝构造的原因是因为赋值重载函数用到了拷贝构造)

image-20230504102747826
image-20230504102747826

但是这里的to_string用的值返回,意味着返回值都需要用到拷贝构造,具有一定的损耗。当函数返回对象是一个局部变量时,倘若用引用返回,在函数销毁时返回对象也随之销毁,参数传不出去。右值引用能解决以上问题。

移动构造和移动赋值

在上面的string类实现移动构造和移动赋值,能减少拷贝构造次数,减少损耗

如字面所述,移动+构造,若传的参数是右值,会将传入的右值的资源移动过来构造自己,避免了深拷贝,即移动的时候被移动的右值对象的资源会被转移。

代码语言:c++
复制
	// 移动构造
string(string&& s)
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);
		}

说明一下,这里移动构造函数是通过把右值对象的内容和string类成员内容进行交换,相比于string类的拷贝构造函数少了一次拷贝构造,降低了损耗。

老旧的编译器会先用返回值对象拷贝构造临时对象,然后用临时对象移动构造参数对象

image-20230504110310247
image-20230504110310247

现在的编译器优化了,跳过了返回值对象拷贝构造临时对象这步,直接用返回值对象移动构造参数对象

image-20230504104552552
image-20230504104552552

移动赋值也如字面意思,移动+赋值,若传的是右值对象,会将右值的资源移动过来赋值给自己,避免了深拷贝,且被移动的右值对象的资源会被转移

代码语言:c++
复制
	//// 移动赋值		
string& operator=(string&& s)
		{
			cout << "string& operator=(string&& s) -- 移动赋值" << endl;
			swap(s);
			return *this;
		}

说明一下:这里移动赋值函数是将右值对象的内容和string类成员的内容进行交换,相比于拷贝赋值函数少了一次拷贝构造,降低了损耗。

老旧的编译器会先用返回值对象拷贝构造临时对象,然后用临时对象移动赋值给参数对象

image-20230504111149881
image-20230504111149881

现在的编译器优化了,跳过了返回值对象拷贝构造临时对象这步,直接用返回值对象移动赋值给参数对象

image-20230504111323404
image-20230504111323404

现在动态的来看移动构造

image-20230504111816354
image-20230504111816354

移动赋值

image-20230504112345782
image-20230504112345782

综上,移动构造和移动赋值的作用是通过移动右值的资源,减少了拷贝构造次数,减少了损耗

万能引用和完美转发

万能引用

首先需要模板,然后在参数列表中是模板参数 &&

image-20230506091746584
image-20230506091746584

模板中的&&不代表右值引用,而是万能引用也称折叠引用,当传过来是左值时,&&折叠成&,当传过来是右值时,则是&&。其既能接收左值又能接收右值

万能引用不仅能接收左值和右值,const左值和const右值也能接收。

下面代码通过万能引用接收左值,右值,const左值,const右值,实例化出不同类型的函数

代码语言:c++
复制
void Func(int& x)
{
	cout << "左值引用" << endl;
}
void Func(const int& x)
{
	cout << "const 左值引用" << endl;
}
void Func(int&& x)
{
	cout << "右值引用" << endl;
}
void Func(const int&& x)
{
	cout << "const 右值引用" << endl;
}
template<class T>
void PerfectForward(T&& t)
{
	Func(t);
}
int main()
{
	int a = 10;
	PerfectForward(a);       //左值
	PerfectForward(move(a)); //右值

	const int b = 20;
	PerfectForward(b);       //const 左值
	PerfectForward(move(b)); //const 右值

	return 0;
}

根据传入参数的类型,推导出函数参数的类型然后实例化函数;例如传入参数a(左值),那么PerfectForward的参数t类型是int&

image-20230506101404778
image-20230506101404778

由于PerfectForward函数用的是万能引用,意味着通过PerfectForward函数按照传入参数的类型匹配调用相同类型的Func函数。但这里的参数里,左值和右值类型调用Func函数左值引用版本,const左值和const右值类型调用Func函数const左值引用版本

原因是右值被引用后会导致右值被放到特定的存储位置,因此该右值可以被取地址,也可以被修改,所以在函数PerfectForward后续的使用中右值会被识别成左值。

经过一次参数传递后,参数的右值属性退化成了左值属性,因此在后续的使用中都退化成了左值。

image-20230506101548071
image-20230506101548071

为了防止右值被引用后退化成左值,这时候需要用到完美转发。

完美转发:在传参的过程中保留对象原生类型属性

为了保证参数被引用后继续保持参数类型属性,需要在传参时用到完美转发

用法:std::forward<模板参数>(参数)

image-20230506103510277
image-20230506103510277
image-20230506103619201
image-20230506103619201

现在回过头来解决参数ret出了函数作用域,函数内的参数也随之销毁了,参数对象传不出去 的问题

代码语言:c++
复制
template<class T>
const  T& fun3( T&& object)
{
	T ret = object;
	for (size_t i = 0; i < ret.size(); i++)
	{
		ret[i] = i;
	}
	//...... 这里对ret进行操作
	return ret;//函数结束,对象没有销毁,通过转移拷贝把对象传了回去
}
int main()
{
	vector<int>v(10, 0);
	vector<int> ret3 = fun3(v);//传了左值过去
	int flag1 = 0;//标记位
image-20230506105814484
image-20230506105814484

类成员的移动拷贝函数和移动赋值运算符重载函数

原来的C++类中,有6个默认成员函数

依次是:构造函数,析构函数,拷贝构造函数,拷贝赋值函数,取地址重载和const取地址重载

前四个较为重要,默认成员函数是我们没有实现该函数而编译器默认生成。

现C++11新增了两个默认成员函数:移动拷贝函数和移动赋值运算符重载函数。

针对这两个函数需要注意:

  1. 对于移动拷贝函数:如果自己没有实现移动拷贝函数,且<font size=4 color="red">拷贝构造函数,析构函数,拷贝赋值运算符重载函数这三者都没有实现</font>,编译器才会默认生成一个移动拷贝函数。默认生成的移动拷贝函数对于内置类型成员,会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,若该成员实现了移动拷贝函数,就调用移动拷贝函数,若<font size=4 color="green">没实现,就调用该函数的拷贝构造函数</font>。
  2. 对于移动赋值运算符重载函数:如果自己没有实现移动赋值函数,且<font size=4 color="red">拷贝构造函数,析构函数,拷贝赋值运算符重载函数这三者都没有实现</font>,编译器才会默认生成一个移动赋值函数。默认生成的移动赋值函数对于内置类型成员,会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,若该成员实现了移动运算符重载函数,就调用移动运算符重载函数,若<font size=4 color="green">没实现,就调用该函数的拷贝赋值函数</font>。
  3. 如果自己实现了移动拷贝函数和移动运算符重载函数,编译器则不会默认生成

这里通过一个简易的string函数和一个Perosn函数调用string

代码语言:c++
复制
namespace pjl
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
		string(const char* str="")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		
		// 移动构造
		//string(string&& s)
		//	:_str(nullptr)
		//	, _size(0)
		//	, _capacity(0)
		//{
		//	cout << "string(string&& s) -- 移动构造" << endl;
		//	swap(s);
		//}
		//// 移动赋值
		//string& operator=(string&& s)
		//{
		//	cout << "string& operator=(string&& s) -- 移动赋值" << endl;
		//	swap(s);
		//	return *this;
		//}

		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) --赋值重载,这里也用到深拷贝" << endl;
			string tmp(s);//
			swap(tmp);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}
		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str=nullptr;
		size_t _size=0;
		size_t _capacity=0; // 不包含最后做标识的\0
	};
}

class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	/*Person(const Person& p)
	* :_name(p._name)
,_age(p._age)
{}*/
/*Person& operator=(const Person& p)
{
if(this != &p)
{
_name = p._name;
_age = p._age;
}
return *this;
}*/
/*~Person()
{}*/
private:
	pjl::string _name;
	int _age;
};

可以看到Person函数没有实现拷贝构造函数,析构函数和拷贝赋值函数,那么当右值参数传进来时,Person类则会默认生成移动拷贝构造函数和移动赋值函数

测试代码

代码语言:c++
复制
int main()
{
	Person s1="this is the test";//左值
	Person s2 = std::move(s1);//传右值--构造
	Person s3;
	s3 = std::move(s1);//赋值
	return 0;
}

当string类没有实现移动拷贝构造函数和移动赋值重载函数时,Person类生成的默认移动构造和移动赋值函数调用string类的拷贝构造函数和拷贝赋值函数

image-20230507003057650
image-20230507003057650

当string类实现了移动拷贝构造函数和移动赋值函数时,Person类生成的默认移动构造和移动赋值运算符重载函数调用string类的移动构造函数和移动赋值赋值函数

image-20230507003527448
image-20230507003527448

强制生成默认函数的关键字default

C++11可以更好的控制要使用的默认成员函数,假设要用都某个默认成员函数,但因为某些原因导致这个成员函数无法生成,这时可以用到关键字default。比如我们实现了拷贝构造函数,这时默认的移动构造函数就不会生成了,我们可以通过使用default关键字显示指定移动构造函数生成。

下面的代码生成了构造函数和拷贝构造,这时候想用到移动构造有两种方法,一是自己实现移动构造函数。二是生成默认的移动构造函数,但由于拷贝构造已经实现,所以编译器不会提供默认的移动构造函数,这时候可以使用default关键字显示指定移动构造函数生成。

代码语言:c++
复制
class Person
{
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age)
	{}
	Person(const Person& p)//拷贝构造
	:_name(p._name)
    ,_age(p._age)
{}
private:
	pjl::string _name;
	int _age;
};

自己实现移动构造函数:由于对象p通过右值引用传进来后退化成了左值属性,p的成员_name是左值属性,且是自己实现的string类,所以Person类给 _name初始化调用的是对象p. _name的拷贝构造,因此这里需要给p的成员 _name用完美转发保持右值属性,以保后续调用移动构造

image-20230507100526260
image-20230507100526260

使用关键字default强制生成默认移动构造函数

image-20230507101629488
image-20230507101629488

禁止生成默认函数的关键字delete

想要能够限制某些默认函数的生成,在C++98中,是将函数权限设置为private,且只声明不实现,这样在外部调用时就会报错。在C++11只需要在函数声明上加上=delete即可。该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数

代码语言:c++
复制
class A
{
public:
	A() {}
	A(const A& aa)//拷贝构造--用传过来的对象拷贝给自己,但是函数是浅析构,析构不到位报错
		:p(aa.p)
	{}
	~A()
	{
		delete []p;//析构函数只析构一次,若对象不止一个int大小调用则报错
	}
private:
	int* p = new int[10];
};

这里我写里一个类A,类中成员对象是是一个int类型的指针,指向10int大小的空间。析构函数是析构1个int大小的空间。

代码语言:c++
复制
//测试代码
int main()
{
	A aa;
	A bb(aa);
	return 0;
}

通过测试,用对象aa去拷贝构造对象bb,拷贝构造时对象aa会创建10int大小的空间,而析构时只析构一个int大小的空间,导致内存泄漏报错。但这样是运行时被检查出来才报错,我想要的时运行前编译时报错。

image-20230507104705531
image-20230507104705531

其一:若我们不想给外部调用拷贝构造函数可以用在C++98的方法:将函数权限设置为private,且只声明实现,这样就能做到编译时报错。

image-20230507104932618
image-20230507104932618

其二:只写拷贝构造函数的声明且后接=delete表示该函数为删除函数即函数没有生成不能调用

image-20230507105244195
image-20230507105244195

lambda表达式

lambda表达式书写格式:capture-list mutable -> return-type { statement}

capture-list :捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。必须写

(parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。有就写

mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。

->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。

{statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量必须写

image-20230507143246710
image-20230507143246710

这里写一个样例

image-20230507144406949
image-20230507144406949

或者是这样(由于捕捉列表没有使用,所以函数体内的参数都是由参数列表定义的,定义了几个参数,在后续调用lambda表达式中就需要传多少个参数)

image-20230508102707374
image-20230508102707374

我用lambda表达式实现了一个交换函数,意为通过lambda表达式把两个变量的值交换(函数体内的参数都是由捕捉列表提供的,所以参数列表不需要定义参数,后续调用lambda表达式也不需要传参)

image-20230508103512093
image-20230508103512093

通过捕捉列表把参数c、d捕捉,然后在函数体内进行交换,但是报错了,原因是此时的捕捉列表捕捉的是父作用域变量值的拷贝,具有常性无法改变且lambda函数总是一个const函数,可以在参数列表后加mutable表示取消参数的常性

添加mutable后运行,通过打印查看参数c、d在lambda表达式内是交换了,但是出了lambda表达式就又换了回来,原因是c、d变量给lambda表达式传的参数是值,改变值不会改变变量,现在改传引用

image-20230508104335373
image-20230508104335373
image-20230508104350084
image-20230508104350084

可以看到变量c、d进行了交换

image-20230508104631318
image-20230508104631318

可以看到变量c、d在lambda表达式下方的话会捕捉不到,原因为编译器具有往上找原则

image-20230508105315929
image-20230508105315929

lambda捕捉父作用域的变量的原则:

  1. =:表示值传递方式捕获所有父作用域中的变量(包括this)
  2. var:表示值传递方式捕捉变量var
  3. &var:表示引用传递捕捉变量var
  4. &:表示引用传递捕捉所有父作用域中的变量(包括this)
  5. this:表示值传递方式捕捉当前的this指针

混合捕捉:

  1. :=, &a, &b:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
  2. &,a, this:值传递方式捕捉变量a和this,引用方式捕捉其他变量

但是不能重复捕捉::=, a:=已经以值传递方式捕捉了所有变量,捕捉a重复

另外还有这些规则:

  1. 在块作用域以外的lambda函数捕捉列表必须为空
  2. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错
  3. lambda表达式之间不能相互赋值,即使看起来类型相同
lambda表达式的底层

这里写了一个内含仿函数成员的类,main函数里实现了两个与Rate类中仿函数成员相同作用的lambda表达式,分别是r2和r3。然后main函数依次调用

代码语言:c++
复制
class Rate
{
public:
	Rate(double rate) : _rate(rate)
	{}

	double operator()(double money, int year)
	{
		return money * _rate * year;
	}

private:
	double _rate;
};

int main()
{
	// 函数对象
	double rate = 0.49;
	Rate r1(rate);//传利率过去
	r1(10000, 2);//调用仿函数

	// lambda
	auto r2 = [=](double monty, int year)->double {return monty * rate * year; };
	r2(10000, 2);

	auto r3 = [=](double monty, int year)->double {return monty * rate * year; };
	r3(10000, 2);
	return 0;
}

这里转到反汇编,可以看到在使用方式上,函数对象和lamba表达式完全一样,都是call相应的函数。实际上底层编译器对于lambda表达式的处理方式完全是按照函数对象(仿函数)处理,定义了一个lambda表达式,编译器会自动生成一个类,该类中重载了operator()

image-20230508171854968
image-20230508171854968

可变参数模板

在C++98/03,类模板和参数模板只能含固定数量的模板参数,可变参数模板可以含0-N个模板参数

代码语言:c++
复制
// Args是一个模板参数包,args是一个函数形参参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}

上面参数args前面有。。。表示这是个可变模板参数,我们无法直接获得参数包args中的每个参数,只能通过展开参数包的方式来获取。

代码语言:c++
复制
template<class T, class ...Args>
void showlist(const T& t)
{
	cout << t << endl;//递归终止函数
}

template<class T,class ...Args>
void showlist(T value, Args ...args)
{
	cout << value << " ";
	showlist(args...);
}

int main()
{
	showlist(1);
	showlist(1,1.1);
	showlist(1, 1.1, string("xxx"));
	return 0;
}
递归函数方式展开参数包

展开传过来的参数包,从前往后依次遍历参数,遍历完一个(打印)然后把参数包剩余参数递归传递给showlist函数,当传递到最后一个参数的时候,此时调用的函数匹配递归终止函数。

image-20230508202758363
image-20230508202758363

另外,sizeof...(参数包)能计算出参数包里有多少个参数

image-20230508203714739
image-20230508203714739
逗号表达式展开参数包
代码语言:c++
复制
template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'T');
	ShowList(1, 'T', std::string("fortunate"));
	return 0;
}

把参数包传给showlist函数,showlist函数通过逗号表达式展开其参数包。expand函数中的逗号表达式:(printarg(args), 0),先执行printarg(args),printarg函数是用来处理参数包中的参数(打印参数),然后再得到逗号表达式后面的0。同时还通过C++11中的另一个特性—初始化列表,通过初始化列表来初始化一个变长数组{(printarg(args), 0)...}将会展开成((printarg(arg1),0),(printarg(arg2),0), (printarg(arg3),0), etc... ),这样就能依次遍历参数包里的参数,printarg函数依次处理参数,然后数组依次获得0。最终数组里的元素都为0,元素个数为参数包中的参数个数。这个数组纯粹是通过构造数组的同时展开参数包

image-20230508204931978
image-20230508204931978

在C++11中容器里的函数也有拓展到能使用到可变模板参数,这类函数称为empalce系列函数

STL容器中的empalce相关接口函数

list和vector里面都提供有emplace系列函数,支持模板的可变参数,并且支持万能引用,那么相对insert和emplace系列接口的优势在哪?

image-20230508212217556
image-20230508212217556

这里实现了一个string类,里面实现了拷贝构造(深拷贝)、移动构造。通过list的emplace_ back和push_back来比较区别

代码语言:c++
复制
namespace pjl
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			cout << "string(char* str) -- 构造" << endl;

			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 拷贝构造
		string(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;

			/*string tmp(s._str);
			swap(tmp);*/
			reserve(s._capacity);
			strcpy(_str, s._str);
			_size = s._size;
			_capacity = s._capacity;
		}

		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);

			return *this;
		}

		// 移动构造
		string(string&& s)
		{
			cout << "string(const string& s) -- 移动拷贝" << endl;

			swap(s);
		}

		// 移动赋值
		string& operator=(string&& s)
		{
			cout << "string& operator=(string s) -- 移动赋值" << endl;
			swap(s);

			return *this;
		}


		~string()
		{
			delete[] _str;
			_str = nullptr;
		}

		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				if (_str)
				{
					strcpy(tmp, _str);
					delete[] _str;
				}
				_str = tmp;

				_capacity = n;
			}
		}

		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}

			_str[_size] = ch;
			++_size;
			_str[_size] = '\0';
		}

		//string operator+=(char ch)
		string& operator+=(char ch)
		{
			push_back(ch);
			return *this;
		}

		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}

		pjl::string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;

			str += ('0' + x);
		}

		if (flag == false)
		{
			str += '-';
		}

		std::reverse(str.begin(), str.end());
		return str;
	}
}

运行后可以看到,对于左值,emplace_back和push_back都是深拷贝;但对于右值,emplace是直接拿着参数对象去构造list的结点,而push_back先是构造一个临时对象,然后临时对象移动构造list的结点。构造+移动构造优化成构造,稍稍减少了一些消耗。但对于只有浅拷贝的类,构造+浅拷贝优化成构造,效率大大提升。

image-20230508220422972
image-20230508220422972
image-20230508220559934
image-20230508220559934

包装器

这里介绍的是function包装器。

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。是对调用对象的包装,用同一种方法调用不同的对象。(统一了不同对象调用的用法)

function类模板原型

代码语言:c++
复制
//std::function在头文件<functional>
// 类模板原型如下
template <class T> function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;

模板参数说明:Ret: 被调用函数的返回类型Args…:被调用函数的形参

下面是使用

代码语言:c++
复制
#include <functional>

int f(int a, int b)//函数
{
	return a + b;
}
struct Functor
{
public:
	int operator() (int a, int b)//仿函数
	{
		return a + b;
	}
};
class Plus
{
public:
	static int plusi(int a, int b)//类中静态成员
	{
		return a + b;
	}
	double plusd(double a, double b)
	{
		return a + b;
	}

};
int main()
{
	// 函数名(函数指针)
	std::function<int(int, int)> func1 = f;
	cout << func1(1, 2) << endl;
	// 函数对象
	std::function<int(int, int)> func2 = Functor();
	cout << func2(1, 2) << endl;
	// lamber表达式
	std::function<int(int, int)> func3 = [](const int a, const int b)	{return a + b; };
	cout << func3(1, 2) << endl;
	// 类的成员函数
	std::function<int(int, int)> func4 = &Plus::plusi;
	cout << func4(1, 2) << endl;
	std::function<double(Plus, double, double)> func5 = &Plus::plusd;
	cout << func5(Plus(), 1.1, 2.2) << endl;
	return 0;
}
  1. 在使用包装器时需注意这样定义对象:function<返回值类型(参数类型1,参数类型2,......)>function对象=被调用的对象,然后给function对象传递参数就能使用。
  2. 当被调用对象是类成员时,需要注意,若赋值的对象是类中静态成员,需要在赋值符号后面指定 &类的名称::类中静态成员名称,&可以不写;若赋值的对象不是类中静态成员,&类的名称::类中静态成员的&必须写!;模板处需要传递类的名称(传递this指针);调用处需要传递类的匿名对象

下面是对function包装器的使用。可以看到这里有一个useF模板函数,参数是两个模板参数。然后是对类中静态成员count进行++和取地址,最后返回第一个模板参数f的调用,传的参数是第二个模板参数x。

代码语言:c++
复制
#include <functional>
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;
}
};

然后进行调用。第一个是将f函数作为对象传给useF函数,第二个是将Functor()类的匿名对象作为对象传给useF函数,第三个传递的是lambda表达式。

代码语言:c++
复制
int main()
{
// 函数名
cout << useF(f, 11.11) << endl;
// 函数对象
cout << useF(Functor(), 11.11) << endl;
// lamber表达式
cout << useF([](double d)->double { return d / 4; }, 11.11) << endl;
return 0;
}

运行后看到useF函数的静态成员count的地址各不相同且count都只进行了一次++,意味着useF函数实例化出不同的函数体。传给useF的参数是f,Functor(),和lambda表达式,这些分别都是不同的类型,所以useF会被实例化出不同的对象。因此useF可能是函数名、函数指针、函数对象(仿函数对象)、也有可能是lamber表达式对象。

image-20230509162323692
image-20230509162323692

然而这三者的返回值类型相同(都是double),传递给useF函数的参数个数相同,形参类型相同,那么这里可以用包装器对这三个对象进行包装,然后通过function对象对这三者进行传参调用,这样就只会实例化出来一份useF函数。

原因是因为包装后,这三个可调用对象都是相同的function类型,因此最终只会实例化出一份useF函数,该函数的第一个模板参数的类型就是function类型的。

代码语言:c++
复制
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;
// lamber表达式
std::function<double(double)> func3 = [](double d)->double { return d /
4; };
cout << useF(func3, 11.11) << endl;
return 0;
}
image-20230509163608920
image-20230509163608920

现在可以看到静态成员变量count的地址是相同的,且count++了三次说明只实例化了一份useF函数。

function包装器使用实例

逆波兰表达式求值

代码语言:c++
复制
class Solution {
public:
    int evalRPN(vector<string>& tokens) {
stack<int> st;
map<string,function<int(int,int)>>optomap={
 {"+",[](int x,int y){return x+y;}},//包装的是lambda表达式
 {"-",[](int x,int y){return x-y;}},
 {"*",[](int x,int y){return x*y;}},
 {"/",[](int x,int y){return x/y;}}
};

for(auto& e:tokens)
{
    if(optomap.count(e)==0)//没找到字符--找到的是数字入-栈
    {
        st.push(stoi(e));
    }else
    {
        int right=st.top();
        st.pop();
        int left=st.top();
        st.pop();
        st.push(optomap[e](left,right));
    }
}
return st.top();
    }
};

这里通过map建立了运算符和function类型的映射,而function类型实现的是运算符相应的操作,这里用的是lambda表达式,而函数指针,仿函数在这里也都能行。

function包装器的意义:

  1. function包装器可以对函数指针,函数对象(仿函数),lambdad等各种可调用对象进行包装,统一类型。(部分场景提高效率)
  2. 包装后明确了可调用对象的返回值和参数类型,更加方便使用。

bind

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

代码语言:c++
复制
// 原型如下:
template <class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
// with return type (2)
template <class Ret, class Fn, class... Args>
/* unspecified */ bind (Fn&& fn, Args&&... args);
  1. 可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

可以这样理解:原先的可调用对象fun1通过bind函数适配器后,生成了一个新的可调用对象fun2

image-20230509211932557
image-20230509211932557
  1. 调用bind的一般形式:auto newCallable = bind(callable,arg_list);callable是原先的可调用对象(函数),arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。newcallable是新生成的可调用对象。当我们调用newcallable时,newcallable会调用callable,并传给他arg_list的参数。

bind函数适配器的作用

  1. 绑定callable的参数
代码语言:c++
复制
//Plus、subFunc、Sub都是可调用对象
int Plus(int a, int b)
{
	return a + b;
}

int SubFunc(int a, int b)
{
	return a - b;
}

class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b * x;
	}
private:
	int x = 20;
};
int main()
{
    	//表示绑定函数plus 参数分别由调用 func1 的第一,二个参数指定
	function<int(int, int)>fun1 = bind(Plus, placeholders::_1, placeholders::_2);
	cout << fun1(2, 3) << endl;
return 0;
}

main函数里的 placeholders::1, placeholders是作用域,这种形如 _1 2等的参数是占位符,表示newcallable的参数,他们占据了传递给newcallable的参数位置,其 _n的n就对应newcallable的第n个位置的参数,比如 _1为newcallable的第一个参数, _2为newcallable的第二个参数,以此类推。

image-20230509213241999
image-20230509213241999

在这里callable是Plus,fun1是newcallable,Plus的参数由调用的fun1的第一,第二个参数决定。fun1把参数2,3传给Plus的a,b

  1. 调整参数的顺序
代码语言:c++
复制
int SubFunc(int a, int b)
{
	return a - b;
}
int main()
{
    	// 调整参数的顺序
	function<int(int, int)>fun2 = bind(SubFunc, placeholders::_2, placeholders::_1);
	cout << fun2(7, 4) << endl;
return 0;
}

在这里callable是SubFunc,fun2是newcallable,fun2的第一个参数7传给 _1,第二个参数 4传给 _2。那么SubFunc的参数依次由调用的fun2的第二,第一个参数决定。fun2把参数4,7传给SubFunc的a,b。 占位符是几就接收第几个参数,占位符的位置决定传过来的参数的位置

image-20230509213624318
image-20230509213624318
  1. 绑定固定参数
代码语言:c++
复制
class Sub
{
public:
	int sub(int a, int b)
	{
		return a - b * x;
	}
private:
	int x = 20;
};

int main()
{
	// 绑定固定参数
	function<int(Sub, int, int)> fun3 = &Sub::sub;
cout<<	"原先的包装器调用:" << fun3(Sub(), 10, 13) << endl;

function<int(int, int)>fun4 = bind(&Sub::sub, Sub(), placeholders::_1, placeholders::_2);

cout << "现在的bind绑定固定参数调用:" << fun4(10, 13) << endl;
    return 0;
}

callable是Sub类中成员函数sub,所以在调用时需要传递Sub()匿名对象。但只要是通过包装器调用类中成员函数就需要传递Sub()匿名对象。现可以通过bind把Sub()匿名对象这个参数绑定在表达式中,后续参数不用传。

bind可以把参数绑定在表达式中,后续调用时不需要再次传参。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • C++11大杂烩
    • 介绍
      • 语法
        • 统一的列表初始化:{}初始化
        • initializer_list
        • 简化声明的方式
        • nullptr
        • 范围for循环
        • stl库中的一些变化
        • final和override
        • 右值引用和移动语义
        • 移动构造和移动赋值
        • 万能引用和完美转发
        • 类成员的移动拷贝函数和移动赋值运算符重载函数
        • 强制生成默认函数的关键字default
        • 禁止生成默认函数的关键字delete
        • lambda表达式
        • 可变参数模板
        • STL容器中的empalce相关接口函数
        • 包装器
        • bind
    相关产品与服务
    容器服务
    腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档