C++进阶—C++11

第一章: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增加的语法特性非常篇幅非常多,所以本节主要讲解实际中比较实用的语法。

https://en.cppreference.com/w/cpp/11

第二章:统一的列表初始化

2.1 {}初始化

在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扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加

class Date {
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day) {
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main() {
	int i = 0;
	int j = { 0 };
	int k{ 0 };

	int array1[]{ 1, 2, 3, 4, 5 };
	int array2[5]{ 0 };
	Point p{ 1, 2 };

	//创建对象时也可以使用列表初始化方式调用构造函数初始化
	Date d1(2025, 07, 24); // old style
	// C++11支持的列表初始化,这里会调用构造函数初始化
	//本质是类型转换,用{}里的内容构造一个临时对象,再用临时对象去拷贝构造
	//编译器优化为直接构造(构造+拷贝构造->优化直接构造)
	Date d2 = { 2025, 07, 25 };
	Date d3{ 2025, 07, 26 };
	//Date& d4 = { 2025, 07, 27 };//不可以引用,因为临时对象具有常性
	const Date& d4 = { 2025, 07, 27 };//const修饰才能引用常量

	Date* p1 = new Date[3]{ d1,d2,d3 };
	Date* p1 = new Date[3]{ {2025, 07, 24},{2025, 07, 25},{2025, 07, 26} };

	return 0;
}

2.2 std::initializer_list

std::initializer_list 是什么类型:
//std::initializer_list 是 C++11 引入的一个轻量级模板类,用于支持列表初始化(uniform initialization),
//即使用 { } 初始化对象。它使得初始化语法更加统一,并允许在构造函数或函数中接收任意长度的初始化列表。

int main() {
	// the type of il is an initializer_list 
	auto il = { 10, 20, 30 };
	cout << typeid(il).name() << endl;
	//class std::initializer_list<int>
	return 0;
}

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

int main() {
	//Date属于多参数构造的类型转换,构造+拷贝构造->优化直接构造
	//需要和对应构造函数的参数个数匹配
	Date d1 = { 2025, 07, 24 };

	//这两种方式的初始化与上面Date初始化并不一样
	//它们{}中的参数个数可以是任意个数
	//这里{}中的参数是initializer_list
	vector<int> v1 = { 1,2,3,4 };
	vector<int> v2 = { 1,2,3,4,5,6 };
	list<int> lt = { 10,20,30 };

	//使用initializer_list赋值
	v1 = { 10,20,30,40, };

	initializer_list<int> il2 = { 10,20,30,40 };
	initializer_list<int>::iterator it2 = il2.begin();
	while (it2 != il2.end()) {
		cout << *it2 << " ";
		++it2;
	}
	cout << endl;

	for (auto e : il2)
		cout << e << " ";
	cout << endl;

	//vector支持initializer_list初始化的实现
	//vector(const initializer_list<T>&lt) {
	//	reserve(lt.size());
	//	for (auto& e : lt) push_back(e);
	//}

	return 0;
}

第三章:声明

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

3.1 auto

在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。

3.2 decltype

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

int main() {
	int i = 1;
	double d = 2.2;
	//把类型以字符串形式获取到
	cout << typeid(i).name() << endl;
	cout << typeid(d).name() << endl;
	//typeid(i).name() j;//这是字符串,不能定义类型

	auto ret = i * d;
	//vector<auto> v;//错误,auto无法获取类型
	//用decltype获取ret的类型去实例化vector
	//decltype可以推导对象的类型。该类型可以用作模板实参,或定义对象
	vector<decltype(ret)> v;
	v.push_back(1);
	v.push_back(1.1);
	for (auto e : v)
		cout << e << " ";
	cout << endl;
	return 0;
}

3.3 nullptr

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

第四章:范围for循环

这个在前面的章节中已经进行了非常详细的讲解。

第五章:智能指针

这个将在智能指针中进行详细的讲解。

第六章:STL中一些变化

<array>、<forward_list>、<unordered_map>、<unordered_set>是C++11中的一些几个新容器,但是实际最有用的是unordered_map和unordered_set。这两个前面已经进行了非常详细的讲解,其他的了解即可。

容器中的一些新方法

比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是可以返回const迭代器的,这些都是属于锦上添花的操作。

实际上C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本:

但是这些接口到底意义在哪?网上都说他们能提高效率,他们是如何提高效率的?

请看下面的右值引用和移动语义章节的讲解。另外emplace还涉及模板的可变参数,也需要再继续深入学习后面章节的知识。

第七章:右值引用和移动语义

7.1 左值引用和右值引用

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

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时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 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

7.2 左值引用与右值引用比较

int main() {
	//左值引用能否给右值取别名
	//不能,但const左值引用可以
	const int& r1 = 10;
	int i = 0;
	int j = i;
	const int& r2 = i + j;

	//右值引用给右值取别名
	int&& rr1 = 10;
	int&& rr2 = i + j;
	//右值引用能否给左值取别名
	//不能,但右值引用可以给move(左值)取别名
	int&& rr3 = move(i);
	return 0;
}

左值是可以取地址的值(非const可以修改,const不可以修改)。
左值引用可以给左移取别名。
左值引用不能给右值取别名,但const左值引用可以给右值取别名。

右值是不能取地址的值。
右值引用可以给右值取别名。
右值引用不能给左值取别名,但可以给move(左值)取别名。

7.3 右值引用使用场景和意义

左值引用的使用场景:
做参数和做返回值都可以提高效率。

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

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

namespace bit {
	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) { //const左值引用既可以引用左值,也可以引用右值
			cout << "string(const string& s) -- 拷贝构造 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		//C++的右值分2类:
		//1.纯右值:内置类型(纯粹的字面值、临时对象或表达式计算结果,没有持久的内存地址(不能取地址))
		//2.将亡值:自定义类型(即将被移动(资源被转移)的对象,通常是“生命周期即将结束”的值。)
		//         有内存地址(可以取地址),但允许被“掏空”(资源被移动)。

		//移动构造
		string(string&& s) {
			//to_string 中的 ret 是即将销毁的局部变量
			//编译器将其视为右值(即使它是左值名称,但处于"将亡值"场景)
			//构造临时对象:需要用一个右值(ret)构造临时对象
			//移动构造函数内部:新对象(临时对象)刚创建,它的成员是默认值。
			//参数 s 是 ret 的别名(引用),持有 ret 的真实数据
			//调用全局的 swap 函数交换每个成员
			//函数结束后:ret(即移动后的 s)被销毁
			//临时对象现在持有原 ret 的资源("1234"),没有发生深拷贝
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);//this:指向正在构造的临时对象(内存由编译器分配)
			//临时对象不新开空间
		}

		// 赋值重载
		string& operator=(const string& s) {
			cout << "string& operator=(string s) -- 赋值重载 深拷贝" << endl;
			//string tmp(s);
			//swap(tmp);
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
		string& operator=(string&& s) {
			//ret 的资源("1234")已通过 swap 转移给临时对象。
			//参数 s:就是刚才那个临时对象(右值引用绑定)。 
			// this:指向 main 中的str。str和s交换资源。
			//临时对象s带着str的资源被销毁,并释放
			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 = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};

	bit::string to_string(int x) {
		bit::string ret;
		while (x) {
			int val = x % 10;
			x /= 10;
			ret += ('0' + val);
		}
		reverse(ret.begin(), ret.end());
		return ret;
		//该场景不能使用左值引用返回,ret出了作用域要销毁
		//也不能使用右值引用返回,右值引用不能引用左值
		//ret拷贝构造临时对象,在赋值给外面的str,发生两次深拷贝
		//ret开辟空间存1234,然后拷贝构造给临时对象,临时对象也开辟空间存1234,
		//出了函数作用域ret销毁并释放空间,当ret赋值给外面的str并走到下一行,s销毁并释放空间

		//ret 是局部变量(左值),但 return 时会隐式转换为将亡值(xvalue)。
		//调用string(string && s),s 就是 ret 转换成的将亡值。
		//移动构造后,ret 的资源被转移,外部得到新对象。
	}
}

int main() {
	//无移动构造和移动赋值:
	//这里有深拷贝发生,to_string函数中要拷贝构造临时对象
	//再用临时对象赋值给s
	
	//有移动构造和移动赋值
	//整个移动构造和移动复制的过程中只有ret开辟了空间存储资源,临时对象把ret的换走了,
	//然后main函数中的str再把临时对象的换走了,也就相当于间接换走了ret的资源。
	bit::string str;	
	str = bit::to_string(1234);
	
	//------------------------------------------------------------------
	//如果没有移动构造,下面这种写法还是会发生一次深拷贝,相当于ret拷贝构造str。
	//有了移动构造就是ret和str交换资源
	bit::string str = bit::to_string(1234);
	
	//-------------------------------------------------------------------
	//下面这里将s1转为右值,虽然不调用拷贝构造(调用移动构造)
	//但资源交换后,s1变为空了
	bit::string s1("hello world");
	bit::string s2 = s1;//这里调用拷贝构造
	bit::string s3 = move(s1);
	
	//-------------------------------------------------------------------
	//move不会改变s1的属性,只是move的返回值是右值
	move(s1);
	bit::string s3 = s1;//调用的是拷贝构造
	
	return 0;
	//右值引用的移动语义主要针对深拷贝,浅拷贝意义不大

	//左值引用(T&)只能绑定到有名字的、持久化的对象(左值),但无法绑定到:
	//临时对象(右值):如函数返回值、表达式结果等。
	//需要被“移动”而非拷贝的资源:比如动态分配的数组、文件句柄等。
	
	//右值引用的核心作用
	//右值引用(T&& )直接绑定到临时对象(右值),并允许“窃取”其资源,避免深拷贝。典型场景包括:
	//函数返回临时对象:如std::vector<int> createVector()。
	//临时对象赋值或传参:如a = createVector()。
	//标准库容器的移动语义:如std::vector的移动构造函数。
}

STL容器插入接口函数也增加了右值引用版本

int main() {
	list<bit::string> lt;
	bit::string s1("hello world");
	lt.push_back(s1);//左值引用插入,拷贝构造 深拷贝
	
	lt.push_back(bit::to_string(1234));
	//调用2次移动构造。
	//当bit::to_string(1234)执行完毕后:
	//它返回一个临时对象(右值)ret。
	//由于 ret 是一个局部变量,在函数返回时,它会被视为即将销毁的右值。
	//此时,编译器会优先调用 移动构造函数(string(string && s))来构造返回值,而不是拷贝构造。
	//第二次移动构造:push_back 接收临时对象
	//push_back 接收到 bit::to_string(1234) 返回的临时对象(右值)。
	//STL 的 list 在插入元素时,会构造一个新的节点来存储该字符串。
	//由于传入的是 右值,list 内部会调用 string 的 移动构造函数 来构造新节点中的string对象。

	lt.push_back("11111");
	//只调用一次移动构造
	//lt.push_back("11111"); 直接传入 const char*,
	//只需一次隐式构造 + 一次移动构造,没有额外的返回值传递过程。

	//右值引用(&& )不仅优化了函数返回值(如 return 临时对象),
	//还优化了参数传递(如函数传参或容器插入操作),
	//特别是对于需要深拷贝的类(如 string、vector 等),
	//它可以避免不必要的深拷贝,直接“窃取”资源,提高性能。
	return 0;
}

7.4 右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

验证自己实现的list

List Class.h

#pragma once

namespace bit {
	template<class T>
	struct list_node { //节点结构体。
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& x = T())//如果T是自定义类型,则x会是该类型的默认构造对象,取决于该类型的默认构造函数如何定义。
			:_data(x), _next(nullptr), _prev(nullptr) {
		}

		//为右值引用提供一个构造函数,不能带缺省参数,否则调用歧义
		list_node(T&& x)
			:_data(move(x)), _next(nullptr), _prev(nullptr) {
		}
	};


	template<class T, class Ref, class Ptr>//迭代器是模拟指针的行为
	struct __list_iterator { //迭代器
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		//迭代器可以理解为节点指针
		__list_iterator(Node* node)//单参数构造函数支持隐式类型转换,也就是说节点指针也可以隐式类型转换为迭代器
			:_node(node) {
		}

		self& operator++() { //前置++
			_node = _node->_next;
			return *this;
		}
		self& operator--() { //前置--
			_node = _node->_prev;
			return *this;
		}

		self operator++(int) { //后置++
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		self operator--(int) { //后置--
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		Ref operator*() { return _node->_data; }
		Ptr operator->() { return &_node->_data; }//提供给自定义类型解引用。自定义类型解引用是 指针->形式,所以返回指针
		//非const和const迭代器中只有上面这两个运算符重载的返回值不同
		bool operator!= (const self& s) { return _node != s._node; }
		bool operator== (const self& s) { return _node == s._node; }
	};


	template<class T>
	class list {
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		const_iterator begin() const { return _head->_next; }//支持隐式类型转换
		const_iterator end() const { return _head; }//支持隐式类型转换
		iterator begin() { return _head->_next; }
		iterator end() { return _head; }

		void empty_init() {
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list() { empty_init(); } //构造函数本质是创建一个哨兵位节点

		//lt2(lt1)
		list(const list<T>& lt) { //lt是const对象,需要const迭代器
			empty_init();//创建(lt2的)哨兵位
			for (auto e : lt)//遍历lt1每个节点赋值给e
				push_back(e);//lt2尾插每个e节点
		}

		//现代写法
		void swap(list<T>& lt) {
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		list<T>& operator=(list<T> lt) {
			swap(lt);
			return *this;
		}

		~list() {
			clear();
			delete _head;//清除哨兵位
			_head = nullptr;
		}

		void clear() {
			iterator it = begin();//begin指向哨兵位的下一节点,即有效数据的头节点
			while (it != end())
				it = erase(it);
		}

		void push_back(const T& x) { insert(end(), x); }//end指向哨兵位,在哨兵位之前插入
		//void push_back(T&& x) { insert(end(), x); }//右值引用版本
		void push_back(T&& x) { insert(end(), move(x)); }
		//右值被右值引用 引用后的属性是左值,这样insert时走的就是左值引用版本
		//所以move改变x的属性为右值

		void push_front(const T& x) { insert(begin(), x); }
		void pop_back() { erase(--end()); }//哨兵位前一节点就是尾结点
		void pop_front() { erase(begin()); }

		iterator insert(iterator pos, const T& x) {
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x) {
			Node* cur = pos._node;
			Node* newnode = new Node(move(x));//右值引用版本
			//这里和push_back同理,右值被右值引用 引用后的属性是左值
			//这样走的就是左值引用版本的构造函数,所以move改变x的属性为右值
			Node* prev = cur->_prev;
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		iterator erase(iterator pos) { //erase后迭代器失效,因为迭代器指向的节点被释放了,所以返回下一个节点的位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;
			--_size;
			return iterator(next);
		}

		size_t size() { return _size; }

	private:
		Node* _head;
		size_t _size;//增加该成员避免每次获取size都要遍历一遍链表
	};
}

Test.cpp

int main() {
	bit::list<bit::string> lt;
	bit::string s1("hello world");//拷贝构造 深拷贝
	lt.push_back(s1);//拷贝构造 深拷贝

	lt.push_back(bit::to_string(1234));
	//具体发生过程:拷贝构造 深拷贝 -> 移动构造 ->拷贝构造 深拷贝

	//bit::to_string(1234)返回值是右值,所以传给了push_back的右值引用版本,
	//但是右值被右值引用 引用后的属性是左值。
	//因为如果不这样,移动构造和移动赋值里的临时对象就无法修改,也就无法实现交换。
	//即右值不能被直接修改,但右值被右值引用后需要被修改。

	lt.push_back("11111");
	return 0;
}

7.5 完美转发

模板中的 && 万能引用
std::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) {
	//Fun(t);//都退化为左值
	//Fun(move(t));//这种方式会导致全部变成右值引用或const 右值引用

	//如果期望实参的属性与形参一致,使用完美转发
	Fun(forward<T>(t));
}

//完美转发不能再非模板使用,因为只有在模板中&&才代表万能引用,
//这里是右值引用,不能传左值
void PerfectForward(int&& t) {
	Fun(forward<int>(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 右值
	//虽然上面传的值有左值和右值,但调用后全部变成左值引用或const 左值引用
	//无论参数是以什么引用形式传入的(左值引用或右值引用),在函数内部使用时,这个参数名本身都是一个左值
	//也就是说,即使你传入的是一个右值引用,在函数内部使用这个参数名时,它仍然是一个左值
	//原因:C++不自动将具名变量(任何有名字的变量或参数)当作右值的核心原因:避免意外的资源窃取导致后续代码出错。
	return 0;
}


使用完美转发修改之前的list

List Class.h
​
#pragma once

namespace bit {
	template<class T>
	struct list_node { //节点结构体。
		T _data;
		list_node<T>* _next;
		list_node<T>* _prev;

		list_node(const T& x = T())//如果T是自定义类型,则x会是该类型的默认构造对象,取决于该类型的默认构造函数如何定义。
			:_data(x), _next(nullptr), _prev(nullptr) {
		}

		//为右值引用提供一个构造函数,不能带缺省参数,否则调用歧义
		list_node(T&& x)
			//:_data(move(x)), _next(nullptr), _prev(nullptr) {
			:_data(forward<T>(x)), _next(nullptr), _prev(nullptr) {
		}
	};


	template<class T, class Ref, class Ptr>//迭代器是模拟指针的行为
	struct __list_iterator { //迭代器
		typedef list_node<T> Node;
		typedef __list_iterator<T, Ref, Ptr> self;
		Node* _node;

		//迭代器可以理解为节点指针
		__list_iterator(Node* node)//单参数构造函数支持隐式类型转换,也就是说节点指针也可以隐式类型转换为迭代器
			:_node(node) {
		}

		self& operator++() { //前置++
			_node = _node->_next;
			return *this;
		}
		self& operator--() { //前置--
			_node = _node->_prev;
			return *this;
		}

		self operator++(int) { //后置++
			self tmp(*this);
			_node = _node->_next;
			return tmp;
		}
		self operator--(int) { //后置--
			self tmp(*this);
			_node = _node->_prev;
			return tmp;
		}

		Ref operator*() { return _node->_data; }
		Ptr operator->() { return &_node->_data; }//提供给自定义类型解引用。自定义类型解引用是 指针->形式,所以返回指针
		//非const和const迭代器中只有上面这两个运算符重载的返回值不同
		bool operator!= (const self& s) { return _node != s._node; }
		bool operator== (const self& s) { return _node == s._node; }
	};


	template<class T>
	class list {
		typedef list_node<T> Node;
	public:
		typedef __list_iterator<T, T&, T*> iterator;
		typedef __list_iterator<T, const T&, const T*> const_iterator;

		const_iterator begin() const { return _head->_next; }//支持隐式类型转换
		const_iterator end() const { return _head; }//支持隐式类型转换
		iterator begin() { return _head->_next; }
		iterator end() { return _head; }

		void empty_init() {
			_head = new Node;
			_head->_next = _head;
			_head->_prev = _head;
			_size = 0;
		}

		list() { empty_init(); } //构造函数本质是创建一个哨兵位节点

		//lt2(lt1)
		list(const list<T>& lt) { //lt是const对象,需要const迭代器
			empty_init();//创建(lt2的)哨兵位
			for (auto e : lt)//遍历lt1每个节点赋值给e
				push_back(e);//lt2尾插每个e节点
		}

		//现代写法
		void swap(list<T>& lt) {
			std::swap(_head, lt._head);
			std::swap(_size, lt._size);
		}
		list<T>& operator=(list<T> lt) {
			swap(lt);
			return *this;
		}

		~list() {
			clear();
			delete _head;//清除哨兵位
			_head = nullptr;
		}

		void clear() {
			iterator it = begin();//begin指向哨兵位的下一节点,即有效数据的头节点
			while (it != end())
				it = erase(it);
		}

		void push_back(const T& x) { insert(end(), x); }//end指向哨兵位,在哨兵位之前插入
		//void push_back(T&& x) { insert(end(), x); }//右值引用版本
		//void push_back(T&& x) { insert(end(), move(x)); }
		////右值被右值引用 引用后的属性是左值,这样insert时走的就是左值引用版本
		////所以move改变x的属性为右值
		void push_back(T&& x) { insert(end(), forward<T>(x)); }

		void push_front(const T& x) { insert(begin(), x); }
		void pop_back() { erase(--end()); }//哨兵位前一节点就是尾结点
		void pop_front() { erase(begin()); }

		iterator insert(iterator pos, const T& x) {
			Node* cur = pos._node;
			Node* newnode = new Node(x);
			Node* prev = cur->_prev;
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		iterator insert(iterator pos, T&& x) {
			Node* cur = pos._node;
			//Node* newnode = new Node(move(x));//右值引用版本
			//这里和push_back同理,右值被右值引用 引用后的属性是左值
			//这样走的就是左值引用版本的构造函数,所以move改变x的属性为右值
			Node* newnode = new Node(forward<T>(x));
			Node* prev = cur->_prev;
			//prev newnode cur
			prev->_next = newnode;
			newnode->_prev = prev;
			newnode->_next = cur;
			cur->_prev = newnode;
			++_size;
			return iterator(newnode);
		}

		iterator erase(iterator pos) { //erase后迭代器失效,因为迭代器指向的节点被释放了,所以返回下一个节点的位置
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* next = cur->_next;
			delete cur;
			prev->_next = next;
			next->_prev = prev;
			--_size;
			return iterator(next);
		}

		size_t size() { return _size; }

	private:
		Node* _head;
		size_t _size;//增加该成员避免每次获取size都要遍历一遍链表
	};
}

​

第八章:新的类功能

默认成员函数

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

  1.  构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载

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

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

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

  • 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
  • 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
  • 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
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:
	bit::string _name;
	int _age;
};
int main() {
	//屏蔽Person(const Person& p)、Person& operator=(const Person& p)、~Person()
	Person s1;
	Person s2 = s1;//调用默认的拷贝构造 string(const string& s)
	Person s3 = std::move(s1);//调用默认的移动构造 string(string&& s)

	return 0;
}

强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

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(Person&& p) = default;
private:
	bit::string _name;
	int _age;
};

禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

class Person {
public:
	Person(const char* name = "", int age = 0)
		:_name(name)
		, _age(age) {
	}
	Person(const Person& p) = delete;
private:
	bit::string _name;
	int _age;
};

第九章:可变参数模板

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(const T& val, Args... args) {
	cout << val << " ";
	_ShowList(args...);
}

template <class ...Args>
void ShowList(Args... args) {
	_ShowList(args...);
}
//ShowList把参数包args传给了_ShowList,然后_ShowList解析参数包第一个参数,并打印1。
//然后递归调用解析参数包第二个参数,以此类推,打印完123后,继续解析,发现里面是0个参数,
//就不匹配void _ShowList(const T& val, Args... args)  而是更匹配void _ShowList(),最后打印一个换行结束。
int main() {
	ShowList(1);
	ShowList(1, 2);
	ShowList(1, 2, 3);
	ShowList(1, 2.2, 'x', 3.3);
	return 0;
}

逗号表达式展开参数包

这种展开参数包的方式,不需要通过递归终止函数,是直接在expand函数体中展开的, printarg不是一个递归终止函数,只是一个处理参数包中每一个参数的函数。这种就地展开参数包的方式实现的关键是逗号表达式。我们知道逗号表达式会按顺序执行逗号前面的表达式。

expand函数中的逗号表达式:(printarg(args), 0),也是按照这个执行顺序,先执行printarg(args),再得到逗号表达式的结果0。同时还用到了C++11的另外一个特性——初始化列表,通过初始化列表来初始化一个变长数组, {(printarg(args), 0)...}将会展开成((printarg(arg1),0), (printarg(arg2),0), (printarg(arg3),0), etc... ),最终会创建一个元素值都为0的数组int arr[sizeof...(Args)]。由于是逗号表达式,在创建数组的过程中会先执行逗号表达式前面的部分printarg(args)打印出参数,也就是说在构造int数组的过程中就将参数包展开了,这个数组的目的纯粹是为了在数组构造的过程展开参数包

//方式一
template <class T>
void PrintArg(T t) {
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args) {
	//PrintArg(args)是展开参数包的,逗号表达式是为了顺序执行且返回最后一个值,
	//就是展开参数包后再返回0。因为0才能去初始化数组,参数包的返回值(void)不能初始化数组。
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}

//方式二
template <class T>
int PrintArg(T t) {
	cout << t << " ";
	return 0;
}
//展开函数
template <class ...Args>
void ShowList(Args... args) {
	//数组开多大空间取决于参数包有几个参数
	//有几个参数PrintArg就会调用几次
	int arr[] = { PrintArg(args) };
	cout << endl;
}

int main() {
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

演示可变参数包使用场景

namespace bit {
	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) { //const左值引用既可以引用左值,也可以引用右值
			cout << "string(const string& s) -- 拷贝构造 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		//移动构造
		string(string&& s) {
			cout << "string(string&& s) -- 移动构造" << endl;
			swap(s);//this:指向正在构造的临时对象(内存由编译器分配)
			//临时对象不新开空间
		}

		// 赋值重载
		string& operator=(const string& s) {
			cout << "string& operator=(string s) -- 赋值重载 深拷贝" << endl;
			//string tmp(s);
			//swap(tmp);
			if (this != &s) {
				char* tmp = new char[s._capacity + 1];
				strcpy(tmp, s._str);
				delete[] _str;
				_str = tmp;
				_size = s._size;
				_capacity = s._capacity;
			}
			return *this;
		}
		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 = nullptr;
		size_t _size = 0;
		size_t _capacity = 0; // 不包含最后做标识的\0
	};
}

int main() {
	std::list<bit::string> lt;
	
	//该场景下,push_back 和 emplace_back没区别
    bit::string s1("111");
	lt.push_back(s1);//拷贝构造 深拷贝
	lt.push_back(move(s1));//移动构造
    
    //void emplace_back (Args&&... args);
	bit::string s2("111");
	lt.emplace_back(s2);//拷贝构造 深拷贝
	lt.emplace_back(move(s2));//移动构造


	//插入匿名对象
	lt.push_back("xxx");//调用构造 和 移动构造
	lt.emplace_back("xxx");//调用构造
	//push_back 的声明是 void push_back(const T& val) 或 void push_back(T&& val)。
	//"xxx" 是 const char* 类型,需要先构造一个临时 bit::string 对象。
	//然后调用 push_back(T&& val)(临时对象是右值,触发移动构造)。

	//emplace_back 的声明是 template <class... Args> void emplace_back(Args&&... args)。
	//它直接将参数包 Args... 转发到 bit::string 的构造函数:
	//对于 emplace_back("xxx"),Args 推导为 const char* 。
	//直接在 list 的节点内存中调用 bit::string("xxx"),省去临时对象和移动操作。

	return 0;
}

多参数场景

int main() {
	std::list<pair<bit::string, int>> lt;
	lt.push_back(make_pair("1111", 1));//构造 + 移动构造
	lt.emplace_back("2222", 2);//构造
	//行为	    push_back	                emplace_back
	//构造时机	先构造临时对象,再放入容器	直接在容器内构造
	//参数处理	必须构造完整对象传入	        接受参数包,延迟构造
	//效率	    可能有额外拷贝 / 移动	        更高效,避免临时对象
	return 0;
}

第十章:lambda表达式

10.1 C++98中的一个例子

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

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

struct Goods {
	string _name; //名字
	double _price; //价格
	int _evaluate; //评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate) {
	}
};

struct ComparePriceLess {
	bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; }
};
struct ComparePriceGreater {
	bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; }
};

int main() {
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	//sort无法直接排序,需要自定义仿函数(即比较规则)
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	
	return 0;
}

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

10.2 lambda表达式

struct Goods {
	string _name; //名字
	double _price; //价格
	int _evaluate; //评价
	Goods(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate) {
	}
};

struct ComparePriceLess {
	bool operator()(const Goods& gl, const Goods& gr) { return gl._price < gr._price; }
};
struct ComparePriceGreater {
	bool operator()(const Goods& gl, const Goods& gr) { return gl._price > gr._price; }
};

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)->bool {return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._evaluate > g2._evaluate; });
	return 0;
}

10.3 lambda表达式语法

lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

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

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

2. 捕获列表说明

捕捉列表描述了上下文中那些数据可以被 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表达式之间不能相互赋值,即使看起来类型相同

lambda表达式 基本使用

int main() {
	auto f1 = [](int x)->int {cout << x << endl; return 0; };
	f1(1);
	cout << typeid(f1).name() << endl;
	//class <lambda_620c015845ff2142109bf3f80a50cb0a>
	//每个lambda都是一个类,f1就是一个类对象
	//f1对象可以调用operator(),所以lambda本质是仿函数

	//省略返回类型,编译器自动推导
	auto f2 = [](int x) {
		cout << x << endl; 
		return 0; 
	};
	f2(2);
	cout << typeid(f2).name() << endl;
	//class <lambda_5d21ffa5b2bbbd0b5a72ac1266224191>

	ComparePriceGreater f3;
	cout << typeid(f3).name() << endl;
	//struct ComparePriceGreater
	return 0;
}

lambda的其他用法

int main() {
	//使用lambda交换两个元素
	int x = 0, y = 1;
	cout << x << " " << y << endl;
	auto f1 = [](int& r1, int& r2) {
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	};
	f1(x, y);
	cout << x << " " << y << endl;


	cout << x << " " << y << endl;
	//[]可以捕获参数作为该类的成员变量,也就是仿函数的参数
	// ()中的参数默认是const修饰
	//所以加mutable可以取消其常量性
	//不需要参数传递()可以省略,但使用mutable参数列表不可省略
	//auto f2 = [x, y] () mutable { //交换失败,因为这是传值捕获
	//auto f2 = [&x, &y] () mutable { //修改为传引用捕获

	//mutable 的作用是允许 lambda 表达式修改 按值捕获的变量。
	//mutable 不影响引用捕获,因为 mutable 控制的是 lambda 对象本身的状态,
	//而引用捕获只是别名,修改的是外部变量。
	auto f2 = [&x, &y] { //可以省略() mutable
		int tmp = x;
		x = y;
		y = tmp;
	};
	f2();
	cout << x << " " << y << endl;

	return 0;
}

[]捕获用法

class AA {
public:
	void func() {
		//捕获列表 [] 只能捕获 当前作用域内的局部变量 或 this 指针(对于成员函数),
		//而不能直接捕获 类的成员变量(如 _a1 和 _a2)。
		//在 AA::func() 内部,_a1 和 _a2 是 类的成员变量,而不是 局部变量。
		//C++ 的 lambda 捕获机制 只能捕获当前作用域内可见的变量(如局部变量、函数参数、全局变量等),
		//而成员变量必须通过 this 指针访问。
		//auto f1 = [_a1, _a2] {//不可行

		//auto f1 = [this] {
		auto f1 = [=] {
			cout << _a1 << endl;
			cout << _a2 << endl;
		};
		f1();
	}
private:
	int _a1;
	int _a2;
};

int main() {
	int x = 0, y = 1, z = 2;
	auto f1 = [=, &z] {
		z++;
		cout << x << endl;
		cout << y << endl;
		cout << z << endl;
	};
	f1();
	//捕获列表[]:用于 让 lambda 访问外部变量(相当于给 lambda 类添加了成员变量)。
	//参数列表():用于 在调用 lambda 时传入临时参数(相当于函数参数)。
	//它们互相独立,捕获变量后仍然可以(或需要)参数列表,取决于你的需求。
	return 0;
}

10.4 函数对象与lambda表达式

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

从使用方式上来看,函数对象与lambda表达式完全一样。

函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。

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

第十一章:包装器

function 包装器 也叫作适配器。 C++ 中的 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() {
	// 函数名
	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函数模板实例化了三份。

包装器可以很好的解决上面的问题

std::function在头文件<functional>

// 类模板原型如下
template <class T> function;     // undefined

template <class Ret, class... Args>
class function<Ret(Args...)>;

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

使用方法
void swap_func(int& r1, int& r2) {
	int tmp = r1;
	r1 = r2;
	r2 = tmp;
}

struct Swap {
	void operator()(int& r1, int& r2) {
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	}
};

// 包装函数名(函数指针)\函数、函数对象、lamber表达式
int main() {
	int x = 0, y = 1;
	cout << x << " " << y << endl;
	auto swaplambda = [](int& r1, int& r2) {
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	};

	//function<void(int&, int&)> f1(swap_func);
	function<void(int&, int&)> f1 = swap_func;
	f1(x, y);
	cout << x << " " << y << endl;

	function<void(int&, int&)> f2 = Swap();
	f2(x, y);
	cout << x << " " << y << endl;

	function<void(int&, int&)> f3 = swaplambda;
	f3(x, y);
	cout << x << " " << y << endl;

	map<string, function<void(int&, int&)>> cmdOP = { {"函数指针",swap_func},{"仿函数",Swap()}, {"lambda",swaplambda} };
	cmdOP["函数指针"](x, y);
	cout << x << " " << y << endl;
	cmdOP["仿函数"](x, y);
	cout << x << " " << y << endl;
	cmdOP["lambda"](x, y);
	cout << x << " " << y << endl;
	return 0;
}

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);

可以将bind函数看作是一个通用的函数适配器,它接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
调用bind的一般形式:auto newCallable = bind(callable,arg_list);

其中,newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中的参数。

arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的“位置”。数值n表示生成的可调用对象中参数的位置:_1为newCallable的第一个参数,_2为第二个参数,以此类推。

演示bind用法

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

int main() {
	function<int(int, int)> f1 = Sub;
	cout << f1(10, 5) << endl;//5

	//调整参数的顺序
	function<int(int, int)> f2 = bind(Sub, placeholders::_2, placeholders::_1);
	cout << f2(10, 5) << endl;//-5

	//调整参数的个数,有些参数可以bind时写死。20固定是第一个
	function<int(int)> f3 = bind(Sub, 20, placeholders::_1);
	cout << f3(5) << endl;//15
	return 0;
}

包装类的成员函数

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() {
	//成员函数取地址要加&和类域,虽然静态成员函数不用&,但建议加上
	function<int(int, int)> f1 = &Plus::plusi;
	cout << f1(1, 2) << endl;

	////成员函数参数中还有this指针,所以不匹配
	//function<double(double, double)> f2 = &Plus::plusd;

	function<double(Plus*, double, double)> f2 = &Plus::plusd;
	Plus ps;
	cout << f2(&ps, 1.1, 2.2) << endl;
	//cout << f2(&Plus(), 1.1, 2.2) << endl;//不可以传匿名对象(是右值),左值才能取地址

	//特殊处理,相当于用对象调函数
	function<double(Plus, double, double)> f3 = &Plus::plusd;
	cout << f3(Plus(), 1.1, 2.2) << endl;

	//使用bind绑定第一个参数
	function<double(double, double)> f4 = bind(&Plus::plusd, Plus(), placeholders::_1, placeholders::_2);
	cout << f4(1.1, 2.2) << endl;
	return 0;
}

作业

1. 下面关于右值引用作用说法错误的是()

A.右值引用于引用一样,只是一个别名,别无它用
B.通过右值引用可以实现完美转发
C.通过右值引用可以将临时对象中的资源转移出去
D.通过右值引用可以实现移动构造函数,提高程序运行效率

答案:A
A:错误,右值引用是C++11中的一个重点,可以实现移动语义、完美转发
B:正确,实现完美转发时,需要用到forward函数
C:正确,右值引用的一个主要作用就是实现移动语义,以提高程序的效率
D:正确

2. 关于引用和右值引用的区别,说法正确的是()

A.引用只能引用左值,不能引用右值
B.右值引用只能引用右值,不能引用左值
C.引用和右值引用都可以引用左值和右值
D.以上说法都不对

答案:C
A:错误,T& 只能引用左值,const T& 是万能引用,左值和右值都可以引用
B:错误,一般右值引用只能引用右值,如果需要引用左值时,可以通过move函数转
C:正确,参考A和B的解析
D:错误

3. 关于右值引用说法正确的是()

A.引用是别名,使用比指针更方便安全,右值引用有点鸡肋
B.右值引用不能引用左值
C.右值引用只能引用右值
D.右值引用于引用一样,都是别名

答案:D
A:错误,右值引用是C++11中的一个重点,可以实现移动语义、完美转发
B:错误,一般右值引用只能引用右值,如果需要引用左值时,可以通过move函数转
C:错误,同上
D:正确,右值引用也是引用,是别名

4. 下面关于lambda表达式说法错误的是()

A.lambda表达式的捕获列表不能省略,但可以使空
B.lambda表达式就是定义了一个函数
C.lambda表达式底层编译器是将其转化为仿函数进行处理的
D.[]{}是一个lambda表达式

答案:B
A:正确,捕获列表是编译器判断表达式是否为lambda表达式的依据,即使为空,也不能省略
B:错误,lambda表达式不是一个函数,在底层编译器将其转化为仿函数
C:正确
D:正确,lambda表达式原型:[捕获列表](参数列表)mutable->返回值类型 {}
如果不需要捕获父作用域中内容时,可以为空,但是[]不能省略,如果没有参数,参数列表可以省略
如果不需要改变捕获到父作用域中内容时,mutable可以省略,返回值类型也可以省略,让编译器根据返回的结果进行推演,但是{}不能省略,因此[]{}是最简单的lambda表达式,但是该lambda表达式没有任何意义

5. 下面关于默认的构造函数说法正确的是()

A.C++98中用户可以选择让编译器生成或者不生成构造函数
B.在C++11中,即使用户定义了带参的构造函数,也可以通过default让编译器生成默认的构造函数
C.delete的作用是删除new的资源,别无它用
D.以下代码会编译通过: class A { public: A(){} A(int)=delete; }

答案:B
A:错误,C++98中用户不能选择,用户如果没有定义构造函数,编译器会生成默认的,如果显式定义,编译器将不再生成
B:正确,C++11扩展了delete和default的用法,可以用来控制默认成员函数的生成与不生成
C:错误,C++11扩展了delete的用法,可以让用户控制让编译器不生成默认的成员函数
D:错误,类定义结束后没有分号

6. 下面关于override说法正确的是()

A.override的作用发生在运行时期
B.override修饰子类成员函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写
C.override可以修饰基类的虚函数
D.override只能修饰子类的虚函数

答案:D
A:override的作用时让编译器帮助用户检测是否派生类是否对基类总的某个虚函数进行重写,如果重写成功,编译通过,否则,编译失败,因此其作用发生在编译时。
B:错误,修饰子类虚函数时,编译时编译器会自动检测是否对基类中那个成员函数进行重写
C:错误,不能,因为override主要是检测是否重写成功的,而基类的虚函数不可能再去重写那个类的虚函数
D:正确

7. 下列关于final说法正确的是()

A.final只能修饰类,表示该类不能被继承
B.final可以修饰任意成员函数
C.final修饰成员函数时,表示该函数不能被子类继承
D.final修饰派生类虚函数时,表示该虚函数再不能被其子类重写

答案:D
A:错误,final修饰类时,表示该类不能被继承,修饰派生类的虚函数时,表示该虚函数不能被子类继承
B:错误,只能修饰派生类的虚函数
C:错误,修饰派生类虚函数时
D:正确

8. 下面关于列表初始化说法正确的是()

A.C++语言从C++98开始,就支持列表方式的初始化
B.列表初始化没有什么实际作用,直接调用对应的构造函数就可以了
C.自定义类型可以支持多个对象初始化,只需要增加initializer_list类型的构造函数即可
D.以下关于c和d的初始化,结果完全相同 // short c = 65535; short d { 65535 };

答案:C
A:错误,列表初始化是从C++11才开始支持的
B:错误,列表初始化可以在定义变量时就直接给出初始化,非常方便
C:正确
D:错误,不同,列表初始化在初始化时,如果出现类型截断,是会报警告或者错误的

9. 下面关于列表初始化说法错误的是()

A.在C++98中,{}只能用来初始化数组
B.在C++98中,new单个int类型空间可以直接初始化,new一段连续int类型空间不能直接初始化
C.在C++11中,{}的初始化范围增大了,任意类型都可以初始化
D.使用{}初始化时,必须要加=号

答案:D
A:正确,C++98中只能初始化数组,C++11中支持列表初始化,才可以初始化容器
B:正确,C++11对于new[]申请的空间,可以直接初始化
C:正确,对于自定义类型,需要提供initializer_list<T>类型的构造函数
D:错误,加 = 和不加 = 没有区别

10. 下面关于范围for说法错误的是()

A.范围for可以直接应用在数组上
B.对于STL提供的所有容器,均可以使用for依次访问器元素
C.使用范围for操作stack,可以简化代码
D.对于自定义类型,想要支持范围for,必须提供begin和end迭代器
E.范围for编译器最终是将其转化为迭代器来进行处理的

答案:C
A:正确,只要是范围确定的,都可以直接使用范围for
B:正确,对于STL提供的容器,采用范围for来进行遍历时,编译器最后还是将范围for转化为迭代器来访问元素的
C:错误,stack不需要遍历,也没有提供迭代器,因此不能使用范围for遍历
D:正确,因为范围for最终被编译器转化为迭代器来遍历的
E:正确

11. 下面关于auto的说法错误的是:()

A.auto主要用于类型推到,可以让代码的书写更简洁
B.在C++11中,auto即可以用来声明自动类型变量,也可以用来进行变量类型推导
C.auto不能推导函数参数的类型
D.auto是占位符,在编译阶段,推演出初始化表达式的实际类型来替换auto位置

答案:B
A:正确
B:错误,C++11中已经去除了auto声明自动类型变量的功能,只可以用来进行变量类型推到
C:正确,因为函数在编译时,还没有传递参数,因此在编译时无法推演出形参的实际类型
D: 正确,auto仅仅只是占位符,编译阶段编译器根据初始化表达式推演出实际类型之后会替换auto
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值