目录
1.列表初始化
1.1 { } 初始化
1.2 std::initializer_list
2.声明
2.1 auto
2.2 decltype
2.3 nullptr
3. 范围for
4.STL中的一些变化
5.右值引用与移动语义
5.1 左值引用与右值引用
5.2 左值引用与右值引用的比较
5.3 右值引用使用场景
5.4 完美转发
6.新的类功能
6.1 默认成员函数
6.2 类成员变量初始化
6.3 default
6.4 delete
6.5 final与override
7.可变参数模板
7.1 递归方式展开
7.2 逗号表达式展开
1.列表初始化
1.1 { } 初始化
{}最初是在C++98中被用于初始化数组或者初始化结构体,在C++11中允许用列表来初始化所有内置类型和自定义类型。{}与变量间的=可以省略。
例如:
int main()
{int a = 4;//C++11之前的初始化方式int b = { 4 };// 新初始化方式,这里的 = 可以删除int* c = new int(4);//旧版本int* d = new int{ 4 };//{}也可用于newreturn 0;
}
{}也可用于调用多参数的构造:
class A
{
public:A(int x = 0, char y = '0', double z = 0):_x(x), _y(y), _z(z){}
private:int _x;char _y;double _z;
};
int main()
{A n(1, 'a', 0.5);//旧版本A t{ 1,'a',0.5 };//新版本return 0;
}
1.2 std::initializer_list
std::initializer_list是一种类型,用来访问初始化列表中的值,一般作为构造函数的参数,C++11中STL的一些容器就增加了用std::initializer_list作参数的构造函数。它可以类似看做是用一个数组存储了{ }里面的内容,然后通过取出里面的元素来初始化。
以vector为例:
int main()
{vector<int>v{1,2,3,4,5};return 0;
}
这里vector的构造函数增加了std::initializer_list作参数的构造函数,它存储了{}里面的内容,这个构造函数的内部实现就是取出{}里面的内容,然后再添加到vector里面。
这样初始化容器就更方便了。比如:
int main()
{vector<pair<int, int>>v{ {1,1},{2,2},{3,3} };return 0;
}
这里我们就不用先构造pair再来初始化了,直接用{}代替即可。
2.声明
2.1 auto
C++98中auto是一个存储类型的说明符,表明变量是局部自动储存类型,但局部域中局部变量的定义默认为自动储存类型,所以auto就没什么价值。C++11中,auto被用于变量类型的自动推导,这就要求用auto定义变量时必须显示初始化,编译器会根据初始化的值来自动推导变量类型。
#include<iostream>
using namespace std;
#include<map>
int main()
{auto t = 1;auto n = 1.0;auto m = 'a';map<int,int>x;auto y=x.begin(); cout << typeid(t).name() << endl;cout << typeid(n).name() << endl;cout << typeid(m).name() << endl;cout << typeid(y).name() << endl;return 0;
}
2.2 decltype
decltype用于将变量的类型声明为表达式的指定类型。
#include<iostream>
using namespace std;
int main()
{int a = 1;double b = 1.1;decltype(a * b) c;//c的类型为a*b的类型,即doublecout << typeid(c).name() << endl;decltype(&a) d;//d的类型为&a的类型,即int*cout << typeid(d).name() << endl;return 0;
}
2.3 nullptr
在C++中,NULL被定义为字面常量0,这就意味着会发生矛盾,因为0既能代表空指针,又能代表整形常量0。因此C++11中用nullptr代表空指针。
3. 范围for
在C++98中使用for来遍历需要指明范围。
#include<iostream>
using namespace std;
int main()
{int arr[] = {1,2,3,4,5,6,7,8};for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++){cout << arr[i] << " ";}cout << endl;return 0;
}
C++11引入了范围for,当我们需要遍历一个完整的容器时,完全可以让编译器自动去推导出遍历的范围。
#include<iostream>
using namespace std;
int main()
{int arr[] = { 1,2,3,4,5,6,7,8 };//语法为://for(变量声明:容器)//{}//容器arr中的值会依次给到k,每次通过访问k就能访问arr中的元素了for (int k : arr){cout << k << " ";}cout << endl;return 0;
}
如果想要修改容器中的值,可以在上面的k前加上引用&。
注意:范围for实际上只是简化了使用者的代码。而底层依旧会编译成原本需要指出范围的版本。
通过范围for,我们范围STL中的部分容器就更方便了。
#include<iostream>
using namespace std;
#include<vector>
int main()
{vector<int> v{ 1,2,3,4,5,6,7,8 };for (int k : v){cout << k << " ";}cout << endl;return 0;
}
4.STL中的一些变化
STL中增加了一些新容器
<array> | Array header(header) |
<forward_list> | Forward list(header) |
<undered_map> | Undered map header(header) |
<undered_set> | Undered set header(header) |
同时STL还增加了一些新接口,如cbegin()、cend()等,但最有用的是增加了插入接口函数的右值版本,下面进行讲解。
5.右值引用与移动语义
5.1 左值引用与右值引用
C++原本中就有引用的语法,C++11中增加了右值引用的语法,我们之前的引用用法被称为左值引用,无论是右值引用,还是左值引用,都是给变量取别名。
对于左值(如变量名,或解引用的指针等),我们通常可以获取它的地址,可以对它进行赋值,左值可以出现在赋值符号的左边,而右值不能出现在赋值符号的左边。对于const修饰的左值,虽然不能修改它的值,但可以获取它的地址。
对于右值(如字面常量,函数返回值(不能是左值引用返回),表达式返回值等),不能取地址,只能出现在赋值符号的右边,不能出现在赋值符号的左边。
需要注意的是,对右值进行引用的变量为左值,如上图的ra,rb等,右值本身不可取地址,但对右值进行引用的变量可以取地址。
5.2 左值引用与右值引用的比较
- 左值引用只能引用左值,不能引用右值
- const 左值引用即可以引用左值,也可以引用右值
- 右值引用只能引用右值,不能引用左值
- 右值引用可以引用move以后的左值
5.3 右值引用使用场景
以下是模拟实现string的部分代码:
namespace bit
{class string{public:string(const char ch = '0'):_str(new char[2] {ch, '\0'}),_size(1),_capacity(1){cout << "string(const char ch )" << endl;}string(const string& t):_str(new char[t._capacity + 1]),_size(t._size),_capacity(t._capacity){cout << "string(const string& t)——拷贝构造" << endl;strcpy(_str, t._str);}string operator=(const string& t){if (&t != this){delete[] _str;_str = new char[t._capacity + 1];_size = t._size;_capacity = t._capacity;strcpy(_str, t._str);return *this;}return *this;}void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}~string(){delete[]_str;_str = nullptr;_size = _capacity = 0;}private:char* _str;size_t _size;size_t _capacity;};
}
左值引用的优势:
作参数和返回值可以提高效率。
void func1(bit::string t)
{}
void func2(bit::string& t)
{}
int main()
{bit::string s("hollo");cout << endl;cout << "func1:" << endl;func1(s);cout << endl;cout << "func2:" << endl;func2(s);return 0;
}
比较上图中func1与func2,发现func2没有拷贝构造,提高了效率。
左值引用的短板:
当函数返回值为临时变量时就不能使用左值引用返回,这时接受返回值就必须进行一次拷贝构造。
bit::string func()
{return "hollo";
}
int main()
{bit::string s = func();return 0;
}
这里编译器进行了优化,将两次拷贝构造优化为直接构造。但对于一些没有进行优化的编译器而言,就必须进行两次构造。
这里可以用右值引用解决这个问题。
先提出移动构造的概念:移动构造就是将右值的资源窃取过来占为己用,而不是进行深拷贝。
string(string&& t):_str(nullptr),_size(0),_capacity(0)
{cout << "string(string&& t)——移动构造" << endl;swap(t);
}
这样对于一些将要销毁的变量,我们通过移动构造直接窃取它们的资源,不必进行拷贝,提高了效率。
我们之前提到过右值引用可以引用move过的左值,C++11中函数move()的作用就是将左值变为右值,然后实现移动构造。
int main()
{bit::string t("666");bit::string s = move(t);return 0;
}
但我们通常不会按上述代码使用,因为这就意味着t中的资源被转移了。
5.4 完美转发
模板中的&&万能引用
这里唯一的缺陷就是x始终为左值,如果传入参数为右值,虽然x是右值引用,但它本身仍然是左值。无法再传递过程中保留属性,因此我们就需要使用完美转发来解决这个问题。
std::forward 完美转发在传参的过程中保留对象原生类型属性
void fun(int& x)
{cout << "左值" << endl;
}void fun(int&& x)
{cout << "右值" << endl;
}
template<typename T>
void perfect(T&& x)
{fun(forward<T>(x));
}
int main()
{int a;perfect(a);perfect(move(a));return 0;
}
如果不使用forward,直接使用x,那么结果都是左值。
6.新的类功能
6.1 默认成员函数
C++11中增加了两个默认成员函数,移动构造函数与移动赋值运算符重载。
注意:
- 如果你没有实现移动构造函数,且没有实现拷贝构造函数,赋值运算符重载,析构函数中的任意一个,那么编译器会自动生成一个移动构造函数,对于内置类型会逐字节拷贝,对于自定义类型会调用它的移动构造函数,如果该类型没有实现移动构造,就调用它的拷贝构造。
- 如果你没有实现移动赋值运算符重载,且没有实现拷贝构造函数,赋值运算符重载,析构函数中的任意一个,那么编译器会自动生成一个移动赋值运算符重载,对于内置类型会逐字节拷贝,对于自定义类型会调用它的移动赋值运算符重载,如果该类型没有实现移动赋值运算符重载,就调用它的赋值运算符重载。
- 如果你实现了移动构造或移动赋值,那么编译器就不会生成默认的拷贝构造与拷贝赋值。
6.2 类成员变量初始化
C++11中允许在类定义时给成员变量缺省值,在编译器默认生成的构造函数中会使用缺省值来初始化变量。
6.3 default
default用于生成因为某些原因而没有默认生成的函数。
如果我们实现了拷贝构造,那么编译器就不会输出默认的移动构造,这时我们可以使用default来生成默认的移动构造。
6.4 delete
delete用于禁止生成某些默认生成的函数。
如果我们没有实现拷贝构造,那么编译器会输出一个默认的拷贝构造,这时我们可以使用delete来禁止编译器删除默认的拷贝构造。
6.5 final与override
final用于一个虚函数时,表明该成员函数不能被重写。
override用于一个虚函数时,会检查它是否完成了对父类的某个虚函数的重写。
二者在多态那部分已进行讲解。
7.可变参数模板
C++11 的模板支持可变参数,这里简单的介绍一下。
以下就是一个可变参数的函数模板。
上面的参数arg前面有省略号,所以它是可变模板参数,被称为参数包,它包含1~n个模板参数,我们不能直接获取它的每个参数,必须展开参数包才能获取。
7.1 递归方式展开
#include<iostream>
using namespace std;
//终止函数递归
void print()
{}
template<class T,class...Args>
void print(T x, Args... arg)
{cout << x << " ";//以递归的方式展开参数包print(arg...);
}
template<class ...Args>
void func(Args... arg)
{print(arg...);
}
int main()
{func('a', 5, 4, 'c', 6.5, 4, 2);return 0;
}
这里我们每次取出一个模板参数,然后剩下的参数包进行递归,当参数包为空时,就不会走当前递归的函数,而是走另一个重载的函数,以此来结束。
7.2 逗号表达式展开
#include<iostream>
using namespace std;
template<class T>
void print(T x)
{cout << x << " ";
}
template<class ...Args>
void func(Args... arg)
{int arr[] = { (print(arg),0)... };
}
int main()
{func('a', 5, 4, 'c', 6.5, 4, 2);return 0;
}
这里我们利用C++11列表初始化的特性,通过列表来初始化变长数组,这样{(print(arg),0)...}就展开成{(print(arg1),0),(print(arg2),0),(print(arg3),0),...,(print(argn),0)},逗号表达式的意义在于调用print函数后,以0作为整个表达式的结果,刚好作为arr数组的一个元素。
C++11STL中新出的成员函数emplace_back就支持可变参数,它与push_back的区别就在于它有些情况能用参数直接进行构造,而不是走拷贝构造或者移动构造,故而在一些情况下比push_back更高效。