目录
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 };//{}也可用于new
return 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的类型,即double
cout << 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更高效。