C++八股进阶
之前那个只是总结了一下常考点,这个是纯手打记笔记加深理解
这里写目录标题
- C++的四种智能指针
- 为什么要使用智能指针?
- 四种智能指针:
- C++中的内存分配情况
- C++中的指针参数传递和引用参数传递
- C++ 中 const 和 static 关键字(定义,⽤途)
- static:控制变量的存储方式和可见性。
- const含义及实现机制
- C++ 中重载重写的区别
- 重载(overload)
- 重写(override)
- 指针和引用的区别
- 野指针和空指针的区别
- 函数传递参数的⼏种⽅式
- new / delete ,malloc / free 区别
- 面向对象的三大特性,并举例说明
- 多态的实现
- 虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
- 编译器处理虚函数表应该如何处理
- 析构函数一般协程虚函数的原因
- 构造函数为什么一般不定义为虚函数
- 析构函数的作用,如何起作用
- 构造函数的执行顺序?析构函数的执行顺序
- 构造函数顺序
- 析构函数顺序
- 纯虚函数(应用于接口继承和实现继承)
- 深拷贝和浅拷贝的区别
- 什么情况下会调用拷贝构造函数
- 为什么拷⻉构造函数必需时引⽤传递,不能是值传递?
- 内存泄露的定义,如何检测和避免
- 说⼀下红⿊树(RB-tree)
- Lambda表达式
- 右值引用
- 智能指针
- final和override
C++的四种智能指针
为什么要使用智能指针?
智能指针的作用是管理一个指针,避免程序员申请的空间在函数结束时忘记释放,造成内存泄露这种情况的发生
使用智能指针可以很大程度的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源,所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间
四种智能指针:
auto_ptr(C11已抛弃):采用所有权模式,存在潜在的内存崩溃问题
auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错.
unique_ptr(替换auto_ptr):实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象,对于避免资源泄露特别有用
unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错
所有权模式,但比auto_ptr更加安全
shared_ptr(共享型,强引用)
实现共享式拥有概念,多个智能指针可以指向相同的对象,该对象和其相关资源会在最后一个引用被销毁时释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享
可以通过成员函数use_count()来查看资源的所有者个数,除了可以通过new来构造,还可以通过传入auto_ptr,unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放
share_ptr是为了解决auto_ptr在对象所有权上的局限性,在使用引用技术的机制上提供了可以共享所有权的智能指针
weak_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就可以。
C++中的内存分配情况
栈:由编译器分配管理和回收,存放局部变量和函数参数
堆:由程序员管理,需要手动new malloc delete free 进行分配和回收,空间较大,但可能会出现内存泄露和空闲碎片的情况
全局/静态存储区:分为初始化和未初始化两个相邻区域,存储初始化和未初始化的全局变量和静态变量
常量存储区:储存常量,一般不允许修改
代码区:存放程序的二进制代码
C++中的指针参数传递和引用参数传递
指针参数传递本质上是值传递:他所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,会在栈中开辟内存空间以存放由主调函数传递进来的参数值,从而形成了实参的一个副本。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变量进行的,不会影响主调函数的实参变量的值
引用参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调参数中的实参变量。因此,被调函数对形参的任何操作都会影响主调函数的实参变量
引⽤传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变量,但是对于任何引用参数的处理都会通过一个间接寻址的方式操作到主调函数的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将应用不到主调函数的相关变量,如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针或者指针引用
C++ 中 const 和 static 关键字(定义,⽤途)
static:控制变量的存储方式和可见性。
- 修饰局部变量:一般情况下,对于局部变量在程序中是存放在栈区的,并且局部的生命周期在包含语句块执行结束时便结束了。但是如果用static关键词修饰的话,该变量便会存放在静态数据区,其生命周期会一直延续到整个程序执行结束,但是要注意的时,虽然用static对全局变量进行修饰之后,其生命周期以及存储空间发生了变换,但其作用域并没有改变,作用域还是限制在其语句块
- 修饰全局变量:对于一个全局变量,它既可以在本文件中被访问到,也可以在同一个工程中其他源文件被访问。用static对全局变量进行修饰改变了其作用域范围,由原来的整个工程可见变成了本文件可见
- 修饰函数:和全局变量类似,也是改变了函数的作用域
- 修饰类:如果C++中对类中的某个函数用static修饰,则表示该函数属于一个类而不是属于此类的任何特定对象,如果对类中的某个变量进行static修饰,则表示该变量在存储空间中只存在一个副本,可以通过类和对象去调用
- 作用五:类成员/类函数声明 static
- 函数体内 static 变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
- 在模块内的 static 全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;
- 在模块内的 static 函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
- 在类中的 static 成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;
- 在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的 static 成员变量
- static 类对象必须要在类外进行初始化,static 修饰的变量先于对象存在,所以 static 修饰的变量要在类外初始化;
- 由于 static 修饰的类成员属于类,不属于对象,因此 static类成员函数是没有 this 指针,this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问非 static 的类成员,只能访问 static修饰的类成员;
- static 成员函数不能被 vrtual 修饰,static成员不属于任何对象或实例,所以加上 vrtual 没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每一个对象分配一个 vptr 指针,而 vptr 是通过 this 指针调用的,所以不能为 virtual;虚函数的调用关系,this->vptr->ctable->virtual function。
const含义及实现机制
- const 修饰基本类型数据类型:基本数据类型,修饰符 const 可以用在类型说明符前,也可以用在类型说明符后其结果是一样的。在使用这些常量的时候,只要不改变这些常量的值即可。
- const 修饰指针变量和引用变量:如果 const 位于小星星的左侧、则 const 就是用来修饰指针所指向的变量,即指
针指向为常量;如果 const 位于小星星的右侧,则 const 就是修饰指针本身,即指针本身是常量。 - const 应用到函数中:作为参数的 const 修饰符:调用函数的时候,用相应的变量初始化,const 常量,则在函数体中,按照 const 所修饰的部分进行常量化,保护了原对象的属性。[注意:参数 const 通常用于参数为指针或引用的情况: 作为函数返回值的 const 修饰符:声明了返回值后,const按照"修饰原则"进行修饰,起到相应的保护作用。
- const 在类中的用法:const 成员变量,只在某个对象生命周期内是常量,而对于整个类而言是可以改变的。因为类可以创建多个对象,不同的对象其 const数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const 数据成员的值是什么。const 数据成员的初始化只能在类的构造函数的初始化列表中进行。const 成员函数:const 成员函数的主要目的是防止成员函数修改对象的内容。要注意,const 关键字和 static 关键字对于成员函数来说是不能同时使用的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数又必须具体到某一个函数。
- const 修饰类对象,定义常量对象:常量对象只能调用常量函数,别的成员函数都不能调用,
补充:const 成员函数中如果实在想修改某个变量,可以使用 mutable 进行修饰。成员变量中如果想建立在整个类中都恒定的常量,应该用类中的枚举常量来实现或者 static const。
C++ 中重载重写的区别
重载(overload)
是指同一可访问区内被声明的几个具有不同参数列表的同名函数,依赖于C++函数名字的修饰会将参数加在后面,可以是参数类型,个数,顺序的不同,根据参数列表决定调用哪个函数,重载不关心函数的返回类型
重写(override)
派生类中重新定义父类中除了函数体外完全相同的虚函数,注意被重写的函数不能是static的,一定要是虚函数,且其他一定要完全相同。要注意一,重写和被重写的函数是在不同的类当中的,重写函数的访问修饰符是可以不同的,尽管virtual中是private的,派生类中重写可以改为public
指针和引用的区别
指针和引用都是一种内存地址的概念,区别是指针是一个实体,引用只是一个别名
指针指向一块内存,指针的内容是所指向的内存的地址,在编译的时候,则是将“指针变量名-指针变量的地址”添加到符号表中,所以说,指针包含的内容是可以改变的,允许拷贝和赋值,有const和非const区别,甚至可以为空,sizeof得到的是指针类型的大小
而对于引用来说,它只是一块内存的别名,在添加到符号表的时候,是将“引用变量名-引用对象的地址”添加到符号表中,符号表一经完成不能改变,所以引用必须而且只能在定义时被绑定到一块内存上,后续不能更改,也不能为空,也没有const和非const的区别
sizeof引用得到代表对象的大小。而sizeof指针得到的是指针本身的大小。另外在参数传递中指针需要被解引用后才可以对对象进行操作,而直接对引用进行的修改会直接作用到引用对象上
作为参数时也不同,传指针的实质是传值,传递的值是指针的地址;传引用的是指是传地址,传递的变量的地址
野指针和空指针的区别
野指针(wild pointer):就是没有被初始化过的指针。用 gcc -wa11 编译,会出现 used uninitialized 警告
空指针:是指针最初指向的内存已经被释放了的一种指针。无论是野指针还是空指针,都是指向无效内存区域(这里的无效指的是"不安全不可控")的指针。 访问"不安全可控"(invalid)的内存区域将导致"Undefined Behavior"。
如何避免使用野指针?在平时的编码中,养成在定义指针后且在使用之前完成初始化的习惯或者使用智能指针。
函数传递参数的⼏种⽅式
值传递:形参是实参的拷⻉,函数内部对形参的操作并不会影响到外部的实参。
指针传递:也是值传递的⼀种⽅式,形参是指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进⾏操作。
引⽤传递:实际上就是把引⽤对象的地址放在了开辟的栈空间中,函数内部对形参的任何操作可以直接映射到外部的实参上⾯
new / delete ,malloc / free 区别
都可以用来在堆上分配和回收空间,new/delete是操作符。malloc/free是库函数
执行new实际上执行两个过程:1.分配未初始化的内存空间(malloc);2.使用对象的构造函数对空间进行初始化,返回空间的首地址。如果在第一部分配空间中出现问题,则抛出std::bad_alloc异常,或被某个设定的异常处理函数捕获处理;如果在第二部构造对象是出现异常,则自动调用delete释放内存
执⾏ delete 实际上也有两个过程:1. 使⽤析构函数对对象进⾏析构;2.回收内存空间(free)。
以上也可以看出 new 和 malloc 的区别,new 得到的是经过初始化的空间,⽽ malloc 得到的是未初始化的空间。
所以 new 是 new ⼀个类型,⽽ malloc 则是malloc ⼀个字节⻓度的空间。delete 和 free 同理,delete 不仅释放
空间还析构对象,delete ⼀个类型,free ⼀个字节⻓度的空间。
为什么有了 malloc/free 还需要 new/delete?因为对于⾮内部数据类型⽽⾔,光⽤ malloc/free ⽆法满⾜动
态对象的要求。对象在创建的同时需要⾃动执⾏构造函数,对象在消亡以前要⾃动执⾏析构函数。由于 malloc/
free 是库函数⽽不是运算符,不在编译器控制权限之内,不能够把执⾏的构造函数和析构函数的任务强加于
malloc/free,所以有了 new/delete 操作符。
面向对象的三大特性,并举例说明
C++面向对象的三大特征是:封装,继承,多态
封装就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让信任的类或者对象操作,对不可信的进行信息隐藏,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分以外的改变或错误的使用了对象的私有部分
继承是指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:他可以使用现有的类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展。通过继承创建的新类成为子类或派生类,被继承的类称为基类,父类或超类。继承的过程,就是从一般到特殊的过程,要实现继承,可以通过继承和组合来实现
实现方式:
实现继承:实现继承是指直接使用基类的属性和方法而无需额外编码的能力
接口继承:接口继承是指仅使用属性和方法的名称,但是子类必须提供实现的能力
多态就是像不同的对象发送同一个消息,不同对象在接收时会产生不同的行为(即方法)。即一个接口,可以实现多种方法
多态与非多态的实质区别就是函数地址是早绑定还是晚绑定的。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并产生代码,则是静态的,即地址早绑定。而如果函数调用的地址不能在编译期间确定,需要在运行时才确定,这就属于晚绑定
多态的实现
多态其实⼀般就是指继承加虚函数实现的多态,对于多态其实一般就是指继承加虚函数实现的多态,对于重载来说,实际上基于的原理是,编译器为函数生成符号表时的不同规则,重载只是一种语言特性,与多态无关,与面向对象也无关,但这又是C++中增加的新规则,所以也算属于 C++,所以如果非要说重载算是多态的一种,那就可以说:多态可以分为静态多态和动态多态。
静态多态其实就是重载,因为静态多态是指在编译时期就决定了调用哪个函数,根据参数列表来决定;动态多态是指通过子类重写父类的虚函数来实现的,因为是在运行期间决定调用的函数,所以称为动态多态
一般情况下我们不区分这两个时所说的多态就是指动态多态。
动态多态的实现与虚函数表,虚函数指针相关。
虚函数相关(虚函数表,虚函数指针),虚函数的实现原理
C++中多态的表象,在基类的函数前加上 virtual 关键字,在派⽣类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型时派生类,就调用派生类的函数,如果是基类,就调用基类的函数。
实际上,当一个类中包含虚函数时,编译器会为该类生成一个虚函数表,保存该类中虚函数的地址,同样,派生类继承基类,派生类中自然一定有虚函数,所以编译器也会为派生类生成自己的虚函数表。当我们定义一个派生类对象时,编译器检测该类型有虚函数,所以为这个派生类生成一个虚函数指针,指向该类型的虚函数表,这个虚函数指针的初始化是在构造函数中完成的
后续如果有⼀个基类类型的指针,指向派⽣类,那么当调⽤虚函数时,就会根据所指真正对象的虚函数表指针去寻
找虚函数的地址,也就可以调⽤派⽣类的虚函数表中的虚函数以此实现多态。
编译器处理虚函数表应该如何处理
编译器建立虚函数表的过程其实一共是三个步骤:
拷贝基类的虚函数表,如果是多继承,就拷贝每个虚函数基类的虚函数表
还有一个基类的虚函数和派生类自身的虚函数共用了一个虚函数表,也称为某个基类为派生类的主基类
查看派生类中是否有重写基类中的虚函数,如果有,就替换成已经重写的虚函数地址,查看派生类是否有自身的虚函数,如果有,就追加自身的虚函数到自身的虚函数表中
析构函数一般协程虚函数的原因
是为了降低内存泄露的可能性。举例来说就是,一个基类的指针指向一个派生类的对象,在使用完毕准备销毁时,如果基类的析构函数没有定义成虚函数,那么编译器根据指针类型就会认为当前对象的类型是基类,调用基类的析构函数,仅执行基类的析构,派生类的自身内容将无法被析构,造成内存泄露
如果基类的析构函数定义成虚函数,那么编译器就可以根据实际对象,执行派生类的析构函数,再执行基类的析构函数,成功释放内存
构造函数为什么一般不定义为虚函数
虚函数调用只需要知道部分的信息,即只需要知道函数接口,而不需要知道对象的具体类型。但是,我们要创建一个对象的花,是需要知道对象的完整信息的。特别是,需要知道创建对象的确切类型,因此,构造函数不应该被定义成虚函数
从目前编译器实现虚函数进行多态的方法来看,虚函数的调用是通过实例化之后对象的虚函数表指针来找到虚函数的地址进行调用的,如果说构造函数时虚的,那么虚函数表指针则是不存在的,无法找到队形的虚函数表来调用虚函数,那么这个调用实际上也是违反了先实例化后调用的准则
析构函数的作用,如何起作用
构造函数只是起初始化值的作用,当实例化一个对象的时候,可以通过实例区传递参数,从主函数传递到其他的函数里面,这样就使其他的函数里面有值了。规则,只要你一实例化对象,系统自动回调用一个构造函数,哪怕不写编辑器也自动调用一次
析构函数与构造函数的作用相反,用于撤销对象的一些特殊任务处理,可以是释放对象分配的内存空间;特点:析构函数与构造函数同名,但该函数前面加~
析构函数没有参数,也没有返回值,而且不能重载,在一个类中只能有一个析构函数。当撤销对象时,编译器也会自动调用析构函数。每一个类必须有一个析构函数,用户可以自定义析构函数,也可以是编译器自动生成默认的析构函数。一般析构函数定义为类的公有成员
构造函数的执行顺序?析构函数的执行顺序
构造函数顺序
- 基类构造函数。如果有多个基类,则构造函数的调用顺序是某类在类派生表中出现的顺序,而不是他们在成员初始化表中的顺序
- 成员类对象构造函数。如果有多个成员类对象则构造函数的调用顺序是在类中被声明的顺序,而不是它们出现在成员初始化表中的顺序
- 派生类构造函数
析构函数顺序
- 调用派生类的析构函数
- 调用成员类对象的析构函数
- 调用基类的析构函数
纯虚函数(应用于接口继承和实现继承)
实际上,纯虚函数的出现就是为了让继承可以出现多种情况:
有时我们希望派生类只继承成员函数的接口
有时我们又希望派生类既继承成员函数的接口,又继承成员函数的实现,而且可以在派生类中重写成员函数以实现多态
有的时候我们又希望派生类在继承成员函数接口和实现的情况下,不能重写缺省的实现
其实,声明一个纯虚函数的目的就是为了让派生类只继承函数的接口,而且派生类中必须提供一个这个纯虚函数的实现,否则含有纯虚函数的类将是抽象类,不能进行实例化
对于纯虚函数来说,我呢吧其实是可以给他提供实现代码的,但是由于抽象类不能实例化,调用这个实现的唯一方式是在派生类中指出其class名称来调用
深拷贝和浅拷贝的区别
当出现类的等号赋值时,会调用拷贝函数,在未定义显示拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的
但当数据成员中有指针式,如果采用简单的浅拷贝,则两类中的两个指针指向同一个地址,当对象快要结束时,会调用两次析构函数,而导致野指针的问题
所以,这时必须采用深拷贝。深拷贝与浅拷贝之间的区别就在于深拷贝会在堆内存中另外申请空间来存储数据,从而也就解决了野指针的问题
简而言之,当数据成员中有指针时,必须要用深拷贝更加安全
什么情况下会调用拷贝构造函数
类的对象需要拷贝时,拷贝构造函数将会被调用,以下的情况都会调用拷贝构造函数
- 一个对象以值传递的方式传入函数体,需要拷贝构造函数创建一个临时对象押入到栈空间中
- 一个对象以值传递的方式从函数返回,需要执行拷贝构造函数创建一个临时对象作为返回值
- 一个对象需要通过另一个对象进行初始化
为什么拷⻉构造函数必需时引⽤传递,不能是值传递?
为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副
本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数
的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,
内存泄露的定义,如何检测和避免
内存泄露简单的说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行事件越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了
说⼀下红⿊树(RB-tree)
红⿊树的定义:
性质1:每个节点要么是⿊⾊,要么是红⾊。
性质2:根节点是⿊⾊。
性质3:每个叶⼦节点(NIL)是⿊⾊。
性质4:每个红⾊结点的两个⼦结点⼀定都是⿊⾊。
性质5:任意⼀结点到每个叶⼦结点的路径都包含数
Lambda表达式
Lambda表达式实际上就是提供了一个类似匿名函数的特性,而匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的
利用Lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且是代码更可读
从本质上来讲,Lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现,但是它简便的语法却给C++带来了深远的影响
从广义上说,Lambda表达式产生的是函数对象。在类中,可以重载函数调用运算符,此时类的对象可以将具有类似函数的行为,我们称这些对象为函数对象或者仿函数。相比Lambda表达式,函数对象有自己独特的优势
Lambda表达式一般都是从方括号开始[],然后结束于花括号{},花括号里面就像定义函数那样,包含了Lambda表达式体,一个简单例子如下
auto basicLambda=[]{cout<<"Hello,world!"<<endl;};
basicLamba();
上面是最简单的lambda表达式,没有参数。如果需要参数,那么就要像函数那样放在圆括号里面,如果有返回值,返回类型要放在->后面,即拖尾返回类型,当然也可以忽略返回类型,lambda会帮你自动推断返回类型:
//拖尾返回类型
auto add=[](int a,iut b)->int {return a+b; };
//自动判断返回类型
auto multiply=[](int a,int b){return a+b; };
int sum=add(2,5);
int product=multiply(2,5);
最前面的[]时Lambda表达式的一个很重要的功能,就是闭包。
Lambda表达式的大致原理:每当你定义一个Lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型
那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所哟,我们上面的lambda表达式的结果就是一个个闭包实例
闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前方的方括号就是用来定义捕捉模式以及变量,我们又将其成为lambda捕捉块
int main(){
int x= 10;
auto add x=[x](int a){return a+x;};//复制捕捉x,lambda表达式无法修改此变量
auto multiply_x=[&x](int a){return a*x;}; //引用捕捉x,lambda表达式可以修改此变量
cout << add x(10)<<""<< multiply x(10)<< endl;//输出:20 100
return 0;
捕获的⽅式可以是引⽤也可以是复制,但是具体说来会有以下⼏种情况来捕获其所在作⽤域中的变量:
- []默认不捕获任何变量;
- [=]默认以值捕获所有变量;
- [&]默认以引用捕获所有变量;
- [x]仅以值捕获x,其它变量不捕获;
- [&x]仅以引用捕获x,其它变量不捕获;
- [=,&x]默认以值捕获所有变量,但是x是例外,通过引用捕获;[&,x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
- [this]通过引用捕获当前对象(其实是复制指针);。
- [*this1] 通过传值方式捕获当前对象;
而 lambda 表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。其实,最常用的是在STL算法中,比如你要统计一个数组中满足特定条件的元素数量,通过 lambda 表达式给出条件,传递给count_if函数:
int val = 3;
vector<int> v {1, 8, 5, 3, 6, 10};
int count = std::count_if(v.beigin(), v.end(), [val](int x) { return x > val; });
// v中⼤于3的元素数
最后给出Lambda表达式的完整语法:
[ capture-list ] ( params ) mutable(optional) constexpr(optional)(c++17) exception
attribute -> ret { body }
// 可选的简化语法
[ capture-list ] ( params ) -> ret { body }
[ capture-list ] ( params ) { body }
[ capture-list ] { body }
capture-list:捕捉列表,这个不⽤多说,前⾯已经讲过,它不能省略;
params:参数列表,可以省略(但是后⾯必须紧跟函数体);
mutable:可选,将 lambda 表达式标记为 mutable 后,函数体就可以修改传值⽅式捕获的变量;
constexpr:可选, C++17 ,可以指定 lambda 表达式是⼀个常量函数
exception:可选,指定lambda表达式可以抛出的异常
attribute:可选,指定lambda表达式的特性
ret:可选,返回值类型
body:函数执行体
右值引用
C++11 引入了右值引用的概念,通过使用 && 符号来声明右值引用,这样可以直接操作临时对象(右值),而不需要复制。这样可以大幅提升性能。
C++03 及之前的标准中,右值是不允许被改变的,实践中也通常使用 const T&的方式传递右值。然而这是效率低下的做法,例如:
Person get(){
Person p;
return p;
}
Person p=get();
上述获取右值并初始化p的过程包含了 Person 的3个构造过程和2个析构过程。 这是C++ 广受诟病的一点,但C++11 的右值引用特性允许我们对右值进行修改。借此可以实现 move语义,即从右值中直接拿数据过来初始化或修改左值,而不需要重新构造左值后再析构右值。一个 move 构造函数是这样声明的:
class Person{
public:
Person(Person&& rhs){...}
...
};
右值引用使得“移动”(move)语义成为可能,这允许开发者从一个右值中直接获取资源而不进行深拷贝。这样可以避免不必要的内存开销。
class Person {
public:
Person(Person&& rhs) {
// 转移资源
this->data = rhs.data; // 假设 data 是指向某些资源的指针
rhs.data = nullptr; // 把右值的资源置为空,防止双重释放
}
// ...
};
智能指针
C++11 对于 C++ 标准库的变更。C++11 把 TR1 并⼊了进来,废弃了 C++98 中的 auto_ptr , 同时
将 shared_ptr 和 uniq_ptr 并⼊ std 命名空间。
int main() {
// 创建一个 std::shared_ptr 指向一个动态分配的 double 类型变量
std::shared_ptr<double> p_first(new double);
{
// 在这个作用域内,创建一个新的 shared_ptr,指向相同的 double 变量
std::shared_ptr<double> p_copy = p_first;
// 使用 p_copy 修改 double 的值
*p_copy = 21.2;
} // 这里结束了 p_copy 的作用域,p_copy 被销毁
// 此时 p_copy 被销毁,但 p_first 仍然存在,所指向的 double 仍然有效
return 0; // 当 p_first 离开作用域时,它所指向的 double 也会被销毁
}
final和override
C++借由虚函数实现运行时多态,但 C++ 的虚函数又很多脆弱的地方
- 无法禁止子类重写它。可能到某一层级时,我们不希望子类继续来重写当前虚函数了。
- 容易不小心隐藏父类的虚函数。比如在重写时,不小心声明了一个签名不一致但有同样名称的新函数。.
C++11 提供了 final 来禁止虛函数被重写/禁止类被继承,override 来显示地重写虚函数。这样编译器给我们不小心的行为提供更多有用的错误和警告。
struct Basel final{};
struct Derivedl:Basel{};
// 编译错:Base1不允许被继承
struct Base2{
virtual void fl()final;
virtual void f2();
};
struct Derived2:Base2{
virtual void fl();// 编译错:f1不允许重写
virtual void f2(int)override; // 编译错:父类中没有 void f2(int)
};