学习C++11的新特性已经很久了,但是许久不看,知识点又模糊了起来。C++11中许多特性看似复杂,但是理解了其中的道理,会觉得豁然开朗。
下面这种方式的数组初始化方式玩过C语言的同学都很熟悉:
int arr[] = { 1, 2, 3, 4 };
c++11中为了提供类似的方式用来初始化vector,提供了下面新的方式
vector<int> v = { 1, 2, 3, 4 };
//官方还提供了下面的方式,但是两种方式实际上相同
vector<int> v{ 1, 2, 3, 4 };
10秒中解密其中的原理,其实c++11中提供了一个新的列表,如下代码块,这个列表只提供了begin、end、和size方法,也就意味着只能进行初始化和遍历
initializer_list<int> l = { 1, 2, 3 ,4 };
而vector中提供了一个新的构造函数,函数签名如下,当我们以列表初始化进行初始vector时,其实只是遍历initializer_list,再一个个赋值给vector底层的数组
vector(initializer_list<T> l);
c++11中提供了一种新的类型推导方式叫decltype,但其实c++原本已经有了这么几种类型推导
auto
typeid
dynamic_cast
来分析下为什么需要这种新的类型推导:
而看看c++11中新提供的decltype有什么特异功能:
int a = 10;
decltype(a) b = 20;
可以看出我们使用decltype关键字拿到了a的类型,并且定义了新的变量b。对比上面三个关键字,他们确实不具备这样的功能,但是decltype在实际开发中到底用处大不大,笔者也还未接触到。
tips:顺便提一句dynamic_cast的底层原理,他为什么可以推导转型是否安全,其实很简单,还记得虚函数表么,其实每个紧挨着虚函数表的上方还有一个local_object的结构体指针,指向的这个结构体中存放着所有继承链的关系。有了这些关系,是否可以转型也就不难理解了。
快乐又轻松的范围for,写起来确实很好用,这里提一下底层原理:其实模拟实现过STL容器的老铁都知道,只要在类中提供了begin和end(注意必须时小写)函数,就可以使用范围for,实际上编译器最后只是帮助我们把范围for遍历替换为了迭代器遍历。
vector<int> v{ 1, 2, 3 };
for (int e : v)
{
cout << e << endl;
}
看这样一个场景:下面类中,假设成员变量a,b永久将被初始化成1,2,所以意味着只有c的值会改变,在你编写构造的函数的时候,你就可以使用下面委派的形式去调用初始化a,b变量那部分的构造函数。这样一种方式就叫做委派构造。
class A {
public:
A()
: a(1)
, b(2)
{}
A(int _c)
:A()
{
c = _c;
}
private:
int a;
int b;
int c;
};
废话不多说,直接来看看加入了哪些新关键字以及他们的作用是什么吧:
上面五个特性只能说是餐前甜点,真正的硬菜其实到现在才开始。要问c++11中设计最惊艳的特性,笔者认为右值引用实至名归。为何这样说,下面我们一起看看。
提右值引用前我们必须提一下大家都很熟悉的左值引用,左值引用其实就是给左值起别名。那么右值引用也不例外,他其实上就是给右值起别名
左值引用和右值引用的使用方式如下:
int a = 10;
int& la = a;//左值引用,a变量是一个左值
int&& ra = 10;//右值引用,10是一个字面常量,是一个右值
很多同学会抱怨,右值引用这样看起来一点用处都没有啊。别着急,其实右值引用真正厉害的东西在于他的产物。从c++11后,一个类中默认的成员函数将增加两个,分别是移动构造和移动拷贝。这两个函数有什么用,我们先来看看下面一段代码有什么问题:
string GetString(char* ptr)
{
string strtmp(ptr);
return strtmp;
}
int main()
{
string str(GetString("hello world!"));
return 0;
}
我们都知道若不以左值引用方式返回时,返回值将执行一次拷贝,然后使用这个临时的拷贝再去构造str
仔细分析这个过程中会存在什么问题:返回临时对象tmp时进行了一次拷贝构造,使用tmp进行拷贝构造str后tmp立马被销毁了,而str自己又进行了一次开辟空间。那么我们为什么不能将tmp的空间直接分配给str?
而这个问题可以由我们的右值引用轻松解决,我们再次介绍下c++中的右值的概念,实际c++中右值分为两种
可以看出,上述例子中的tmp就是一个将亡值,也就是说这个将亡值可以进行右值引用,当我们接收到这个将亡值之后,可以将他的空间给str,并将自己置为空:
string(string&& s)
:_str(s._str)
{
s._str = nullptr;
}
string& operator=(string&& s)//上述例子会走此函数
{
if (this != &s)
{
_str = s._str;
s._str = nullptr;
}
return *this;
}
这样我们就使用右值引用技术完美解决了多次拷贝和拷贝构造的问题,现在我们做一个小小的总结:右值引用的出现其实是为了弥补传值返回多次拷贝的问题,而右值引用技术本身只是在筛选对象一样筛选出那些将亡值,真正防止拷贝的核心是右值引用的产物:移动拷贝和移动构造函数
到这里,右值引用就介绍完毕了,但是关于右值引用技术这里不得不提其他的俩个函数:有同学会问,使用右值引用可不可以引用左值?
int a = 10;
int&& ra = a;//错误,右值不能引用左值
但是如果我们非要引用他就可以使用move函数:这个函数的作用是将一个左值强制转化为右值引用,通过右值引用使用该值,实现移动语义。
int a = 10;
int&& ra = move(a);
另一个不得不提的函数叫forward函数,右值在经过传参后会丢失右值属性,使用forward可以保留属性,这个函数作为了解就可以,实际工作中使用的几率非常小。
lambda表达式同样是为了提高编码效率的一个新的特性,现在我们假设要对下面的结构体以价格进行排序:
struct Goods
{
string _name;
double _price;
};
struct Compare
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price <= gr._price;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, { "菠萝", 1.5 } };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), Compare());
return 0;
}
可以看出每次书写仿函数都要重新定义一个类,并且代码的可阅读性并不是那么的高,那么看看lambda表达式可以怎么写
int main()
{
Goods gds[] = { { "苹果", 2.1 }, { "相交", 3 }, { "橙子", 2.2 }, {"菠萝", 1.5} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& l, const Goods& r)
{
return l._price < r._price;
});
return 0;
}
sort函数的第三个参数其实就是一个lambda表达式,来看看表达式的通用格式如何书写:
[capture-list] (parameters) mutable -> return-type { statement }
笔者很久都不懂捕获列表到底有什么用,但是我们看下面的例子就可以明白为什么需要捕获列表:
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
// 函数对象
double rate = 0.49;
Rate r1(rate);
r1(10000, 2);
// lambda表达式
auto r2 = [=](double monty, int year)->double{return monty*rate*year; };
r2(10000, 2);
return 0;
}
lambda表达式的底层原理也很简单:实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
这里简单提一下c++11中提供的线程库操作,这里举得例子只是笔者自己加深印象,有意愿的同学可以自己深入研究:
int main()
{
int n1 = 100;
int n2 = 200;
thread t([&](int num){
n1 += num;
n2 += num;
}, 500);
t.join();
//t.detach();
cout << n1 << " " << n2 << endl;
system("pause");
return 0;
}
C++11中还有比较重要的特性有:智能指针、unordered系列容器。但这些特性笔者之前的博客已经有详细介绍过,有兴趣的同学可以自行翻阅。