C++ | C++11知识点

        前言: 本篇内容讲述了C++11中比较重要的内容为:右值引用、可变参数模板、lambda表达式和包装器。
         ps:包装器博主在另一篇文章讲述的,本篇文章包装器板块为原文链接。

花括号初始化

c++11添加了任意类型都可以使用花括号进行初始化

自定义类型的花括号初始化

#include <iostream>
#include <string>
#include <vector>
int main()
{
	std::string s1 = "nihao";
	std::string s2 = "niyehao";
	std::vector<std::string> vec1 {	s1, s2 };
	std::vector<std::string> vec2 = { s1, s2 };
	return 0;
}

内置类型的花括号初始化

#include <iostream>
int main()
{
	int k { 1 };
	int arr[] {1, 2, 3, 4, 5, 6, 7, 8};
	std::cout << k << std::endl;
	for (int i = 0; i < sizeof(arr) / sizeof(int); i++)
	{
		std::cout << arr[i] << " ";
	}
	std::cout << std::endl;
	return 0;
}

多参数自定义类型的花括号初始化

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>

struct _example
{
	_example(std::string s, std::vector<int> v, int _a, std::unordered_map<int, int> m)
		:str(s), vec(v), a(_a), mp(m)
	{
	}

	std::string str;
	std::vector<int> vec;
	int a;
	std::unordered_map<int, int> mp;
};

int main()
{
	int a { 10 };
	std::string str { "nihao" };
	std::vector<int> vec { 1, 2, 3, 4, 5, 6, 7, 8 };
	std::pair<int, int> p{1, 1};
	std::unordered_map<int, int> mp { p };
	_example exa { str, vec, a, mp };
	std::cout << exa.str << std::endl;
	for (int i = 0; i < exa.vec.size(); i++)
	{
		std::cout << exa.vec[i] << " ";
	}
	std::cout << std::endl;
	std::cout << exa.a << std::endl;
	auto it = exa.mp.begin();
	if (it != exa.mp.end())
	{
		std::cout << it->first << ":" << it->second << std::endl;
	}
	return 0;
}

auto和decltype

auto和decltype都能自动推导变量的类型。
但是auto只能通过其他变量进行初始化, 不能单独使用, 如下:

#include <iostream>
#include <string>
int main()
{
	int a = 0;
	auto b = a;
	std::string str = "nihao";
	auto str2 = str;
	std::cout << b << " : " << str2 << std::endl;
	return 0;
}

decltype能够接收一个变量, 推导出这个变量的类型。更加灵活:

#include <iostream>
#include <string>
int main()
{
	int a = 10;
	decltype(a) b = a;
	decltype(b) c;
	c = a;
	std::cout << b << " " << c << std::endl;
	return 0;
}

范围for

范围for就是遍历一次容器或者数组。 如果是容器或者自定义类型, 那么要封装迭代器, 要有begin和end接口。 那么就能使用范围for。 如果是数组, 那么数组本身就是指针, 所以本身就可以使用范围for。

#include <iostream>
int main()
{
	std::vector<int> vec { 1, 2, 3, 4, 5, 6, 7, 8 };
	for (auto e : vec)
	{
		std::cout << e << std::endl;
	}
	return 0;
}

右值引用

右值引用的符号为&&:

int &&a = int(1); //右值引用

我们平时说的引用其实都是左值引用, 就像下面的示例:
int a = 0;
int &b = a; //这就是一个左值引用。
之所以叫做左值引用是因为他引用的是一个左值。 所以右值引用引用的就是右值。 什么是左值, 什么是右值?

左值和右值

左值和右值的本质区别是能否被取地址能被取地址的值就是左值。不能被取地址的值就叫做右值。 平时写的变量都是左值,因为它们可以被取地址。像临时变量就是右值,字面常量也是右值, 因为它们都不可以被取地址。
对于右值来说, 自定义类型的右值也叫做将亡值

左值引用和右值引用的相同点和不同点

相同点

说完左值右值后我们来谈一谈引用。 不管是左值引用还是右值引用, 博主总结他们都有两个特点:

	引用可以延长临时变量的生命周期。
	引用本身是左值。

下面为示例:

struct exam
{
	exam(int e = 0)
    :_e(e)
	{}
	int _e;
};  
 		
#include <iostream>
int main()
{
    const int &a = int(4);
	const exam &e = exam(10);

	std::cout << a << ":" << e._e << std::endl; //本该在创建后就销毁的数据被延长了。

	int &&a = int(1);  //a右值引用了int(1), 让右值延长生命周期
	auto pa = &a;   //a引用右值后,变得可以被取地址了, 说明成为了左值。
	std::cout << pa << std::endl;
    return 0;
}

不同点

左值引用既可以引用左值也可以引用右值:

#include <iostream>
int main()
{
	int a = 10;
	int &b = a;  //引用左值
	
	const int &c = 1;      //引用右值, 因为右值不可修改, 所以加上const防止权限放大
	const int &d = int(1); //引用右值, 因为临时变量不可修改, 所以加上const防止权限放大
	return 0;
}

右值引用只能引用右值

#include <iostream>
int main()
{
	int &&a = 1;
	int &&b = int(1);
	//int &&c = a;   //语法错误, 因为a是一个左值。
	return 0;
}

move

move能够将一个左值临时转化为左值。 作用域为这一行。

#include <iostream>
int main()
{
	int b = 0;
	int &&a = move(b);   //不会报错, 说明b此时变成了右值
	
	return 0;
}

右值引用的作用

右值引用的出现是为了解决深拷贝开销大的问题。 比如下面这种场景:

#include <iostream>
#include <vector>
using namespace std;
vector<vector<int>> Test1()
{
	vector<vector<int>> vv = {{1, 2, 3}, {2, 3, 4}, {3, 4, 5}};
	return vv;
}
void Test2(vector<vector<int>> &vv)
{
	vv.push_back({1, 2, 3});
	vv.push_back({2, 3, 4});
	vv.push_back({3, 4, 5});
}
int main()
{
	vector<vector<int>> vv1 = Test1();
	vector<vector<int>> vv2;
	Test2(vv2);
	return 0;
}

这种场景描述起来就是:函数返回, 函数内部局部变量销毁, 无法返回引用。所以这种情况一般有两种处理方法, 一种就是直接返回临时对象, 如Test1。 还有一种就是输出型参数, 如同Test2。
对于vector<vector<int>> 来说, Test2还没有问题。 但是如果使用Test1,那么开销就大了。 因为对于这个函数返回的过程如下:
在这里插入图片描述
对于Test1(), 它返回的时候是先拷贝出一份**vector<vector<int>>**临时变量。 进行深拷贝, 拷贝出一份资源, 然后vv释放。 之后这个临时变量作为返回值初始化vv1。 初始化vv1的时候, 要调用vv1的拷贝构造。 同样是深拷贝, 那么就还要拷贝出一份临时资源, 拷贝完成之后vv1释放。 这个过程太繁琐了, 因为管理的资源被拷贝了两次, 大佬们就想, 这个被管理的资源, 其实不用被释放,可不可以直接把他转移给vv1呢? 所以就有了移动语义, 所以在c++11种, 容器中都添加了移动构造、移动赋值、移动插入。
下面看一下什么是移动语义

移动语义

移动语义:将一个将亡值管理的资源移动到另一个变量里面

当我们使用一个将亡值构造对象的时候,如果没有移动构造, 就会采用拷贝构造, 那么就会采用深拷贝。 如果有移动构造, 那么就去调用移动构造了。 下面是一个移动构造的例子:

#include <iostream>
using namespace std;
struct my_memory
{
	my_memory(int a = 10)
		:_size(a), _memory(new int(_size))
	{
	}
	my_memory(const my_memory&a)
	{
		cout << "拷贝构造(const my_mymory&a) "<< endl;
		_size = a._size;
		for (int i = 0; i < _size; i++) _memory[i] = a._memory[i];
	}
	my_memory(const my_memory &&a)
	{
		cout << "移动构造(const my_mymory&&a)" << endl;
		_size = a._size;
		_memory = a._memory;  //转移资源
	}
	int _size;
	int *_memory;
}; 

int main()
{
	my_memory m1;
	cout << m1._memory << endl;
	my_memory m2 = move(m1);
	cout << m1._memory << endl;
	return 0;
}

在这里插入图片描述
运行结果会发现, Test1创建的局部变量内部管理的资源成功转移给了m1, 完成了移动构造。
除了移动构造对于移动拷贝和移动插入类似, 都是右值引用接收将亡值。 然后将将亡值的资源转走。如下图为移动赋值:

#include <iostream>
using namespace std;
struct my_memory
{
	my_memory(int a = 10)
		:_size(a), _memory(new int(_size))
	{
	}
	my_memory(const my_memory&a)
	{
		cout << "拷贝构造(const my_mymory&a) "<< endl;
		_size = a._size;
		for (int i = 0; i < _size; i++) _memory[i] = a._memory[i];
	}
	my_memory(const my_memory &&a)
	{
		cout << "移动构造(const my_mymory&&a)" << endl;
		_size = a._size;
		_memory = a._memory;  //转移资源
	}
	my_memory& operator=(my_memory &&a)
	{
    	cout << "移动赋值" << endl;
    	int *tmp = _memory;
   		_memory = a._memory;
    	a._memory = tmp;
    	return *this;
	}

	int _size;
	int *_memory;
}; 

int main()
{
	my_memory m1;
	cout << m1._memory << endl;
	my_memory m2;
	m2 = move(m1);
    cout << m2._memory << endl;
	return 0;
}

在这里插入图片描述

万能引用

c++11中新增了一个叫做万能引用的语法, 就是一个函数内部的参数可以接收左值, 也可以接收右值。 定义方式就是:

template<class T>
返回值 func_name( T &&t)
{
	//函数体
}

在这里插入图片描述

final和override

final

final是一个关键字。
final可以用来修饰虚函数, 表示一个虚函数不能被重写。 即:最后一个虚函数。
在这里插入图片描述
final也可以用来修饰类, 表示这个类不能被继承。 即:最后一个类。
在这里插入图片描述

override

override同样是一个关键字, 用来修饰虚函数。被override修饰的虚函数必须是继承自基类的正在重写的虚函数, 否则语法出错。 所以override就是用来检查, 检查一个虚函数是否是继承自基类的
在这里插入图片描述

可变参数模板

概念

可变参数模板让模板的参数可以是任意的。我们平时说的可变参数包有两种:

  • 模板的可变参数包
  • 函数的可变参数包

可变参数包的定义都是三个点:“”。

  1. 模板的可变参数包class…或者typename…后定义参数包类型名称。
  • 如下为示例代码:

      template<class...  Args>
      void func1(){}
      template<typename... Args>
      void func2(){}
    
  1. 函数的可变参数包是函数的参数是参数包类型, 这个时候这个参数包就是函数的可变参数包。参数包的名称… 表示这是一个参数包的类型, 后面就是参数名。
  • 如下为示例代码:

      template<class... Args>
      void func1(const Args&... args)
      {
      	cout << "nihao" << endl;
      }
    

想要使用模板的可变参数包,我们得先知道可变参数包有什么用。

  • 当我们既不确定要处理的实参的数目, 也不知道要处理的类型有哪些的时候,就可以使用可变参数包。

sizeof

可变参数包的个数可以使用sizeof计算出来, sizeof计算参数包不会计算参数的大小, 而是会计算参数包内参数的个数。

  • 下面为示例:

      template <typename... Args>
      void func2(const Args &...args)
      {
      	cout << sizeof...(args) << endl;
      }
    
      int main()
      {
      	func2(1, 2, 1.1, "nihao", string("nihao"));
    
      	return 0;
      }
    

在这里插入图片描述

参数包的扩展

我们使用参数包, 就要将参数包扩展。 参数包扩展后会隐式的得到一个参数列表:

template <class T, class... Args>	
void print(const T &t, const Args &...args) // Args... 这表示一个扩展。 参数包...。args扩展成一个参数列表
{
	cout << t << endl;

	print(args...);     //参数包..., 表示扩展。 args扩展成一个参数列表
}
  • 下面为调用示例:

    void print(){}
    
    template <class T, class... Args>
    void print(const T &t, const Args &...args) // Args... 这表示一个扩展。 参数包...
    {
    	cout << t << endl;
    
    	print(args...);     //参数包..., 表示扩展
    }
    
    int main()
    {
    	print(1, 2, "hello", string("world"));
    	return 0;
    }
    
  • 上面的第一个print用来结束递归。 因为当下面的第二个print解析参数包到最后一层, 参数列表里面已经没有参数了, 就应该调用一个无参的print, 但是第二个print最少有一个参数cosnt T&, 所以要重载一个无参的print结束递归。

  • 第一次扩展参数包是扩展出了四个参数, print里面一共有着四个参数。 打印第一个参数

    void print(const int &t, const int& int, const char*, const string &)
    {
    	cout << t << endl;
    	print(args...); //将剩下的三个参数传进去
    }
    
  • 第二次扩展参数包, 是扩展出了三个参数,print里面一共有三个参数, 打印第一个参数

    void print(const  int&t, const char*, const string &)
    {
    	cout << t << endl;
    	print(args...); //将剩下的两个参数传进去
    }
    
  • 第三次扩展参数包, 是扩展出了两个参数, print里面一共有两个参数, 打印第一个参数

      void print(const char* t, const string &)
      {
      	cout << t << endl;
      	print(args...); //将剩下的一个参数传进去
      }
    
  • 第四次扩展参数包,是扩展出了一个参数, print里面只有一个参数

    void print(const string &)
    {
    	cout << t << endl;
    	print();   //无参,调用无参print。 
    }
    

理解包扩展

上面只是对参数包单纯进行扩展。其实参数包还可以作为参数传递给函数。意思就是希望参数包中的每个参数都去调用函数

  • 下图为示例:

    void print(){}
    template <class T, class... Args>
    void print(const T &t, const Args &...args) // Args... 这表示一个扩展。 参数包...
    {
    	cout << t << endl;
    
    	print(args...);     //参数包..., 表示扩展
    }
    
    int double_num(int x)
    {
    	return 2 * x;
    }
    
    template<class... Args>
    void print_double(const Args&... args)
    {
    	print(double_num(args)...);  //将每个参数去调用double_num函数。
    }
    
    int main()
    {
    	print_double(1, 2, 3, 4, 5);
    	return 0;
    } 
    

完美转发

forward完美转发是一个函数, 可以用来保持参数的特性。在c++11中可以使用forward配合参数包编写函数。我们利用标准模板库容器(stl容器) 里面的emplace_back来看一下forward完美转发怎么配合参数包的。

  • 假如下面是vector类:

      class my_vector
      {
      	template<class... Args>
      	void emplace_back(const Args&&... args);
      };
    
  • 标准模板库容器里面的emplace_back是一个有可变参数模板的函数, 它本质上是一个插入函数, 但是插入数据时对节点的拷贝操作由调用拷贝构造深拷贝变成了调用移动构造进行资源转移

    class my_vector
    {
    	template<class... Args>
    	void emplace_back(const Args&&... args)
    	{
    		insert(forward<Args>(args)...);  //假设这里的insert是vector的构造节点操作。 又因为这里本质上传过去的是右值, 所以构造节点时会调用参数包参数类型的移动构造去构造节点。
    	}
    }
    
  • 假如这里的参数包里面的右值类型是string类型,右值为"hello world", 那么就相当于下面:

      forward<string>(string("hello world"));
    

最后这里就是调用了string类型的移动构造, 省去了string类型的深拷贝。

lambda表达式

  • lambda表达式是一中可调用对象。和回调函数, 仿函数类似。
  • lambda表达式最终会被编译器转化为匿名仿函数对象

lambda的基本语法

	[capature](params) mutable(可以不加, 默认情况下不加)-->  return_type {body}
语法作用
[ ]捕捉作用域内的变量
()参数列表
mutable可以不加,将仿函数改为非const调用
return_type返回值,不写可以自动推导
body函数体

捕获方式

  • 捕捉作用域内的变量的方式有很多, 下面是一些捕获方式
捕获符号作用
[&]引用捕捉, 所有变量引用进入lambda
[=]值捕捉, 所有变量的值拷贝进入lambda
[&变量名]引用捕捉某个变量
[变量名]值捕捉某个变量
[ ]不捕捉任何变量
[=, &x]默认值捕获,但 x 是引用捕获
thisthis指针捕获
  • 捕捉到lambda里面的值,其实就等价于一个类里面的成员变量。 如果是引用捕捉, 就相当于在类里面创建了一个引用变量进行引用值捕捉就相当于创建了一个普通变量进行拷贝

mutable

如果匿名仿函数对象是一个右值, 那么就说明这个匿名仿函数的this指针式const类型的, 所以它只能调用const成员方法。 也就是说lambda表达式其实本质上就相当于一个仿函数类里面的const修饰的operator()方法

  • 如果修改捕捉到的值,不能直接修改。
  • 加上mutable,可以修改

代码演示

int main()
{
	int a = 0;

	auto f1 = [a](int x)
	{
		cout << x << endl;
	};

	auto f2 = [a] (int x) mutable 
	{
		a = x;
		cout << a << endl;
	};

	f1(10);
	f2(10);

	return 0;
}

在这里插入图片描述

包装器

c++11中引入了包装器库 functional。 关于包装器的内容博主之前讲过:
https://blog.youkuaiyun.com/strive_mianyang/article/details/139565882?spm=1001.2014.3001.5501

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值