C++11新特性
1统一列表初始化
1.1{}
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,**使用初始化列表时,可添加等号(=),也可不添加。**创建对象时也可以使用列表初始化方式调用构造函数初始化。主要是为了new的时候能够初始化。
C++98 | C++11 |
---|---|
int x1 = 1; |
int x1 {1}; //可以这样写但是不建议 |
int array1[] = { 1, 2, 3, 4, 5 }; |
int array1[]{ 1, 2, 3, 4, 5 }; //可以这样写但是不建议 |
int array2[5] = { 0 }; |
int array2[5]{ 0 }; //可以这样写但是不建议 |
struct Point { int _x; int _y;}; //无构造函数 Point* p = new Point[2]; //不支持new的时候初始化 |
struct Point { int _x; int _y;}; Point* p = new Point[2]{{1,2}, {3,4}}; |
class Point { private:int _x; int _y; Point(int x, int y) :_x(x), _y(y){} }; //有构造函数 Point p(1, 2);
|
class Point { private:int _x; int _y;}; Point p{ 1, 2 }; //可以这样写但是不建议 |
int* p1 = new int[4]; //不支持new的时候初始化 |
int* p1 = new int[4]{1,1,2,2}; |
1.2std::initializer_list
自定义类型用{}去初始化,其实是去匹配对应的构造函数,构造函数支持传几个参数在{}里就只能传几个参数!
而stl库里的容器比如vector、list、deque、forward_list等在使用{}初始化时,可以传入多个参数(不指定个数)。怎么做到的呢?C++11中vector和list的构造函数支持了使用initializer_list初始化。
据我们模拟实现vector的构造函数时可知,没有写那么多个区分参数个数的构造函数。是因为在C++11中把{}单独封装成了std::initializer_list
这个类型,类似于vector容器,只不过vector是自己开辟空间存储数据,而{}自己不用存,到常量区找数据就行。【{}跟常量字符串一样,其内容存放在常量区,然后initializer_list这个类型里有2个指针分别指向常量区的开始地址和结束地址。】
vector(std::initializer_list<T> il)
:_start(nullptr), _finish(nullptr), _endofstorage(nullptr)
{
reserve(il.size()); //提前开辟空间
auto it = il.begin();
while (it != il.end())
{
push_back(*it);
++it;
}
}
vector<T>& operator=(std::initializer_list<T> l) {
vector<T> tmp(l);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
2多种简化声明
2.1auto
用auto修饰的变量必须进行显示初始化,方便让编译器将定义对象的类型设置为初始化值的类型。
2.2decltype
declare声明、断言。关键字decltype将变量的类型声明为表达式指定的类型。
typeid(a).name()
可以取到变量的类型名,但只能用于输出打印;而typeid(a) ret;
不仅可以拿到变量的类型名,还能用来定义一个对象。
2.3nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
3范围for循环
底层就是依靠编译器做了替换,就是迭代器的begin、end和++就能完成。
4stl中的一些变化
- 新容器:array、forward_list、unordered_map、unorder_set
- 已有容器的新接口:移动构造、移动赋值、emplace_xxx插入接口、右值引用版本的插入接口
归个类:
- 序列式容器:vector、list、deque、array,deque适合头尾插入删除多的场景,vector适合尾插尾删多的场景,list适合随机插入和删除的场景。将array看作固定大小的静态数组,检查越界访问更严格,普通数组的越界是抽查。
- 关联式容器:map、multimap、set、multiset、bitset、unordered_map、unordered_multimap、unorder_set、unordered_multiset
- 容器适配器:queue、stack、priority_queue
5final与override
final修饰类,表示不允许被继承,用来修饰虚函数,表示不允许被重写。override修饰虚函数,检查子类虚函数是否完成重写
6引用
引用是为了在函数传参和函数传返回值的时候减少拷贝,提高效率。
6.1左值&左值引用
左值是一个数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,还可以给它赋值。左值可以出现在赋值符号的左边和右边。最重要的区分点是左值可以取地址。比如定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 左值引用只能引用左值,不能引用右值;
- 但是const左值引用既可引用左值,也可引用右值。
const左值引用
const左值引用既可引用左值,也可引用右值。
左值引用的短板
左值引用解决了函数传参的拷贝问题,且在函数的左值引用参数前加上const,就可以同时接收左值和右值。如 void fun(const int& x);
;但并没有解决传返回值的问题,因为一个函数栈帧里面的局部变量出栈帧就销毁了,无法用引用返回,如int& fun(const int& x);
==>这就是左值引用尚未解决的问题。想一下杨辉三角vecot<vector<int>>
这样的返回值,在传返回值时会有2次拷贝构造,消耗很大,在没有右值引用之前的解决方法之一是用输出型参数带回数据。如果这个临时变量比较小,4字节这样,就会存放在寄存器里;如果临时变量比较大,寄存器放不下,就会压在上一个栈帧的边缘处(这样的话,就算当前栈帧销毁了也不影响数据的保留)。新一点的编译器只需要一次拷贝构造,直接拿着要销毁的栈帧里的返回值去构造接收该值的变量。
6.2右值&右值引用
右值也是一个数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等。右值不可以取地址。
右值又可以分为纯右值(内置类型表达式的值)和将亡值(自定义类型表达式的值)。
- 右值引用只能右值,不能引用左值;
- 但是右值引用可以move以后的左值。
**无论左值引用还是右值引用,都是给对象取别名。**左值可以出现赋值符号的左边,右值只能在赋值符号的右边,不能出现在赋值符号左边。
const右值引用
注意右值是不能取地址的,但是给右值取别名后(即右值引用后),会导致右值被存储到特定位置(比如字面量10本来是在常量区/已初始化数据区,但是会被拷贝一份放到某个区域,比如栈上),且可以取到该位置的地址。虽然局部变量出函数栈帧就销毁了,但是在用右值引用接收函数返回值时,该函数返回值会被存储到特定的地方。
-
int&& rr1 = 10; &rr1;
不能取字面量10的地址,但是rr1对其进行右值引用后,就可以对rr1取地址,也可以修改rr1;==>移动构造和移动赋值函数可以swap的原因 -
const int rr2&& 10; &rr2;
此时可以对字面量10取地址,但不能对rr2做修改。
注意!!!右值引用再进行传递,属性就变成左值了!
7新增2个默认成员函数
原来C++类中,有6个默认成员函数:构造函数、析构函数、拷贝构造函数、拷贝赋值重载、取地址重载、const 取地址重载。取地址相对而言没那么重要。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
如果用户自己写了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。==>说明如果写了移动构造,就得写拷贝构造。
- 无拷贝构造,有移动构造==>编译器不生成默认拷贝构造
- 无移动构造函数、析构函数 、拷贝构造、拷贝赋值重载==>编译器必生成默认移动构造
- 无移动赋值重载函数、析构函数 、拷贝构造、拷贝赋值重载==>编译器必生成默认移动赋值运算符重载
默认生成的移动构造函数/移动赋值运算符重载函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动构造/移动赋值运算符重载函数,如果实现了就调用移动构造/移动赋值运算符重载函数,没有实现就调用拷贝构造/拷贝赋值。
有默认拷贝构造函数、无默认移动构造函数的情况下,如果有右值,就只能走拷贝构造,效率上比较吃亏。
7.1移动构造
如果用户没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动构造。
默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
// 拷贝构造
string(const string& s) :_str(nullptr) {
std::cout << "string(const string& s) -- 深拷贝" << std::endl;
string tmp(s._str);
swap(tmp);
}
// 移动构造
string(string&& s) {
std::cout << "string(string&& s) -- 移动构造" << std::endl;
swap(s);//右值是将亡值,没必要做深拷贝
}
- 在没有右值引用前 ,本来是两次拷贝构造,优化成一次拷贝构造;
- 有了右值引用后,本来是拷贝构造+移动构造,优化成一次移动构造。
对于传值返回的函数,要传出的值是出了作用域就要销毁掉的,编译器就会把它move成右值,调用移动构造。对于需要深拷贝的自定义类型就不用担心传值返回了
7.2移动赋值
如果用户没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载,那么编译器会自动生成一个默认移动赋值。
默认生成的移动赋值运算符重载函数,对于内置类型成员会执行逐成员按字节拷贝(浅拷贝);对于自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)。
// 赋值重载
string& operator=(const string& s) {
std::cout << "string& operator=(string s) -- 深拷贝" << std::endl;
string tmp(s);
swap(tmp);
return *this;
}
// 移动赋值
string& operator=(string&& s)//右值是将亡值{
std::cout << "string& operator=(string&& s) -- 移动语义" << std::endl;
swap(s);//右值是将亡值,没必要做深拷贝
return *this;
}
右值引用单独使用没效果,要搭配移动构造和移动赋值使用,能大大降低拷贝的次数。
右值引用和左值引用减少拷贝的原理不一样,左值引用是取别名,直接起作用;而右值引用是间接起作用,实现移动构造和移动赋值,在构造和赋值的场景下,如果是右值或出作用域就要销毁的左值,编译器可以优化识别为右值,就转移资源。
总结右值引用的好处:右值引用的价值之一:就是补齐这个最后一块短板,传值返回的拷贝问题;右值引用的价值之二:对于插入一些插入右值数据,也可以减少拷贝。
7.3强制生成默认函数的关键字default
C++11为了让用户更好地控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成,就可以利用该关键字强制生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
Person(const Person& p) :_name(p._name) ,_age(p._age) {}
Person(Person&& p) = default;//强制生成移动构造
7.4禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,做法是将该函数设置成private,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete
即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
Person(Person&& p) = delete;//不生成移动构造
8万能引用/引用折叠
通过改造list的代码可知,push_back()增加一个右值引用,就要写两个版本,一个const左值引用版本,一个右值引用版本,才可以实现右值引用。
若某些普通函数不想区分左值和右值,但是传参的时候又有左值和右值混用,那必然要写两个版本的函数,这挺麻烦的。比如下面这个情况
void fun1(const int& x);
void fun1(int&& x);
//int x = 1; fun1(x); --> 调用左值引用版本
//fun1(10); --> 调用右值引用版本
所以在C++11中又扩展了模板的功能,引入万能引用。注意,万能模板只能在未被实例化的时候才能用!
template<class T>
void fun2(T&& t);
这样既能引用左值、const左值,也能引用右值、const右值。
9完美转发
基于将万能引用作为中转站+右值再次传递就会变成左值这个问题引入的解决措施。就是保持右值属性再次传递。
void fun(int& x);
void fun(const int& x);
void fun(int&& x);
void fun(const int&& x);
template<class T>
void fun2(T&& t)
{
fun(std::forward<T>(t));
}
10lambda表达式
仿函数的出现是为了取代函数指针,想一下当某个对象有很多的属性(int price; int evaluate; string name;等),我们要对每个属性挨个进行排序的时候,就要针对每个属性写一个对应的用于比较大小的仿函数,这就比较麻烦。
lambda表达式实际是一个可调用对象,是没有具体类型的(编译器在实现的时候会加上一串随机字符串确保每个lambda表达式的唯一性<lambda xxxxxxxxx>
),用auto即可,其底层就是仿函数。
10.1书写格式
书写格式为:[capture-list] (parameters) mutable -> return-type { statement
}
-
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。必写 -
(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略。有就写,没有可以不写 -
mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空)。一般不需要 -
->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。一般不写,可以自动推导返回类型 -
{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。必写
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{};
,不过该lambda函数不能做任何事情。
auto compare = [](int x, int y) ->bool { return x > y; };//实现比较功能
//后续调用
compare(1, 2);
10.2捕捉列表
捕捉列表的写法:传值捕捉、引用捕捉、混合捕捉。
捕捉列表描述了哪些数据可以被lambda使用,以及使用的方式传值还是传引用。注意!捕捉不是传参!
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
//-------------使用捕捉列表传递参数!这里底层是传值捕捉,只不过用const修饰了,称为复制捕捉
int a = 0, b = 1;
auto compare = [b](int x) ->bool { return x > b; };
compare(a);
//-------------使用捕捉列表传递参数!引用捕捉
int a = 0, b = 1;
auto swap = [&a, &b]() { int tmp = a; a = b; b = tmp; };//这个写法像取地址,实际上是引用捕捉
swap(a, b);
要注意lambda表达式的引用捕捉的写法!
- 父作用域指包含lambda函数的语句块,只能捕捉父作用域lambda表达式之前的变量;
- 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]表示以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量;[&,a, this]表示值传递方式捕捉变量a和this,引用方式捕捉其他变量;
- 捕捉列表不允许变量重复传递,否则就会导致编译错误。比如:[=, a]中
=
已经以值传递方式捕捉了所有变量,捕捉a重复; - 在块作用域以外的lambda函数捕捉列表必须为空;
- 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错;
- lambda表达式之间不能相互赋值,即使看起来类型相同
10.3线程与lambda表达式结合使用
a.让子线程执行从0-100的打印工作
int i = 0;
thread t1([&i]
{
for(; i < 100; ++i) { cout << "thread1: " << i << endl;}
});
b.创建100个线程
vector<thread> vThreads;
int n;
cin >> n;
vThreads.resize(n);
int i = 0;
int num = 0;//标记线程编号
for (auto& t : vThreads)
{
t = thread([&i, num]//这里用的是移动赋值
{
for (; i < 100; ++i)//让每个线程打印1-100
{
cout << "thread: " << num << "->" << i << endl;
}
});
++num;
}
for (auto& t : vThreads)
{
t.join();
}
10.4函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。从使用方式上来看,函数对象与lambda表达式完全一样。实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。
改造list
1、添加右值引用和移动语义
修改前
list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x) {}//构造函数
void push_back(T x) { insert(end(), x); }
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos._pnode;
node* prev = cur->_prev;
//链接prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
修改后
list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x) {}//构造函数
list_node(T&& x) :_next(nullptr) ,_prev(nullptr) ,_data(move(x)) {}//移动构造//修改属性
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x) { insert(end(), move(x); }//虽然接收右值,但在该函数内x是左值,所以要move一下
iterator insert(iterator pos, const T& x);
iterator insert(iterator pos, T&& x)
{
node* newnode = new node(move(x);//修改属性
node* cur = pos._pnode;
node* prev = cur->_prev;
//链接prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
注意:用右值参数接收的右值,性质会改变,在该函数作用域内变成左值了【右值引用本身是左值】,想让它保持右值属性,一定要move一下。
2、万能引用+完美转发
修改如下,只需要把强制转换成右值的move
替换为std::forward<T>
。
list_node(const T& x) :_next(nullptr) ,_prev(nullptr) ,_data(x) {}//构造函数
list_node(T&& x) :_next(nullptr) ,_prev(nullptr) ,_data(std::forward<T>(x)) {}//把强制转右值改成完美转发
void push_back(const T& x) { insert(end(), x); }
void push_back(T&& x) { insert(end(), std::forward<T>(x); }//把强制转右值改成完美转发
iterator insert(iterator pos, const T& x);
iterator insert(iterator pos, T&& x)
{
node* newnode = new node(std::forward<T>(x);//把强制转右值改成完美转发
node* cur = pos._pnode;
node* prev = cur->_prev;
//链接prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
++_size;
return iterator(newnode);
}
注意:这里不能把左值引用版本的函数删去。因为我们已经实例化了list,模板参数已经确定了,万能引用就起不了作用。
补充
1、不想让某个类对象被拷贝的做法
知识基础:1、将拷贝构造函数设置为私有==>防外部调用。存在的缺陷:无法阻止类内某函数内部去访问拷贝构造函数,在运行的时候才报错;2、只声明不实现==>链接找不到,编译出错
C++98做法:只声明为私有,不实现。
C++11做法:加一个禁止生成默认函数的关键字delete。