●🧑个人主页:你帅你先说.
●📃欢迎点赞👍关注💡收藏💖
●📖既选择了远方,便只顾风雨兼程。
●🤟欢迎大家有问题随时私信我!
●🧐版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。
📌📌📌为您导航📌📌📌
1.C++11简介
在2003年C++标准委员会曾经提交了一份
技术勘误表
(简称TC1),使得C++03这个名字已经取代了C++98称为C++11之前的最新C++标准名称。不过由于TC1主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140
个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率。
2.列表初始化
在C++98中,标准允许使用花括号{}对数组元素进行统一的列表初始值设定。比如:
int arr1[] = {1,2,3,4,5};
int arr2[5] = {0};
但对于自定义类型,这种语法就不支持了。
所以C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
#include <iostream>
using namespace std;
class A
{
public:
A(int a, int b)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
int a = 2;
int a{ 2 };
int arr1 = {1,2,3,4,5};
int arr1[]{ 1,2,3,4,5 };
//前面两种就省略了一个赋值符号,没有看出来新语法有什么优势,接下来这三种则是C++98所不能支持的写法
A a = {1,2};
int* ptr1 = new int[5]{ 1,2,3 };
A* ptr2 = new A[2]{ {1,2},{3,4} };
}
在C++11里提供了一个容器initializer_list,用于解决自定义类型初始化的问题。
例如有了这个容器,标准库里的其它容器就能这样初始化了
vector<int> v = {1,2,3,4,5,6,7,8};
list<int> lt = {1,2,3,4,5};
map<string,int> = {{"苹果",5},{"西瓜",10}};
除了初始化列表,initializer_list还提供了赋值(即operator=),例如:
vector<int> v = {1, 2, 3, 4, 5};
v = {6, 7, 8, 9, 10};
//此时容器里的内容被修改
3.变量类型推导
3.1auto的使用
我们前面说过,auto可以自动推导变量的类型,这一特点可能对于普通类型没有很大的优势,而对于迭代器类型有这很大的便利,迭代器类型一般前缀名字很长,有了auto就可以这样:
//正常写法
list<int>::iterator it = lt.begin();
//使用auto
auto it = lt.begin();
3.2decltype类型推导
decltype是根据表达式的实际类型推演出定义变量时所用的类型。
为什么需要decltype?
auto使用的前提是:必须要对auto声明的类型进行初始化,否则编译器无法推导出auto的实际类型。但有时候可能需要根据表达式运行完成之后结果的类型进行推导,因为编译期间,代码不会运行,此时auto也就无能为力。
decltype的使用
int x;
decltype(x) z = 2;
4.STL新增内容
在C++11里STL新增了以下几个内容。
除了array,其余的在前面我们都讲过了。
我们发现array其实就是一个静态数组,但静态数组我们本身就可以直接定义了,为什么还要再封装成一个容器使用?
有两个原因:
1.支持迭代器,更好兼容STL容器玩法
2.对于越界的检查
除了增加容器,还新增了
1.移动构造、移动赋值
2.右值引用版本的插入接口
5.右值引用
在说右值之前,我们先来了解一下什么是左值。
左值是一个表示数据的表达式(变量名或解引用的指针)。我们可以获取它的地址也可以对它赋值,左值既可以出现在赋值符号的左边也可以出现在右边,
右值只能出现在赋值符号的右边。
int main()
{
int a = 10;
int& r1 = a;
int* p = &a;
int& r2 = *p;
//a、p、*p都是左值
}
思考一下,接下来这个是左值吗?
const int b = 10;
这种情况我们也认为是左值,所以换言之,可以取地址的对象,就是左值。
铺垫完了左值,我们再来看看右值。
右值是一个表示数据的表达式(字面常量、表达式返回值、传值返回函数的返回值)。右值引用就是对右值进行引用,给右值取别名,
右值不能取地址。
例如:
int main()
{
int x = 1;
int y = 2;
10;//字面常量
x + y;//表达式返回值
int ret = fmin(x,y);//传值返回函数的返回值
//右值引用
int&& r1 = 10;
int&& r2 = x + y;
int&& r3 = fmin(x,y);//假设fmin返回两者中最小值
}
大家思考一下:
左值引用能否引用右值?
很明显,无法直接引用,但我们可以通过加const对右值进行引用。
右值引用能否引用左值?
这个问题比较复杂了,在这里直接说答案,后面会再介绍。
不能直接引用,但是右值引用可以引用move以后的左值。
这里还有一点说明:
右值引用后的变量可以进行赋值、取地址,相当于右值引用后又变成了一个左值,因为右值引用后会开一块空间来存储这个值。
右值引用是用来解决左值引用的不足。
string operator+(char ch)
{
string tmp(*this);
push_back(ch);
return tmp;
}
这段代码会导致返回时有一次深拷贝,影响程序效率。
左值引用针对于下面这种场景完美解决了拷贝的问题。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
但对于+
的情况没办法很好的解决。
所以就利用右值引用来解决这个问题,通过增加一个移动构造函数。
//在这里补充一个概念,C++将右值分为两种:纯右值、将亡值(即自定义类型的返回值)
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{
this->swap(s);//本质就是资源转移,传过来的右值即将被销毁不如把资源让出来。
}
有了移动构造函数,就能解释前面我们所说的编译器对传值返回的优化问题,我们先来回顾一下。
string sayHello(string& str)
{
string ret("hello ");
ret += str;
return ret;
}
int main()
{
string s("world");
string ret = sayHello(s);
return 0;
}
在编译器没有优化的情况下
此时发生了两次拷贝构造。
编译器优化了之后
此时只发生了一次拷贝构造。
有了移动构造函数,还能再进行优化。
相信有了上面的铺垫你也能猜到,直接在函数结束前拿sayHello里的ret直接去与main函数里的ret交换资源,一次拷贝构造都不用调用,这对性能的影响是非常重大的。
💡:这里编译器会进行优化是因为构造了一个新对象,如果是下面这种场景,编译器无法进行优化。
string sayHello(string& str)
{
string ret("hello ");
ret += str;
return ret;
}
int main()
{
string s("world");
cout << sayHello(s) << endl;
return 0;
}
既然有移动构造,相应的就会有移动赋值。
string& operator=(string&& s)
{
swap(s);
return *this;
}
string sayHello(string& str)
{
string ret("hello ");
ret += str;
return ret;
}
int main()
{
string s("world");
string ret;
ret = sayHello(s);
}
这个过程发生了一次移动构造,一次移动赋值。
调用sayHello函数,在函数结束前ret与临时对象交换资源,然后临时对象再与main函数里的ret交换资源。
与右值引用相关联的还有万能引用
。
模板中的&&不代表右值引用,而是万能引用,既能接收左值又能接收右值。
但前面我们说过,右值引用后会开一块空间来存储,相当于又变成了左值引用,所以在后续使用中都退化成了左值。
为了解决这个问题C++给出了完美转发
的概念,即在传参过程中保留对象原生类型属性。
template <typename T>
void Fun(T& t)
{
cout << "左值引用" << endl;
}
template <typename T>
void Fun(T&& t)
{
cout << "右值引用" << endl;
}
template <typename T>
void PerfectForward(T&& t)
{
Fun(forward<T>(t));//保证t的属性不被改变
}
int main()
{
PerfectForward(10);
return 0;
}
6.lambda表达式
6.1语法
书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list]
: 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。(parameters)
:参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略mutable
:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修饰符时,参数列表不可省略(即使参数为空
)。默认情况下捕获的值不能修改,有了mutable就可以修改捕获的值。
->returntype
:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{statement}
:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。
💡:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。
因此C++11中最简单的lambda函数为:[]{}
; 该lambda函数不能做任何事情。
6.2捕捉列表说明
捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用。
- [var]:表示值传递方式捕捉变量var
- [=]:表示值传递方式捕获所有父作用域中的变量(包括this)
- [&var]:表示引用传递捕捉变量var
- [&]:表示引用传递捕捉所有父作用域中的变量(包括this)
- [this]:表示值传递方式捕捉当前的this指针
还有几点需要注意的:
a. 父作用域指包含lambda函数的语句块。
b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量 [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量。
c. 捕捉列表不允许变量重复传递
,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a导致重复。
d. 在块作用域以外的lambda函数捕捉列表必须为空。
e. 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。即在全局作用域中捕捉列表必须为空
f. lambda表达式之间不能相互赋值,即使看起来类型相同
6.3lambda表达式的使用
int main()
{
// 最简单的lambda表达式, 该lambda表达式没有任何意义
[]{};
// 省略参数列表和返回值类型,返回值类型由编译器推导为int
int a = 3, b = 4;
[=]{return a + 3; };
// 省略了返回值类型,无返回值类型
auto fun1 = [&](int c){b = a + c; };
fun1(10)
cout<<a<<" "<<b<<endl;
// 各部分都很完善的lambda函数
auto fun2 = [=, &b](int c)->int{return b += a+ c; };
cout<<fun2(10)<<endl;
// 赋值捕捉x
int x = 10;
auto add_x = [x](int a) mutable { x *= 2; return a + x; };
cout << add_x(10) << endl;
return 0;
}
7.类的新功能
7.1default关键字
在类里面,我们知道,如果我们不写构造函数,编译器会自动生成一个默认构造函数,若写了构造函数,编译器则不会生成。
但经常会有这样一个场景,写了有参构造函数,没写无参构造函数,在定义对象时需要无参的情况,这个时候再自己去写无参构造函数显的太麻烦了。default关键字可以强制编译器生成默认构造函数。
class A
{
public:
A() = default;
A(int a,int b)
{
_a = a;
_b = b;
}
private:
int _a;
int _b;
};
int main()
{
A a;
A aa(1,2);
}
7.2delete关键字
default是强制生成构造函数,对应的delete是强制不生成构造函数。
使用场景是有的时候我们不想要发生拷贝,而编译器是会自动生成默认拷贝构造函数的,所以用delete关键字可以防止拷贝。
7.3新增的默认构造函数
注意了,前面我们说过一个类有6个默认成员函数,那是在C++98里,到了C++11里多了移动构造和移动赋值构造两个,即C++11里有8个默认成员函数。
与之前6个不同,它们的生成规则更复杂了。
- 如果你没有自己实现移动构造函数,且
析构函数 、拷贝构造、拷贝赋值
重载都没有
实现。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型 成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造
,如
果实现了就调用移动构造,没有实现就调用拷贝构造。- 如果你没有自己实现移动赋值重载函数,且
析构函数 、拷贝构造、拷贝赋值
重载都
没有实现,那么编译器会自动生成一个默认移动赋值。默认生成的移动赋值函数,对于内置 类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值
,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
7.4类成员变量初始化
我们知道,类成员如果是自定义类型,它会去调用它的构造函数,而针对于内置类型,则不会处理,C++11则增加了给缺省值来给内置成员变量初始化。
class A
{
private:
int a = 10;
string s;
};
8.可变参数模板
8.1Args…
C++11的新特性可变参数模板能够接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。
// Args是一个模板参数包,args是一个函数形参 参数包
// 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args... args)
{}
由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。
// 递归终止函数
template <class T>
void ShowList(const T& t)
{
cout << t << endl;
}
// 展开函数
template <class T, class ...Args>
void ShowList(T value, Args... args)
{
cout << value <<" ";
ShowList(args...);
}
int main()
{
ShowList(1);
ShowList(1, 'A');
ShowList(1, 'A', std::string("sort"));
return 0;
}
可能有的人没看懂这个过程,我们以ShowList(1, 'A', std::string("sort"))
为例,
第一次调用函数
value = 1 args包含'A'
、"sort"
。
第二次调用函数(即进入递归)
value = ‘A’ args = “sort”
第三次调用函数
此时调用的是重载版本的ShowList(const T& t)
其实这就一次次拆包的过程,把参数包里的参数一个个拿出来。
8.2emplace_back
可变参数模板的应用
template <class... Args>
void emplace_back(Args&&... args);
emplace系列的接口,支持模板的可变参数,并且万能引用。
int main()
{
std::list< std::pair<int, char> > mylist;
// emplace_back支持可变参数,拿到构建pair对象的参数后自己去创建对象
// 除了用法上,和push_back没什么太大的区别
mylist.emplace_back(10, 'a');
mylist.emplace_back(20, 'b');
mylist.emplace_back(make_pair(30, 'c'));
mylist.push_back(make_pair(40, 'd'));
mylist.push_back({ 50, 'e' });
return 0;
}
喜欢这篇文章的可以给个一键三连
点赞👍关注💡收藏💖