【Cpp】第十九章-Cpp11新特性

Cpp11新特性

  Cpp11中新增了很多新的语法,很多之前我们都已经有介绍过

初始化列表

如何使用

  在Cpp11中允许使用初始化列表初始化任何类型,不论是内置类型还是自定义类型都可以使用初始化列表进行初始化,而在Cpp98的版本中是不能初始化自定义类型的。

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
45
46
47
#include <iostream>
#include <vector>
class Test
{
public:
Test(int a, int b)
:_a(a)
,_b(b)
{
}
void Print()
{
std::cout << _a << " " << _b << std::endl;
}
private:
int _a;
int _b;
};
int main()
{
//内置类型的初始化列表初始化
int a = {10};
int b = {3 + 4};
std::cout << a << " " << b << std::endl;
int arr[] = {1, 2, 3, 4};
for(int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
std::cout << arr[i] << " ";
}
std::cout << std::endl;
//自定义类型初始化列表初始化
std::vector<int> arr2 = {4, 3, 2, 1};
for(auto e : arr2)
{
std::cout << e << " ";
}
std::cout << std::endl;
Test test = {7, 8};
test.Print();
}



10 7
1 2 3 4
4 3 2 1
7 8

initializer_list

  但是自定义类型想要支持像vector这样的初始化列表并不是天然就支持的,而是在Cpp11中新增了一个容器叫initializer_list,初始化列表,借助这个容器我们可以实现vector这样的初始化。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <iostream>
#include <vector>
class Test
{
public:
Test(int a, int b)
:_a(a)
,_b(b)
{
}
void Print()
{
std::cout << _a << " " << _b << std::endl;
}
private:
int _a;
int _b;
};
template<class T>
class Vector
{
public:
Vector(size_t n = 0)
:_start(new T[n])
,_finish(_start + n)
,_endOfStorge(_start + n)
{
}
//我们想要使用初始化列表初始化vector多亏下面这样的构造函数
Vector(const std::initializer_list<T>& list)//初始化列表容器
:_start(new T[list.size()])
,_finish(_start + list.size())
,_endOfStorge(_start + list.size())
{
//std::initializer_list容器有三个公有接口,start(), end()提供遍历,size()提供大小
int index = 0;
for(auto e : list)
{
_start[index] = e;
index++;
}
}
void Print()
{
T* start = _start;
while(start != _finish)
{
std::cout << *start << " ";
start++;
}
std::cout << std::endl;
}
private:
T* _start;
T* _finish;
T* _endOfStorge;
};
int main()
{
Vector<int> vec = {1, 2, 3, 4, 5};
vec.Print();
}


1 2 3 4 5

  因此如果我们在自己今后写自定义类型时,想要在初始化时利用初始化列表进行不定长参数的初始化时,就可以借助initializer_list来实现。
  如果我们想让一个自定义类型不再支持初始化列表进行初始化我们也可以通过加explicit关键字来禁用。

变量类型推导

  变量类型推导其中的典型代表就是我们一直在使用的auto关键字,它可以帮助我们简化代码书写,一些很复杂的类型一个auto即可代替,但是auto是编译时类型识别,除此之外还有一个关键字这里要提一下,即decltype,这个关键字我们之前并没有使用过,不过这个关键字是RTTI的,它可以在运行时进行类型识别。
  autodecltype之间最显著的差别就是,auto在编译时就将变量类型确定下来,因此如果我们声明一个变量而不去定义它那么编译器此时就无法识别它的变量类型,因此这是不合法的书写方式,但是decltype却有办法去识别类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int fun()
{
return 10;
}
int main()
{
//auto a;//这是不合法的,因为编译器此时无法判断它的类型
decltype(3 + 1) b;//decltype可以通过推导括号中表达式的类型来定义变量
std::cout << typeid(b).name() << std::endl;//typeid可以识别变量类型,它也以RTTI的思想来实现的
decltype(fun()) c;
std::cout << typeid(c).name() << std::endl;//typeid可以识别变量类型,它也以RTTI的思想来实现的
}


i
i

委派构造函数

  委派构造函数即在一个类的构造函数中可以调用这个类的其它构造函数。Cpp11之前是不允许这样的语法的,但是在Cpp11中加入了这样的新特性,但是却也带来了问题,就像是下面这样的情况。

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
#include <iostream>

class A
{
public:
A()
:A(10, 20)//无参构造中调用带参构造
{

}
A(int a, int b)
:A()//带参构造中调用无参构造
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
A a;
std::cout << "finish" << std::endl;//这里调用就会出现死递归调用,从而崩掉
}


崩溃

默认函数控制

  默认函数控制可以帮助我们很好的管理一个类中的默认成员函数,控制其是否应该自动生成。我们之前想要禁用拷贝构造,赋值运算符重载往往是将他们的声明放在private中,但是在Cpp11中我们可以这样写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>

class A
{
public:
A(int a)
:_a(a)
{
}
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
int main()
{
A a(1);
//A b(a);//禁用拷贝
}

  同时我们也可也让编译器自动帮我们生成默认构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

class A
{
public:
A(int a)
:_a(a)
{
}
A() = default;//生成默认构造
A(const A& a) = delete;
A& operator=(const A& a) = delete;
private:
int _a;
};
int main()
{
A a(1);
A b;//合法
//A b(a);//禁用拷贝
}

右值引用

左值和右值

  什么是右值呢?在C语言中就有左值和右值的概念,这里简单总结下可以理解为:
  1、左值就是可以出现在复制运算符左右两边的值,左值往往是可以取地址的。
  2、右值就是只可以出现在赋值运算符右边的值,右值往往不可以取地址。
  常见的右值有常量,临时变量和将亡值(即将销毁的值)。

左值引用和右值引用

  我们之前所使用的引用都是左值引用,左值引用既可以引用左值,也可也引用右值,因为我们可以使用指向常量的引用去引用常量,例如const int& ra = 10;这条语句是合法的。
  而右值引用只可以引用右值。例如int&& rra = 10;,此时不需要加const就可以直接引用右值,这就是一条典型的右值引用。,当然右值引用也可也引用临时变量和将亡值。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int fun(int a)
{
return a;
}
int main()
{
int a = 10;
fun(a);
const int & ra = fun(a);//这里fun(a)返回的是一个临时变量,我们不可以直接使用引用指向它,但是左值引用加上const就可以指向右值
int&& rra = fun(a);//但是我们如果使用右值引用就可以直接引用临时变量
}

移动语义

  如果我想要右值引用去引用左值可以么?在Cpp中移动语义可以帮助我们完成将一个左值变为右值,从而可以让右值引用去引用它。我们可以通过调用std::move()完成移动语义。

1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
int a = 10;
int && ra = std::move(a);//移动语义,将左值改为右值
const int& rra = std::move(a);
}

移动构造

  既然左值引用既可以引用左值也可也引用右值,那还要右值引用有什么用呢?我们先看一下这个例子。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <string.h>

//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("Misaki");
std::cout << str << std::endl;
}
int main()
{
test();
}


String(char* str = )
String(const String& s)
Misaki

  这个例子中细心的同学会发现,在这个例子中其实会生成三个对象。首先有参构造一个,然后由于我们的函数返回值返回对象,于是拷贝构造临时对象,然后再用临时对象拷贝构造最终我们需要的str对象,于是会发生依次构造,两次拷贝构造,但是由于编译器的优化会帮我们减少为一次构造一次拷贝构造(如果是更厉害的编译器会直接优化成一次构造,比如MinGW,为此为了演示我换上了vs),但是仍然有多余的损耗,因为我们为了构造str而构造了temptemp构造后是一个将亡值,随后很快会进行释放,紧接着我们用它构造str又开辟了新的空间,为了构造str我们先释放了temp的空间又重新为str开辟了空间,尽管他们空间中的内容应该是一样的。所以这是多次一举的行为,十分低效。
  那么有没有一种办法可以让我们的str直接利用temp开辟好的空间,而不再多此一举自己重新开辟空间呢?这样的做法明显是更加高效的,答案是有的。在Cpp11中由于右值引用的出现,于是出现了移动构造,即利用右值引用进行构造。这里右值引用的多是一个将亡值,即即将释放资源的值,既然他们的资源即将要释放,而我们构造新对象所需要的资源刚好和他们要释放的资源一样,那不如直接把他们的资源拿来用。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include <iostream>
#include <string.h>

//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
//移动构造
String(String&& s)
:_str(s._str)
{
s._str = nullptr;//这里一定要记着将将亡值的原本指针置空,否则会把我们拿来用的资源释放了
std::cout << "String(String&& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("Misaki");
std::cout << str << std::endl;
}
int main()
{
test();
}


String(char* str = )
String(String&& s)
Misaki

  这里编译器判断temp是一个右值,自动调用了移动构造,移动构造中所做的事情就是将即将释放的资源直接拿来给新创建的对象使用,于是省掉了一次先释放空间再申请空间的过程。移动构造可以提升代码效率,减少拷贝。

移动赋值

  有移动构造就有移动赋值。

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <iostream>
#include <string.h>

//实现一个简单的string类
class String
{
friend std::ostream& operator<<(std::ostream& out, const String& str);
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
std::cout << "String(char* str = )" << std::endl;
}
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
std::cout << "String(const String& s)" << std::endl;
}
//移动构造
String(String&& s)
:_str(s._str)
{
s._str = nullptr;//这里一定要记着将将亡值的原本指针置空,否则会把我们拿来用的资源释放了
std::cout << "String(String&& s)" << std::endl;
}
String& operator=(const String& s)
{
if (this != &s)
{
char* temp = _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
delete[] temp;
}
return *this;
}
//移动赋值
String& operator=(String&& s)
{
if (this != &s)
{
char* temp = _str;
_str = s._str;
s._str = temp;
std::cout << "String& operator=(String&&)" << std::endl;
}
return *this;
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
std::ostream& operator<<(std::ostream& out, const String& str)
{
out << str._str;
return out;
}
String getString(const char* str)
{
String temp(str);
return temp;
}
void test()
{
String str = getString("Misaki");
str = String("misaki");
std::cout << str << std::endl;
}
int main()
{
test();
}


String(char* str = )
String(String&& s)
String(char* str = )
String& operator=(String&&)
misaki

  我们在写移动构造和移动赋值的时候要注意如果一个类中有自定义类型成员,我们也应该让他们进行移动构造或移动赋值,可以通过移动语义将内部成员从左值转换为右值从而调用他们的移动构造或移动赋值。

完美转发

  我们看下面一段代码。

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
#include <iostream>

void Fun(int& x)
{
std::cout << "lvalue ref" << std::endl;
}
void Fun(const int& x)
{
std::cout << "const lvalue ref" << std::endl;
}
void Fun(int&& x)
{
std::cout << "rvalue ref" << std::endl;
}
void Fun(const int&& x)
{
std::cout << "const rvalue ref" << std::endl;
}
template<class T>
void PerfectForward(T t)
{
Fun(t);
}
int main()
{
PerfectForward(10);//rvalue ref
int a = 10;
PerfectForward(a);//lvalue refj
PerfectForward(std::move(a));//rvalue ref
const int b = 8;
PerfectForward(b);//const lvalue ref
PerfectForward(std::move(b));//const rvalue ref
}


lvalue ref
lvalue ref
lvalue ref
lvalue ref
lvalue ref

  我们明明传入了不同的类型进入函数模板,本应该产生不同的结果,为什么进到函数模板中就全部变成了左值引用呢?
  这里就是在我们把值传递进函数模板时编译器会将所有值的属性全部更改为左值。那么有没有办法让值在传递过程中属性不发生改变呢?这里就要用到完美转发,来让值的属性不变。在使用完美转发之前我们还要直到一点,在模板中,自定义类型加&&表示未定义类型,表示传入模板的是什么类型参数,此时就使用什么类型的参数,例如下面这样。

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
#include <iostream>

void Fun(int& x)
{
std::cout << "lvalue ref" << std::endl;
}
void Fun(const int& x)
{
std::cout << "const lvalue ref" << std::endl;
}
void Fun(int&& x)
{
std::cout << "rvalue ref" << std::endl;
}
void Fun(const int&& x)
{
std::cout << "const rvalue ref" << std::endl;
}
template<class T>
void PerfectForward(T&& t)//在模板中这里不是右值引用,代表未定义类型
{
Fun(t);
}
int main()
{
PerfectForward(10);//rvalue ref
int a = 10;
PerfectForward(a);//lvalue refj
PerfectForward(std::move(a));//rvalue ref
const int b = 8;
PerfectForward(b);//const lvalue ref
PerfectForward(std::move(b));//const rvalue ref
}


lvalue ref
lvalue ref
lvalue ref
const lvalue ref
const lvalue ref

  但是至此结果还是不对,于是我们再加上完美转发。

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
#include <iostream>

void Fun(int& x)
{
std::cout << "lvalue ref" << std::endl;
}
void Fun(const int& x)
{
std::cout << "const lvalue ref" << std::endl;
}
void Fun(int&& x)
{
std::cout << "rvalue ref" << std::endl;
}
void Fun(const int&& x)
{
std::cout << "const rvalue ref" << std::endl;
}
template<class T>
void PerfectForward(T&& t)//在模板中这里不是右值引用,代表未定义类型
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10);//rvalue ref
int a = 10;
PerfectForward(a);//lvalue refj
PerfectForward(std::move(a));//rvalue ref
const int b = 8;
PerfectForward(b);//const lvalue ref
PerfectForward(std::move(b));//const rvalue ref
}


rvalue ref
lvalue ref
rvalue ref
const lvalue ref
const rvalue ref

  至此结果就正确了,完美转发可以帮助我们保证值在参数传递过程中属性不会改变。

lambda表达式

  lambda表达式是为了方便我们传入回调函数的语法。之前我们在传入回调函数时往往是利用仿函数,传入一个对象,随后在方法内通过调用对象的operator()来回调函数,但是在Cpp11中为了更加方便我们书写回调函数,加入了lambda表达式。

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
#include <iostream>
#include <algorithm>
#include <vector>

int main()
{
std::vector<int> arr = {1, 10, 3, 4, 5, 2, 5, 7};
std::sort(arr.begin(), arr.end(), std::less<int>());//通过仿函数的方式传入,从小到大排序
for(auto e : arr)
{
std::cout << e << " ";
}
std::cout << std::endl;
std::sort(arr.begin(), arr.end(), //lambda表达式传入
[](int a, int b)->bool
{
return a > b;
}
);
for(auto e : arr)
{
std::cout << e << " ";
}
std::cout << std::endl;
}



1 2 3 4 5 5 7 10
10 7 5 5 4 3 2 1

  lambda表达式就是一个匿名函数,可以分为以下几个部分:

1
2
3
4
[]:捕捉列表
():参数列表
->:返回值
{}:函数体

  捕捉列表或许是比较特殊的一个部分,我们可以通过它来捕捉外层作用域内的变量。[=]表示以值得形式捕捉外部作用域内所有变量;[a, b]表示以值的形式捕捉a,b变量;[&]表示以引用的形式获取外部所有变量;[this]表示获取成员变量的this指针,只能在成员函数内部使用。捕捉到的变量可以直接在函数内部使用。
  我们默认从外部捕捉的变量都是const的,不可修改,要想修改外部捕获进来的值,需要在参数列表后面加mutable关键字。
  lambda表达式的返回值和参数部分都可以省略掉,因此[]{}也可以看作是一个lambda表达式

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助