C++11

本期我们来学习C++11

目录

C++11简介

统一的列表初始化

{}初始化

std::initializer_list

声明

auto

decltype

nullptr

范围for循环

STL中一些变化

右值引用和移动语义

完美转发

lambda表达式

新的功能

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

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

可变模板参数

包装器

function

bind


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 。

统一的列表初始化

{}初始化

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

cpp 复制代码
struct Point
{
	Point(int x,int y)
		:_x(x)
		,_y(y)
	{
		cout << "Point(int x,int y)" << endl;
	}
	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与其的对比

C++11 扩大了用大括号括起的列表 ( 初始化列表 ) 的使用范围,使其可用于所有的内置类型和用户自
定义的类型, 使用初始化列表时,可添加等号 (=) ,也可不添加
C++11想做到的是一切皆可用{ } 初始化,并且可以不写 =

这三者是等价的,都会调用构造函数
这里第三种写法很容易和第一种混淆,所以我们不推荐在实际中不写 = ,容易让人产生误解,这是C++11的一个不好的地方

想让{ } 来初始化一切可能是为了方便这种写法,纠结[ ] 和 { } 间不写 = ,但是也导致了上面的误导,这里ptr3初始化的是匿名对象
我们建议日常定义中不要去掉 = ,但是我们要能看懂

另外,这种写法本质是多参数的构造函数支持隐式类型转换,和单参数的是一样的
如果我们不想让这种事情发生,可以加一个关键字

explicit这个关键字

此时这些地方就都不能通过了
我们再把explicit这个关键字先去掉

此时这种写法是不支持的

但是我们加上const就好了
原因是{3,3}会生成临时对象,临时对象具有常性,这里和以前的单参数是一样的

std::initializer_list

了解了上面的特性,我们来看一个问题

这两个是不是一样的?

答案是不一样

vector后面是可以不断加值的,而point不行

point是多参数构造支持隐式类型转换,而vector是直接的构造

这里可以直接调用构造的原因是C++新增了一个类型,叫做initializer_list

我们来看看什么是initializer_list

也就是说,只要是一个{ } 括起来的列表,就可以识别成**initializer_list,**而{ }里有几个值,它不关心

它还有size,begin,end等等,大家猜一猜它是怎么实现的

{10,20,30} 是一个常量数组,存在常量区里

我们这里sizeof一下,是8,如果是64位下就是16

它的就是使用了两个指针,一个指向数组的起始位置,一个指向结尾的下一个位置,可以帮助我们读取数组,注意,只可以读,不可以写

也就是说,给了我们一个常量数组,我们去调用这个函数的构造,然后让指针指向位置

这里的ptr1是不支持的,因为会有冲突

我们可以直接写,这里就是直接调用initializer_list的构造函数

我们再回到原理的问题,vector是如何支持的?

是因为它直接写了一个支持initializer_list的构造函数

这也是为什么之前point的{ } 里不可以增加值,而vector可以,point的构造函数参数需要一一对应,而initializer_list可没说{ } 里要写多少个值

这里的实现也非常简单,使用reserve,然后用范围for即可,我们可以把我们之前的代码拿出来,加上这个,也可以支持initializer_list

这是我们自己的vector

cpp 复制代码
        vector(initializer_list<T> lt)
		{
			reserve(lt.size());
			for (auto e : lt)
			{
				push_back(e);
			}
		}

我们加一个构造即可

此时就成功了

map不可以这样写

因为map这里的T是一个pair

要这样写才行 ,这里是一个双重含义,外边的括号是initializer_list,里面的是类似之前point的

另外不止是构造,有时候赋值也是支持的

声明

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

auto

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

我们之前也经常使用auto,这里就不在讲解

decltype

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

假设我们这里想定义一个和pf相同类型的变量,我们该怎么办呢?typeid只能看却不能用,我们的一种办法是使用auto,pf1 = pf,但是这里是初始化了,如果我们不想初始化呢?

我们就可以使用decltype

我们来看一个使用场景

我们这里创建bb时,就可以使用decltype,就方便了很多

typeid推出的类型是一个字符串,只能看不能用,decltype可以把对象的实际类型推出,这个类型可以用来再定义变量,或者作为模板的实参

他还可以推导表达式,在一些特殊的情况下我们是会用到的

nullptr

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

cpp 复制代码
#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

范围for循环

范围for也是我们经常使用的,这里也就不再介绍

STL中一些变化

首先就是新容器,其中unordered我们之前已经进行了详细讲解,这里就不再多说

array就是静态数组,类型上不一样,物理上一样

对于普通的静态数组,越界是可能检查不出来的

而array是可以的,a1[15]会转换为指针的解引用,而a2[15]是operator[ ]函数调用,是内部检查

array非常的鸡肋,他的初衷是想替代静态数组

但是我们为什么不直接用vector呢?还可以初始化(C++11里还增加了不少类似array一样鸡肋的东西,所以一直在被骂)

forward也是非常鸡肋,是一个单链表

他只支持头插头删,不支持尾插尾删,他的insert是在当前位置之后插入

他的唯一优点可能就是每一个节点少了一个指针。。。

新容器看完了我们来看新接口

他加了一堆cbegin,cend等等,这是const对象的,这也是被吐槽的一个地方

还增加了支持initializer_list,所有容器均支持{ }列表初始化的构造函数,这个还是很不错的

还有一个重大更新,这个算一个黑科技

所有容器均新增了emplace系列,这里涉及右值引用和模板可变参数,我们下面会讲

可以使性能提升,有效地方是相当大的

push_back这些也改了,增加了一个重载的右值引用版本

所有的容器都增加了移动构造和移动赋值,这可以使我们不用再担心深拷贝,使深拷贝的性能提高了百分之90

这些一切的矛头都指向了右值引用和移动语义,下面我们就来看看他们到底是什么吧

右值引用和移动语义

传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名
什么是左值?什么是左值引用?
左值是一个表示数据的表达式 ( 如变量名或解引用的指针 ) , 我们可以获取它的地址,一般 可以对它赋 值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边 。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值 ( 这个不能是左值引用返回) 等等, 右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址 。右值引用就是对右值的引用,给右值取别名。

左值和右值的区别就是是否可以取地址,左值可以取地址,右值不行

看这里的报错我们也可以看出来

我们看这个常量字符串,也是左值,只是不能被修改而已

引用是取别名,左值引用就是给左指取别名,右值引用就是给右值取别名

右值引用是使用两个&&

这里的r6如果用一个&就编译不过了

那么左值引用能否可以给右值取别名,右值引用能否给左指取别名呢?

这里是不行的

但我们加上const就可以了,没有左值引用之前有些地方也得引用右值

我们在之前的函数在不修改的时候都会告诉大家尽量加上const,加上const的好处就是既可以引用左值也可以引用右值

这里的r3同样不行

我们再加上const就可以了

所以const左值引用是可以给右值取别名

直接引用也是不行的

但我们move一下就可以了,这个move可能会带来其他影响(这里没有)

所以右值引用可以引用move以后的左值

左值引用的使用场景和价值是什么?

使用场景:1.做参数,2.做返回值,价值是减少拷贝

左值引用在哪些场景下解决问题不到位呢?

局部对象的返回不能用左值引用,这是C++没有处理好的场景

比如这里

即使加上const也无法解决问题

这里的关键在于str的生命周期结束了,传值返回是返回拷贝,拷贝就有代价

这里用左值引用也可以,不需要const的,因为str本身就是左值,但是str本身已经销毁了,引用那块空间又如何?

这是第一个问题,栈帧销毁后,还有一些其他问题,str指向的空间也是被销毁的(调用了析构函数)

所以我们以前解决这种问题时是传值返回

str在返回时会先拷贝临时对象,然后再把临时对象拷贝给ret,但是这里连续拷贝两次,如果str很大的话,比如100w字节,那么代价就太大了,所以编译器在这里进行了优化,只拷贝了一次

但即使编译器优化了,代价还是太大了,而且有些场景甚至无法优化

比如这里

下面我们利用一份简洁的string来验证一下

cpp 复制代码
namespace bai
{
	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
	};
}

我们看结果 ,这里第一个深拷贝是ret1的,其他3个是ret2的,不过ret2这里其实只调用了两次深拷贝,这里的赋值,他的拷贝是利用拷贝构造完成的,我们用的是现代写法

我们接着往下看

首先,这里的两个func是构成函数重载的,左值会匹配上面的,右值会匹配下面的

那此时呢?上面的加了const,调用是否存在歧义?现在都能接收右值

首先是构成重载的,这里编译并不会报错

并且没有调用歧义,编译器会自动寻找最合适的

我们把内置类型的右值叫做纯右值 ,自定义类型的右值叫做将亡值

我们看自定义类型,比如 s1+s2,这里就是运算符重载,我们再看to_string,这里就是函数调用

我们看s1+s2,是一个表达式,但本质还是函数调用(调用operator+),他的返回值是一个临时对象,就是一个右值,是一个将亡值

我们看这里,str是会拷贝临时对象,然后临时对象再拷贝给ret2,func()这个表达式的返回值就是一个右值,一个将亡值,为什么叫将亡值呢?因为他的生命周期只在这一行,下一行就会析构

ret2 = func(),这里是一个赋值,如果右边的是左值,这里只能老老实实去拷贝,是深拷贝,如果是右值将亡值,这里就可以用移动拷贝,就可以直接把资源拿过来,还会把自己不要的资源扔掉

我们运行一下

这里代价就小了很多 ,拷贝构造就变成了移动构造,移动构造是直接转移资源,代价要小很多

右值引用可以在某些场景下极大的提高效率

我们再来看一个场景

我们先看这段代码

对于拷贝构造,也有移动拷贝

我们知道,这里编译的优化指的是合二为一,连续的构造/拷贝构造都会合二为一

那么这里合二为一后是拷贝构造还是移动构造呢?按我们的理解应该是拷贝构造,因为str是一个左值,但是这样的场景太多了,str符合将亡值的特征,出了作用域就没了,所以编译器会把str识别成右值-将亡值

所以这里我们的运行结果是移动拷贝,这样就降低了代价,提高了效率,如果这里返回的是map,那么效率的提高是非常客观的

这里是一样的,把str识别成了将亡值

直接转移资源,效率得到了提升

大家要记住,左值引用的核心是减少拷贝,提高效率,右值引用的核心价值是进一步减少拷贝,弥补左值引用没有解决的场景,如传值返回

这句话里,右值引用的重点是弥补左值引用没有解决的场景,场景1:自定义类型中深拷贝的类,必须传值返回的场景

浅拷贝的类,移动构造不需要实现,传值返回拷贝代价不是很大,也就是说,右值引用是专门用来解决自定义类型中深拷贝的类

另外大家要注意move

我们看调试的监视窗口,copy1和2没什么问题,包括move(ret2)后再构造copy2,但是我们看copy3,这里就是直接把ret2的资源给copy3了,这里说明返回的ret2的右值,move的底层实现是有些复杂的,所以大家把一个值move后再拷贝构造,就赋予了可以抢走资源的权力,这里大家要注意一下,move的意义就是我们想把一个值的资源交给另一个,就可以使用move,move不会改变属性,而move返回的值的属性会被改变

我们再看下一个场景

我们之前会认为这两段代码没什么区别,但是我们看看结果

首先这里是list

我们尾插了一个节点,里面存的是string

这是以前的push_back(c++98),s1传给了val,val是s1的别名,s2传给val,val是s2的别名

如果是库里面,要进行如下操作,Node* newnode = new Node(val),这里会调用Node的构造函数

所以这里的s1构造时其实是有点麻烦的

一直到构造函数这里,才会进行拷贝构造

这里要开一个和s1一样大的空间

但如果这里是一个右值呢?C++11之后我们可以使用右值引用

我们看下面的函数

这里就会经历和s1一样的情况,一直到构造函数,不过到了构造函数时,这里会将资源直接转移过去,不过我们日常的push_back都不会这样写

而是这样写的,这样写也是移动构造,因为这里传参时不能直接传给val,而是会先构造临时对象,是一个右值,资源就会直接转移过去,也就是说,这样写比以前的方式要少拷贝一次

如果我们把移动构造的代码屏蔽掉,这里的结果就是三个深拷贝,也就是说三个push全是构造+拷贝构造,这里为什么右值也会出现深拷贝呢?

因为我们是const引用

那么右值引用出现的意义是什么呢?我们就可以区分左值和右值,如果是左值,我们就深拷贝,是右值的话我们就转移资源

这里总结一下,在容器的插入接口,如果插入对象是右值,可以利用移动构造转移资源给数据结构中的对象,也可以减少拷贝,所以,所有的容器在C++11时都增加了右值引用版本

完美转发

cpp 复制代码
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);
}

int main()
{	
	PerfectForward(10);				// 右值
	int a;
	PerfectForward(a);				// 左值
	PerfectForward(std::move(a));	// 右值
	const int b = 8;
	PerfectForward(b);				// const 左值
	PerfectForward(std::move(b));   // const 右值
	return 0;
}

我们看上面的代码,PerfectForward函数是一个模板函数,使用了右值引用,我们下面传入了一个左值a,是否可以呢?

按照我们前面学习的知识是不行的

但是这里是可以的,模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。

如果这里是一个具体的类型,那他是写死的,就是一个右值引用,但如果是模板,那就是万能引用,如果实参是左值,就是左值引用(也叫做引用折叠)

如果是一个左值,这里推模板时

相当于int&&折叠成int&

实参是右值,就是一个右值引用,这里就没有什么折叠了

我们再看看运行的结果,非常奇怪,怎么都是左值引用呢?难道都折叠了吗?

我们修改成这样的代码,这里的结果呢?

这里大家要知道一个知识

r 和 rr 都是左值,地址是一样的

我们知道右值不能取地址,右值也不能修改

但右值引用是需要支持修改的

这里str被识别为右值,需要把str的资源转移给ret,这里是移动构造

这里是需要修改s的,如果s的属性是右值,那就出问题了

我们这样理解,你是右值,不能修改,我是你的引用,会开一块空间,把你储存起来

右值引用变量的属性会被编译器识别成左值,否则是移动构造的场景下,无法完成资源转移,必须要修改

此时我们回过头来看,虽然这里是右值引用,但t的属性是左值,所以才会有那样的结果

如果我们就是想让t保持原有的属性呢?这里就要用到完美转发

完美转发是库里面的一个函数,如果t是左值引用,保持左值属性,是右值引用就保持右值属性

下面我们来修改一下我们以前的链表

C++-----list_c++ list是什么结构-CSDN博客

我们先把上面拷贝构造这里的代码屏蔽一点

我们看这里的运行结果,都是构造+深拷贝,但是我们的string是写了移动构造的

这里换成std的就可以调用移动构造

另外我们仔细对比,会发现我们自己的s1这里调用了两次深拷贝?其实不是,这里是list的哨兵位节点,我们使用的是new

就是这样导致的,new会先调用构造,库里面使用的内存池,哨兵位的只开空间,而不调用new,所以没有

按照我们以前写的,不管我们传入左值还是右值,最后都会匹配到左值引用上

所以我们需要加一些东西

首先我们先加一个来区分左值和右值,下一步他会调用insert,所以我们要给insert也加一个

我们还要继续加

在最开始的位置也要加一个区分,此时运行会报错

我们需要先修改成这样

还要在empty这里加一个匿名对象

此时我们再看结果,怎么还是这样呢?

这里大家调试一下就会发现,和之前的原因一样,右值引用的属性会被编译器识别为左值

所以我们需要用到完美转发

这里也需要修改,只要我们想传到下一层,并且保持属性不变,就需要完美转发

此时运行还是有问题,我们接着调试

最后发现这里也是需要修改的,遇到问题我们就调试一下

此时我们再看结果,就解决了

下面我们讨论一些别的问题

这里的构造我们可以删掉一个吗?

答案是不行的,显示这里有问题

这里可能会有疑问,这不是模板吗?

但注意,这是类的模板,万能引用有个前提,这个T并不是实例化,而是推出来的

而这里的T并不是推出来的,是在list_node那里就实例化了

那如果我们就是想要删掉这个,有什么办法吗?

那就要写出这样

lambda表达式

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

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2,
3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end());
}

我们有一个商品,需要对他进行排序,这里使用默认的sort是会报错的

我们重载一个operator小于或者大于可以解决吗?可以

那如果我们第一次排序需要对价格进行排序,第二次排序需要对评价进行排序,那又该怎么办呢?

这里就不可以了,因为operator我们只能重载一个,这里只有仿函数才能解决问题

cpp 复制代码
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;
	}
};
struct CompareEvaluateGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._evaluate > gr._evaluate;
	}
};

命名也是一个问题,我们的命名是比较规范的,外一有人写了个compare1,compare2等等,那怎么办呢?下面我们就来看看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 函数不能做任何事情

写出来就是这样的,我们目前不用管捕捉列表,那么怎么使用呢?

这里使用auto就非常舒服

我们的排序就可以这样写了

另外这个返回值是可以省略的

我们可以这样使用,不用auto推出了再传进去

此时就可以有各种排序,这样看是不是就比仿函数好用呢?

lambda是一个可调用对象,在C++里,还有函数指针,仿函数也都是可调用对象

函数指针建议能不用就不用,写起来难受,看起来也难受

仿函数就是一个类,重载operator(),对象可以像函数一样使用,一般在模板使用,但是有些地方使用不合适,所以就有了lambda

lambda是一个匿名函数对象,写出来后一般传给auto,或者模板使用,一般在函数内部直接定义使用

大家要记住lambda的语法,其中->int 因为会自动推导,所以很多时候就忽略了

里面的函数体也可以写多行,比如这里我们写了一个swap,大家可以调试看看他是怎么走的

我们在函数体里调用其他函数是什么结果?我们来看一看

这里报错了,下面我们调用一个全局的看看

这里是可以调用的,那我们想要调用局部的该怎么办呢?

我们先看看捕捉列表的使用,这里就是捕捉了rate,可以直接使用,并且我们的add2没写返回值类型,他可以自动推出来

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

第一种就是传值捕捉,对应的还有传引用捕捉

首先,我们捕捉了x和y后是不能修改x和y的

传值捕捉是拷贝过去的,也就是说我们int x,和swap1 = [ x , y] 中两个x不是同一个x,函数体里调用时还是会建立一个栈帧的,我们可以认为是一个函数调用,并且是const的,不能修改

但是我们加上mutable就可以修改的,mutable的意思是可变的

不过即使里面的x和y交换了,外边的x和y也没有交换,和我们最初学的函数传值调用是一样的

如果我们想要让外面的x和y改变,这里就要用引用捕捉

这里看着像取地址,但其实是引用

假设外面有很多变量,我们一个一个写太麻烦了,怎么办呢?

这时候就可以这样写,这里我们还省略了参数列表

我们还可以组合起来使用

这里有一个const变量e,也可以捕捉

但是e是不能修改的

我们可以看到e确实被捕捉了

这两个e的地址还是一样的,所以引用捕捉是很灵活的,普通变量就是普通捕捉,const变量就const捕捉

回到最初的话题,想要使用add,捕捉一下就行

这里f2也不能赋值给f1

我们把他们的类型打印出来,非常的长,还不一样,是不同的类

这里就有疑问了,他们不是匿名对象吗?怎么是类?

lambda和范围for其实有点像,我们没学习范围for前看着很神奇,但底层就是迭代器而已,lambda的底层是仿函数,也就是说,我们定义了f1和f2,编译器生成了两个类

后面的一长串是uuid,每次生成的都不一样,有兴趣的大家可以百度一下,这样做可以让每个lambda都不一样,而f1和f2就是仿函数的对象

箭头这里就是调用operator[ ]

cpp 复制代码
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,但他们是一样的

lambda底层就是一个仿函数,底层就是把lambda替换成仿函数,生成一个仿函数类,类的名字为了不冲突叫做lambda+uuid,这样不同的lambda就是不同的类

新的功能


原来 C++ 类中,有 6个默认成员函数,C++11 新增了两个:移动构造函数和移动赋值运算符重载。
如果是深拷贝的类,移动构造和移动赋值我们需要自己实现,而如果是浅拷贝的类,不需要实现
那我们不写,自动生成的会干什么事情?什么情况下会自动生成?

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

cpp 复制代码
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:
	bai::string _name;
	int _age;
};
int main()
{
	Person s1;
	Person s2 = s1;
	/*Person s3 = std::move(s1);
	Person s4;
	s4 = std::move(s2);*/
	return 0;
}

这里我们屏蔽析构,拷贝构造,赋值,此时编译器就会默认生成移动构造

对于自定义类型string _name,会调用他的移动构造或拷贝构造

我们的string是实现移动构造的,怎么回事呢?

原因是s2 = s1是调用拷贝构造,Person类中没有拷贝构造,而且是左值,左值怎么会调用移动构造呢?

此时就是默认生成的移动构造

我们把析构函数放出来,又变成了这样

原因就是不能实现他们三中的任意一个

大家再仔细想想,如果一个类需要我们显示的写析构,那么这个类就是深拷贝的类,比如string,vector,list都要写析构,都是深拷贝的类,析构,拷贝构造,拷贝赋值是三位一体的,再想想迭代器,不需要写析构,不需要拷贝构造,所以编译器生成的非常香的

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

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

我们可以看到,此时即使我们写了析构函数,也会生成默认成员函数

这里也是一样的

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

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

我们可以看到有了这行代码后,,s2和s3就报错了

可变模板参数

我们之前提过一次,printf和scanf都是可变模板参数,参数中的三个点就是可变参数

就像这样

底层有一个数组,把实参存起来,访问时会依次取出来

模板参数和函数参数是类似的,模板参数传递的是类型,函数参数传递的是对象

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

模板参数的Args是我们取的,一般都会选这个名字

我们把带省略号的参数称为"参数包",它里面包含了0到N(N>=0)个模版参数

如果传递的时候只有一个参数,那么参数包里就有一个类型,(Args... args)里用这个类型定义了一个形参,如果有两个类型,就用这两个类型定义两个对象,上面的Args是参数的类型,下面的(Args... args)是参数的包

就像这样,我们再来看看传了几个参数

这里使用sizeof查看,因为是可变参数,所以要加...,还要加在括号外面,非常奇怪,要特殊记一下,这里我们的第一个数字都传给value,剩下的传给参数包

我们也可以把他的底层认为是一个数组,和函数可变参数一样,但其实并不是这样,我们只是可以这样想

所以会有人想到这样使用参数包,认为底层是数组,直接取出来用,但是是不行的,那我们怎么把参数包的内容取出来呢?

第一种办法是加一个T,然后这样写,非常的奇怪

原理是递归函数方式展开参数包,上面的ShowList是递归终止函数,下面的是展开函数

如果只有一个参数,比如我们只传了一个1,那么就直接去终止函数,如果是多个参数,比如剩下的三个,就要去展开函数,第一个值传给val,推出T的类型是int,剩下的参数传给参数包,然后里面再次调用ShowList,继续传参数,此时参数就少了一个,比如我们传递了1,2,3.14,此时就只有2和3.14了,2匹配val,而3.14继续往下传,此时就调用终止函数,是一个编译递归

非常奇怪,不过我们日常中基本不会使用,简单了解就行

emplace_back只有参数包,那底层是怎么走的呢?

cpp 复制代码
template <class T>
void _ShowList(T val)
{
	cout << val << " ";
	cout << endl;
}

template <class T,class ...Args>
void _ShowList(T val, Args... args)
{
	cout << val << " ";
	_ShowList(args...);
}
template <class ...Args>
void ShowList(Args... args)
{
	_ShowList(args...);
}

int main()
{
	ShowList(1);
	ShowList(1,2);
	ShowList(1,2,"xxx");
	ShowList(1,2,3.14);

	return 0;
}

那就是再多一层 ,将之前的两个ShowList变为子函数,原理还是一样的

那如果是无参的呢?

我们将终止函数修改成这样即可

我们将他改名为CppPrint,就实现了一个类似C语言的print函数,这些了解即可

上面说这是第一种方式,下面我们来看第二种,更加抽象

cpp 复制代码
template<class T>
void PrintArg(T t)
{
	cout << t << " ";
}

void CppPrint()//重载0个参数版本
{
	cout << endl;
}
//args代表0-N的参数包
template <class ...Args>
void CppPrint(Args... args)
{
	int a[] = { (PrintArg(args),0)... };
	cout << endl;
}

他的意思是将参数包的第一个值,传给PrintArg,剩下的参数包就是int a[ ]里的...,编译器在这里会进行推导,编译器知道有多少个参数,比如这里有三个参数

那么就会变成这样,相当于调用三次 ,数组是根据{ }里的值,来判断数组要开多大,才会展开,但是不能显示的去表示,所以就写成...,这里的(PrintArg(args),0)是逗号表达式,逗号表达式取的是后面的值,用0初始化数组,非常抽象,各位了解即可

我们可以给他简化一下,不用逗号表达式

我们再来看点别的东西

cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		,_day(day)
	{}
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(2021, 7,23);

	return 0;
}

这段代码我们通过可变参数来调用构造函数

如果我们少传一个参数,是不行的,因为没有默认参数,那如果加上的话就很多样化了

我们把默认参数先全设置为1

此时我们可以传递0到3个参数

这里p5也可以运行,p5调用的是拷贝构造

因为有了参数包,我们传递的参数就非常灵活,可以传递0到n个

下面我们来看看emplace系列,我们看到他们都使用了&&,但这里不是右值引用,带有模板参数的是万能引用,当你是左值时这里就是左值引用,是右值时是右值引用

我们再看这段代码,我们在创建pair时用了make_pair

调用的是右值引用的版本

emplace_back也可以这样传

emplace_back的底层就和我们上面的Date类似,先传给new Node(args..),参数包一直往下传,直接构造,就像图里的10和20,而30使用了make_pair,调用的是拷贝构造,emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象

这里他俩并没有什么区别

cpp 复制代码
int main()
{
	std::list< std::pair<int, bai::string> > mylist;

	mylist.emplace_back(10, "sort");
	mylist.emplace_back(make_pair(20, "sort"));
	mylist.push_back(make_pair(30, "sort"));
	mylist.push_back({ 40, "sort" });
	return 0;
}

这个场景下他们会有区别,当pair的一个参数是string时

​​​​​​

我们看第一个,是直接通过string构造的,因为pair的参数int和sort作为参数包一直往下传,最后直接初始化pair即可,直接初始化相当于在节点里用这个参数构造string

再看第二个,这里是编译器进行了强制优化,老一点的编译器可能是先构造再拷贝构造

再看push_back,他们就是先构造再拷贝构造,不过我们写了右值,所以这里是移动构造,因为push_back不是参数包,必须是pair,必须先创建pair对象,再移动构造

这里意味着,emplace_back可以不断往下传参数包,直接构造,而push_back只能先构造,再拷贝构造(或者移动构造)

所以有些人会说emplace_back更高效一点,其实如果我们使用移动构造的话,并不会差多少,移动构造直接转移资源,如果没有移动构造,比左值引用,那emplace_back就会高效一点,右值引用就大差不差了

我们可以认为emplace_back是一个更强大的push_back,适应性更高,更加灵活push_back只能传日期类对象,而emplace_back可以传对象,也可以传对象的参数包

包装器

function

C++中有非常多的可调用类型,比如函数指针,仿函数,lambda,有什么办法可以把他们统一控一下吗?于是就有了包装器

cpp 复制代码
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,可以通过函数指针,仿函数和lambda,那么函数模板会被实例化为3份

我们可以看地址,或者静态变量count是否变化来得知,可以被实例化为3份,接下来我们来看看包装器,把他们变为1份,并且这其实还不是核心,有时候我们需要把可调用对象存到容器里,比如存到vector<>,那这里怎么写呢?lambda我们甚至写不出来

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

cpp 复制代码
#include <functional>

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

需要一个头文件,包装器的本质可以理解为适配器模式,他可以包装出我们想要的东西

这里看到,包括仿函数对象,都可以包装,上面返回值和参数都是double,所以我们这样写,语法大家要记一下

有了包装器,我们就可以这样玩,把对象存到vector里,这里取出了vector中的对象,对象是一个包装器,包装器包装了函数指针,lambda和仿函数对象

还可以更简便一点,不用先初始化,包装器解决的是可调用对象的类型问题,在此之前,比如我们想要把可调用对象存到容器里,我们写函数指针就只能存函数指针,写仿函数对象就只能存仿函数对象,lambda我们都没办法写,但是有了包装器我们就都可以存

bind

我们这里有一个减法,假设我们输入的是10,5,那么就是10-5,在不改变这个函数的情况下,如果我们想要让参数位置换一下,变成5-10该怎么办?于是就有了bind(绑定)

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

cpp 复制代码
// 原型如下:
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 为第二个参数,以此类推

我们先来看这段代码

这是绑定的语法,placeholders是一个命名空间,_1代表第一个参数,_2代表第二个参数,以此类推(如果我们把命名空间展开可以直接写_1和_2,不过一般我们不展开)

我们可以看到有各种数字,就代表了参数,我们继续看上面的代码

如图,我们输入的10传给了_1,然后_1传给a,5传给了_2 ,_2传给b

知道了这些,我们就会调整参数顺序了,就是把_1和_2位置换一下

此时10还是传递给_1,但是_1会传递给b,5传递给_2,_2传递给a

bind的一个作用是,当我们看到有些函数的参数顺序很别扭时,我们可以包装一下他

比如有人写了第二个函数,但是我们自己写一般是第一种,所以看着就很别扭,就需要调整一下

我们再看一个例子

我们有一个Plus函数,我们使用时要传递三个参数,但是如果我就是不想传递第三个参数怎么办呢?有人可能会想到缺省参数,但是缺省是写死的,如果想第三个参数可以变化怎么办?

我们就可以使用bind来完成,直接传第三个参数,绑定后就可以只传递两个参数,这里相当于函数简化

我们又有了一个Plus2

首先4.0这些rate应该写在前面,然后大家认为后面的参数应该是_2和_3还是_1和_2呢?

答案是_1和_2

这里我们可以把绑定的rate认为是一个缺省参数,我们只需传两个参数,所以是_1和_2更合理一点

如果是_2和_3,那这种情况该怎么传?传_1和_3吗?是不合适的

这里大家认为固定的参数不参与排序即可

我们再看这个,我们有一个类Sub,有一个static的sub函数,而我们此时bind是找不到的,因为类是一个域,编译时默认只会在全局去找,如果没有指定或者展开,是不会去类域里找的

所以我们得指定类域

那当我们有一个非静态的ssub呢?

此时就报错了

首先我们需要加上&符号, 非静态的成员函数取地址前面要加&符号,这是规定

静态的可以不加,也可以加,不过推荐加上,这样不容易混淆

此时Sub2还是报错,为什么呢?非静态的实际是几个参数?看起来是三个,实际上是4个

我们需要一个Sub对象,然后传一下,此时我们绑定了两个参数,一个是对象st的地址,另一个是rate,这是一种写法,我们再来看另一种

传递一个对象过去,Sub2也是一样,传递指针可以,传递对象也可以

这里的指针和对象不是传递给this指针,是传递给operator( )调用,是指针就指针调用,是对象就对象调用

绑定的底层和lambda类似,生成了一个仿函数,仿函数调用函数有多种方式

这里我们typeid的话是一个function,因为我们上面写的就是一个function

如果我们使用auto,就可以看到这样一个,底层是一个叫做Binder的类,最终还是重载的还是operator(),这里我们了解即可

这里对象像函数一样调用,那什么样的对象可以像函数一样调用呢?就是仿函数

以上即为本期全部内容,希望大家可以有所收

C++11中的内容还差智能指针和线程库,这两个后续会单独开一篇来讲

如有错误,还请指正

相关推荐
sukalot几秒前
windows C++-windows C++-使用任务和 XML HTTP 请求进行连接(二)
c++·windows
_.Switch3 分钟前
Python机器学习模型的部署与维护:版本管理、监控与更新策略
开发语言·人工智能·python·算法·机器学习
醉颜凉6 分钟前
银河麒麟桌面操作系统修改默认Shell为Bash
运维·服务器·开发语言·bash·kylin·国产化·银河麒麟操作系统
NiNg_1_23412 分钟前
Vue3 Pinia持久化存储
开发语言·javascript·ecmascript
带带老表学爬虫20 分钟前
java数据类型转换和注释
java·开发语言
qianbo_insist23 分钟前
simple c++ 无锁队列
开发语言·c++
zengy524 分钟前
Effective C++中文版学习记录(三)
数据结构·c++·学习·stl
BigYe程普33 分钟前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
彭于晏68935 分钟前
Android广播
android·java·开发语言
弱冠少年1 小时前
websockets库使用(基于Python)
开发语言·python·numpy