C++11新特性

C++从诞生到现在一直是一门主流的编程语言,期间经历了多次更行迭代,最近的一次大版本更新就是C++11,而现在大部分公司也把C++11作为主流的应用版本。有人说C++现在越来越不像C++了,一部分原因就是C++11更行了很多重要的新东西,当然也有部分是比较鸡肋的,所以今天就把最重要最常用的新特性给大家罗列出来,去讲解清楚。

新特性

  • 列表初始化(initializer_list)
  • auto(自动识别类型)
  • decltype
  • 右值引用和移动构造
  • forward
  • 类的新特性
    • 默认生成移动构造条件
    • default/delete
  • 可变参数模板
  • lambda表达式

列表初始化(initializer_list)

在C++11出来之前,我们只见过也是最常用{}初始化只是数组或者结构体,例如:

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出来之后可以用列表初始化各种类型,还可以不加=

int main()
{
	//日期类
	Date d1(2023, 1, 1);
	Date d2 = { 2023,1,1 };
	Date d3{2023,1,1};

	int x1 = 1;
	//可以用{}初始化变量或者数组,可以不加=
	int x2 = { 2 };
	int x3{ 3 };

	int arr[]{ 1,2,3,4,5 };
	int arr2[5]{ 0 };

	vector<int> v1 = { 1,1,1,1 };
	vector<int> v2{ 1,1,1,1 };

	list<int> ls1 = { 1,2,3,4,5 };
	list<int> ls2{ 1,2,3,4,5 };

	map<int, int> m1 = { {1,1},{2,2} };
	map<int, int> m2 { {1,1},{2,2} };
	return 0;
}

为什么这么多的容器或者类型都支持列表初始化呢?是通过什么方式让内置、自定义类型和容器都支持列表初始化呢?

C++11中新增了一个类型叫做initializer_list,只要是{}都会被编译器自动识别成这个类型
C++11新特性_第1张图片

在C++11中这些容器的构造函数中,都支持了用initializer_list类型的构造函数,下面这是一部分,基本上所有的容器都支持了列表初始化。
C++11新特性_第2张图片
这是一个很方便的语法特性,值得我们去学习使用。

auto(自动识别类型)

C++11标准中新增了auto关键字,可以用于声明变量,其作用是自动推导变量的类型。
使用auto声明变量时,编译器会根据右边表达式的类型自动推断出变量的类型,并将其类型推导为所初始化的表达式的类型。例如:

auto i = 10;    // 推导为 int 类型
auto d = 2.3;   // 推导为 double 类型
auto s = "hello"; // 推导为 const char* 类型(C风格字符串)

需要注意的是,auto声明的变量必须要进行初始化,否则无法推导出变量的类型,编译会报错。
auto还可以与特定修饰符一起使用,如const、&和*等。例如:

const auto *p = &x; // 推导为const int*
auto &a = x;        // 推导为int&
auto b = &x;        // 推导为int*

因此,通过结合auto和各种修饰符,可以方便地定义各种类型的变量,从而简化代码,并提高程序的可读性和可维护性。

还可以验证上面的列表是否真的为initializer_list类型
C++11新特性_第3张图片
注:typeid().name()是一个推导变量类型的函数。

decltype

C++11标准中新增了一个decltype操作符,可以用于在不需要实际执行表达式的情况下获取表达式的类型。C++11新特性_第4张图片

右值引用和移动构造

不过什么是右值,什么是左值?我们需要先把这个概念搞清楚。可能很多人觉得在赋值符号左边的就是左值,在赋值符号右边的就是右值,这种想法是错误的

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

//我们之前学习的引用就是左值引用
int a=0;//a是左值
int* p = &a;//p也是左值,因为他们都是变量,都可以取到地址。
int& b = a;//左值引用

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

int a = 1;
int b = 2;
//int& ret = (a+b);//这段代码是错误的,因为a+b是一个表达式,这个表达式会先产生一个临时对象,然后赋值给ret,这个临时对象具有常性,这是一种权限的放大,所以不能引用
const int& ret = (a+b);//加const就可以了,左值引用引用右值
int&& ret = (a+b);//右值引用右值
int&& ret1 = 10;

//那么右值能不能引用左值呢?
int a = 10;
//int&& ret = a; //这段代码是错误的,右值不能直接引用左值,但是move之后可以了
int&& ret = std::move(a);
//只有右值引用才能调用move函数,因为它是将对象的资源所有权转移给目标对象,而左值在转移资源所有权时,会使得源对象变为无效状态,这是不符合语义的。
//要保证目标对象有足够的空间来接收资源。如果目标对象的类型与源对象的类型不同,需要进行类型转换。
//所以move的使用要慎重

左值引用和右值引用的总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值
  3. 右值引用只能右值,不能直接引用左值。
  4. 但是右值引用可以move以后的左值,也可以通过模板来间接引用左值。

那么右值引用真正的意义是什么?
右值引用的真正意义是移动语义(资源的移动),利用临时对象或者表达式的特性,避免资源的重复分配或者拷贝,从而提高程序的执行效率。

右值分为:

  • 普通右值(内置类型的字面量)
  • 将亡值 (move(自定义类型),函数返回值,自定义类型表达式)

例如:

string s1("hello");
string s2("world");

string ret1 = s1; //调用拷贝构造进行深拷贝
string ret2 = s1+s2; //这个表达式的返回值是一个右值,也是将亡值,因为s1+s2的结果会放到一个临时对象的空间内 然后深拷贝到ret2。

这个将亡值都要消失之前,还要进行一次拷贝,如果是自定义类型无伤大雅,如果是自定义类型就可能需要深拷贝,这是一种资源浪费。右值引用之后,就可以指向这个将亡值的空间,减少拷贝。

移动构造
而在C++11中STL所有容器用右值引用实现了移动构造,让自定义类型减少拷贝,提升效率。移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,就是窃取别人的资源来构造自己,所以它叫做移动构造。所以移动构造是针对自定义类型设计的,对内置类型意义不大。例如:
C++11新特性_第5张图片
这里列举了一部分,那么移动构造怎么实现的呢?例如:
这个string类的代码不完整,主要是为了观察移动构造和拷贝构造的区别。

class string
	{
	public:
		string(const char* str = "")
		:_size(strlen(str))
		, _capacity(_size)
		{
			_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);
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

这个函数的返回值是一个string,当这个返回值返回结果时,会先构建一个临时对象空间,把str拷贝到临时空间,然后从临时空间拷贝到返回的ret变量中,但是新一点的编译器会做优化,把两次拷贝减少为一次,str直接拷贝给ret。如果实现了移动构造,只需要一次移动构造就可以了,极大的减少了深拷贝带来的浪费。

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

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

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

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

	std::reverse(str.begin(), str.end());
	return str;
}
int main(){
	string ret = to_string(12345);
	return 0;
}

还有一个和移动构造作用一样的是移动赋值,移动赋值运算符重载和移动构造的关系就像 拷贝构造和拷贝赋值运算符重载的关系是一样的。

//以上面的string类为例
string& operator=(string&& str)
{
	swap(str);
}

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);
}
int main()
{
	PerfectForward(100);		  //右值
	
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值

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

这是最终的输出结果,可能很多小伙伴有一些疑问,为什么右值可以引用左值,为什么都是左值引用?
C++11新特性_第6张图片

  1. PerfectForward里面是右值引用,引用左值会不会报错?。

答案是不会,因为通过模板可以引用折叠。
万能引用(引用折叠):既可以引用左值,也可以引用右值

  1. 为什么都是输出结果都是左值?

左值和右值有一个本质的区别就是左值可以取地址,右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址
第一次传入100的时候,100是一个右值,但是通过右值引用之后,t就是一个有空间的左值了,再调用Fun函数,就会输出左值引用。这其实是一种属性丢失,如果想让t继续保持右值属性,可以用forward()。

例如:
C++11新特性_第7张图片

类的新特性

默认生成移动构造条件

C++类中,有6个成员函数不写,会被自动生成:

  1. 构造函数

  2. 析构函数

  3. 拷贝构造函数

  4. 拷贝赋值重载

  5. 取地址重载

  6. const 取地址重载

     如果不了解类和对象特性的,可以看看我之前详解的类和对象特性,毕竟类和对象是很重要的基础:[类和对象](http://t.csdn.cn/KlNqO)
    

C++11中新增了移动构造和移动拷贝赋值,这两个成员函数也是可以被自动生成的,但是自动生成是有一下条件的:

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

default/delete

移动构造默认生成的条件还是有些苛刻的,C++11中增加了default关键字可以让类的成员函数被强制生成。

//例如强制生成默认的移动构造
class string
	{
	public:
		string(const char* str = "")
		:_size(strlen(str))
		, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 移动构造
		string(string&& s) = default;
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

有强制生成成员函数,也有禁止自动生成成员函数,用delete关键字。主要还是针对默认生成的成员函数。

class string
	{
	public:
		string(const char* str = "")
		:_size(strlen(str))
		, _capacity(_size)
		{
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 移动构造
		string(string&& s) = delete;
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的\0
	};

可变参数模板

C语言中有可变参数,例如printf。可变参数主要是STL库里面的类模板会用,我们以了解为主。
C++11新特性_第8张图片
C++11中新增了可变参数模板,下面就是可变参数模板的写法

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

int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 'a');
	ShowList(12, "sdf", 23.0);
	return 0;
}

可以传不同类型或者相同类型的若干参数,那么怎么解析这个参数包呢?

template <class ...Args>
void ShowList(Args... args)
{
	cout << sizeof...(args) << endl;//代表传进去的参数个数
}

C++11新特性_第9张图片

怎么显示打印这些可变参数呢?
1. 递归方式

void ShowList()
{
	cout << endl;
}
//新增一个模板参数和函数形参,利用递归的思维解决。
template <class T,class ...Args>
void ShowList(const T& val,Args... args)
{
	cout << val << " ";
	ShowList(args...);//当参数包为0的时候,调用对应的无参函数
}
int main()
{
	ShowList();
	ShowList(1);
	ShowList(1, 'a');
	ShowList(12, "sdf", 23.0);
	return 0;
}

C++11新特性_第10张图片
2. 逗号表达式

template <class T>
void PrintArg(T t)
{
	cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args... args)
{
	int arr[] = { (PrintArg(args), 0)... };
	cout << endl;
}
int main()
{
	ShowList(1);
	ShowList(1, 'A');
	ShowList(1, 'A', std::string("sort"));
	return 0;
}

C++11新特性_第11张图片

lambda表达式

学习lambda表达式之前,我还是想要给大家举个例子来说明为什么学习lambda表达式,它能给我们带来什么好处。

例子:
在C++98中,如果想要对一个Goods类进行排序,可以使用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(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
}

随着C++语法的发展,人们开始觉得上面的写法太复杂了,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,在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)->bool { return g1._price < g2._price; });

	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)->bool { return g1._evaluate < g2._evaluate; });

	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool { return g1._evaluate > g2._evaluate; });
}

很明显sort函数的第三个参数就是lambda表达式,我们解析一下每个部分是什么意思
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }

lambda表达式各部分说明

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

这些都是一些死板的概念,想要更加深刻的理解lambda表达式,我们还需借助观察几个例子来理解:

	int x = 1, y = 0;
	auto swap = [](int x, int y) {
		int tem = x;
		x = y;
		y = tem;
	};
	cout << x <<" "<< y << endl;

大家觉得这个函数能完成x和y的交换吗?答案是不能,因为表达式内的空间是一块独立的,和一个函数一样,只不过lambda是一个匿名函数。
C++11新特性_第12张图片
所以把参数列表变成引用类型就可以了。
C++11新特性_第13张图片
[capture-list]捕捉列表内可以捕获上下文的数据,然后表达式内可以用,那么下面这种方式可以完成交换吗?

	//传值捕捉
	int x = 1, y = 0;
	auto swap2 = [x, y]()
	{
		int tmp = x;
		x = y;
		y = tmp;
	};

	swap2();
	cout << x << " " << y << endl;

首先这个代码会报错,报错原因如下图。因为默认情况下,lambda函数总是一个const函数,所以需要加上mutable。
C++11新特性_第14张图片

但是加上mutable也不能完成交换,因为这是传值捕捉,就是相当于拷贝给表达式。
C++11新特性_第15张图片
所以捕获列表也需要引用捕捉,才能完成对x和y的交换并且可以不加mutable
C++11新特性_第16张图片

所以捕捉列表有两种捕捉方式,引用捕捉和传值捕捉。lambda还支持对所有的参数进行全部捕捉。例如:

	//对所有参数进行引用捕捉
	auto swap = [&](){};
	//对所有参数进行传值捕捉
	auto swap = [=](){};
	
	//混合捕捉
	auto swap = [&x,y](){};

	//对x传值捕捉,其他引用捕捉
	auto swap = [&,x](){};

	//对x引用捕捉,其他传值捕捉
	auto swap = [&x,=](){};

这就是C++11最常用的一些特性,希望对您有所帮助。

你可能感兴趣的:(C++,c++,开发语言,c语言)