C++ 11特性

Posted 任我驰骋.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++ 11特性相关的知识,希望对你有一定的参考价值。

C++ 11

一、C++11简介

在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于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++98中的初始化问题

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

int array1[] = 1,2,3,4,5;
int array2[5] = 0;

对于一些自定义的类型,却无法使用这样的初始化。比如:

vector<int> v1,2,3,4,5;

就无法通过编译,导致每次定义vector时,都需要先把vector定义出来,然后使用循环对其赋初始值,非常不方便。C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。

int main()
  
	 // 内置类型变量
	 int x1 = 10;
	 int x210;
	 int x3 = 1+2;
	 int x4 = 1+2;
	 int x51+2;
	 // 数组
	 int arr1[5] 1,2,3,4,5;
	 int arr2[]1,2,3,4,5;
	 
	 // 动态数组,在C++98中不支持
	 int* arr3 = new int[5]1,2,3,4,5;
	 
	 // 标准容器
	 vector<int> v1,2,3,4,5;
	 map<int, int> m1,1, 2,2,,3,3,4,4;
	 return 0;
 

注意:列表初始化可以在之前使用等号,其效果与不使用=没有什么区别。

自定义类型的列表初始化

标准库支持单个对象的列表初始化

class Point

public:
	 Point(int x = 0, int y = 0): _x(x), _y(y)
	 
private:
	 int _x;
	 int _y;
;
int main()

	 Pointer p 1, 2 ;
	 return 0; 
 

多个对象的列表初始化
多个对象想要支持列表初始化,需给该类(模板类)添加一个带有initializer_list类型参数的构造函数即可。注意:initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。

#include <initializer_list>
template<class T>
class Vector 
public:
	 // ... 
	 Vector(initializer_list<T> l): _capacity(l.size()), _size(0)
	 
		 _array = new T[_capacity];
		 for(auto e : l)
		 	_array[_size++] = e;
	 
	 
	 Vector<T>& operator=(initializer_list<T> l) 
		 delete[] _array;
		 size_t i = 0;
		 for (auto e : l)
		 	_array[i++] = e;
		 return *this;
	  
	 // ...
private:
	 T* _array;
	 size_t _capacity;
	 size_t _size;
;

三、变量类型推导

为什么需要类型推导
在定义变量时,必须先给出变量的实际类型,编译器才允许定义,但有些情况下可能不知道需要实际类型怎么给,或者类型写起来特别复杂。

#include <map>
#include <string>
int main()

	 short a = 32670;
	 short b = 32670;
	 
	 // c如果给成short,会造成数据丢失,如果能够让编译器根据a+b的结果推导c的实际类型,就不会存
	在问题
	 short c = a + b;
	 
	 std::map<std::string, std::string> m"apple", "苹果", "banana","香蕉";
	  // 使用迭代器遍历容器, 迭代器类型太繁琐
	 std::map<std::string, std::string>::iterator it = m.begin();
	 while(it != m.end())
	 
		 cout<<it->first<<" "<<it->second<<endl;
		 ++it;
	 
	 
	 return 0;
 

C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型,可以给程序的书写提供许多方便。将程序中c与it的类型换成auto,程序可以通过编译,而且更加简洁。

decltype类型推导

为什么需要decltype
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。

template<class T1, class T2>
T1 Add(const T1& left, const T2& right)

 return left + right; 

如果能用加完之后结果的实际类型作为函数的返回值类型就不会出错,但这需要程序运行完才能知道结果的实际类型,即RTTI(Run-Time Type Identification 运行时类型识别)。

C++98中确实已经支持RTTI:
typeid只能查看类型不能用其结果类定义类型
dynamic_cast只能应用于含有虚函数的继承体系中

运行时类型识别的缺陷是降低程序运行的效率。

decltype:

decltype是根据表达式的实际类型推演出定义变量时所用的类型

  1. 推演表达式类型作为变量的定义类型
int main()

	 int a = 10;
	 int b = 20;
	 
	 // 用decltype推演a+b的实际类型,作为定义c的类型
	 decltype(a+b) c;
	 cout<<typeid(c).name()<<endl;
	 return 0; 

  1. 推演函数返回值的类型
void* GetMemory(size_t size) 

 	return malloc(size);

int main()

	 // 如果没有带参数,推导函数的类型
	 cout << typeid(decltype(GetMemory)).name() << endl;
	 
	 // 如果带参数列表,推导的是函数返回值的类型,注意:此处只是推演,不会执行函数
	 cout << typeid(decltype(GetMemory(0))).name() <<endl;
	 
	 return 0; 

四、范围循环for

参考之前的博客

五、final与override

参考之前的博客

六、智能指针

参考之前的博客

七、新增加容器–静态数组array、forward_list以及unordered系列

参考之前的博客

八、默认成员函数控制

在C++中对于空类编译器会生成一些默认的成员函数,比如:构造函数、拷贝构造函数、运算符重载、析构函数和&和const&的重载、移动构造、移动拷贝构造等函数。如果在类中显式定义了,编译器将不会重新生成默认版本。有时候这样的规则可能被忘记,最常见的是声明了带参数的构造函数,必要时则需要定义不带参数的版本以实例化无参的对象。而且有时编译器会生成,有时又不生成,容易造成混乱,于是C++11让程序员可以控制是否需要编译器生成。

显式缺省函数

在C++11中,可以在默认函数定义或者声明时加上=default,从而显式的指示编译器生成该函数的默认版本,用=default修饰的函数称为显式缺省函数。

class A 
public:
	 A(int a): _a(a)
	 
	 // 显式缺省构造函数,由编译器生成
	 A() = default;
	 
	 // 在类中声明,在类外定义时让编译器生成默认赋值运算符重载
	 A& operator=(const A& a);
private:
	 int _a;
;
A& A::operator=(const A& a) = default;
int main()

	 A a1(10);
	 A a2;
	 a2 = a1;
	 return 0; 

删除默认函数

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

class A 
public:
	 A(int a): _a(a)
	 
 
 // 禁止编译器生成默认的拷贝构造函数以及赋值运算符重载
	 A(const A&) = delete;
	 A& operator(const A&) = delete;
private:
	 int _a;
;
int main()

	 A a1(10);
	 // 编译失败,因为该类没有拷贝构造函数
	 
	 //A a2(a1);
	  
	 // 编译失败,因为该类没有赋值运算符重载
	 A a3(20);
	 a3 = a2;
	 return 0; 
 
 

注意:避免删除函数和explicit一起使用

九、右值引用

C++98中提出了引用的概念,引用即别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通过指针来实现的,因此使用引用,可以提高程序的可读性。

void Swap(int& left, int& right) 

	 int temp = left;
	 left = right;
	 right = temp; 
 
int main()

	 int a = 10;
	 int b = 20;
	 Swap(a, b);

为了提高程序运行效率,C++11中引入了右值引用,右值引用也是别名,但其只能对右值引用。

int Add(int a, int b) 

	 return a + b; 
 
int main()

	 const int&& ra = 10;
	 
	 // 引用函数返回值,返回值是一个临时变量,为右值
	 int&& rRet = Add(10, 20);
	 return 0; 
 

为了与C++98中的引用进行区分,C++11将该种方式称之为右值引用。

左值和右值

左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。

int g_a = 10;
// 函数的返回值结果为引用
int& GetG_A()

	 return g_a; 
 
int main()

	 int a = 10;
	 int b = 20;
	 
	 // a和b都是左值,b既可以在=的左侧,也可在右侧,
	 // 说明:左值既可放在=的左侧,也可放在=的右侧
	 a = b;
	 b = a;
	 const int c = 30;
	 // 编译失败,c为const常量,只读不允许被修改
	 //c = a;
	 // 因为可以对c取地址,因此c严格来说不算是左值
	 cout << &c << endl;
	 
	 // 编译失败:因为b+1的结果是一个临时变量,没有具体名称,也不能取地址,因此为右值
	 //b + 1 = 20;
	 
	 GetG_A() = 100;
	 return 0; 
 

因此关于左值与右值的区分不是很好区分,一般认为:

  1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
  2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间),
    C++11认为其是左值。
  3. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
  4. 如果表达式运行结果或单个变量是一个引用则认为是左值。

总结:

  1. 不能简单地通过能否放在=左侧右侧或者取地址来判断左值或者右值,要根据表达式结果或变量的性质判断,比如上述:c常量
  2. 能得到引用的表达式一定能够作为引用,否则就用常引用。

C++11对右值进行了严格的区分:

C语言中的纯右值,比如:a+b, 100
将亡值。比如:表达式的中间结果、函数按照值的方式进行返回。

引用与右值引用比较

在C++98中的普通引用与const引用在引用实体上的区别:

int main()

	 // 普通类型引用只能引用左值,不能引用右值
	 int a = 10;
	 int& ra1 = a; // ra为a的别名
	 //int& ra2 = 10; // 编译失败,因为10是右值
	 
	 const int& ra3 = 10;
	 const int& ra4 = a;
	 return 0; 

注意: 普通引用只能引用左值,不能引用右值,const引用既可引用左值,也可引用右值。

C++11中右值引用:只能引用右值,一般情况不能直接引用左值。

int main()

	 // 10纯右值,本来只是一个符号,没有具体的空间,
	 // 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
	 int&& r1 = 10;
	 r1 = 100;
	 
	 int a = 10;
	 int&& r2 = a; // 编译失败:右值引用不能引用左值
	 return 0; 
 

问题:既然C++98中的const类型引用左值和右值都可以引用,那为什么C++11还要复杂的提出右值引用呢?

答案肯定与效率有关

移动语义

C++11提出了移动语义概念,即:将一个对象中资源移动到另一个对象中的方式,可以有效缓解该问题。

在C++11中如果需要实现移动语义,必须使用右值引用。上述String类增加移动构造:

String(String&& s)
	 : _str(s._str)
 
	 s._str = nullptr;

因为strRet对象的生命周期在创建好临时对象后就结束了,即将亡值,C++11认为其为右值,在用strRet构造临时对象时,就会采用移动构造,即将strRet中资源转移到临时对象中。而临时对象也是右值,因此在用临时对象构造s3时,也采用移动构造,将临时对象中资源转移到s3中,整个过程,只需要创建一块堆内存即可,既省了空间,又大大提高程序运行的效率。

注意:

  1. 移动构造函数的参数千万不能设置成const类型的右值引用,因为资源无法转移而导致移动语义失效。
  2. 在C++11中,编译器会为类默认生成一个移动构造,该移动构造为浅拷贝,因此当类中涉及到资源管理时,用户必须显式定义自己的移动构造。

右值引用引用左值

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

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT

	 // forward _Arg as movable
	 return ((typename remove_reference<_Ty>::type&&)_Arg);

注意:

  1. 被转化的左值,其生命周期并没有随着左值的转化而改变,即std::move转化的左值变量lvalue不会被销毁。
  2. STL中也有另一个move函数,就是将一个范围中的元素搬移到另一个位置。

完美转发

完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。

void Func(int x) 

 // ......

template<typename T>
void PerfectForward(T t) 

 Fun(t);

PerfectForward为转发的模板函数,Func为实际目标函数,但是上述转发还不算完美,完美转发是目标函数总希望将参数按照传递给转发函数的实际类型转给目标函数,而不产生额外的开销,就好像转发者不存在一样。

所谓完美:**函数模板在向其他函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值;如果相应实参是右值,它就应该被转发为右值。**这样做是为了保留在其他函数针对转发而来的参数的左右值属性进行不同处理(比如参数为左值时实施拷贝语义;参数为右值时实施移动语义)。

右值引用作用

C++98中引用作用:因为引用是一个别名,需要用指针操作的地方,可以使用指针来代替,可以提高代码的可读性以及安全性。

C++11中右值引用主要有以下作用:

  1. 实现移动语义(移动构造与移动赋值)
  2. 给中间临时变量取别名:
int main()

	 string s1("hello");
	 string s2(" world");
	 string s3 = s1 + s2; // s3是用s1和s2拼接完成之后的结果拷贝构造的新对象
	 stirng&& s4 = s1 + s2; // s4就是s1和s2拼接完成之后结果的别名
	 return 0; 

  1. 实现完美转发

十、lambda表达式

C++98中的一个例子:

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

struct Compare

	 bool operator()(const Goods& gl, const Goods& gr)
	 
		 return gl._price <= gr._price;
	 
;

int main()

	 Goods gds[] =   "苹果", 2.1 ,  "相交", 3 ,  "橙子", 2.2 , "菠萝", 1.5 ;
	 sort(gds, gds+sizeof(gds) / sizeof(gds[0]), Compare());
	 return 0; 
 

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

lambda表达式:
lambda

以上是关于C++ 11特性的主要内容,如果未能解决你的问题,请参考以下文章

C++11 现代C++风格的新元素--简介

c++新特性11 (10)shared_ptr七reset

C++11新特性:5—— C++返回值类型后置(跟踪返回值类型)

并发编程基础概念

C++了解C++11新特性

C++了解C++11新特性