COSMIC的后端学习之路——2.1 C++11新特性(1)


本博客总结自零声教育的课程

本博客将和大家结合代码和范例一起学习一些C++11新特性,包括智能指针、右值引用 和 移动语义(move)、forward完美转发、emplace_back 减少内存拷贝和移动、unordered container 无序容器、匿名函数lambda和C++11 标准库(STL)

下述内容相关的代码均在我的github中,希望大家搭配代码来阅读以下内容,以下是我的github代码链接,恳请大家批评指正!
GitHub代码
代码在 2.1_C++11_1 文件夹中

知识树

在这里插入图片描述

1、智能指针

C++11中引入了智能指针的概念
★智能指针的作用:
方便程序员【管理堆内存】:使用普通指针容易造成堆内存泄露(忘记释放内存)、堆内存二次释放、程序发生异常时内存泄露等问题,使用智能指针能够更好的管理堆内存
举例:
在这里插入图片描述
若用16行的new,则析构函数不会调用到,因为我们没有手动释放,会导致内存泄露
若用第17行的智能指针,则会自动调用析构函数
★因此智能指针的作用:为了避免程序员没有手动释放内存而导致的内存泄露问题

C++11的3个智能指针:shared_ptr、unique_ptr、weak_ptr(auto_ptr已被C++11弃用)
能用shared_ptr就不用unique_ptr、weak_ptr
std::share_ptr是使用最多的

(1)std::shared_ptr:共享的智能指针

★std::shared_ptr使用【引用计数】,每一个shared_ptr的拷贝都【指向相同的内存】:
举例:
若这样定义智能指针,则三个智能指针pb、pb2、pb3都是指向同一个对象——new A,即:对象只有一个,而指针是有多个:
在这里插入图片描述
上述这样使用智能指针的场景:
比如音视频流(每一个观众都有一个队列):darren老师在讲课的时候,只有一份视频1,但是要将每一帧视频变成50份传给50个观众,这时候不需要将视频拷贝50份,而是可以将每一帧视频定义50个智能指针,这50个智能指针都指向同一个对象——视频1这一帧视频:
在这里插入图片描述

问题:已经用智能指针申请资源了,什么时候释放资源?换言之对于上述情况,应该由哪个观众释放资源?
答:★★★智能指针有引用计数的机制:有一个观众引用了资源(new出来的视频对象),就进行一次+1的引用计数操作,每有一个观众不看资源,则释放智能指针、引用计数-1;若引用计数减为0,则表示没有观众引用该对象了,则该对象自动释放掉(引用计数为0,自动释放该对象)
(shared_ptr共享被管理对象,【同一时刻可以有多个shared_ptr拥有对象的所有权】,当最后一个shared_ptr对象析构时,被管理对象自动销毁,内存才会被释放)

简单来说,shared_ptr实现包含了两部分:
在这里插入图片描述
1、一个指向堆上创建的对象的裸指针:pb
2、一个指向内部隐藏的、共享的管理对象:new A

★pb.use_count():返回当前这个堆上对象被多少对象引用了,简单来说就是【返回引用计数的个数】:
举例:
在这里插入图片描述

注意:使用智能指针要包含头文件:#include

①初始化

1、定义智能指针p1,有两种方式:
在这里插入图片描述
(1)构造函数(有参构造函数)
(2)make_shared:可以用make_shared来构造智能指针,因为他【更高效】:
在这里插入图片描述
注意:★不能将原始指针直接赋值给一个智能指针,例如定义智能指针是错误的:
在这里插入图片描述
★★★原因:shared_ptr不能通过“直接将原始这种赋值”来初始化,需要通过【①构造函数②make_shared】来初始化

2、用p1.reset分配资源:
★对于一个未初始化的智能指针,可以通过reset方法来初始化:
在这里插入图片描述
(如果reset()的括号中有东西则是分配资源(分配内存),若只是reset()则是释放资源)
若没有像上述【①构造函数②make_shared】这样直接分配资源,则可以用上面的reset分配资源

3、p1和p2都指向new int(1)这个对象
在这里插入图片描述
4、这时候打印引用计数值,则为2,因为有两个指针p1和p2指向new int(1)这个对象:

5、若reset不传入参数,则意为释放资源,引用计数-1,p1变为空:
在这里插入图片描述

6、这时候再打印输出引用计数的值,则为1,因为p1指针已经被释放掉了,现在只有p2指针指向new int(1)这个对象:
在这里插入图片描述
7、判断p1是否引用了数据:此时p1应该为空,所以会进入if判断,输出p1为空
★智能指针可以通过重载的bool类型操作符来判断:
在这里插入图片描述
8、判断p2指针是否为空:因为p2指针没有手动释放资源,所以p2指针不为空,因此不会进入下面的if判断:
在这里插入图片描述

注意:★★★【智能指针】pb、pb2、pb3都是存放在【栈上】的,【退出作用域的时候会自动释放这部分内存】,使得引用计数-1;★★★new出来的int(1)对象是存放在堆上的(多个栈上的只能指针可以指向同一个在堆上的对象)

②获取原始指针

获取原始指针:当需要获取原始指针时,可以通过get方法来返回原始指针:
★★★谨慎使用get函数去get裸指针,因为万一不小心在其他地方写了delete p,手动释放了智能指针,但是后面还是会自动释放智能指针,【重复释放会引起程序崩溃】:
在这里插入图片描述
ptr.get()的返回值就相当于一个裸指针的值,不合适的使用这个值,上述陷阱的所有错误都有可能发生,遵守以下几个约定:
①不要保存ptr.get()的返回值,无论是保存为裸指针还是shared_ptr都是错误的
②保存为裸指针不知什么时候就会变成空悬指针,保存为shared_ptr则产生了独立指针
③不要delete ptr.get()的返回值p,会导致对一块内存delete两次,产生错误

③指定删除器(自定义删除对象)

如果用shared_ptr管理非new对象或是没有析构函数的类时,应当为其传递合适的删除器
在这里插入图片描述
当p的引用计数为0时,自动调用删除器DeleteIntPtr来释放对象的内存。删除器可以是一个lambda表达式(匿名函数),上面的写法可以改为:
在这里插入图片描述
当我们用shared_ptr管理【动态数组】时,需要指定删除器,因为【shared_ptr的默认删除器不支持数组对象】,代码如下所示:
在这里插入图片描述

④一些错误用法

1、不要用一个原始指针初始化多个shared_ptr:
在这里插入图片描述

2、不要在函数实参中创建shared_ptr:
在这里插入图片描述
原因:因为C++的函数参数的计算顺序在不同的编译器不同的约定下可能是不一样的,一般是从右到左,但也可能从左到右,所以,可能的过程是先new int,然后调用g(),如果恰好g()发生异常,而shared_ptr还没有创建,则int内存泄漏了。因此正确的写法应该是先创建智能指针,再传入函数参数:
在这里插入图片描述

3、一般类继承std::enable_shared_from_this的目的是为了能够调用shared_from_this()返回智能指针。不要将this指针作为shared_ptr返回出来(不能将智能指针返回出来给别的地方去使用),因为【this指针本质上是一个裸指针】,这样可能会【导致重复析构】:
在这里插入图片描述
右侧return shared_ptr(this)返回的是一个独立的智能指针,两个智能指针指向同一个对象,因此右侧运行后调用了两次析构函数,但是右侧的引用计数只有一次+1的操作,因为是【用同一个指针(this)构造了两个智能指针sp1和sp2】:
在这里插入图片描述
右侧错误的原因:★由于【用同一个指针(this)构造了两个智能指针sp1和sp2】,而他们之间是没有任何关系的,【在离开作用域之后this将会被构造的两个智能指针各自析构】,导致重复析构的错误;虽然返回了一个独立的智能指针,但是sp1和sp2的资源是一样的,会调用两次析构函数,会导致两次析构
★正确返回this的shared_ptr的做法是:让目标类通过std::enable_shared_from_this类,然后使用基类的成员函数shared_from_this()来返回this的shared_ptr(如上图左侧)
★左侧这样的话,虽然sp1和sp2都指向同一个资源,但是sp1和sp2都有+1的操作:
比如:
如果右侧输出sp1和sp2的引用计数值,则均会输出1
如果左侧输出sp1和sp2的引用计数值,则均会输出2

4、避免循环引用,循环引用会导致内存泄漏(shared_ptr相互引用时的死锁问题):
在这里插入图片描述
A类中定义了一个B资源,B类中定义了一个A资源:
在这里插入图片描述
在这里插入图片描述
如下图:定义了一个指向A的智能指针ap,定义了一个指向B的智能指针bp,又将bp赋值给A中的B资源,将ap赋值给B中的A资源,这样形成了一种类似死锁的结构,锁住了了,因此不会调用双方的析构函数,会导致内存泄露:
★本质上是因为:智能指针的引用计数值没有减为0导致的:循环引用导致ap和bp的引用计数为2,在离开作用域之后,ap和bp的引用计数减为1,并不回减为0,【导致两个指针都不会被析构,产生内存泄漏】
在这里插入图片描述
上述代码形成了死锁,不会自动释放内存,所以需要手动释放内存;因为【★★★类的析构函数析构后才会释放类内部的成员变量】,所以上述代码的类中的bptr和aptr的内存没有被释放掉,需要释放这两部分的内存,否则会出现内存泄露:
释放bptr和aptr的内存:ap->bptr.reset() 和 bp->aptr.reset()
★解决:将上述代码的 把A和B任何一个成员变量改为weak_ptr 就可以:
在这里插入图片描述
★★★解决过程:在对A的成员赋值时,即执行ap->bptr=bp;时,由于bptr是weak_ptr,它并【不会增加引用计数】,所以bp的引用计数仍然会是1,在离开作用域之后,bp的引用计数为减为0,B指针会被析构,析构后其内部的aptr的引用计数会被减为1,然后在离开作用域后ap引用计数又从1减为0,A对象也被析构,不会发生内存泄漏

注意:
1、野指针:定义指针时不赋初值,该指针叫野指针。强烈建议:初始化所有指针/尽量等定义了变量之后再定义指向它的指针
2、若重复定义了多个智能指针,则★★★后面定义的先析构:
在这里插入图片描述

(2)std::unique_ptr:独占的智能指针

★独占性:unique_ptr类型的智能指针自己【独占】一个对象,不想给别人用这个对象,又想能够自动释放这个对象

1、创建unique_ptr类型的智能指针my_ptr:
在这里插入图片描述
★还可以用make_unique来构造智能指针:
std::make_shared是c++11的一部分,但std::make_unique不是,它是在c++14里加入标准库的:
在这里插入图片描述
使用new的版本重复了被创建对象的键入,但是make_unique函数则没有。重复类型违背了软件工程的一个重要原则:应该避免代码重复,代码中的重复会引起编译次数增加,导致目标代码膨胀

2、判断my_ptr是否为空:此时my_ptr肯定不为空,所以不进入if中:
在这里插入图片描述

3、如果创建另外一个unique_ptr类型的智能指针my_ptr2,令其等于my_ptr,则会报错:
在这里插入图片描述
注意:不允许通过赋值将一个unique_ptr赋值给另一个unique_ptr(因为独占性,unique_ptr自己独占一个对象,不想给别人用这个对象):

4、将my_ptr指针的资源移给my_other_ptr,此时my_ptr为空了,因为资源被移给my_other_ptr了:
★move语义:把别人的资源移走,移动到自己里面
在这里插入图片描述
★unique_ptr不允许复制,但可以通过函数返回给其他的unique_ptr,还可以通过std::move来转移到其他的unique_ptr,这样它本身就不再拥有原来指针的所有权了

5、判断my_ptr是否为空:此时my_ptr为空,因为上面my_ptr的资源被移走了,所以my_ptr为空:
在这里插入图片描述

6、判断my_other_ptr是否为空:因为my_other_ptr得到了my_ptr的资源,所以my_other_ptr不为空:
在这里插入图片描述

①unique_ptr和shared_ptr的区别:

1、unique_ptr具有独占性
2、unique_ptr可以指向一个数组,但是shared_ptr不行:
①用unique_ptr指向一个数组:
在这里插入图片描述
②用shared_ptr指向一个数组(需要指定删除器):
当我们用shared_ptr管理动态数组时,需要指定删除器,因为shared_ptr的默认删除器不支持数组对象:
在这里插入图片描述
3、unique_ptr指定删除器和shared_ptr有区别:【unique_ptr需要确定删除器的类型】,所以不能像shared_ptr那样直接指定删除器:
在这里插入图片描述
在这里插入图片描述
4、使用场景不同:关于shared_ptr和unique_ptr的使用场景是要根据实际应用需求来选择:
如果希望只有一个智能指针管理资源或者管理数组就用unique_ptr
如果希望多个智能指针管理同一个资源就用shared_ptr

(3)std::weak_ptr:弱引用的智能指针

share_ptr虽然已经很好用了,但是有一点share_ptr智能指针还是有内存泄露的情况,当两个对象相互使用一个shared_ptr成员变量指向对方,会造成循环引用,使引用计数失效,从而导致内存泄漏(见上述shared_ptr的循环引用)

weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的shared_ptr,weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构【不会引起引用计数的增加或减少】

weak_ptr是用来【解决shared_ptr相互引用时的死锁问题】,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数;他和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr
总结:
1、weak_ptr是用来解决shared_ptr相互引用时的死锁问题
2、weak::ptr:不控制对象的生命周期,它的构造和析构不会引起引用记数的增加或减少(见下图):
在这里插入图片描述
有一个shared_ptr类型的智能指针指向new A这个对象,有两个weak_ptr类型的智能指针指向new A这个对象,但是weak_ptr类型的智能指针的构造和析构不会引起引用记数的增加或减少,因此输出的ap.use_count()为1

★weak_ptr指针【不能直接操作对象的成员、方法】,需要先wp.lock()获取shared_ptr:
★lock具体就是利用weak_ptr获取shared_ptr的:
在这里插入图片描述
weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,主要是为了通过shared_ptr获得资源的监测权,它的构造不会增加引用计数,它的析构也不会减少引用计数,【纯粹只是作为一个旁观者来监视shared_ptr中管理的资源是否存在】。weak_ptr还可以返回this指针和解决循环引用的问题
★★★weak_ptr本质上只是用来监视shared_ptr

①weak_ptr的基本用法

1、通过use_count()方法获取当前观察资源的引用计数:
在这里插入图片描述

2、通过expired()方法判断所观察资源是否已经释放:(expired:到期)
在这里插入图片描述

3、通过lock方法获取监视的shared_ptr:
在这里插入图片描述
★{ }表示作用域,作用域之外智能指针已释放掉

②weak_ptr返回this指针

shared_ptr章节中提到不能直接将this指针返回shared_ptr,需要通过派生std::enable_shared_from_this类,并通过其方法shared_from_this来返回指针
★★★原因(底层原理):std::enable_shared_from_this类中有一个weak_ptr,这个weak_ptr用来观察this智能指针;调用shared_from_this()方法是,会【调用内部这个weak_ptr的lock()方法,将所观察的shared_ptr返回】
在外面创建A对象的智能指针和通过对象返回this的智能指针都是安全的,因为shared_from_this()是内部的weak_ptr调用lock()方法之后返回的智能指针,在离开作用域之后,spy的引用计数减为0,A对象会被析构,不会出现A对象被析构两次的问题
在这里插入图片描述
输出结果:Deconstruction A
★注意:【获取自身智能指针的函数仅在shared_ptr的构造函数被调用之后才能使用】,因为:enable_shared_from_this内部的weak_ptr只有通过shared_ptr才能构造(因为如果shared_ptr没有被构造的话,weak_ptr没有监听的对象,自然不会被构造)

③weak_ptr使用注意事项

1、weak_ptr在使用前需要检查合法性:
在这里插入图片描述
因为上述代码中sp和sp_ok离开了作用域,其容纳的K对象已经被释放了,因此得到了一个容纳NULL指针的sp_null对象。
★在使用wp前需要调用wp.expired()函数判断一下,因为wp还仍旧存在,虽然引用计数等于0,仍有某处“全局”性的存储块保存着这个计数信息,直到最后一个weak_ptr对象被析构,这块“堆”存储块才能被回收。否则weak_ptr无法知道自己所容纳的那个指针资源的当前状态
此时判断wp.expired(),得到“weak_ptr无效,shared_ptr资源已释放“:
在这里插入图片描述

如果shared_ptr sp_ok和weak_ptr wp属于同一个作用域呢?如下所示:
在这里插入图片描述
此时判断wp.expired(),得到“weak_ptr还有效,shared_ptr资源没有释放“,因为wp和sp_ok属于同一个作用域

(4)智能指针安全性问题

★面试高频问题:shared_ptr是不是线程安全的?
答:
1、多线程情况下引用计数是安全的:多个线程来的时候,引用计数是安全的,即赋值几次,引用计数就加几次
2、★★★智能指针是否安全需要结合实际使用分情况讨论:
用 传地址 / 引用 的方式,智能指针是不安全的;传值的方式,智能指针本身是安全的,但是对于数据的操作可能不安全
①情况一:不同线程共用一个智能指针是不安全的:例如十个线程都指向同一个sp1,则不安全(★说白了:通过传地址的方式去传智能指针是不安全的):
举例:比如std::thread的回调函数,是一个lambda表达式,其中引用捕获了一个shared_ptr:
在这里插入图片描述
又或者通过回调函数的参数传入的shared_ptr对象,参数类型引用:
在这里插入图片描述

②情况二:多线程代码操作的不是同一个shared_ptr的对象:
这里指的是管理的数据是同一份,而shared_ptr不是同一个对象:
举例1:
sp1 = new(视频1);
sp1_1 = sp1;
sp1_2 = sp1;

sp1_50 = sp1
这样的话,多线程是安全的;但若传的都是sp1的地址,则多线程是不安全的
因此,★最好每个sp在自己的作用域中起作用就好了,退出去即释放,不要用传地址的方式,用传值的方式即可:
在这里插入图片描述
举例2:多线程回调的lambda的是按值捕获的对象(注意是按值捕获,不是按地址捕获!):
在这里插入图片描述
另个线程传递的shared_ptr是值传递,而非引用:
在这里插入图片描述
这时候每个线程内看到的sp,他们所【管理的是同一份数据】,用的是同一个引用计数,但是各自是不同的对象,当发生多线程中修改sp指向的操作的时候,是不会出现非预期的异常行为的,也就是说,如下操作是安全的:
在这里插入图片描述

③情况三:所管理数据的线程安全性问题:
所管理的对象必然不是线程安全的,必然 sp1、sp2、sp3智能指针实际都是指向对象A, 三个线程同时操作对象A,★对象的数据安全必然是需要对象A自己去保证
线程不安全:比如如果某个队列拿到数据以后瞎搞,把new(视频1)这个对象给清空了,则其他的都会产生问题:
在这里插入图片描述
即:多个智能指针指向同一个对象,如果涉及到读写,读写肯定要对象自己保证

2、右值引用 和 移动语义(move)

(1)右值引用

C++11中添加了右值引用和移动语义,

★右值引用和移动语义作用:可以避免无谓的复制,提高程序性能

C++11中的所有的值分为左值、将亡值和纯右值,将亡值和纯右值属于右值
★左值是表达式结束后仍然存在的持久对象;★右值是指表达式结束时就不存在的临时对象
区分左值和右值的便捷方法是【看能不能对表达式取地址】,如果能则为左值,否则为右值
1、左值 lvalue 是有标识符、可以取地址的表达式,最常见的左值有:
①变量、函数或数据成员的名字
②【返回左值引用的表达式】,如 ++x、x = 1、cout << ’ ’
③字符串字面量如 “hello world”
PS:字符串字面量(stringliteral)是指:双引号引住的一系列字符
2、纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为“临时对象”。最常见的纯右值有:
①【返回非引用类型的表达式】,如 x++、x + 1、make_shared(42)
②除字符串字面量之外的字面量,如 42、true
3、将亡值是C++11新增的、与右值引用相关的表达式,比如:将要被移动的对象、T&&函数返回的值、std::move返回值和转换成T&&的类型的转换函数返回值

①&&的特性

右值引用就是对一个右值进行引用的类型。因为右值没有名字,所以我们只能通过引用的方式找到它。无论声明左值引用还是右值引用都必须立即初始化,因为【引用类型本身并不拥有所绑定对象的内存】,只是该对象的一个别名

通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样,只要该变量还活着,该右值临时量将会一直存活下去

★右值引用有两个&&,&&不代表就是右值,也可以是左值(根据具体传值来判断);若只有一个&,则一定是左值

&& 的总结:
1、左值和右值是独立于它们的类型的,右值引用类型可能是左值也可能是右值
2、auto&& 或函数参数类型自动推导的 T&& 是一个未定的引用类型,被称为 universal references,它可能是左值引用也可能是右值引用类型,取决于初始化的值类型
3、所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当 T&& 为模板参数时,输入左值,它会变成左值引用,而输入右值时则变为具名的右值引用
4、编译器会将已命名的右值引用视为左值,而将未命名的右值引用视为右值

②右值引用优化性能,避免深拷贝

对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除:
在这里插入图片描述
打印
constructor A
constructor A
ready return
destructor A, m_ptr:0xf87af8
destructor A, m_ptr:0xf87ae8
destructor A, m_ptr:0xf87af8
main finish
在上面的代码中,默认构造函数是浅拷贝,main函数的 ★★★a 和Get函数的 b 会指向同一个指针 m_ptr,在析构的时候会导致重复删除该指针。正确的做法是提供深拷贝的拷贝构造函数,★★★添加深拷贝构造函数(其他不变):
在这里插入图片描述
运行结果
constructor A
constructor A
ready return
copy constructor A
destructor A, m_ptr:0xea7af8
destructor A, m_ptr:0xea7ae8
destructor A, m_ptr:0xea7b08
main finish
这样就可以保证拷贝构造时的安全性,但有时这种拷贝构造却是不必要的,比如上面代码中的拷贝构造就是不必要的,上面代码中的 Get 函数会返回临时变量,然后通过这个临时变量拷贝构造了一个新的对象 b,临时变量在拷贝构造完成之后就销毁了,如果堆内存很大,那么,这个拷贝构造的代价会很大,带来了额外的性能损耗。有没有办法避免临时对象的拷贝构造呢?答案是肯定的。添加下面的代码:
在这里插入图片描述
运行结果
constructor A
constructor A
ready return
move constructor A
destructor A, m_ptr:0
destructor A, m_ptr:0xfa7ae8
destructor A, m_ptr:0xfa7af8
main finish

上面的代码中没有了拷贝构造,取而代之的是移动构造(Move Construct)。从移动构造函数的实现中可以看到,它的参数是一个右值引用类型的参数 A&&,这里没有深拷贝,只有浅拷贝,这样就避免了对临时对象的深拷贝,提高了性能。这里的 A&& 用来根据参数是左值还是右值来建立分支,如果是临时值(右值),则会选择移动构造函数。
★★★移动构造函数只是将临时对象的资源做了浅拷贝,不需要对其进行深拷贝,从而避免了额外的拷贝,提高性能。这也就是所谓的移动语义(move 语义),右值引用的一个重要目的是用来【支持移动语义】的
★★★移动语义作用:可以将资源(堆、系统对象等)通过浅拷贝方式从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,可以大幅度提高 C++ 应用程序的性能,消除临时对象的维护(创建和销毁)对性能的影响

举例:
在这里插入图片描述
在这里插入图片描述

实现了调用拷贝构造函数的操作和拷贝赋值操作符的操作:
MyString(“Hello”) 和 MyString(“World”) 都是临时对象,也就是右值,虽然它们是临时的,但程序仍然调用了拷贝构造和拷贝赋值,造成了没有意义的资源申请和释放的操作
移动语义的目的:如果能够【★★★直接使用临时对象已经申请的资源】,既能节省资源,又能【节省资源申请和释放的时间】
解决:★★★用c++11的右值引用来定义这两个函数:
在这里插入图片描述
★有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计【右值引用的拷贝构造函数和赋值函数】,以提高应用程序的效率

(2)移动语义(move)

★move:拿别人的代码,把别人置空
★move:将左值变为右值

移动语义是通过【右值引用来匹配临时值】的,那么,普通的左值是否也能借移动语义来优化性能呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义
★move是将对象的状态或者所有权从一个对象转移到另一个对象,【★★★只是转义,没有内存拷贝】:
在这里插入图片描述

代码:
在这里插入图片描述
在这里插入图片描述
1、a = MyString(“Hello”); 传入的是右值,因此进入这个构造函数,输出move。。。:
在这里插入图片描述
2、MyString b = a; =赋值函数传入的是左值,因此要进入这个=赋值函数,输出copy。。。:
在这里插入图片描述
3、MyString c = std::move(a),move移动语义将左值变为右值,不需要创建额外内存,因此要进入这个构造函数,输出move。。。:
在这里插入图片描述

3、forward完美转发

forward 完美转发实现了参数在传递过程中 保持其值属性的功能,即若是左值,则传递之后仍然是左值,若是右值,则传递之后仍然是右值

★move:将左值变为右值
★forward分为两种:出现&&,要【根据具体传值来判断是左值还是右值】(T &&t)
①把左值变右值
②原来是什么值就是什么值

现存在一个函数:
在这里插入图片描述
这种引用类型既可以对左值引用,亦可以对右值引用(因为&&既可以是左值引用,也可以是右值引用);但要注意,★★★引用以后,这个val值它本质上是一个左值!

在这里插入图片描述
注意这里,★★★a是一个右值引用,但其本身a也有内存名字,所以a本身是一个左值,因此,再【用右值引用来引用左值a是不对的】
因此我们有了std::forward()完美转发,这种T &&val中的val是左值,但【★★★如果我们用std::forward (val),就会【按照参数原来的类型转发】★★★】:
在这里插入图片描述
这样是正确的

代码:
在这里插入图片描述
在这里插入图片描述
解释:
1、func(1) :1本身是右值
①1是右值,传入void func(T &&t)后,被引用以后,这个t值它本质上是一个左值,因此Print(t)输出“L“
②std::move(t)将左值转化为右值,因此Print(std::move(t))输出“R“
③1原本是右值,std::forward会按参数原来的类型转发,因此它还是一个右值,因此Print(std::forward(t))输出“R“
2、func(x) :x本身是左值
①与上面类似,传入void func(T &&t)后,被引用以后,这个t值它本质上是一个左值,因此Print(t)输出“L“
②std::move(t)将左值转化为右值,因此Print(std::move(t))输出“R“
③x原本是左值,std::forward会按参数原来的类型转发,因此它还是一个左值,因此Print(std::forward(t))输出“L“
3、func(std::forward(y)) :
①与上面类似,传入void func(T &&t)后,被引用以后,这个t值它本质上是一个左值,因此Print(t)输出“L“
②std::move(t)将左值转化为右值,因此Print(std::move(t))输出“R“
③【传入参数的时候,std::forward()将左值转变为右值】,因此std::forward(y)是右值,std::forward会按参数原来的类型转发,因此它还是一个右值,因此Print(std::forward(t))输出“R“

在这里插入图片描述
下面是移动构造函数:可以将a.m_ptr直接移给成员变量m_ptr(浅拷贝),然后将a.m_ptr置空,即:将栈上对象的指针对象赋值到原来对象里面去,把原来栈上对象对应的指针置空(拿取别人的资源,且将别人置空)
【★★★注意一定要置空,否则会重复析构】,所以【析构函数中也要加一层判断,若指针非空,则释放资源】:
在这里插入图片描述

4、emplace_back 减少内存拷贝和移动

emplace_back是C++11的stl新引入的接口
PS:无论是新容器还是就容器,尽量用新接口

★emplace_back的本质:【就地构造,不用构造后再次复制到容器中】。因此效率更高

★emplace_back的作用:减少内存拷贝和移动,是右值引用的方式

emplace_back的速度更快:
因此如果以后有vector什么的,如果有emplace_back则直接用,不用push_back,因为速度更快;相当于push_back已经弃用了,用emplace_back即可

将一个string对象添加到testVec中:
在这里插入图片描述
底层实现:
①string(16, ‘a’)会创建一个string类型的临时对象,这涉及到一次string构造过程
②vector内会创建一个新的string对象,这是第二次构造
③push_back结束时,最开始的临时对象会被析构
这两行代码会涉及到两次string构造和一次析构
★若用emplace_back,底层实现:直接在vector中构建一个对象,而非创建一个临时对象、再放进vector、再销毁
emplace_back可以省略对临时对象的一次构造和一次析构,从而达到优化的目的

调用左值引用的push_back,且将会调用一次string的拷贝构造函数,比较耗时,这里的string还算很短的,如果很长的话,差异会更大

拷贝构造函数:
在这里插入图片描述

移动构造函数:
在这里插入图片描述

5、unordered container 无序容器

C++11 增加了无序容器 unordered_map/unordered_multimap 和
unordered_set/unordered_multiset,由于这些容器中的元素是不排序的,因此比有序容器
map/multimap 和 set/multiset 效率更高:
★★★map 和 set 内部是红黑树,在插入元素时会自动排序
★★★无序容器内部是散列表(Hash Table),通过hash而不是排序来快速操作元素,使得效率更高。由于无序容器内部是散列表,因此无序容器的 key 需要提供 hash_value 函数,其他用法和map/set 的用法是一样的。不过对于自定义的 key,需要提供 Hash 函数和比较函数

★unordered_map和map的区别:
1、内部实现机理不同:
①map:map内部实现了一个红黑树,红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作
②unordered_map: unordered_map内部实现了一个散列表(hash + 数组),通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的
2、优缺点以及适用场景不同:
(1)map:
①优点:
a. 有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
b. 红黑树,内部实现一个红黑树使得map的很多操作在logn的时间复杂度下就可以实现,效率非常高
②缺点:
空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
③适用场景:
对于那些有顺序要求的问题,用map会更高效一些
(2)unordered_map:
①优点:因为内部实现了哈希表,因此其查找速度非常的快O(1)
②缺点:哈希表的建立比较耗费时间
③适用场景:对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑使用unordered_map

总结:
1、性能方面:
插入:红黑树 > hash(hash之所以插入慢是因为可能存在hash碰撞的问题)
查找:hash O(1) > 红黑树 O(logn)
2、unorder_map占用的内存要高,但是unordered_map执行效率要比map高很多
3、对于unordered_map或unordered_set容器,其遍历顺序与创建该容器时输入的顺序不一定相同,因为遍历是按照哈希表从前往后依次遍历的

总结:
1、C++11 在性能上做了很大的改进,最大程度减少了内存移动和复制,通过右值引用、 forward、emplace 和一些无序容器我们可以大幅度改进程序性能
2、
右值引用仅仅是通过【改变资源的所有者】来避免内存的拷贝,能大幅度提高性能。
forward 能根据参数的实际类型转发给正确的函数
emplace 系列函数通过【直接构造对象】的方式避免了内存的拷贝和移动。
无序容器在插入元素时不排序,提高了插入效率,不过对于自定义 key 时需要提供 hash 函数和比较函数

6、匿名函数lambda

★基本语法:
在这里插入图片描述

在这里插入图片描述
一般情况下,编译器可以【自动推断出lambda表达式的返回类型】,所以【可以不指定返回类型】,省一些代码(尽量手动去写,避免不小心传入的参数的数据类型不同)
★但是如果【函数体内有多个return语句时】,编译器无法自动推断出返回类型,此时必须指定返回类型

需要【在匿名函数内使用外部变量】,要【用捕获列表来传参】:
1、捕获列表是可以传参数的,比如要在lambda表达式中打印d,要将参数d传入到捕获列表中,否则会报错(c同理);传入多个参数的话用逗号隔开即可:
在这里插入图片描述
2、直接传入的话【不能修改捕获列表中的变量的值】:(d传入为30,修改了d的值无效,所以d输出还是30)
在这里插入图片描述
3、★要想修改变量,必须【添加取地址符&(引用符号)】,否则如果【值传递的话无法通过编译】:(d虽然是以30传入的,但是打印输出的是20,因为将d修改为20了)
在这里插入图片描述

补充知识:

  1. 如果捕获列表为[&],则表示所有的外部变量都按引用传递给lambda使用;
  2. 如果捕获列表为[=],则表示所有的外部变量都按值传递给lambda使用;
  3. 匿名函数构建的时候对于按值传递的捕获列表,会【立即将当前可以取到的值拷贝一份作为常数】,然后将该常数作为参数传递

匿名函数的简写:
匿名函数由捕获列表、参数列表、返回类型和函数体(4部分)组成;可以忽略参数列表和返回类型,但不可以忽略捕获列表和函数体,如:
在这里插入图片描述

lambda捕获列表:
在这里插入图片描述

7、C++11 标准库(STL)

STL定义了强大的、基于模板的、可复用的组件,实现了许多通用的数据结构及处理这些数据结构的算法
STL包含三个关键组件:
①容器(container,流行的模板数据结构):用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等
②迭代器(iterator):迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集
③算法(algorithm):提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作

(1)STL容器的分类

STL容器,可将其分为四类:
①序列容器:描述了线性的数据结构(也就是说,其中的元素在概念上” 排成一行"), 例如数组、向量和 链表
②有序关联容器
③无序关联容器
关联容器描述非线性的容器,它们通常可以快速锁定其中的元素。这种容器可以存储值的 集合 或者 键-值对
④容器适配器:栈和队列都是在序列容器的基础上加以约束条件得到的,因此STL把stack和queue作为容器适配器来实现,这样就可以使程序以一种约束方式来处理线性容器

①序列容器:
在这里插入图片描述
②有序关联容器(键按顺序保存):
在这里插入图片描述
③无序关联容器:
在这里插入图片描述
④容器适配器:
在这里插入图片描述

(2)迭代器

迭代器在很多方面与指针类似;
迭代器存有它们所指的特定容器的状态信息,即迭代器对每种类型的容器都有一个实现。

★it.end()返回一个指向容器中【最后一个元素的下一个元素】的迭代器(这个元素并不存在,常用于【判断是否到达了容器的结束位】),【end()只在相等或不等的比较中使用】

★使用一个 iterator 对象来指向一个可以修改的容器元素,使用一个 const_iterator 对象来【指向一个不能修改的容器元素】

在这里插入图片描述

1、每种容器所支持的迭代器类型决定了这种容器是否可以在指定的 STL 算法中使用:
2、【支持随机访问迭代器的容器可用于所有的 STL 算法】(除了那些需要改变容器大小的算法,这样的算法不能在数组和 array对象中使用)
3、【指向数组的指针】可以【代替迭代器】用于几乎所有的 STL 算法中,包括那些要求随机访问迭代器的算法。下表显示了每种 STL 容器所支持的迭代器类型:
在这里插入图片描述

下表显示了在 STL容器的类定义中出现的几种预定义的迭代器 typedef。不是每种 typedef 都出现在每个容器中
我们使用常量版本(加const)的迭代器来【访问只读容器或不应该被更改的非只读容器】,使用反向迭代器(加reverse)来以相反的方向访问容器

下表显示了可作用在每种迭代器上的操作。除了给出的对于所有迭代器都有的运算符,迭代器还必须提供默认构造函数、拷贝构造函数和拷贝赋值操作符。 前向迭代器支持++和所有的输入和输出迭代器的功能。双向迭代器支持–操作和前向迭代器的功能。随机访问迭代器支持所有在表中给出的操作。另外,对于输入迭代器和输出迭代器,不能在保存迭代器之后再使用保存的值:
在这里插入图片描述

(3)算法

STL包含了大约70个标准算法,表格中提供了这些算法的实例及概述
还可以使用相似的方法创建自己的算法,这样它们就能和STL容器及迭代器一起使用了

STL使用范例:
C++ 参考手册 https://zh.cppreference.com/w/cpp
在这里插入图片描述

(4)正则表达式

具体范例见代码:
https://zh.cppreference.com/w/cpp/regex

8、感谢大家的观看,我是COSMIC

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值