C++11新特性
#《C++11新特兴》
概述
- 提高运行效率的语言特性:右值引用、泛化常量表达式
- 原有语法的使用性增强:初始化列表、统一的初始化语法、类型推导、范围for循环、Lambda表达式、final和override、构造函数委托
- 语言能力的提升:空指针nullptr、default和delete、长整数、静态assert
- C++标准库的更新:智能指针、正则表达式、哈希表等
PART1:提高运行效率的语言特性
特性1:右值引用
首先先介绍左值lvalue
是和右值rvalue
。
基本定义
左值
lvalue
全称是location value
表示存储在内存中==有明确存储地址(可取地址)==的数据。例如一个变量。右值
rvalue
全称是read value
表示==提供数据值(不可取地址)==的数据。例如,一个整数或字符串。c++11
中的右值分为两种:- 纯右值:例如字符串、数字之类的。
- 将亡值:表示一个即将消亡的对象。例如与右值引用相关的表达式,比如:
T&&
类型函数的返回值,std::move
的返回值等。
==简单来说,左值可以被取地址,右值不可以被取地址。==
书写方式
左值引用,采用一个
&
。1
2int a=4;
int& b=a; //左值引用常量左值引用是一个万能引用类型,它可以接受一个左值、右值、常量左值和常量右值。
右值引用,采用两个
&&
。1
int&& b=6; // 右值引用只能赋值右值和右值引用
作用
左值引用:==提高程序效率==,不进行拷贝对象仅仅是起别名。例如再传递形参时,不需要拷贝对象。
右值引用:==延长临时对象的生命周期==。主要是对一些临时对象的寿命延长,提高程序的效率。
考虑情景:在
c++
中进行对象赋值操作的时候,很多情况下会发生对象之间的拷贝构造,如果堆内存较大,则深拷贝的代价较高,因此在有些情况下需要避免对象的深拷贝,就可以使用右值引用进行性能的优化。案例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using namespace std;
class Test {
public:
int* m_p;
public:
Test():m_p(new int(10))
{// 构造函数
cout << " construct function" << endl;
cout << "address m_p=" << m_p<<endl;
}
// 析构函数
~Test() {
cout << " delete construct" << endl;
delete m_p;
m_p = nullptr;
}
// 复制构造函数
Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
// 有指针需要进行深拷贝
cout << "copy constructor" << endl;
}
};
Test getobject() {
Test obj;
return obj;
}
int main() {
Test t = getobject();
return 0;
}上述代码运行结果如下:
首先调用构造函数生成一个临时对象
obj
,然后进行拷贝构造函数赋值main
中的对象t
,然后临时对象obj
和对象t
调用析构函数,释放内存。但该过程存在的问题是==临时对象并没有使用就进行释放==,如果可以使用创建出的临时对象obj
则可以节省资源,因此考虑右值引用。为了使用右值引用,我们对类中==添加移动构造函数(将堆中的资源进行移动,并将原来的指针置为
nullptr
,否则会析构两次相同地址)==,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
using namespace std;
class Test {
public:
int* m_p;
public:
Test():m_p(new int(10))
{// 构造函数
cout << " construct function" << endl;
cout << "address m_p=" << m_p<<endl;
}
// 析构函数
~Test() {
cout << " delete construct" << endl;
delete m_p;
m_p = nullptr;
}
// 复制构造函数
Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
// 有指针需要进行深拷贝
cout << "copy constructor" << endl;
}
// 移动构造函数
Test( Test&& obj) :m_p(obj.m_p) {
obj.m_p = nullptr;
cout << "move constructor" << endl;
}
};
Test getobject() {
Test obj;
return obj;
}
int main() {
/*
这里没有使用拷贝构造函数的原因是因为右边的对象是一个临时对象,编译器会调用移动构造函数,否则会使用拷贝构造函数
*/
Test&& t2 = getobject();
cout << "address m_p=" << t2.m_p << endl; // 使用堆中的空间
return 0;
}此时的数据结果如下所示,可以看到此时调用的是移动构造函数(==此时没有调用拷贝构造的原因是因为=右边是一个临时对象,编译器会调用移动构造函数,否则会使用拷贝构造函数。==):
另外一种不使用移动构造函数的方法是直接返回一个右值引用,具体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using namespace std;
class Test {
public:
int* m_p;
public:
Test():m_p(new int(10))
{// 构造函数
cout << " construct function" << endl;
cout << "address m_p=" << m_p<<endl;
}
// 析构函数
~Test() {
cout << " delete construct" << endl;
delete m_p;
m_p = nullptr;
}
// 复制构造函数
Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
// 有指针需要进行深拷贝
cout << "copy constructor" << endl;
}
};
Test getobject2() { // 返回一个将忘值
return Test(); // 不能取地址
}
Test&& getobject2() { // 返回一个将忘值
return Test(); // 不能取地址
}
int main() {
// 如果没有移动构造函数,使用右值引用初始化要求更高,要求右侧是一个临时的不能取地址的对象。
Test&& t2 = getobject2();
cout << "address m_p=" << t2.m_p << endl; //使用所有资源
Test&& t3 = getobject3();
cout << "address m_p=" << t3.m_p << endl; //使用所有资源
return 0;
}
注意:
- 如果==不在类中定义移动构造函数==,则对右值引用初始化==要求更高==,要求右侧是一个临时的不能取地址的对象,如代码中的
return Test();
。 - 定义移动构造函数函数是使用==部分==临时对象的值,而使用右值引用(不定义移动构造函数)则是使用==所有==临时对象的资源。
注:const
和constexpr
都可以用于定义常量,但是constexpr
在==编译阶段==会将对应的常量替换为数据提高运行效率。
特性2:泛化的常量表达式
解决问题&作用
c++11之前存在常量关键字const
,存在两种含义:变量只读和修饰常量。如下案例所示:
1 | void f(const int num){ // 此时是变量只读的含义并非常量 |
常量表达式发生在编译阶段,而非常量表达式在运行阶段计算。常量表达式可以提高运行效率。
c++11中引入了constexpr
用于修饰常量表达式,常量表达式是指由多个常量组成并且在编译阶段就能得到计算结果的表达式。
而问题在于如何让编译器知道一个表达式是常量表达式,因此在c++11中引入了constexpr
关键字用于指示常量表达式,提高程序运行效率。
使用方法
修饰常量:可以在任何自定义类型前加上
constexpr
关键字,而在定义时对于自定义类型的struct
和class
之前加constexpr
是无效的,而可以对定义一个该类型的常量对象。常量表达式函数:
修饰函数:需要符合以下几个条件
- 函数必须要有返回值,并且
return
的表达式必须是常量表达式。 - 使用之前必须要有对应的定义语句。
- 函数体中不能出现非常量表达式以外的语句(
using
指令、typedef
语句以及static_assert
断言、return
语句除外) - 以上规则对于成员函数也是适用的。
- 函数必须要有返回值,并且
修饰模版函数
模版类型中存在不确定性,因此函数模版实例化后的模版函数是否符合常量表达式函数的要求也不确定。==如果
constexpr
修饰的模版函数实例化结果不满足常量表达式函数的要求,则constexpr
会被自动忽略,即该函数等同于一个普通函数==。1
2
3
4template <typename T>
constexpr T funct(T t){ //由于不能确定T是否传入一个常量类型因此不能返回值可能不符合常量表达式要求,会被自动忽略。
return t;
}
修饰构造函数
可以使用
constexpr
修饰一个构造函数,这样可以得到一个常量构造函数,但是要求是==构造函数的函数体必须为空,并且采用初始化列表的方式为各个成员赋值==。1
2
3
4
5
6
7
8class Test{
public:
int a;
public:
constexpr Test():a(100){
}
};
Tips:
采用
using
定义新类型和typedef
定义新类型效果一样,只是using
更加直观。1
2using mytype=int;
mytype num1=10;函数模版与模版函数的区别:函数模版是指创建函数的一个模版,模版函数是使用模版实例化的一个函数。