1.C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了
C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞
进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。
从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于
C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中
约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,
C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个
重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本节博客
主要讲解实际中比较实用的语法。

小故事:

1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际
标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫
C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也
完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的
时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。

2.初始化列表

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

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int array1[] = { 1, 2, 3, 4, 5 };
 int array2[5] = { 0 };
 Point p = { 1, 2 };
 return 0;
}

C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加

struct Point
{
 int _x;
 int _y;
};
int main()
{
 int x1 = 1;
 int x2{ 2 };
 int array1[]{ 1, 2, 3, 4, 5 };
 int array2[5]{ 0 };
 Point p{ 1, 2 };
 // C++11中列表初始化也可以适用于new表达式中
 int* pa = new int[4]{ 0 };
 return 0;
}

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

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(2022, 1, 1); // old style
 // C++11支持的列表初始化,这里会调用构造函数初始化
 Date d2{ 2022, 1, 2 };
 Date d3 = { 2022, 1, 3 };
 return 0;
}

3.initializer_list

C++11新增的轻量级模板类(头文件 <initializer_list> ),核心是封装花括号 {} 里的初始化数据,作为“数据桥梁”供容器、函数快速接收初始化列表。

代码示例:

int main()
{
	//initializer_list只可读,不可写,且不支持[],支持范围for
	vector<int> v1 = { 1,2,3,4 };//使用initializer_list来构造vector
	initializer_list<int> it = { 1,2,3,4 };//initializer_list主要使用于构造函数的参数,方便初始化容器对象,vector(initializer_list<int> ret)
	initializer_list<string> t = {"sss","ss","aaa"};
	auto rit = { 1,2,3,4 };
	for (auto e : rit)
	{
		cout << e << endl;
	}
	cout << typeid(rit).name() << endl;//打印类型
	return 0;
}

initializer_list本质:

本质是通过两个指针,具有迭代器。

使用场景:

std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加 std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator= 的参数,这样就可以用大括号赋值。 

3.声明

c++11提供了多种简化声明的方式,尤其是在使用模板时。

3.1 auto

这个我们再之前已经使用过非常多了,就是通过=右边的类型来推导左边的类型。

看看代码:

int main()
{
int i = 10;
auto p = &i;
auto pf = strcpy;
cout << typeid(p).name() << endl;
cout << typeid(pf).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//map<string, string>::iterator it = dict.begin();
auto it = dict.begin();
return 0;
}

3.2 decltype

auto要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型,auto必须要靠变量才可以初始化,decltype可以直接使用表达式,将变量的类型声明做为表达式指定的类型。无需初始化。

看看代码:

template<class T1,class T2>
void F(T1 f1, T2 f2)
{
	decltype(f1 * f2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret;
	decltype(&x) p;
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');
	return 0;
}

4. nullptr

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

5.新容器

图里面圈出来的就是C++ 11新添加的4个容器,unordered_map和unordered_set之前我们统计详细说过了。

5.1 array

array数组,和c原因呢的原生数组类似,但是对于边界的检查更加严格,但是实用性没有vector高,所以我们通过代码了解简单使用就行。

#include <iostream>
#include <array>  // 必须包含头文件
using namespace std;

int main() {
	// 1. 初始化:固定大小(编译时确定),两种常用方式
	array<int, 5> arr1 = { 1, 2, 3, 4, 5 };  // 传统初始化
	array<int, 5> arr2{ 6, 7, 8, 9, 10 };    // 统一初始化(C++11+)

	// 2. 元素访问:下标[](无越界检查)、at()(有越界抛异常)
	cout << "arr1[2] = " << arr1[2] << endl;    // 输出3
	cout << "arr2.at(3) = " << arr2.at(3) << endl;  // 输出9

	// 3. 遍历:3种简单方式
	cout << "\n遍历arr1:" << endl;
	// 方式1:普通for循环
	for (size_t i = 0; i < arr1.size(); ++i) {
		cout << arr1[i] << " ";
	}
	cout << endl;

	// 方式2:范围for(最简洁)
	for (int val : arr2) {
		cout << val << " ";
	}
	cout << endl;

	// 4. 常用接口(核心)
	cout << "\narr1大小:" << arr1.size() << endl;  // 输出5(固定大小,编译时确定)
	cout << "是否为空:" << (arr1.empty() ? "是" : "否") << endl;  // 输出否
	cout << "首元素:" << arr1.front() << endl;  // 输出1
	cout << "尾元素:" << arr1.back() << endl;  // 输出5

	// 5. 填充(所有元素设为同一个值)
	array<int, 3> arr3;//定义不会初始化,里面是随机值。
	arr3.fill(0);  // 填充为{0, 0, 0}
	cout << "\narr3填充后:" << arr3[0] << " " << arr3[1] << endl;

	return 0;
}

5.2 forward_list

#include <iostream>
#include <forward_list>  // 必须包含头文件
using namespace std;

int main() {
	// 1. 初始化:多种方式
	forward_list<int> fl1{ 1, 2, 3, 4, 5 };  // 统一初始化
	forward_list<int> fl2(3, 10);           // 3个元素,均为10({10,10,10})
	forward_list<int> fl3(fl1.begin(), fl1.end());  // 用迭代器拷贝初始化

	// 2. 元素访问:无[]/at(),只能通过迭代器或front()(仅首元素)
	cout << "fl1首元素:" << fl1.front() << endl;  // 输出1

	// 3. 遍历:只能用迭代器(单向,不能反向)
	cout << "\n遍历fl1:";
	for (auto it = fl1.begin(); it != fl1.end(); ++it) {
		cout << *it << " ";  // 输出1 2 3 4 5
	}
	cout << endl;

	// 4. 核心操作:插入、删除、拼接(单向链表专属高效操作)
	// 头部插入
	fl1.push_front(0);  // fl1变为{0,1,2,3,4,5}
	// 指定位置前插入(需先找到前驱位置,用insert_after)
	auto it = fl1.begin();  // 指向0
	fl1.insert_after(it, 6);  // 在0后面插6 → {0,6,1,2,3,4,5}

	// 删除指定位置后元素
	fl1.erase_after(it);  // 删除6 → 恢复{0,1,2,3,4,5}

	// 拼接(将fl2拼到fl1末尾,fl2后续会为空)
	fl1.splice_after(fl1.end(), fl2);  // fl1变为{0,1,2,3,4,5,10,10,10}

	cout << "操作后遍历fl1:";
	for (int val : fl1) {  // 范围for也支持(底层是迭代器)
		cout << val << " ";
	}
	cout << endl;

	// 5. 其他常用接口
	cout << "\nfl1是否为空:" << (fl1.empty() ? "是" : "否") << endl;  // 否
	fl1.clear();  // 清空所有元素
	cout << "清空后是否为空:" << (fl1.empty() ? "是" : "否") << endl;  // 是

	return 0;
}

 forward_list 是单向链表,仅能通过当前节点访问下一个节点,无法直接获取前驱节点——删除/插入需操作“目标节点”,但找到目标节点后,没法回头改它前面节点的指针,只能先找“目标节点的前驱”,再通过 insert_after / erase_after 操作前驱的下一个(即目标节点)。

6.左右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们
之前学习的引用就叫做左值引用。左值引用就是给左值取别名,右值引用就是给右值取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	/*10 = 1;
	x + y = 1;
	fmin(x, y) = 1;*/
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,后面就再说到这个特性。

 int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;
	const double&& rr2 = x + y;
	rr1 = 20;
	//rr2 = 5.5;  // 报错
	return 0;
}

6.1 左右值引用的比较

左值引用:

1. 左值引用只能引用左值,不能引用右值。

2. 但是const左值引用既可引用左值,也可引用右值

int main()
{
	double x = 1.1;
	double y = 2.2;
	//左值引用怎么给右值取别名(const左值引用可以)
	const int& r2 = 10;
	const double& r3 = x + y;
 }

右值引用:

1. 右值引用只能右值,不能引用左值。

2. 但是右值引用可以move以后的左值

std:move 介绍

定义:

        是标准库里面的一个函数模板,用来那左值属性转化为右值。

实现原理:

        C++ 的 move (移动语义)核心是将对象资源“所有权”转移,而非拷贝,通过右值引用( T&& )绑定临时对象/将亡值,避免冗余拷贝。

int main()
{
	double x = 1.1, y = 2.2;
	int a = 0;
	/*10,x+y为右值,r4,r5为10,x+y的别名*/
	int&& r4 = 10;
	cout << r4 << endl;
	r4 = 1;
	cout << r4 << endl;
	double&& r5 = x + y;
	//右值引用怎么给左值取别名
	//右值引用可以引用move以后的左值
	int&& r6 = move(a);
	r6 = 1;
	cout << a << endl;//右值引用引用左值后可以修改
	return 0;
 }

如果没有使用右值引用去接受move后的值的话,是不会改变属性的。

void func(const int& r)
{
	cout << "void func(const int& r)" << endl;
}

void func(int&& r)
{
	cout << "void func(int&& r)" << endl;
}

int main()
{
	int a = 0;
	int b = 1;
	func(a);//a为左值
	move(a);//不会改变a的属性
	func(a);
	func(move(a));//可以理解为move返回的是右值,是a的浅拷贝
	return 0;
}

表格比较:

对比维度 左值引用 右值引用
绑定对象 左值(如变量、返回左值的表达式) 右值(如字面量、临时对象、转换后的左值)
核心用途 传递参数、避免拷贝(只读/修改原对象) 移动语义、完美转发,转移资源(不拷贝)
可修改性 const 时可修改绑定对象 可修改绑定的右值(转移后源对象需置空)
生命周期 依赖原对象,不能绑定临时对象(const 除外) 延长临时对象生命周期至引用自身生命周期

6.2 判断左右值的方法

核心判断法:能取地址(&变量/表达式有意义)、有名字、可修改(非const)的是左值;不能取地址、无名字、是临时对象/将亡值的是右值,一句话总结“能留用的是左值,用完就丢的是右值”。

具体分辨技巧(好记又实用)

1. 左值:变量( int a )、函数返回左值引用( T& func() )、数组元素( arr[0] )、解引用( *p )—— 本质是“持久存在的实体”,能被多次访问。

2. 右值:字面量( 10 、 "abc" )、临时对象( string("test") )、函数返回值( int func() )、 std::move(左值)  转换后的值—— 本质是“临时产生的、生命周期短暂的对象”。

6.3 右值引用的使用场景和意义

namespace bb
{
	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(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()
		{
			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
	};
}

6.3 看看左值引用


void func2(const bit::string& s)
{}
int main()
{
 bit::string s1("hello world");
 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
 func1(s1);
 func2(s1);
 // string operator+=(char ch) 传值返回存在深拷贝
 // string& operator+=(char ch) 传左值引用没有拷贝提高了效率
 s1 += '!';
 return 0;
}

左值引用可以在一些场景里面减少拷贝的次数,提高效率。

6.3.1 左值引用的缺点

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,
只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,
传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造),如果是传引用返回,str已经被释放了。

bit::string func()
{
	bit::string str("xxxxxxxxxxxxxx");
	return str;
}
int main()
{
	bit::string s2 = func();
}

构造一个str对象,str会拷贝构造一个临时对象,s2再拷贝构造str,就会有两次拷贝构造,拷贝构造也需要开辟空间,浪费空间资源。这边是在没有编译器优化和移动构造的情况下,编译器的优化为直接使用str去构造s2,只有一个构造。

6.4 右值引用的处理

右值引用分为两种,一种是内值类型的右值,也叫纯右值,还有一种是自定义类型的右值,也叫将亡值,很好理解,也就是马上就要死亡的值。

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不
用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

所以在上面的情况中,编译器会把str视为将亡值,反正str也马上就释放了,直接把它的资源给别人。

 //是否构成函数重载 -- 是
void func(const int& r)
{
	cout << "void func(const int& r)" << endl;
}

void func(int&& r)
{
	cout << "void func(int&& r)" << endl;
}

int main()
{
	int a = 0;
	int b = 1;
	func(a);//a为左值

	// 走更匹配的,有右值引用的重载,就会走右值引用版本
	func(a + b);//a+b为右值

	return 0;
}

虽然const T&可以接收左值也可以接收右值,如果没有右值引用的话,func(a)和func(a+b)都会走func(const int&r),有了右值引用后,编译器会走最合适的,就会走右值引用版本,两个构成函数重载。

string(const string& s)
	:_str(nullptr)
{
	cout << "string(const string& s) -- 深拷贝" << endl;
	string tmp(s._str);
	swap(tmp);
}
// 移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	cout << "string(string&& s) -- 移动语义" << endl;
	swap(s);
}

移动构造中并没有去新开辟空间,拷贝数据,效率也会随之提高,所以在没有优化的情况下,会变成拷贝构造加移动构造,同样编译器也会直接优化为一个构造。

不仅仅有移动拷贝还有移动赋值。

bb::string func()
{
	bb::string str("xxxxxxxxxxxxxx");
	return str;
}
int main()
{
	bb::string s2;
	s2 = func();
}

在没有移动赋值和编译器优化的情况下,是构造s2加拷贝构造str临时对象加临时对象赋值s2,同样,在使用移动赋值后,编译器将str看成为将亡值,使用移动构造+移动赋值就完成了,优化后就是直接移动赋值。

7. 关于模板

7.1 模板的&&万能引用

C++11 引入的特性,本质是 模板参数  T  +  T&&  结合引用折叠,能接收左值、右值、const/非 const 所有值类别,是实现“完美转发”的基础。

1. 唯一形式:仅存在于  template <typename T> void func(T&& arg) , T  必须是模板推导参数(非模板/固定类型的  T&&  是右值引用)。

2. 全能接收:通过引用折叠适配所有参数——左值入参推导为左值引用,右值入参推导为右值引用。

3. 依赖转发:自身不“完美”,需搭配  std::forward<T>(arg)  才能保留参数原始值类别,否则  arg  会被当作左值。

4. 推导关键:左值入参时  T  推导为  Type& ,右值入参时  T  推导为  Type ,最终由引用折叠确定实际类型。

// 万能引用:既可以接收左值,又可以接收右值
// 实参左值,他就是左值引用(引用折叠)
// 实参右值,他就是右值引用
template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}
int main()
{
	PerfectForward(10);           // 右值

	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值
}

因为有了模板的存在,如果传过去的参数是左值,就会折叠(&&->&)生成左值引用,如果是右值就会生成(&&->&&)右值引用。

代码结果:

7.2 完美转发

上面万能引用中提到了需要搭配forward才可以改变它的属性,这是怎么一回事呢?难道是右值引用不是右值吗?

int main()
{
	int&& rrr = 10;
	rrr++;
	cout << rrr << ":" << &rrr << endl;
}

运行结果:

看上面的代码完美会发现,右值引用是支持修改加取地址的,所以编译器是把右值引用当成了左值的。那么也就不难解释了,编译器在接受到右值引用时,会把它当作左值,所以在调用func()时,无论传入的是左值还是右值,最后都是左值。

这么做其实在说左右值时就体现出来了。

这里如果编译器不把右值引用s当作左值的话,这么可以调用swap函数呢。

可是有些时候,我们并不想要编译器进行这种操作,想要保持原来的属性,使用右值引用来接收,就保持右值的属性啊。

就可以使用forward关键字来实现。

void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) {  cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// 万能引用:既可以接收左值,又可以接收右值
// 实参左值,他就是左值引用(引用折叠)
// 实参右值,他就是右值引用
template<typename T>
void PerfectForward(T&& t)
{
	// 完美转发,t是左值引用,保持左值属性
	// 完美转发,t是右值引用,保持右值属性
	Fun(forward<T>(t));//加上forward保持原来的属性
	//没有加forward时,编译器会将收到的都识别为左值,因为右值引用支持修改,如果为右值不可以支持修改

}
int main()
{
	PerfectForward(10);           // 右值

	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

	const int b = 8;
	PerfectForward(b);		      // const 左值
	PerfectForward(std::move(b)); // const 右值
	int& r = a;
	//右值不能取地址,同时也不支持修改,右值引用不为右值,支持修改
	int&& rr = move(a);
	rr++;

	int&& rrr = 10;
	rrr++;

	return 0;
}

运行结果:

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

8. 可变参数模板

C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比
C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改
进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现
阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了。

下面就是一个基本可变参数的函数模板

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

上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数
包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,
只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特
点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变
参数,所以我们只能用一些奇招来一一获取参数包的值。

void _showlist()
{
	//结束条件的函数
	cout << endl;
}
template<class T, class ...Args>
void _showlist(T val, Args...args)
{
	cout << val << " ";
	_showlist(args...);
}
template<class ...Args>
void CppPrint(Args...args)
{
	_showlist(args...);
}
int main()
{
	CppPrint();
	CppPrint(1);
	CppPrint(1,2);
	CppPrint(1,2,2.2);
	CppPrint(1,2,2.2,string("xxxxx"));//打印可变参数里面的每个值
	return 0;
}

ps:模板参数Args前面有省略号,代表它是一个可变模板参数,我们把带省略号的参数称为参数包,参数包里面可以包含0到N ( N ≥ 0 ) 个模板参数,而args则是一个函数形参参数包。

模板参数包Args和函数形参参数包args的名字可以任意指定,并不是说必须叫做Args和args。

应用场景:

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{
		cout << "Date构造" << endl;
	}

	Date(const Date& d)
		:_year(d._year)
		, _month(d._month)
		, _day(d._day)
	{
		cout << "Date拷贝构造" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
template <class ...Args>
Date* Create(Args... args)
{
	Date* ret = new Date(args...);

	return ret;
}
int main()
{
	Date* p1 = Create();
	Date* p2 = Create(2023);
	Date* p3 = Create(2023, 9);
	Date* p4 = Create(2023, 9, 27);

	Date d(2023, 1, 1);//构造加拷贝构造
	Date* p5 = Create(d);//拷贝构造
	return 0;
}

初始化的值传给可变参数包,可变参数包往下传递到构造。

9. 新接口

新接口中增加了cbegin,cend,crbegin,crend,返回const迭代器,但是在之前的模拟实现中,用begin和end也可以返回const的迭代器,c++11提供属于锦上添花的操作了。

有了initializer的出现,都支持{}初始化。

容器都新增加了移动构造和移动赋值。

所有的容器都新增了emplace系列。

以list来举例

有些人会说emplace会比push更高效,但是其实不会高效到哪里去。

int main()
{
	list<pair<int, char>> mylist;
	// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
	// 那么在这里我们可以看到除了用法上,和push_back没什么太大的区别
	mylist.emplace_back(10, 'a');
	mylist.emplace_back(20, 'b');
	mylist.emplace_back(make_pair(10, 'a'));
	mylist.push_back(make_pair(40, 'd'));
	mylist.push_back({ 50,'e' });
	for (auto e : mylist)
	{
		cout << e.first << " " << e.second << endl;
	}
}

先看第一个场景,在上面的代码中,emplace系列因为是使用了参数包,将参数一层一层的传递下去在编译器的优化下,都会从构造+移动拷贝变为一个构造,和push在用法上面其他没有如何区别。

int main()
{
	//	下面我们试一下带有拷贝构造和移动构造的bit::string,再试试呢
	//	我们会发现其实差别也不到,emplace_back是直接构造了,push_back 是先构造,再移动构造,其实也还好。
	std::list<  bit::string > mylist;
	mylist.emplace_back( "sort");
	mylist.push_back( "sort" );
	return 0;
}

在这个场景中,就会体现出来区别了,emplace因为参数包的传递,可以直接使用“sort”去构造pair对象,但是push就需要去构造一个匿名对象然后移动拷贝。

虽然push多了一个移动构造,但是代价足够低,所以没有高效到哪里去。

10. 新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载。

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。 

namespace bit
{
	class string
	{
	public:
		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(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;

			swap(s);
		}

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

			return *this;
		}

		string& operator=(string&& s)
		{
			cout << "string& operator=(string && s) -- 移动拷贝" << endl;
			swap(s);

			return *this;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\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(Person&& p) = default;
	Person(const Person& p) = default;


	/*~Person()
	{}*/

private:
	bit::string _name;
	int _age;
};

int main()
{
	Person s1;
	Person s2 = s1;
	Person s3 = std::move(s1);
	Person s4;
	/*s4 = std::move(s2);
	s4 = s2;*/
	return 0;
}

运行结果:

因为把析构函数 、拷贝构造、拷贝赋值重载注释了,所以会去调用默认生成的移动构造,左值调用深拷贝,右值调用移动拷贝。

11. lambda表达式

11.1 前言

在C++98中,如果想要对一个数据集合中的元素进行排序,可以使用std::sort方法。

#include <algorithm>
#include <functional>
int main()
{
int array[] = {4,1,8,5,3,7,0,9,2,6};
// 默认按照小于比较,排出来结果是升序
std::sort(array, array+sizeof(array)/sizeof(array[0]));
// 如果需要降序,需要改变元素的比较规则
std::sort(array, array + sizeof(array) / sizeof(array[0]), greater<int>());
return 0;
}

如果待排序元素为自定义类型,需要用户定义排序时的比较规则:

struct Goods
{
	string _name;
	double _price;
	int _evaluate;
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		,_price(price)
		,_evaluate(evaluate)
	{}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price > gr._price;
	}
};
struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};//仿函数
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), CompareEvaluateGreater());
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,
都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,
这些都给编程者带来了极大的不便。因此,在C++11语法中出现了Lambda表达式。

11.2 lambda书写

lambda表达式分为以下几个部分:

[ ] 参数捕捉列表

( ) 参数列表

mutable 关键字

-> returntype 返回值类型

{ } 函数体

lambda表达式各部分说明

  1. [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
  2. (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。
  4. ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回
    值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推
    导。
  5. {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获
    到的变量。

注意:
在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为
空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

int main()
{
 vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 
3 }, { "菠萝", 1.5, 4 } };
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._price < g2._price; });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._price > g2._price; });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._evaluate < g2._evaluate; });
 sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2){
 return g1._evaluate > g2._evaluate; });
}

对于排序就可以使用lambda表达式,不再需要去手写一个一个的仿函数,通过上面的代码也可以看出lambda表达式就是匿名函数。

那如果想要显示的去调用lambda表达式呢?

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	auto lm = [](const Goods& x, const Goods& y) {return x._price < y._price; };
	cout << lm(v[0],v[1]) << endl;
}

可以使用auto来接收,用法和普通函数一致。

11.3 lambda的捕捉列表

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。

[var]:表示值传递方式捕捉变量var

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

注意:
 a. 父作用域指包含lambda函数的语句块
 b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。
 比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
 c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。
 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

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

void (*PF)();
int main()
{
	// 最简单的lambda表达式, 该lambda表达式没有任何意义
	[] {};
	// 省略参数列表和返回值类型,返回值类型由编译器推导为int
	int a = 3, b = 4;
	[=] {return a + 3; };
	// 省略了返回值类型,无返回值类型
	//用引用的方式访问父类全部变量,可以改变外部的值。
		auto swap1 = [](int& x, int& y)
		{
			int tmp = x;
			x = y;
			y = tmp;
		};
	swap1(a,b);//a=4,b=3
		cout << a << " " << b << endl;
	// 各部分都很完善的lambda函数
	//只有b采用引用的方式传入,其他的变量均传值
	auto fun2 = [=, &b](int c)->int {return b += a + c; };
	cout << fun2(10) << endl;
	// 复制捕捉x
	int x = 10;
	//lambda里面的x是+const属性拷贝
	//auto add_x = [x](int a) { x *= 2; return a + x; };//报错

	//取消const属性的关键字 mutable,取消了const属性,但是也不会修改外部x的值。
	auto add_x = [x](int a) mutable { x *= 2; return a + x; };
	cout << add_x(10) << endl;
	cout << x << endl;//10
	//lambda表达式中间不能相互赋值
	auto f1 = [] {cout << "hello world" << endl; };
	auto f2 = [] {cout << "hello world" << endl; };
	// lambda表达式底层是仿函数,可以打印看看类型名字
	//f1 = f2;   // 编译失败--->提示找不到operator=()
	cout << typeid(f1).name() << endl;
	cout << typeid(f2).name() << endl;
	// 允许使用一个lambda表达式拷贝构造一个新的副本
	auto f3(f2);
	f3();
	// 可以将lambda表达式赋值给相同类型的函数指针
	PF = f2;
	PF();
	return 0;
}

运行结果:

11.4 函数对象和lambda表达式

函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的
类对象。

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);
// lamber
 auto r2 = [=](double monty, int year)->double{return monty*rate*year; 
};
 r2(10000, 2);
 return 0;
}

从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如
果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

12. 包装器

12.1 function包装器

function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。

那么我们来看看,我们为什么需要function呢?

ret = func(x);
// 上面func可能是什么呢?那么func可能是函数名?函数指针?函数对象(仿函数对象)?也有可能
是lamber表达式对象?所以这些都是可调用的类型!如此丰富的类型,可能会导致模板的效率低下!
为什么呢?我们继续往下看
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;
 // lamber表达式
 cout << useF([](double d)->double{ return d/4; }, 11.11) << endl;
 return 0;
}

上面的代码会导致usfF被示例出3份。

运行结果:

可以看出来确实示例出了3份,3个static int count是独立的,打印出来的地址不同。

包装器可以很好的解决该问题。

std::function在头文件<functional>
// 类模板原型如下
template <class T> function;     // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
模板参数说明:
Ret: 被调用函数的返回类型
Args…:被调用函数的形参

使用function来解决该问题。

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()
{
	//这里的useF会实例出3份,导致效率低下,用包装器解决问题
	// 函数指针
	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) {return d / 4; };
	function<double(double)> f3 = Functor();
	vector<function<double(double)>> v = { f1,f2,f3 };
	vector<function<double(double)>> v1 = { f,[](double d) {return d / 4; }, Functor() };
	double n = 3.3;
	for (auto f : v1)
	{
		cout << f(n) << endl;
	}
	return 0;
}

原本3种可调用类型直接作为 F ,导致 F 是3种不同类型,触发3次实例化;
用 std::function<double(double)> 包装后,传给 useF 的实参类型统一 为 std::function<double(double)> , F 固定为该类型,仅实例化1次。使用vector进行统一的管理。

12.2 bind

std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可
调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而
言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M
可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺
序调整等操作。

// 原型如下:
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);

fn 是传递的 函数对象,args 是传给函数的 可变参数包,这里使用了 万能引用(引用折叠),使其在进行模板类型推导时,既能引用左值,也能引用右值。

1.使用bind改变修改参数绑定顺序。

int Sub(int a, int b)
{
	return a - b;
}
int main()
{
	function<int(int, int)> rSub = bind(Sub, placeholders::_1, placeholders::_2);
	cout << rSub(10, 5) << endl;

	function<int(int, int)> rSub = bind(Sub, placeholders::_2, placeholders::_1);
	cout << rSub(10, 5) << endl;
	return 0;
}

bind(Func, std::placeholders::_2, std::placeholders::_1) 通过 placeholders::_1 和 placeholders::_2 指定了新的参数顺序,即将原本的第二个参数和第一个参数交换。

当我们调用 Sub(10, 5) 时,实际上是将 5 作为第一个参数,10 作为第二个参数传递给rSub。

这种参数顺序的改变,在一些特定的应用场景下非常有用,特别是在函数签名不一致时,可以方便地进行适配。

2. bind绑定指定特定参数

double Plus(int a, int b, double rate)
{
	return (a + b) * rate;
}
double PPlus(int a, double rate, int b)
{
	return  rate * (a + b);
}
int main()
{
	function<double(int, int)> Plus1 = bind(Plus, placeholders::_1, placeholders::_2,4.0);
	function<double(int, int)> Plus2 = bind(Plus, placeholders::_1, placeholders::_2, 4.2);

	function<double(int, int)> PPlus1 = bind(PPlus, placeholders::_1, 4.0, placeholders::_2);
	function<double(int, int)> PPlus2 = bind(PPlus, placeholders::_1, 4.2, placeholders::_2);
	cout << PPlus1(5, 3) << endl;
	cout << PPlus2(5, 3) << endl;

	cout << Plus1(5, 3) << endl;
	cout << Plus2(5, 3) << endl;
	return 0;
}

我们通过 bind(Plus, std::placeholders::_1,std::placeholders::_2,4.0) 在绑定时会自动把4.0绑定给std::placeholders::_3。

后续调用时,我们只需要传递第一二个参数 5,3就可以。

ps: 如果特定参数的设置是不会影响std::placeholders::_1,std::placeholders::_2序号的排序的,还是按照顺序来填。

3. 绑定成员函数

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);
	cout << Sub1(1, 2) << endl;

	SubType st;
	function<double(int, int)> Sub2 = bind(&SubType::ssub, &st, placeholders::_1, placeholders::_2, 3);
	cout << Sub2(1, 2) << endl;

	function<double(int, int)> Sub3 = bind(&SubType::ssub, SubType(), placeholders::_1, placeholders::_2, 3);
	cout << Sub3(1, 2) << endl;
	return 0;
}

在绑定静态成员时,需要告诉编译器需要去哪一个类里面去寻找成员位置。

在绑定非静态成员时,因为有this指针的存在,所以还需要传一个对象的地址过去,可以自己定义,也可以传匿名对象,后续就可以通过新的对象来调用。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐