C++ 11
目录
1. 统一的列表初始化
1.1 {}初始化
1.2 std::initializer_list
2. 声明
2.1 auto
2.2 decltype
2.3 nullptr
3. 范围for循环
4. 智能指针
5. STL中一些变化
6. 右值引用和移动语义
6.1 左值引用和右值引用
什么是左值?什么是左值引用?
什么是右值?什么是右值引用?
6.2 左值引用与右值引用总结/比较
6. 3 移动构造和移动赋值(右值引用作用)
移动构造
综上
移动赋值
6.4 右值引用引用左值及其一些更深入的使用场景分析
6.5 完美转发
模板中的&& 万能引用
7. 新的类功能
7.1 默认成员函数
7.2 类成员变量初始化
7.3 强制生成默认函数的关键字default
7.4 禁止生成默认函数的关键字delete
8. 可变参数模板
参数包
STL容器中的empalce相关接口函数
9. lambda表达式
9.1 lambda表达式语法
1. lambda表达式各部分说明
9.2 捕获列表说明
9.3 函数对象与lambda表达式
10. 包装器
10.1 function包装器
10.2 bind
C语言总结 在这 常见八大排序 在这
作者和朋友建立的社区: 非科班转码社区-CSDN社区云 💖 💛 💙
期待hxd的支持哈 🎉 🎉 🎉
最后是打鸡血环节: 少年不该止步于此 🚀 🚀 🚀
最近作者和好友建立了一个公众号
公众号介绍:
专注于自学编程领域。由USTC、WHU、SDU等高校学生、ACM竞赛选手、CSDN万粉博主、双非上岸BAT学长原创。分享业内资讯、硬核原创资源、职业规划等,和大家一起努力、成长。( 二维码在文章底部哈! )
1. 统一的列表初始化
1.1 {}初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如:struct Point {int _x;int _y; }; int main() {int array1[] = { 1, 2, 3, 4, 5 };int array2[5] = { 0 };Point p = { 1, 2 };return 0; }
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。struct Point {int _x;int _y; }; int main() {int x1 = 1;int x2{ 2 };int array1[]{ 1, 2, 3, 4, 5 };int array2[5]{ 0 };Point p{ 1, 2 };// C++11中列表初始化也可以适用于new表达式中int* pa = new int[4]{ 0 };return 0; }
创建对象时也可以使用列表初始化方式调用构造函数初始化int main() {Date d1(2022, 1, 1); // old style// C++11支持的列表初始化,这里会调用构造函数初始化Date d2{ 2022, 1, 2 };Date d3 = { 2022, 1, 3 };return 0; }
1.2 std::initializer_list
std::initializer_list是什么类型:
std::initializer_list使用场景:std::initializer_list一般是作为构造函数的参数,C++11对STL中的不少容器就增加std::initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。让模拟实现的vector也支持{}初始化和赋值://让模拟实现的vector也支持{}初始化和赋值 namespace my_vector {template<class T>class vector {public:typedef T* iterator;vector(initializer_list<T> l){_start = new T[l.size()];_finish = _start + l.size();_endofstorage = _start + l.size();iterator vit = _start;typename initializer_list<T>::iterator lit = l.begin();while (lit != l.end()){*vit++ = *lit++;}//for (auto e : l)// *vit++ = e;}vector<T>& operator=(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;}private:iterator _start;iterator _finish;iterator _endofstorage;}; }
对于容器为什么支持{}初始化是因为
常量的 {} 他会认为是 initializer_list,是一个容器。就有点像一个常量数组。(调用容器里面initializer_list的构造)
容器里面支持{}构造是因为有这个构造
2. 声明
decltype是通过表达式或者对象
typeid是获取这个类型的字符串2.1 auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。C++11中废弃auto原来的用法,将其用于实现自动类型腿断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。2.2 decltype
关键字decltype将变量的类型声明为表达式指定的类型。
2.3 nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。#ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
3. 范围for循环
for(auto e:vc) {cout<<e<<endl; }
4. 智能指针
后面单独拎出来
5. STL中一些变化
多了一个 array(对于访问越界不是抽查,是严格检查)
forword_list(单向链表)
6. 右值引用和移动语义
6.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。int main() {// 以下的p、b、c、*p都是左值int* p = new int(0);int b = 1;const int c = 2;// 以下几个是对上面左值的左值引用int*& rp = p;int& rb = b;const int& rc = c;int& pvalue = *p;return 0; }
什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:(字面常量、表达式返回值)(临时变量)函数返回值(传值返回)(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。int main() {double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;x + y;fmin(x, y);// 以下几个都是对右值的右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;x + y = 1;fmin(x, y) = 1;return 0; }
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。int main() {double x = 1.1, y = 2.2;int&& rr1 = 10;const double&& rr2 = x + y;rr1 = 20;rr2 = 5.5; // 报错return 0; }
第二点就属于权限问题,加const就可以传左值也可以传右值了。
6.2 左值引用与右值引用总结/比较
综上:
左值和右值的根本区别是能不能取地址
右值虽然不能取地址,但是右值引用的变量是可以取地址的
左值和左值也可以是表达式
无论是左值还是右值都是给对象取别名
左值引用只可以引用左值
右值引用只可以引用右值
左值引用可以引用const修饰的右值
右值引用可以引用move的左值
(move可以把左值属性变成右值属性,右值属性就是别人在拷贝我的时候可以掠夺我的资源了(右值中的将亡值),左值就是老老实实深拷贝(都是对于自定义类型,因为内置类型左值就是左值,右值就是右值))6. 3 移动构造和移动赋值(右值引用作用)
移动构造
移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。(后置 ++ 和 + 返回临时对象)这个就是移动构造
对于编译器的优化
右边本来str是左值,但是因为编译器看到str马上销毁了就可以转换(move)识别为右值,然后移动构造两次。然后优化就是一次移动构造。(移动构造有说法是延长了对象的生命周期,其实不是,仔细想想就发现应该是说延长了资源的生命周期)
综上
左值引用就是传进来的时候效率高了不用深拷贝,但是因为不一定能动原来的值,所以就老老实实深拷贝。但是对于右值引用,传进来的值(主要是说自定义类型)是将亡值,简单说就是生命周期就只有这么一下(一行),用了就销毁了,所以我们可以直接掠夺他的数据直接换到我们的对象里面(因为我们对象的声明周期更长),这样也就减少了深拷贝,加快了速率(直接swap很快)。
移动赋值
注意这里是移动构造+移动赋值,没有直接优化成移动赋值,上面优化是因为那是移动构造+移动构造才优化成了移动构造,而移动构造+移动赋值不能一下优化。
PS:STL容器,C++11以后,都提供移动构造和移动赋值。
6.4 右值引用引用左值及其一些更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。template<class _Ty> inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT {// forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg); }
int main() {my_string::string s1("hello world");// 这里s1是左值,调用的是拷贝构造my_string::string s2(s1);// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的// 资源被转移给了s3,s1被置空了。my_string::string s3(std::move(s1));return 0; }
6.5 完美转发
模板中的&& 万能引用
std::forward 完美转发在传参的过程中保留对象原生类型属性
(如果不是模板,那么右值引用是只能接收右值的)
把传进来的数据,再往下一层传,发现他们原来的属性退化成了左值(因为右值引用了之后,引用他的别名是一个左值!)解决:完美转发(保持他的属性)
(也就是说完美转发的作用是为了万能引用的时候去保存传进去数据的原有属性(原来是左值就是左值,是右值就是右值))
用处:
对使用了万能引用的地方去使用数据都需要用完美转发来保持他原有的属性
7. 新的类功能
7.1 默认成员函数
原来C++类中,有6个默认成员函数:1. 构造函数2. 析构函数3. 拷贝构造函数4. 拷贝赋值重载5. 取地址重载6. const 取地址重载最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。C++11 新增了两个:移动构造函数和移动赋值运算符重载。
- 针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下: 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
7.2 类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化,这个我们在雷和对象默认就讲了,这里就不再细讲了。7.3 强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。class Person { public:Person(const char* name = "", int age = 0):_name(name), _age(age){}Person(const Person& p):_name(p._name), _age(p._age){}Person(Person&& p) = default; private:my_strig::string _name;int _age; }; int main() {Person s1;Person s2 = s1;Person s3 = std::move(s1);return 0; }
7.4 禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。继承和多态中的fifinal与override关键字多态章节已经进行了详细。
8. 可变参数模板
参数包
C++11的新特性可变参数模板能够让您创建可以接受可变参数的函数模板和类模板,相比C++98/03,类模版和函数模版中只能含固定数量的模版参数,可变模版参数无疑是一个巨大的改进。然而由于可变模版参数比较抽象,使用起来需要一定的技巧,所以这块还是比较晦涩的。现阶段呢,我们掌握一些基础的可变参数模板特性就够我们用了。下面就是一个基本可变参数的函数模板:// Args是一个模板参数包,args是一个函数形参参数包 // 声明一个参数包Args...args,这个参数包中可以包含0到任意个模板参数。 template <class ...Args> void ShowList(Args... args) {}
上面的参数args前面有省略号,所以它就是一个可变模版参数,我们把带省略号的参数称为“参数包”,它里面包含了0到N(N>=0)个模版参数。我们无法直接获取参数包args中的每个参数的,只能通过展开参数包的方式来获取参数包中的每个参数,这是使用可变模版参数的一个主要特点,也是最大的难点,即如何展开可变模版参数。由于语法不支持使用args[i]这样方式获取可变参数,所以我们的用一些奇招来一一获取参数包的值。必须递归解参数包(编译时)
就是每次的第一个参数传给了val,后面args的值就越来越小,当最后只有一个值的时候,因为(有模板有现成的,肯定先匹配现成的函数)有了只有一个参数的ShowLit所以直接调用,然后结束递归。
或者这样写:
STL容器中的empalce相关接口函数
template <class... Args> void emplace_back (Args&&... args);
首先我们看到的emplace系列的接口,支持模板的可变参数,并且万能引用。那么相对insert和emplace系列接口的优势到底在哪里呢?emplace比pushback的优势就是拿到参数可以直接构造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' });for (auto e : mylist)cout << e.first << ":" << e.second << endl;return 0; }
就不会去调用拷贝构造和移动构造
9. lambda表达式
9.1 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement}1. lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来 判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda 函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以 连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量 性。使用该修饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回 值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推 导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获 到的变量。
注意:在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。int main() {// 最简单的lambda表达式, 该lambda表达式没有任何意义[] {};// 省略参数列表和返回值类型,返回值类型由编译器推导为intint 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;// 复制捕捉xint x = 10;auto add_x = [x](int a) mutable { x *= 2; return a + x; };cout << add_x(10) << endl;return 0; }
通过上述例子可以看出,lambda表达式实际上可以理解为无名函数,该函数无法直接调用,如果想要直接调用,可借助auto将其赋值给一个变量。9.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表达式之间不能相互赋值,即使看起来类型相同void (*PF)(); int main() {auto f1 = [] {cout << "hello world" << endl; };auto f2 = [] {cout << "hello world" << endl; };// 此处先不解释原因,等lambda表达式底层实现原理看完后,大家就清楚了//f1 = f2; // 编译失败--->提示找不到operator=()// 允许使用一个lambda表达式拷贝构造一个新的副本auto f3(f2);f3();// 可以将lambda表达式赋值给相同类型的函数指针PF = f2;PF();return 0; }
9.3 函数对象与lambda表达式
函数对象,又称为仿函数,即可以想函数一样使用的对象,就是在类中重载了operator()运算符的类对象。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);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year;};r2(10000, 2);return 0; }
从使用方式上来看,函数对象与lambda表达式完全一样。函数对象将rate作为其成员变量,在定义对象时给出初始值即可,lambda表达式通过捕获列表可以直接将该变量捕获到。实际在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载operator()。总结:
lambda的原理是转换成了一个仿函数(也就是一个函数对象),仿函数的名称是啥lambda_uuid(uuid唯一)这样就生成了唯一名称,跟其他的lambda对象就不冲突,lambda的对象就是这个类型仿函数的对象,调用就是仿函数的调用。(uuid是生成不同的字符串,rand是生成的整型)
10. 包装器
包装器其实就是用一个统一的类型去包了一下(function)
10.1 function包装器
function包装器 也叫作适配器。C++中的function本质是一个类模板,也是一个包装器。那么我们来看看,我们为什么需要function呢?通过上面的程序验证,我们会发现useF函数模板实例化了三份。
包装器可以很好的解决上面的问题:
PS:静态的也可以加&,所以其实对于类里面的函数,都加上好记一点。
当然,可调用对象都可以包,lambda也一样:
PS:分别包的是 函数指针(和成员函数),仿函数对象,lambda。
function的用处的例子(一般网络编程里面,用这种写法比较多):
10.2 bind
但是function的话,是要类型相同,像下面这种,多了一个参数那应该怎么包装呢?我们就要用到functional里面的bind。
std::bind函数定义在头文件中,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象(callable object),生成一个新的可调用对象来“适应”原对象的参数列表。一般而言,我们用它可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个(M可以大于N,但这么做没什么意义)参数的新函数。同时,使用std::bind函数还可以实现参数顺序调整等操作。例如:
function是用来包装可调用对象(函数指针,仿函数,lambda),bind就可以调整可调用对象的参数(个数和顺序,生成一个新的可调用对象)
第一个是传可调用对象,后面就是参数包(本来有三个参数,我们这里是this,所以把第一个参数绑死(我们给了一个Plus对象),另外两个参数(占位对象)显示传,但是注意他们的命名空间),这样func4就可以不传this了,因为上面我们已经绑死了。调整个数例子(三个参数->两个参数):
一个参数:
PS:-1,-2,-3表示你要显示传的参数个数(要自己传的)
调整顺序:就是把你传进去的第一个参数和第二个参数换位置了
然后我们再回到开始的问题:
这样就可以传进去了!
最后的最后,创作不易,希望读者三连支持💖
赠人玫瑰,手有余香💖