C++11是继C++03之后的一次大更新,出现了很多不同的语法,这里来逐一学习一下吧。
统一的列表初始化
C++11实现了初始化的大一统,无论是内置类型还是自定义类型都可以通过{}来进行初始化。
内置类型初始化
先来观测下列代码:
cpp
int main()
{
int x = { 10 };//列表初始化
int y{ 15 };//忽略'='的列表初始化
char ch{ 'c' };
cout << x << endl << y << endl << ch << endl;
return 0;
}
运行结果:
10
15
c
上述结果表明,对于内置类型可以以= { 参数 } 或 { 参数 }的形式进行初始化。
自定义类型
先以STL自带的容器进行试验:
cpp
int main()
{
string str1{ "Love" };
cout << str1 << endl;
return 0;
}
输出结果为:
Love
实际上本质是参数的隐式类型转换,不相信的话我们来以简单的自定义类型试验一下。
先定义一个简单的Point类型:
cpp
class Point
{
public:
Point(const int x,const int y)
:_x(x)
,_y(y)
{
cout << "构造" << endl;
}
Point(const int x)
:_x(x)
, _y(x)
{
cout << "构造" << endl;
}
void Show()
{
cout << _x << ' ' << _y << endl;
}
private:
int _x;
int _y;
};
int main()
{
Point x{ 1,2 };
x.Show();
Point y={ 11,2 };
y.Show();
Point z = 1;
z.Show();
}
运行结果为:
构造
1 2
构造
11 2
构造
1 1
可以看到是调用了对应的构造函数来隐式类型转换,然后拷贝给Point。
如果像下面这种:
cpp
explicit Point(const int x,const int y)
:_x(x)
,_y(y)
{
cout << "构造" << endl;
}
error C3445: "Point" 的复制列表初始化不能使用显式构造函数
或者
cpp
Point& w = 2;
error C2440: "初始化": 无法从"int"转换为"Point &"
initializer_list初始化
之前写vector的区间构造也提到过,我们可以以下列方式来初始化vector的一堆值:
cpp
vector<int>v = { 1,2,3 };
这种形式和上述的隐式类型转换相似又不一致,确实是调用了vector的构造函数,具体是:

没错,这里其实是传入了迭代器 来进行初始化。
C++11中将{}封装成了一个类:
cpp
auto x = { 1,2,3 };
cout << typeid(x).name() << endl;
结果:
class std::initializer_list<int>
也就是说所有花括号都是initializer_list这个类,上面vector的初始化,实际上就是传入initializer_list的迭代器来实现的。
具体应用
有了这些语法,我们能实现部分容器的高效初始化:
cpp
map<string, int>m = {
{"apple",19},
{"banana",10},
{"Love",0}
};
可以看到,这里其实是首先从{"apple",19}隐式类型转换为pair<string,int>,然后再根据initializer_list初始化到m上。
声明
C++11增加了许多简化声明的方式。
auto
auto关键字前面已经讲过了,可以自动推导类型。像:
cpp
int add(int x, int y)
{
return x + y;
}
int main()
{
string str{ "love" };
auto ss = str;
auto func = add;
cout << typeid(ss).name() << endl;
cout << typeid(func).name() << endl;
return 0;
}
结果:
class std::basic_string<char,struct std::char_traits<char>,class std::allocator<char> >
int (__cdecl*)(int,int)
除此之外,auto还可以当作返回值类型:
cpp
auto minus(int x, int y)
{
return x - y;
}
int main()
{
cout << ::minus(10, 6) << endl;
return 0;
}
结果:
4
当然了,auto不能作参数列表。
cpp
int times(int x,auto y)
{
return x * y;
}
error C3533: 参数不能为包含"auto"的类型
事实上我们有模板功能来实现接入参数的类型随机性,也不需要auto来实现。
decltype
decltype顾名思义就是declaim type,获取类型。不同于typied.name()得到的仅是字符串,decltype得到的类型名是可以直接当作类型使用的:
cpp
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
decltype(t1 * t2) ret;
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
decltype(x * y) ret; // ret的类型是double
decltype(&x) p;
// p的类型是int*
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
}
运行结果:
double
int const * __ptr64
int
nullptr
在C语言中,NULL表示空指针,但是NULL同时又表示整数0.
考虑到程序的安全性,C++11以nullptr形式定义空指针,实际上就是(void*)0;
范围for
范围for也是C++11新增的语法,前面实现各种容器的时候已经讲过。只要容器实现了迭代器都能使用范围for。以下是一个vector的例子:
cpp
vector<int> v = { 1,2,3,4,5 };
for (auto& e : v)
{
cout << e << ' ';
}
cout << endl;
相当于:
cpp
vector<int> v = { 1,2,3,4,5 };
auto it = v.begin();
while (it != v.end())
{
cout << *it << ' ' << endl;
it++;
}
cout << endl;
结果都是:
1
2
3
4
5
智能指针
这部分篇幅较长,在后续文章再进行讲解。
STL新增容器
C++11在STL中增加了新的容器:array、forward_list、unordered_set、unordered_map
具体使用就不多介绍了。
右值引用
右值引用则是C++11中新增的相当重要的语法。
左值和右值
首先需要明确何为左值何为右值?
简单来说,左值就是可以取地址的数值表达式,右值就是不能取地址的数值表达式
如:
cpp
int main()
{
int* p = new int(0);
int b = 1;
const int c = 2;
cout << &p << endl << &b << endl << &c << endl;
return 0;
}
结果:
000000D91AEFF5C8
000000D91AEFF5E4
000000D91AEFF604
可以看到p、b、c这些都是左值。
常见的右值有:字面常量、表达式返回值、函数返回值
cpp
int add(int x, int y)
{
return x + y;
}
int main()
{
cout << &10 << endl;
cout << &(2 - 1) << endl;
cout << &add(2, 10) << endl;
}
结果:
error C2101: 常量上的"&"
error C2101: 常量上的"&"
error C2102: "&"要求左值
其中 error C2102明确表明了,只有左值才能取地址。
不过这里要郑重申明,右值只是不能取地址,不代表没有地址
左值引用和右值引用
左值引用自然就是对左值的引用,右值引用就是对右值的引用:
左值引用
cpp
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
右值引用
cpp
int&& rr1 = 10;
int&& rr2 = 2 - 1;
int&& rr3 = add(2, 10);
可以看到,右值引用是类型名+"&&"
接下来我们需要考虑两件事情,那就是左值引用能否引用右值,右值引用能否引用左值?
cpp
int& rr1 = 10+2;
int x = 20;
int&& y = x;
error C2440: "初始化": 无法从"int"转换为"int &"
error C2440: "初始化": 无法从"int"转换为"int &&"
可以看到左值引用直接引用右值和右值引用直接引用左值都会报错,那是否没有办法了,自然不是。
cpp
const int& rr1 = 10 + 2;
int x = 20;
int&& y = move(x);
像这种形式就可以通过编译了。也就是说const 左值引用可以引用左值也可以引用右值,右值引用只能应用右值或者move以后的左值。
这里的move函数实际上就是将左值转换成右值。
- 还有一点比较重要的。我们都知道左值引用本身也是左值,那右值引用是左值还是右值呢?
cpp
int main()
{
int&& x = 10;
cout << &x << endl;
return 0;
}
000000CE6D8FF614
可以看到,右值引用本身也是左值。同时也证明了右值确实也有地址。
总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
移动构造和移动赋值
右值其实又细分为纯右值和将亡值。纯右值是指字面常量那些,将亡值则是指临时变量或传值返回。
基于将亡值即将销毁的特性,我们可以利用其资源来完成构造和拷贝:
cpp
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "移动构造" << endl;
swap(s);
}
string& operator=(string&& s)
{
cout << "string& operator=(string&& s)" << endl;
swap(s);
return *this;
}
是的我们直接将右值的资源换给了左值,右值可以安心去世了。
左右值引用使用场景
首先明确下,引用的意义是减少拷贝,提高效率。
但是对于一些函数返回值是常量拷贝时,左值引用就派不上用场了。
基于前文实现的string的实现,我们来观测一下下面情况:
cpp
mystring::string ret1;
ret1 = mystring::string::to_string(1234);
string(char* str) -- 构造
string(char* str) -- 构造
string& operator=(string s) -- 深拷贝
可以看到,先是对ret1进行了一次构造,然后在to_string函数里面又进行了一次构造。实际上最后应该进行两次拷贝构造,分别是to_string函数里string返回时,拷贝给一个临时变量,临时变量再拷贝给ret1.
当然两次拷贝构造被编译器优化成了一次,如果是下面这样写的话,优化更甚:
cpp
mystring::string ret2 = mystring::string::to_string(1234);
cout << ret2.c_str() << endl;
string(char* str) -- 构造
1234
可以看到,他竟然优化成了一次构造,不得不说现在的编译器是真的厉害啊。
回到原文,考虑一开始的情况。可以发现to_string返回的是值,对他进行深拷贝然后再深拷贝给ret1代价无疑非常大。如果我们实现了移动构造,那么代价就可以缩小:
cpp
mystring::string ret1;
ret1 = mystring::string::to_string(1234);
string(char* str) -- 构造
string(char* str) -- 构造
string(string&& s) -- 移动赋值
可以看到拷贝构造变成了赋值构造,代价大大降低了。
当然了,我们可不要做一个喜新厌旧的人。移动拷贝并非所有情况都是最优的。我们之所以用移动构造代替拷贝构造能提升效率,本质上拷贝构造调的是深拷贝,如果是浅拷贝的话拷贝构造效率自然会比移动构造高。
完美转发
前面提到过,右值引用本身会退化成左值,这就为某些事请埋下了伏笔。
且看下面函数:
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
首先说明下:
template
void PerfectForward(T&& t)
这里T&&可不是指T类型的右值引用,这里是万能引用模板的格式。当传入左值引用就会识别成左值引用,传入右值引用就会识别成右值引用。
那么来看看代码的运行结果吧:
左值引用
左值引用
左值引用
const 左值引用
const 左值引用
可以看到由于右值引用退化成了左值,调用Fun函数变成了全部都是左值函数了。
当然了我们可以强制类型转换一下:
cpp
template<typename T>
void PerfectForward(T&& t)
{
Fun((T&&)t);
}
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
结果自然正确了,但这不是C++11提供的最佳方案。C++11提供了完美转发函数forward能完美传递对应的左右值类型,使用方法如下:
cpp
template<typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
右值引用
左值引用
右值引用
const 左值引用
const 右值引用
可以看到forward其实是个仿函数,最终也实现了类型的完美对应。
lambda表达式
lambda表达式同样是C++11中新增的及其重要的语法。
使用格式
首先声明,lambda表达式本质就是一个匿名函数。它可以代替仿函数作为传入参数,节省了写仿函数的功夫。
先看具体格式:
capture-list\] (parameters) mutable -\> return-type { statement }
```cpp
int main()
{
auto func1 = [](int x, int y)->int {return x + y; };
auto func2 = [](int x, int y){return x + y; };
auto func3 = [] {cout << "Love" << endl; };
cout << func1(10, 2) << endl << func2(20, 10) << endl;
func3();
return 0;
}
```
12
30
Love
可以看到,返回值类型可以自动推导因此可以省略。如果没有参数,甚至可以省略参数列表。
### 应用
```cpp
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
void show(vector