文章目录
C++11简介
相比于C++98/03,C++包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好的用于系统开发和库开发、语法更加泛化和简单化、更加稳定和安全,功能更加强大,很好的提高了其使用者的开发效率。
列表初始化
花括号的初始化问题
在C++98标准中,允许使用"{}"对数组元素进行统一的列表初始值设定。但是对于自定义类型,却无法进行这样的初始化。相对于C++98而言,C++11扩大了用花括号括起的列表(初始化列表)的使用范围,使其可用于所有内置类型和用户自定义的类型的列表初始化,使用初始化列表时,可添加等号,也可不添加。
内置类型的初始化列表
void main()
{
//内置类型变量
int x = 10;
int y{ 10 };
int z = { 10 };
int i = 1 + 2;
int n = { 1 + 2 };
int m{ 1 + 2 };
//数组
int arr1[3]{ 55,66,77 };
int arr2[]{ 55,66,77 };
//动态数组 C++98中不支持
int* arr = new int[3]{ 55,66,77 };
//标准容器
vector<int> v{ 11,22,33 };
map<int, int> m{ {1,1},{2,2},{3,3} };
}
列表初始化可以在花括号前使用等号,不使用也可以,两者没有什么区别。
void main()
{
int *ptr = new int[10]{ 1,2,3,4,5,6,7,8,9,10 };
delete[]ptr;
}
ptr是数组指针,也就是指向数组的指针,我们在堆上面开辟了一段空间,最后记得进行释放,释放的时候,ptr的类型要写对,带中括号。
多个对象的列表初始化
相较于C++98,C++11支持多个对象的列表初始化,因为C++底层提供了一个类模板,即
initializer_list
,并对内置类型提供了initializer_list类型参数的构造函数。initializer_list是系统自定义的类模板,该类模板中主要有三个方法:begin()、end()迭代器以及获取区间中元素个数的方法size()。
对于自定义类,如果想要该类支持列表初始化,只需要给该类添加一个带有initializer_list类型参数的构造函数。
class Pointer
{
public:
Pointer():m_x(0),m_y(0)
{}
Pointer(initializer_list<int> list)
{
m_x = *list.begin();
auto it = list.end();
--it;
m_y = *it;
}
Pointer(int x,int y):m_x(x),m_y(y)
{}
public:
int m_x;
int m_y;
};
void main()
{
list<int> lt = { 1,2,3,4,5 };//内置容器的列表初始化
Pointer p = { 1,2 };//自定义类型的列表初始化
}
变量类型推导
在定义变量时,必须要先给出变量的实际类型,才能定义变量,因为定义变量需要开辟栈空间,所以必须确定类型以确定开辟的大小空间。
但是在很多情况下,我们可能不知道实际类型怎么给出,或者实际类型特别复杂,直接给出类型特别麻烦,于是就有了变量类型推导这回事儿。
auto
C++11中,可以使用auto来根据变量初始化表达式类型推导变量的实际类型
。
void main()
{
map<int, string> m;
map<int, string>::iterator it = m.begin();
auto it1 = m.begin();
}
it1和it的类型相同,且使用起来并没有什么不同,但是很显然,用auto自动推导it1的类型比直接定义要简洁方便许多。
decltype
auto使用的前提是,必须对auto声明的变量进行
初始化
,否则编译器无法推导出auto的实际类型,但很显然,这样有其局限性,比如我们的变量需要根据表达式完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto就不能满足我们的需求,于是运行时类型识别(RTTI)就应运而生了。
在C++98中,确实已经支持了RTTI:
- typeid只能查看类型不能用其结果类定义类型
- dynamic_case只能用于含有虚函数的继承体系中
不过运行时类型识别有其缺陷,会降低程序运行的效率。
decltype是根据表达式的实际类型
推演出定义变量时所用的类型。
- 推演
表达式类型
作为变量的定义类型
void main()
{
int a = 1;
double b = 1.23;
decltype(a + b) c;
cout << typeid(c).name() << endl;
}
c的定义就是用decltype推演出a+b这个表达式的实际类型来定义的,auto是做不到这一点的。
- 推演
函数返回值
的类型
int GetMax(int x, int y)
{
return x > y ? x : y;
}
void main()
{
cout << typeid(decltype(GetMax)).name() << endl;//A句
cout << typeid(decltype(GetMax(9, 8))).name() << endl;//B句
}
执行结果:
int __cdecl(int,int)
int
A句不带参数,就是推导函数的类型
;B句带参数列表,推导的就是函数返回值的类型
,要注意的是,B句只是进行类型推演,并不会执行函数
。
默认成员函数控制
显式缺省函数
在C++11中,可以在默认函数定义或者声明时加上“=default”,从而显式的指示编译器生成该函数的默认版本,用“=default”修饰的函数称为显式缺省函数
。
class Test
{
public:
Test() = default;
//无参构造函数
Test(const Test&) = default;
//显式生成默认的拷贝构造函数
Test& operator=(const Test&) = default;
~Test() = default;
Test(int d):m_data(d)
{}
private:
int m_data;
};
void main()
{
Test t;
}
删除默认函数
如果要限制某些默认函数的生成,在C++11中,只需要在函数声明加上“=delete”即可,我们称“=delete”修饰的函数称为删除函数
。
右值引用
右值引用的基本概念
C++98中提出了引用的概念,很大的提高了程序的可读性,引用就是别名,引用变量与其引用实体共用一块内存空间,引用的底层是通过指针来实现的,避免了我们直面指针。
为了提高程序的运行效率,C++11中引入了右值引用
,右值引用也是起别名,但是只能对右值进行引用
。与右值相对的是左值,我们要右值引用,第一个问题就是得弄清楚什么是右值。
一般情况下,我们认为可以放在等号左边的,能够取地址的称为左值,只能放在右边的,或者说不能取地址的称为右值,但是这样区分太笼统了,不一定完全正确。
C++1对右值进行了严格的区分:
- C语言中的纯右值,比如a+b、10
- 将亡值。典型的就是表达式的中间结果,函数按照值的方式返回的值。
void main()
{
int a = 10;
int &b = a;//引用
const int &c = 10;//常引用
int &&d = 10;//右值引用
}
10是右值,具有常性,要引用10,有两种办法,第一种就是常引用,加const;第二种就是右值引用,右值引用有两个&符号。
10是一种符号,该符号本身并不具备空间,这也就是为什么普通引用会报错,左值引用必须加const的原因,我们说引用是起别名,起别名的前提是地址空间存在,这样才能给一块空间起别名,这也就可以理解为什么普通引用不能引用10,加上const之后,我们会开辟一段临时空间,并用10对这段空间进行初始化,常引用引用的是这块空间。
对于这种本身是一种符号常量的,C++11引入了右值引用专门来引用他们。同样的,右值引用也会开辟空间,并用10进行初始化,右值引用这块开辟的空间。
对将亡值的右值引用:
int fun(int a, int b)
{
int result = a + b;
return result;
}
void main()
{
int res = fun(1, 2);
//int &res1 = fun(1, 2);编译不能通过
int &&res2 = fun(1, 2);
const int &res3 = fun(1, 2);
}
首先我们需要明白的是,fun函数最后得到的计算结果result是一个临时变量,生存作用域仅限于fun函数的函数体内部,出了函数体,result就会被销毁,那我们是怎么得到函数返回值的呢?我们会创建一个临时变量,这个临时变量被result初始化,保存了函数的返回值,这个临时变量会去初始化我们的res,res就是我们接收函数返回值的变量。
需要注意的是,我们之所以能用右值去引用函数返回值,是因为我们上面介绍的那个充当result和res之间的桥梁的临时变量是具有
常性
的,它的常性决定了它是右值,可以用右值引用。
普通引用只能引用左值,不能引用右值,const引用既可以引用左值,又可以引用右值。
C++11中右值引用:只能引用右值,一般情况下
不能直接引用左值。
右值引用存在的必要性
如果一个类涉及到资源管理,用户必须显式提供拷贝构造、赋值运算符的重载以及析构函数,否则系统自己生成的默认的函数会造成浅拷贝,造成程序崩溃。这种浅拷贝和深拷贝的问题我们之前用String类详细的分析过,今天String类做一个例子,我们看一下它对"+"的重载函数:
class String
{
public:
String(const char *str = ""):m_data(nullptr)
{
cout<<"Create Obj : "<<this<<endl;
m_data = new char[strlen(str)+1];
strcpy(m_data, str);
}
String(const String &s)
{
cout<<"Copy Create Obj : "<<this<<endl;
m_data = new char[strlen(s.m_data)+1];
strcpy(m_data, s.m_data);
}
String(String &&s)
{
//移动构造
cout<<"Move Create Obj : "<<this<<endl;
m_data = s.m_data;
s.m_data = nullptr;
}
String& operator=(const String &s)
{
cout<<"Assign Obj : "<<this<<endl;
if(this != &s)
{
char *new_data = new char[strlen(s.m_data)+1];
strcpy(new_data, s.m_data);
delete []m_data;
m_data = new_data;
}
return *this;
}
~String()
{
cout<<"Free Obj : "<<this<<endl;
if(m_data)
{
delete []m_data;
m_data = nullptr;
}
}
public:
String operator+(const String &s)
{
cout<<"operator+"<<endl;
char *new_data = new char[strlen(m_data) + strlen(s.m_data) + 1];
strcpy(new_data, m_data);
strcat(new_data, s.m_data);
String new_str(new_data);
return new_str;
}
private:
char *m_data;
};
我们在上面解释了函数给调用者返回执行结果时生成临时对象的问题,对于上述加法的重载也是一样的,按照值的方式返回,一定会拷贝构造一个临时对象,临时对象在创建好之后,new_str就会被销毁,创建的临时对象和new_str内容完全相同,但是却拥有不同的地址空间,这对于空间来说是一种浪费,同时程序的效率也会降低,而且临时对象的作用不算很大,就相当于建立了一个桥梁,用于过渡。
那这种情况怎么优化呢?此时就到右值引用登场了。
移动语义
C++11中提出了移动语义的概念,即:将一个对象中的资源移动到另一个对象中
的方式,有效的缓解了上述必须拷贝构造临时变量带来的空间浪费以及运行效率降低的问题。
我们有一个主函数:
int main()
{
String s("Hello");
String s1("world");
String s2 = s + s1;
return 0;
}
当我们类中没有移动构造的函数的时候,我们会构造临时对象,现在我们在String类中显式的定义了一个移动构造函数,现在程序的执行效果是:执行+的重载函数后,构造出new_str对象,new_str对象有一个new_data指针,new_data指针指向的空间保存了字符串信息,我们现在用mmm表示这块空间,new_str在出了函数作用域之后就会被析构,所以C++认为new_str是
将亡值
,也就是右值,那么在构造临时对象的时候,由于最佳参数匹配原则
,选择了参数为右值的移动构造函数
,而不是普通的拷贝构造函数,我们再研究一下移动构造的函数体:
String(String &&s)
{
//移动构造
m_data = s.m_data;
s.m_data = nullptr;
}
我们临时对象的m_data指针指向了new_str的new_data指针所指的地址空间,再将new_str的new_data指针置空,也避免了在析构new_str对象时造成mmm空间被释放。这个过程省去了旧空间的释放和新空间的申请以及空间内容的拷贝。在临时对象去构造最终结果s2时,过程是一样的。
总的来说,整个过程,只需要创建一块堆内存即可,既节省了空间,又大大提高了程序运行的效率。
在这个过程中,右值扮演了一个重要的角色,将亡值是右值,以此在构造的时候,根据最佳参数匹配原则,匹配了参数是右值的移动构造函数。
在移动构造的使用过程中,需要注意的是:
- 移动构造函数的参数千万
不能设置成const类型
的右值引用,否则资源无法转移会导致移动语义失效
。 - 在C++11中,编译器会为类默认生成一个移动构造,该移动构造是
浅拷贝
,因此当类中涉及到资源管理时,我们必须显式
定义自己的移动构造。
右值引用引用左值
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?在一些场景下,我们需要用右值去引用左值实现移动语义。那这种情况要怎么办呢?move函数就登场了。
当需要用右值引用引用一个左值时,可以通过move函数
将左值转化为右值。C++11中std::move()函数位于头文件中,要注意不能被该函数的名字迷惑了,它不搬移任何东西
,唯一的功能就是将一个左值强制转化为右值
进行引用,以实现移动语义。
int main()
{
String s1("Hello");
String s2(s1);//拷贝构造
String s3(move(s1));//移动构造
return 0;
}
上述代码需要注意的是,在经历了s3的移动构造后,s1会变成无效的字符串,因为s1和s3之间实现了移动语义,s1中的资源被转移到了s3中。
被转换成右值的左值,其生命周期不会随着左值的转化而改变。
完美转发
完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另外一个函数。
我们来观察一下下面这段代码:
void Fun(int &x)
{
cout << "lvalue ref" << endl;
}
void Fun(int &&x)
{
cout << "rvalue ref" << endl;
}
void Fun(const int &x)
{
cout << "const lvalue ref" << endl;
}
void Fun(const int &&x)
{
cout << "const rvalue ref" << endl;
}
template<typename T>
void PerfectForward(T &&t)
{
Fun((t));
}
int main()
{
PerfectForward(10); // rvalue ref
int a;
PerfectForward(a); // lvalue ref
PerfectForward(std::move(a)); // rvalue ref
const int b = 8;
PerfectForward(b); // const lvalue ref
PerfectForward(std::move(b)); // const rvalue ref
return 0;
}
执行结果:
lvalue ref
lvalue ref
lvalue ref
const lvalue ref
const lvalue ref
很明显,执行结果并不符合我们的预期,PerfectForward为转发的模板函数,Fun为实际目标函数,完美转发希望参数按照传递给转发函数的实际类型转给目标函数,不产生额外的开销
,就好像不存在转发者一样,但是上述执行结果明显看出目标函数匹配的参数不是我们传递的参数的真实类型。
所谓完美:就是函数模板在向目标函数传递自身形参时,如果相应实参是左值,它就应该被转发为左值,如果相应的实参是右值,它就应该被转发为右值,避免转发函数针对转发而来的参数的左右值属性进行不同的处理。
C++11通过forward函数来实现完美转发
,做到参数的完美匹配。
上述代码,我们将在调用Fun函数前先通过forward函数对参数进行处理,实现完美转发。
void PerfectForward(T &&t)
{
Fun(forward<T>(t));
}
我们来看一下执行结果:
rvalue ref
lvalue ref
rvalue ref
const lvalue ref
const rvalue ref
右值引用作用
C++98中引用作用:引用是一个别名,需要用指针操作的地方,可以用引用代替,提高了代码的安全性和可读性
。
C++11中右值引用主要有以下作用:
- 实现
移动语义
,主要体现在移动构造和移动赋值 给中间临时变量取别名
,主要体现在将亡值- 实现
完美转发