您的位置:首页 > 文旅 > 美景 > 赣州seo外包怎么收费_微信直接下载安装_seo技术介绍_今日重大事件

赣州seo外包怎么收费_微信直接下载安装_seo技术介绍_今日重大事件

2024/12/23 1:52:49 来源:https://blog.csdn.net/m0_74171054/article/details/141927387  浏览:    关键词:赣州seo外包怎么收费_微信直接下载安装_seo技术介绍_今日重大事件
赣州seo外包怎么收费_微信直接下载安装_seo技术介绍_今日重大事件

目录

1. C++简介

2. 统一的列表初始化 --- initializer_list

3. 声明 

3.1 decltype

3.2 auto 和 nullptr 

4. 范围for循环

5. 智能指针  

6. 右值引用和移动语义 

 7. 类的新增默认成员函数和关键字

8. 可变参数模板  

9. lambda表达式 

10. 包装器 

10.1 function -- 函数包装器 

 10.2 bind -- 绑定函数参数

11. 线程库


1. C++简介

  2003年C++标准委员会提交了一份技术勘误表(简称TC1),使得C++03这个名字取代C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞进行修复,语言的核心部分则没有改动,因此习惯性的把两个标准合并称为C++98/03标准。

  可本来计划五年一更的下个标准却姗姗来迟。从C++03到C++11,花了 8 年时间,于2011 年 8 月 12 日最终获得 ISO 批准,成为迄今为止版本之间最长的间隔,至此迎来 第二个真正意义上的新标准 ,从那时起,目前 C++ 每 3 年定期更新一次。

  相比于 C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。因此, C++11能更好地用于系统开发和库开发、语法更加泛化和简单化、更加稳定和安全;不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多。

  所以,C++11至今是我们的学习重点! 由于篇幅有限,本文主要讲解实际中比较 常用和实用 的语法。

2. 统一的列表初始化 --- initializer_list

  在C++98中,标准允许使用花括号{ }数组或者结构体元素进行统一的列表初始值设定。

  C++11扩大了 { } 用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号(=),也可不添加,比如:

struct A
{int _x;int _y;
};
int main()
{int array1[] = { 1, 2, 3, 4, 5 };int array2[5]{ 0 };A a1 = { 1, 2 };A a2{ 1, 2 };int m = 1;int n{ 2 };//n{m}或n(2)或n(m)int* pb = new int{ 0 };//pb = new int(0);int* pa = new int[4] { 0 };return 0;
}

  创建对象时也可以使用列表初始化方式调用构造函数初始化:

  而之所以支持上面{ }的各种用法,是因为C++11增加的新模板:initializer_list

  此类型用于访问 C++ 初始化列表中的值,该列表是 const T 类型的元素列表 

  这种类型的对象由编译器从初始化列表声明中自动构造,就像分配了某类型元素的数组一样(没有重载使用 【】),初始化列表声明是用大括号括起来的逗号分隔元素列表:const T

  该对象引用此数组的元素,但不包含它们:复制一个对象会生成另一个对象,该对象引用相同的底层元素,而不是它们的新副本(引用语义);此临时数组的生命周期与对象相同。如下示例:

  此模板类不是隐式定义的,即使该类型是隐式使用的,也应包含<initializer_list>以访问它。

  另一特点在于:仅采用此类型的一个参数的构造函数是一种特殊类型的构造函数,称为 initializer-list 构造函数。当使用 initializer-list 构造函数语法时,Initializer-list 构造函数优先于其他构造函数,如下示例 

struct myclass {myclass (int,int);myclass (initializer_list<int>);
};myclass foo {10,20};  // calls initializer_list ctor
myclass bar (10,20);  // calls first constructor 

  initializer_list的使用场景: 

  其一般是作为构造函数的参数,这样初始化容器对象就更方便了;也可以作为operator= ()的参数,这样就可以用大括号赋值;还可以作为插入函数 insert() 的参数,这样就可以一次插入一段数据,比如:

  . . . . . . 

3. 声明 

3.1 decltype

  此关键字将变量的类型声明为表达式指定的类型。 

const int x = 1;
double y = 2.2;decltype(x * y) ret; // ret的类型是double
decltype(&x) p;      // p的类型是int*const int* px = &x;
decltype(px) _px;//_px的类型是const int*

3.2 auto 和 nullptr 

  点击跳转至小编之前的文章 《C++入门》 浏览。

4. 范围for循环

  点击跳转至小编之前的文章 《C++入门》 浏览。

5. 智能指针

  先看下面代码中的问题:

int div()
{int a, b;cin >> a >> b;if (b == 0)throw invalid_argument("除0错误");return a / b;
}void Func()
{// 情况1: 如果p1这里new 抛异常会如何?// 情况2: 如果p2这里new 抛异常会如何?// 情况3: 如果div调用这里又会抛异常会如何?int* p1 = new int;int* p2 = new int;cout << div() << endl;delete p1;delete p2;
}int main()
{try {Func();}catch(exception& e){cout << e.what() << endl;}return 0;
}

  情况1:无内存泄漏

  情况2:发生内存泄漏,p1

  情况3:发生内存泄漏,p1,p2 

  内存泄漏指因为疏忽或错误造成运行中的程序未能释放已经不再使用的内存的情况。其并不是指内存在物理上的消失,而是运行中的应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,别的程序又不能使用,因而造成了内存的浪费。

  其发生方式通常有以下几种:

  1. 常发性内存泄漏:发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

  2. 偶发性内存泄漏:发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生;常发性和偶发性是相对的,对于特定的环境,偶发性的也许就变成了常发性的,所以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏:发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块且仅有一块内存发生泄漏。

  4. 隐式内存泄漏:程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终操作系统会回收所有资源。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存可能导致最终耗尽系统的所有内存,会导致响应越来越慢,最终卡死。所以,称这类内存泄漏为隐式内存泄漏。

  在C/C++程序中一般我们关心两种方面的内存泄漏:

  堆内存泄漏(Heap leak) :通过malloc / calloc / realloc / new等从堆中分配的一 块内存,用完后必须通过调用相应的 free或者delete 释放。如果程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

  系统资源泄漏:指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

【关于什么是系统资源 及 如何管理,这里做简单介绍(以windows为例):

  Windows操作系统中的应用程序需要大量与用户交互的元素,如窗口、按钮、菜单、光标、位图等,这些都由操作系统的用户界面(User)图形设备接口(GDI, Graphical Device Interface)管理。这些元素被称为系统资源

  在早期的Windows系统中,User资源堆GDI资源堆是以固定大小存在的,比如在Windows 3.x和Windows 95/98中,User和GDI资源堆分别为64KB,限制了系统可以同时管理的窗口和图形对象数量。但在现代Windows系统(如Windows NT架构之后),这些资源堆是动态分配和扩展的。系统可以根据实际需要分配更多的内存,从而避免了早期系统中由于固定堆大小导致的资源不足问题。

  关于系统资源的管理机制,现代Windows系统的资源管理由内核模式组件来执行,特别是通过以下机制:

  • 虚拟内存管理:Windows通过虚拟内存管理将物理内存(RAM)和磁盘空间结合使用,允许操作系统和应用程序利用超过物理内存容量的内存。通过这种方式,系统资源的管理不再局限于固定大小的堆。
  • 句柄管理:操作系统通过分配句柄(handle)来管理窗口、图形对象和其他资源,每个对象都有一个唯一的句柄。Windows的句柄数量上限也是动态的,受限于系统可用内存和内核对象的限制。
  • 内存分页:通过内存分页技术,系统可以根据需要将不常用的内存页移至硬盘,从而腾出更多内存给当前活跃的应用程序和资源。
  • ......

  除此之外,还和硬件配置密切相关,比如内存、处理能力、显卡性能等等。

  用户可以通过 任务管理器(Task Manager)或 性能监视器(Performance Monitor)查看和管理系统资源的使用情况:处理器使用率、内存占用、磁盘读写以及GPU的负载,帮助用户了解系统的整体运行状况。】

  实际中,内存泄漏挺常见,解决方案大致两种:

  1:事后查错型。如,Linux下几款内存泄漏检测工具  ,Windows下的第三方工具VLD

  2:提前预防型。如,养成良好的设计/编程规范;有些公司内部规范使用内部实现的私有内存管理库,这套库自带内存泄漏检测的功能选项;采用RAII思想来管理资源

  这里,我们学习 “如何采用RAII思想管理资源”

  RAII(Resource Acquisition Is Initialization:资源获取时初始化)是一种利用类的实例化对象生命周期来控制程序资源(如内 存、文件句柄、网络连接、互斥量等等)的简单技术。 

  这种做法有两大好处:

        1:不需要显式地释放资源(析构)

        2:对象所需的资源在其生命期内始终保持有效

  在此基础上,如果这个类的成员函数还重载了 * 和 -> , 让其实例化对象可以像指针一样使用,我们就把这个对象就叫做 “智能指针”。

  至此,我们把管理一份资源的责任托管给了一个对象,如下示例代码:

template<class T>
class SmartPtr {
public:SmartPtr(T* ptr = nullptr): _ptr(ptr){cout << "SmartPtr(T* ptr = nullptr)" << endl;}~SmartPtr(){cout << "~SmartPtr()" << endl;if (_ptr)delete _ptr;}T& operator*() { return *_ptr; }T* operator->() { return _ptr; }//......
private:T* _ptr;
};struct Date
{int _year;int _month;int _day;
};
int main()
{SmartPtr<int> sp1(new int);*sp1 = 10;SmartPtr<Date> sp2(new Date);sp2->_year = 2024;sp2->_month = 9;sp2->_day = 14;return 0;
}

  除了*和->,我们还希望这个类对象在 拷贝/赋值 时也像指针一样进行浅拷贝

  可现在的问题是:被赋值对象(=号左边),比如叫 A,管理的资源是否要同时释放?

  情况一:同时释放。那么在此之前如果有代码 B = A 或者 B(A),对象B析构时就会报错:释放已经delete掉的空间。解决办法有二,其一为:在代码“B = A 或者 B(A)”后禁止A被赋值,即A=... ;其二为:在“B = A 或者 B(A)”的同时A::_ptr=空。

  显然,后者更符合指针的行为。C++98版本的库中就是基于此想法提供了auto_ptr的智能指针:

  如下简化代码:

template<class T>
class auto_ptr {
public://......auto_ptr(auto_ptr<T>& ap):_ptr(ap._ptr){ ap._ptr = NULL; }auto_ptr<T>& operator=(auto_ptr<T>& ap){if (this != &ap){if (_ptr)delete _ptr;_ptr = ap._ptr;ap._ptr = NULL;}return *this;}//......
private:T* _ptr;
};

   其核心思想总结为:管理权转移。

  也正因如此,它和普通指针一样,容易出现 访问空指针 的错误。这么看,好像也没那么“智能”,所以很多公司明确要求不能使用auto_ptr。

  于是C++11开始提供更靠谱,场景更丰富的智能指针,从命名上就能意会:

  首先是:unique_ptr

  简单粗暴——禁止拷贝 

  其次是:shared_ptr 

  支持拷贝。 

  所以回到之前的问题,继续讨论:情况二:资源是否释放由一定的标准决定。

  shared_ptr直译为 共享指针,即允许多个对象共享同一份资源,通过 引用计数 来实现:

  1. 在shared_ptr其内部,给每个资源都维护了一份计数,用来记录该份资源被几个对象共享

  2. 对象被销毁时,说明自己不使用该资源了,其引用计数减1

  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源

  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

  如下简化代码:

namespace nxf
{template<class T>
class shared_ptr {
public:shared_ptr(T* ptr = nullptr){if (_ptr = ptr)_pcount = new int(1);}//析构~shared_ptr() { destroyed(); }//拷贝构造shared_ptr(const shared_ptr<T>& sp){_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}//赋值重载shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){destroyed();_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}T& operator*() const{assert(_ptr);return *_ptr;}T* operator->() const{assert(_ptr);return _ptr;}T* get() const { return _ptr; }int use_count() const{if (_pcount)return *_pcount;elsereturn 0;}private:void destroyed(){if (_pcount && 0 == --(*_pcount)){delete _ptr;delete _pcount;_ptr = nullptr;_pcount = nullptr;cout << "destroyed()" << endl;}}T* _ptr = nullptr;int* _pcount = nullptr;
};
}int main()
{nxf::shared_ptr<int> sp1(new int(1));auto sp2(sp1);auto sp3 = sp2;cout << sp1.use_count() << endl;*sp1 += 100;*sp2 -= *sp3;cout << *sp1 << endl;cout << sp1.get() << endl;cout << sp2.get() << endl;cout << sp3.get() << endl << endl;nxf::shared_ptr<int> sp4(new int(2));sp2 = sp4;cout << sp2.use_count() << endl;cout << sp2.get() << endl;cout << sp4.get() << endl << endl;cout << sp1.use_count() << endl;return 0;
}

  示例输出:

  至此,拷贝后 资源的处理 得到了解决。

  但有个很特殊的场景:“循环引用”。(注意,这里的 "引用" 不是指语法,而是对特殊场景/结构的形象叫法)

  会导致 内存泄漏如下代码:

template<class T>
const int* shared_ptr<T>::get_pcount() const { return _pcount; }//仅为验证增加struct A
{nxf::shared_ptr<A> _sp;
};void func(map<const A*, const int*>& mp)
{nxf::shared_ptr<A> sp1(new A);sp1->_sp = sp1;nxf::shared_ptr<A> sp2(new A);nxf::shared_ptr<A> sp3(new A);sp2->_sp = sp3;sp3->_sp = sp2;cout << "func()函数返回前:" << endl;cout << sp1.get() << ":>" << sp1.use_count() << endl;cout << sp2.get() << ":>" << sp2.use_count() << endl;cout << sp3.get() << ":>" << sp3.use_count() << endl;mp.insert({ make_pair(sp1.get(), sp1.get_pcount()), make_pair(sp2.get(), sp2.get_pcount()), make_pair(sp3.get(), sp3.get_pcount()) });
}int main()
{map<const A*, const int*> m;func(m);cout << "func()函数返回后:" << endl;for (auto& e : m){cout << e.first << ":>" << *(e.second) << endl;}//......return 0;
}

  示例输出:

  如下图示:

   事实上,“循环引用” 可以总结为一个固定的模型:即上图中的 对象sp2和sp3,因为,“两个”对象间不管是直接引用,还是间接引用,最终效果都一样(引用计数+1);唯一不同的是相互引用的次数,但是关心这个意义不大,因为只要出现一次,所包含对象一定内存泄漏。

  而之所以叫 “循环引用”,一方面是其逻辑关系上的形似,更重要的一面是因为 :即使触发了引用计数==0的条件,也是死循环:比如上例中,释放对象sp3要先释放sp3::_sp,sp3::_sp管理着sp2,释放sp2要先释放sp2::_sp,sp2::_sp管理着sp3;释放sp3要......,但是内存泄漏的直接原因却不是这个死循环,因为正常情况下根本不会到这一步。

  C++11的解决办法是:shared_ptr对象间的相互引用都用:weak_ptr (weak shared pointer)

  因为它不支持RAII,并且不参与对象的生命周期管理,也就是字面直译:弱引用

  如下简化代码:

namespace nxf
{template<class T>class weak_ptr{public:weak_ptr():_ptr(nullptr){}weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}T& operator*(){ return *_ptr; }T* operator->() { return _ptr; }private:T* _ptr;};
}

  那么前面的 “循环引用” 只要做如下修改就行:

struct A { nxf::weak_ptr<A> _sp; };

  关于 shared_ptr ,还有下面两点内容(需本文点9/10/11):

  1. 定制删除器  

  因为并不是所有的资源释放都是 :delete;还有delete[],malloc/realloc的要用free,文件资源要用 fclose,......;所以std::shared_ptr提供了这样一类接口:

  允许你自定义资源的释放方式。

  简化后的实现原理大致是这样的:

namespace nxf
{template<class T>class shared_ptr {function<void(T*)> _del = [](T* ptr) {delete ptr; };//默认删除器public:template<class D>//模板自定义shared_ptr(T* ptr, D del):_ptr(ptr),_pcount(new int(1)),_del(del){}//析构~shared_ptr(){//......_del(_ptr);//......}//......private:T* _ptr = nullptr;int* _pcount = nullptr;};
}

   演示示例:

 // 仿函数的删除器template<class T>struct FreeFunc {void operator()(T* ptr) { free(ptr); }};//函数指针的删除器template<class T>void DeleteArryFunc(T* ptr) { delete[] ptr; }int main()
{nxf::shared_ptr<int> sp1((int*)malloc(6), FreeFunc<int>());//仿函数nxf::shared_ptr<char> sp2(new char[10], DeleteArryFunc<char>);//函数指针nxf::shared_ptr<FILE> sp3(fopen("test.c", "w"), [](FILE* p) { fclose(p); });//lambda表达式return 0;
}

  2.线程安全 

  更新中...... 

6. 右值引用和移动语义 

  C++11不仅增加了几个新容器,如:<array>, <forward_list>,  <unordered_map>和<unordered_set> ,还基本给每个容器中都增加了一些新方法,但仔细看的话,其实很多都是用得 比较少的。

  比如提供了cbegin和cend方法返回const迭代器等等,但是实际意义不大,因为begin和end也是 可以返回const迭代器的,这些都是属于锦上添花的操作。

  其中真正 实质性的重大更新之一 是引入了右值引用(&&)和 移动语义,如下示例接口:

  . . . . . . 

  此后,许多接口函数最后都是使用此版本,提高了效率。 

  在讲为什么之前,先解决你的疑惑:右值 是什么?有没有左值,如果有,它又是什么?如何判定区分呢?

  左值是一个表示数据的表达式,如变量名或解引用的指针可以获取它的地址 并 对它赋值;左值可以出现赋值符号的左边,右值不能出现在赋值符号左边;定义时const修饰符后的左值,不能给它赋值,但是可以取它的地址;左值引用就是给左值的引用,给左值取别名

  比如:

// 以下的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;

  小编在之前的文章《C++入门》 中所讲解的 引用 就是左值引用。

  右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引 用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址右值引用就是对右值的引用,给右值取别名

  比如:

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;

  需要注意的是:右值引用后的引用属性 使用时(如:赋值,取地址,传参,......) 变为左值。比如:

 double x = 2.1, y = 3.2;int&& rr1 = 10;const double&& rr2 = x + y;//不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。//如果不想rr1被修改,可以用const int&& rr1 去引用rr1 = 20;auto prr1 = &rr1;*prr1 = 30;rr2 = 5.5;  // 报错

  因为:

  语法上:引用都是别名,不开空间:左值引用是给左值取别名,右值引用是给右值取别名。 

  底层:引用是用指针实现的。左值引用是存当前左值的地址。右值引用,是把当前右值拷贝到栈上的一个临时空间,存储这个临时空间的地址 。看下面的汇编:

   另一需要注意的点是:左值引用只能引用左值,不能引用右值, 但是const左值引用既可引用左值,也可引用右值引用。右值引用只能右值,不能引用左值, 但是右值引用可以move以后的左值。比如:

double x = 1.1;const int& rr1 = 10;double&& rr3 = move(x);
rr3 = 3.3f;//x的值变为3.3

  move是C++11新增的函数模板,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用。   

  现在,可以回答前面的问题了:

  使用 引用 的一大目的就是:减少拷贝 ——> 提高效率

  左值引用解决的场景是:传参,函数返回值(不销毁);没有解决的场景是:函数返回值要销毁,必须进行一次或多次深拷贝。如下场景:

  即使编译器优化后只有一次拷贝,但函数内的 str 拷贝结束后被释放的行为 对极度追求效率的C++来说 依然是一种 “浪费”,因为在大型项目中,海量数据的多次深拷贝的性能开销是不可估量的。

  而 str 作为一个 将亡值 ,既然函数结束后就没有任何用处了,那么不如返回时,直接把 str 中的资源转移给 ret 来管理,不就避免拷贝了吗?确实如此,也就是下面的重点:移动语义。这样实现的构造函数叫 移动构造,和 普通的拷贝构造 形成重载。

  但接下来的问题是:这两个函数是 自动调用 的,函数调用原则是 参数类型匹配,也就是说 如何用参数类型来区别这两个函数?而这就是为什么要先介绍 右值引用 的真正目的,移动构造的参数类型为右值引用,所以右值有些时候也叫 将亡值。

  下面是引入 右值引用 和 移动语义后的情况:

  除此之外,赋值时也会有拷贝的浪费,如下示例为有 右值引用 和 移动语义后的情况: 移动赋值

   上述的两个示例中,函数返回时str都被编译器默认move成右值,但有的情况编译器却不会,比如下面这个场景:

template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};template<class T>
class List
{typedef ListNode<T> Node;
public://...........void PushBack(T&& x){Insert(_head, x);}void Insert(Node* pos, T&& x){//.......Node* newnode = new Node;newnode->_data = x; //......}void Insert(Node* pos, const T& x){//........Node* newnode = new Node;newnode->_data = x; //........}//.........
private:Node* _head;
};
int main()
{List<nxf::string> lt;lt.PushBack("1111");return 0;
}

  在这段代码中:

  首先要知道的是: 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值

  接着,来分析有什么问题:字符串 "1111" 隐式类型转换构造临时string对象,为 右值,然后调用 “ void PushBack(T&& x) ”,此时参数x是右值引用;下一步,调用 Insert(),我们预期调用的是“ void Insert(Node* pos, T&& x) ” 进行 资源转移 ,但是前面说过,右值引用的引用属性在 使用时 变为左值,所以此时传参给Insert()函数的x处理成左值,所以直接调用的是参数类型最匹配的 “void Insert(Node* pos, const T& x)” 版本进行深拷贝。即使调用前者,语句 “newnode->_data = x”依旧是string的深拷贝,因为此时使用的x还是被处理成左值。

  要解决上面的问题,法一为:在这两个关键位置对x显式move;法二为:在这两个关键位置对x操作 forward<T>(x) ,称 完美转发 :保持x的原生类型属性

  可用下面的一段代码对其测试:

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(std::forward<T>(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;
}

 7. 类的新增默认成员函数和关键字

  C++98中,类有6个默认成员函数:《类和对象(中)--- 类的六个默认成员函数》 

  C++11 新增了两个:移动构造函数  和  移动赋值运算符重载   

  即上述 点6 内容,不再赘述。

  需要注意的点如下:

  如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。

  移动赋值也是如此。

  因为实现其中任意一个都表示:有资源的申请和使用,需要你自己处理,否则就是浅拷贝!

  default 和 delete 关键字: 

  强制生成默认函数 = default

  禁止生成默认函数 = delete

  继承和多态中的final与override关键字

  它们常用为:《C++常见特殊类的设计》

8. 可变参数模板  

   其实大家对此并不陌生,比如:

  但可变模版参数比较抽象,使用起来需要一定的技巧,比较晦涩。所以,此处只讲一些基础的可变参数模板特性,让大家先能看懂,深入的东西留给大家有时间或碰到了再研究。

  下面是一个基本可变参数的函数模板:

// Args是一个模板参数包,args是一个函数形参参数包
// 这个参数包中可以包含0到任意个模板参数。
template <class ...Args>
void ShowList(Args&&... args)
{}

  使用时,无法直接获取参数包args中的每个参数,比如 args[i],只能通过展开参数包的方式,这是其的一个主要特点,也是最大的难点。  

  这里介绍两种展开方式(编译时):

  1:递归函数方式展开参数包

void _ShowList()
{cout << endl;
}template<class T, class ...Args>
void _ShowList(T&& val, Args&&... args)
{cout << val << " ";_ShowList(args...);
}template <class ...Args>
void ShowList(Args&&... args)
{_ShowList(args...);
}int main()
{ShowList(10);ShowList(20, 'b');ShowList(30, 'c', std::string("hello word!"));return 0;
}

  输出:  展开过程如下: 

  2. 逗号表达式展开参数包 

template <class T>
void PrintArg(T&& t)
{cout << t << " ";
}
//展开函数
template <class ...Args>
void ShowList(Args&&... args)
{int arr[] = { (PrintArg(args), 0)... };cout << endl;
}
int main()
{ShowList(10);ShowList(20, 'a');ShowList(30, 'b', std::string("hello linux!"));return 0;
}

  这种展开参数包的方式,不通过递归终止函数,而是利用 逗号表达式自动从左往右依次计算 

 表达式 + 通过初始化列表来初始化一个变长数组 的语法特性 。最终创建元素值都为0的数组纯粹是为了在数组构造的过程展开参数包。

  STL容器中新增的可变参数模板接口为 emplace系列(Construct and insert element:构建并插入元素)

  相较  insert ,push* 接口,其主要优势在于:用法上可以直接给插入对象的参数,提升效率。如下示例:

class Date
{
public:Date(int y, int m, int d);//......
private:int _y;int _m;int _d;
};int main()
{//对于 浅拷贝 的类对象,减少一次拷贝构造list<Date> lt1;lt1.push_back({ 1970, 1, 1 });//构造+拷贝构造lt1.emplace_back(1970, 1, 1);//直接构造//对于 深拷贝 的类的对象,减少一次移动构造list<string> lt2;lt2.push_back( "sort");//构造+移动构造lt2.emplace_back("print");//直接构造return 0;
}

9. lambda表达式 

  在C++98的 <algorithm> 中提供了一个 对范围内的元素进行排序的函数模板 std::sort

   特别注意:std::sort 函数要求传入的迭代器类型是随机访问迭代器,因为它需要通过下标快速访问容器中的元素。比如,std::vector

 然而,如 std::list 提供的是双向迭代器,这类迭代器只允许在列表中按顺序前进或后退,不能像随机访问迭代器那样直接跳到某个位置。因此,std::sort 无法对 std::list 进行排序

  比如:

vector<int> lt = { 14, 32, 1, 0, 45, -2, 0 };
//升序
sort(lt.begin(), lt.end());
//降序
sort(lt.begin(), lt.end(), greater<int>());

  如果待排序元素为自定义类型,需要用户定义排序时的比较规则: 

struct Goods
{string _name;  // 名字double _price; // 价格double _sales; // 销量Goods(const char* str, double price, double sales):_name(str), _price(price), _sales(sales){}
};struct Compare_Sales_Less
{bool operator()(const Goods& gl, const Goods& gr){return gl._sales < gr._sales;}
};struct Compare_Price_Greater
{bool operator()(const Goods& gl, const Goods& gr){return gl._price > gr._price;}
};
//......int main()
{vector<Goods> v = { { "苹果", 2.1, 678 }, { "香蕉", 3, 361.8 }, { "橙子", 2.2,564 }, { "菠萝", 1.5, 123.8 } };sort(v.begin(), v.end(), Compare_Sales_Less());//销量升序sort(v.begin(), v.end(), Compare_Price_Greater());//价格降序//......return 0;
}

  随着C++语法的发展,上面的写法太复杂了,每次为了实现一个algorithm算法, 都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名, 这些都给编程者带来了极大的不便。因此,C++11的写法是:

sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._sales < gr._sales; });//销量升序
sort(v.begin(), v.end(), [](const Goods& gl, const Goods& gr) {return gl._price > gr._price; })//价格降序
//......

  像上面这样,就叫做 lambda 表达式/函数,其完整的书写格式为: 

  [capture-list] (parameters) mutable -> return-type { statement }

  说明:

  1:[capture-list] : 捕捉列表:该列表总是出现在lambda表达式的开始位置,编译器根据 [ ] 来判断接下来的代码是否为lambda表达式,捕捉列表能够捕捉变量供lambda 表达式使用: 

        [var]:表示值传递方式捕捉变量var

        [=]:表示值传递方式捕获所有父作用域中的变量

        [&var]:表示引用传递捕捉变量var

        [&]:表示引用传递捕捉所有父作用域中的变量

        (包括this)

注意:a. 父作用域指包含lambda表达式的语句块

           b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割

                比如:[=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

                           [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

           c. 捕捉列表不允许变量重复传递,否则就会导致编译错误

                比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复 

           d. 在块作用域以外的lambda表达式捕捉列表必须为空

           e. 在块作用域中的lambda表达式仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错

           f. lambda表达式之间不能相互赋值(后面讲) 

  2. (parameters):参数列表:与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略 

  3. mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性,使用该修饰符时,参数列表不可省略(即使参数为空)

  4. ->returntype:返回值类型:用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略,返回值类型明确情况下,也可省略,由编译器对返回类型进行推导

  5. {statement}:函数体:在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量

 那么,C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情。

现在看下面这段代码:

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.23;Rate r1(rate);r1(10000, 2);// lamberauto r2 = [=](double monty, int year)->double {return monty * rate * year; };r2(10000, 2);return 0;
}

  从使用方式上来看,lambda表达式 和 仿函数 完全一样。

  实际在底层编译器对于lambda表达式的处理方式,完全就是按照仿函数的方式处理的,即:如果定义了一个lambda表达式,编译器会自动生成一个类,在该类中重载了operator()。

  如下汇编代码:

   这就是前面问题的答案——lambda表达式之间不能相互赋值,因为没有operator=().

   但是,有两个特别的点是:

void (*PF)();
int main()
{auto f1 = []{cout << "hello world" << endl; };// 1:允许使用一个lambda表达式拷贝构造一个新的副本auto f2(f1);f3();// 2:可以将lambda表达式赋值给相同类型的函数指针PF = f1;PF();return 0;
}

10. 包装器 

10.1 function -- 函数包装器 

  先看代码:

template<class T>
void test(T&& f)
{static int count = 0;cout << "count:" << ++count << endl;cout << "count:" << &count << endl << endl;return f();
}void func() {};struct A
{void operator()() {};
};int main()
{//函数指针test(func);//仿函数test(A());//lambdatest([] {});return 0;
}

  输出: 

  test函数模板实例化了三份,原因是:推演实例化的T类型每次都不一样。

  可实际生活中,T 的类型是很丰富的,那么就会造成模板的效率下降,代码冗余。 

  那么,如果模板可以少实例化,甚至说只实例化一份,上面的问题就解决了;可以做到吗?

  仔细观察,发现上面代码的传参共性是:都是可调用元素(指像函数一样使用)。

  所以,C++11的办法就是:把它们包装成同一种类型的元素,就可以只实例化一份代码。具体实现就是:function -- 函数包装器

  也叫作适配器,其本质是一个类模板

  Ret : 被调用函数的返回类型

  Args… :被调用函数的形参 

  此类实例化的对象可以包装以下任何种类的可调用对象:函数、函数指针、指向成员的指针或任何类型的函数对象(即,其类定义的对象,包括闭包)。 

  拿10.1开始的示例代码来为例,优化后的写法就是:

int main()
{function<void()> fun;//函数指针fun = func;test(fun);//仿函数fun = A();test(fun);//lambdafun = [] {};test(fun);return 0;
}

   或者:test(function<void()>(func)  /  function<void()>(A())  /  function<void()>([]{})),匿名对象的生命周期只在这一行。

  输出:

  如果是包装类的成员函数,写法如下示例:

class B
{
public:void func1() { ; }static void func2() { ; }
};int main()
{function<void(B)> f1 = &B::func1;//有this指针f1(B());function<void()> f2 = &B::func2;//无this指针f2();return 0;
}

  注意:“ & ”符号一定要写,否则编译错误。 

  除此之外,function包装器在实际中有着更丰富的用法。

  举个例子:逆波兰表达式求值 

  . . . . . . 

 10.2 bind -- 绑定函数参数

    函数模板: 

  它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。

  一般而言,它可以把一个原本接收N个参数的函数,通过绑定一些参数,返回一个接收M个(M 可以大于N,但这么做没什么意义)参数的新函数;同时,使用std::bind函数还可以实现参数顺序调整等操作。 

  调用bind的一般形式:auto newCallable = bind(callable,arg_list); 

  其中,callable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的 callable的参数。当我们调用newCallable时,newCallable会调用callable,并传给它arg_list中 的参数。 

  arg_list中的参数可能包含形如_n的名字,其中n是一个整数,这些参数是“占位符”,表示传递给callable的参数的“位置”,如下: 

  此命名空间声明未指定数量的对象:_1,_2 ,_3 ,..., 用于在对 function 的调用中指定占位符。

  当调用返回的函数对象时,带有 placeholder 的参数将替换为调用中的第一个参数,被调用中的第二个参数替换,依此类推.....

   如下示例代码:

int sub(int l, int r) { return l - r; }class A
{
public:A(int a = 10):_a(a){}double func(char c, int i) { return (c + _a) * i * 1.0; }
private:int _a;
};int main()
{//绑定函数 sub 的第一参数为1, 第二参数由 f1 传递 --- 指定参数function<int(int)> f1 = bind(sub, 1, placeholders::_1);cout << f1(20) << endl;调整参数顺序auto f2 = bind(sub, placeholders::_2, placeholders::_1);cout << f2(1, 2) << endl;//绑定成员函数//--- 根据对象调整结果function<double(A)> f3 = bind(&A::func, placeholders::_1, 'b', 3);A a;cout << f3(a) << endl;cout << f3(A(0)) << endl;//--- 对象确定,根据传参调整结果function<double(char, int)> f4 = bind(&A::func, a, placeholders::_1, placeholders::_2);cout << f4('c', 40) << endl;//混合function<double(A, int)> f5 = bind(&A::func, placeholders::_1, 'a', placeholders::_2);cout << f5(A(), 50) << endl;//......return 0;
}

    特别是绑定成员函数时,一定要理清其和 function 包装的关系:function 包装 bind 的返回结果,所以function的模板参数看的是 bind的args参数包,而不是直接看成员函数的参数列表。  

   可见,有了bind绑定后,面对问题时更加灵活了!!!

11. 线程库

   更新中......

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com