面经汇总 类和数据抽象

面经汇总 类和数据抽象

Posted by zhaostu4 on November 28, 2019

T:2019/11/28 W:四 17:0:11 [HTML]: @TOC

类和数据抽象

1. C++中类成员的访问权限

  • 参考: c++三种继承方式public,protect,private
  • 三种访问限定符号
    • public:可以被任意实体访问
    • protected:只允许子类及本类的成员函数还有友元函数访问
    • private:只允许本类的成员函数以及友元函数访问
  • 三种继承方式: public 继承, protect 继承, private 继承
  • 组合结果
基类中 继承方式 子类中
public public继承 public
public protected继承 protected
public private继承 private
protected public继承 protected
protected protected继承 protected
protected private继承 private
private public继承 子类无权访问
private protected继承 子类无权访问
private private继承 子类无权访问

  • 总结
    • private属性被继承, 子类无法访问
    • public属性被继承, 子类一定能访问(但被private继承,孙类无法访问)
    • protected属性被继承, 子类一定能访问(但被private继承,孙类无法访问) —
    • 父类中的public成员被那种方式继承,在子类中就表现那种属性
    • 父类中的protected成员无论是被public继承 还是protected继承 , 在子类中都是protected成员
    • 父类中的private成员无论被何种方式继承, 在子类中总是为private成员

  • C++通过 public, protected, private 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。
  • 在类的内部(定义类的代码内部),无论成员被声明为 publicprotected 还是 private,都是可以互相访问的,没有访问权限的限制。
  • 在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 privateprotected 属性的成员

2. C++structclass的区别

  • C++中,可以用structclass定义类,都可以继承。
  • 区别在于:
    • struct的默认继承权限和默认访问权限是public,而class的默认继承权限和默认访问权限是private


3. C++类内可以定义引用数据成员吗?

  • 可以,但是必须通过构造函数初始化列表进行初始化.

对面对象和泛型编程

1. 什么是右值引用,跟左值又有什么区别?

  • 右值引用是C++11中引入的新特性 , 它实现了转移语义精确传递。它的主要目的有两个方面: 1) 避免无谓的复制,提高了程序的性能,节省运算存储资源,提高效率。 2) 能够更简洁明确地定义泛型函数。
  • 左值和右值的概念: 1) 左值:能对表达式取地址、或具名对象/变量一般指表达式结束后依然存在的持久对象。 2) 右值:不能对表达式取地址,或匿名对象一般指表达式结束就不再存在的临时对象。
  • 右值引用和左值引用的概念: 1) 左值引用是绑定到左值上的引用, const 左值引用 可以绑定到右值上, 具体实现和右值引用类似. 2) 右值引用是绑定到右值的引用, - 具体的实现过程是, 将右值对象会被储存在内存中形成一个匿名对象,然后将此匿名对象绑定到右值引用上. - 右值它本身是一个左值, 所以也可以绑定到右值引用上
  • 引用折叠和右值引用参数 (引用折叠规则)
    • 虽然通常情况下无法将右值引用绑定到一个左值上
    • 右值引用的类型推断: 当我们将一个左值传递给函数的右值引用作为参数时(函数参数为T&&),编译器推断模板类型参数为实参的左值引用类型
    • 引用折叠规则:
      • 当定义了引用的引用时,则这些引用形成了“折叠”,通常情况下引用会折叠成一个普通的左值引用类型. 但是有一个例外:
      • 这个例外就是右值引用的右值引用:
        • X& &&, X& &&, X&& &都折叠成X& 左值引用和右值引用放一堆, 会折叠为左值引用
        • 类型X&& &&折叠成X&& 多个右值引用放一堆, 会折叠为右值引用
  • 参考:

编译和底层

1. C++源文件从文本到可执行文件经历的过程?

  • 参考文章hello程序是如何编译出来的
  • 对于C++源文件,从文本到可执行文件一般需要四个过程:
    • 预处理阶段:对源代码文件中文件包含关系(头文件)、预编译语句(宏定义)进行分析和替换,生成预编译文件。
    • 编译阶段:将经过预处理后的预编译文件转换成特定汇编代码,生成汇编文件
    • 汇编阶段:将编译阶段生成的汇编文件转化成机器码,生成可重定位目标文件
    • 链接阶段:将多个目标文件及所需要的库连接成最终的可执行目标文件

2. include头文件的顺序以及双引号””和尖括号<>的区别?

  • Include头文件的顺序: 为先引用先包含
    • 如果头文件a中定义了一个变量, 而头文件b使用了这个这个变量,但是并没有包含头文件a, 哪个包含头文件bcpp文件必须先包含a 再包含b, 否则会报类型未知的错误.
    • 双引号尖括号的区别:编译器预处理阶段查找头文件的路径不一样。
      • 对于双引号包含的头文件,查找头文件路径的顺序为: 1) 当前项目头文件目录 2) 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径) 3) 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径
      • 对于使用尖括号包含的头文件,查找头文件的路径顺序为: 1) 编译器设置的头文件路径(编译器可使用-I显式指定搜索路径) 2) 系统变量CPLUS_INCLUDE_PATH/C_INCLUDE_PATH指定的头文件路径

3. malloc的原理,另外brk系统调用和mmap系统调用的作用分别是什么?

  • 参考:
  • Malloc函数用于动态分配内存: Malloc在申请内存时,一般会通过brk或者mmap系统调用进行申请。
    • 当申请内存小于128K时,会使用系统函数brk在堆区中分配;
    • 当申请内存大于128K时,会使用系统函数mmap在映射区分配。
  • 堆区内存映射区内存的特点:
    • 分配在映射区的内存可以随时释放不会产生内存碎片, 遵守访问时分配的策略,也即通过缺页中断分配真实物理地址
    • 分配在堆区的内存,只能顺序释放(即高位释放之后,低位才能真正释放), 所以堆区的内存会存在内存碎片的问题, 在堆区的内存访问时是有可能产生缺页中断的
  • 为了减少内存碎片和系统调用的开销,malloc采用内存池的方式进行内存管理 1) 首先, 先申请大块内存作为堆区,然后将堆区分为多个内存块,以块作为内存管理的基本单位。 2) 然后, 采用隐式链表结构将所有的堆区分成连续的, 大小不一的块, 包含已分配块和未分配块; 3) 同时, 采用显示链表结构来管理所有的空闲块,即使用一个双向链表将空闲块连接起来,每一个空闲块记录了一个连续的、未分配的地址。 1) 当进行内存分配时,Malloc会通过显示链表遍历所有的空闲块,选择满足要求的块进行分配; 2) 当进行内存合并时,malloc采用边界标记法,根据每个块的前后块是否已经分配来决定是否进行块合并。 3) 当用户申请内存时,直接从堆区分配一块合适的空闲块。 4) 当用户释放内存时,将释放内存块添加到显示链表上,然后尝试进行内存合并

4. C++的内存管理是怎样的?

  • C++中,虚拟内存分为代码段, 数据段, BSS段, 堆区, 文件映射区以及栈区六部分。

  • 代码段:包括只读存储区文本区,其中只读存储区存储字符串常量文本区存储程序的机器代码
  • 数据段:存储程序中已初始化的全局变量静态变量
  • BSS段:存储未初始化的全局变量静态变量(局部+全局),以及所有被初始化为0全局变量静态变量
  • 堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。
  • 映射区:存储动态链接库以及调用mmap函数进行的文件映射
  • 栈:使用栈空间存储函数的返回地址, 参数, 局部变量, 返回值

5. C++/C的内存分配

在这里插入图片描述

  • 32bitCPU可寻址4G线性空间,每个进程都有各自独立的4G逻辑地址,其中0 ~ 3G是用户态空间,3~4G是内核空间,不同进程相同的逻辑地址会映射到不同的物理地址中。
  • 其逻辑地址其划分如下:
    • 各个段说明如下:
      • 3G用户空间1G内核空间
    • 静态区域:
      • text segment(代码段):包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。
      • data segment(数据段):存储程序中已初始化的全局变量和静态变量
      • bss segment:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量,对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零。即未初始化的全局变量编译器会初始化为0
    • 动态区域:
      • heap(堆): 当进程未调用malloc时是没有堆段的, 只有调用malloc时采用分配一个堆, 并且在程序运行过程中可以动态增加堆大小(移动break指针), 从低地址向高地址增长。分配小内存时使用该区域。 堆的起始地址由mm_struct 结构体中的start_brk标识,结束地址由brk标识。
      • memory mapping segment(映射区):存储动态链接库等文件映射, 申请大内存(malloc时调用mmap函数)
      • stack(栈):使用栈空间存储函数的返回地址, 参数, 局部变量, 返回值, 从高地址向低地址增长。在创建进程时会有一个最大栈大小,Linux可以通过ulimit命令指定。

6. 如何判断内存泄漏?

  • 内存泄漏通常是由于调用了malloc/new等内存申请的操作,但是缺少了对应的free/delete
  • 为了判断内存是否泄露,
    • 我们一方面可以使用linux环境下的内存泄漏检查工具Valgrind,
    • 另一方面我们在写代码时可以添加内存申请和释放的统计功能,统计当前申请和释放的内存是否一致,以此来判断内存是否泄露。(尽量使用智慧指针来管理内存)

7. 请你来说一下什么时候会发生段错误

  • 段错误通常发生在访问非法内存地址的时候
  • 具体来说分为以下几种情况:
    • 使用野指针
    • 试图修改字符串常量的内容

8. 什么是memory leak,也就是内存泄漏

  • 内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  • 内存泄漏的分类:

    • 堆内存泄漏 (Heap leak)。对内存指的是程序运行中根据需要分配通过malloc,realloc, new等从堆中分配的一块内存,再是完成后必须通过调用对应的 free或者delete 删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这块内存将不会被使用,就会产生Heap Leak.
    • 系统资源泄露(Resource Leak)。主要指程序使用系统分配的资源比如 Bitmap,handle,socket等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

    • 没有将基类的析构函数定义为虚函数。当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确是释放,因此造成内存泄露。

9. newmalloc的区别

1) 首先, newc++操作符可以重载, 而mallocc语言的库函数 2) malloc分配的内存不够的时候,可以用realloc扩容。new没用这样操作。 3) new需要制定数据类型,内存将按照数据类型的大小进行分配, 返回的是指定数据类型的指针, 而malloc分配内存需要制定内存大小, 返回值通常是void*, 通常需要进行类型转换 4) new不仅分配一段内存,而且会调用构造函数malloc不会。 5) new分配的内存要用delete销毁,malloc要用free来销毁;delete销毁的时候会调用对象的析构函数,而free则不会。 6) new如果分配失败了会抛出bad_malloc的异常,而malloc失败了会返回NULL。 7) 申请数组时: new[]一次分配所有内存,多次调用构造函数,搭配使用delete[]delete[]多次调用析构函数,销毁数组中的每个对象。而malloc则只能sizeof(int) * n

10. 共享内存相关api

  • Linux允许不同进程访问同一个逻辑内存,提供了一组API,头文件在sys/shm.h中。
  • 新建共享内存shmget
    • int shmget(key_t key,size_t size,int shmflg);
      • key: 共享内存键值,可以理解为共享内存的唯一性标记。
      • size: 共享内存大小
      • shmflag: 创建进程和其他进程的读写权限标识。
      • 返回值: 相应的共享内存标识符,失败返回-1
  • 连接共享内存到当前进程的地址空间shmat
    • void *shmat(int shm_id,const void *shm_addr,int shmflg);
      • shm_id: 共享内存标识符
      • shm_addr: 指定共享内存连接到当前进程的地址,通常为0,表示由系统来选择。
      • shmflg: 标志位
      • 返回值: 指向共享内存第一个字节的指针,失败返回-1
  • 当前进程分离共享内存shmdt
    • int shmdt(const void *shmaddr);
  • 控制共享内存shmctl: 和信号量的semctl函数类似,控制共享内存
    • int shmctl(int shm_id,int command,struct shmid_ds *buf);
      • shm_id: 共享内存标识符
      • command: 有三个值
      • IPC_STAT: 获取共享内存的状态,把共享内存的shmid_ds结构复制到buf中。
      • IPC_SET: 设置共享内存的状态,把buf复制到共享内存的shmid_ds结构。
      • IPC_RMID: 删除共享内存
      • buf: 共享内存管理结构体。

11. reactor模型组成

  • 参考:
  • reactor模型要求主线程只负责监听文件描述上是否有事件发生,有的话就立即将该事件通知工作线程,除此之外,主线程不做任何其他实质性的工作,读写数据、接受新的连接以及处理客户请求均在工作线程中完成。 在这里插入图片描述
  • 其模型组成如下: 1) Handle (句柄):即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件, 一个连接 Socket, Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接。 2) Synchronous Event Demultiplexer(同步事件复用器):阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。 3) Initiation Dispatcher(初始事件分发器):用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexerselect方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。 4) Event Handler(事件处理器):定义事件处理方法: handle_event(),以供InitiationDispatcher回调使用。 5) Concrete Event Handler(具体的事件处理器): 事件EventHandler接口,实现特定事件处理逻辑。

12. 设计一下如何采用单线程的方式处理高并发

  • 在单线程模型中可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来事务
    • 例如, 首先使用select函数处理客户端connect请求
    • 然后使用异步回调函数处理逻辑

13. C++ STL 的内存优化

  • 二级配置器结构 : STL内存管理使用二级内存配置器。 1) 分配原则
    • 如果要分配的区块大于128bytes,则移交给第一级配置器处理。
    • 如果要分配的区块小于128bytes,则以交给第二级配置器处理。

2) 第一级配置器 - 第一级配置器以malloc()free()realloc()等C函数执行实际的内存配置、释放、重新配置等操作,并且能在内存需求不被满足的时候,调用一个指定的函数。 - 一级空间配置器分配的是大于128字节的空间 - 如果分配不成功,调用句柄释放一部分内存 - 如果还不能分配成功,抛出异常


  • 内存池和链表机制是分开的, 内存池维护了一款空的内存空间,如果自由链表不够的时候,就像内存池申请

2) 第二级配置器 - 在STL的第二级配置器中使用了内存池机制,避免太多小区块造成的内存碎片,小额区块带来的不仅是内存碎片,配置时还有额外的负担。区块越小,额外负担所占比例就越大。 - 分配原则: 内存池管理(memory pool),又称之次层配置(sub-allocation) - 每次配置一大块内存,并维护对应的16个空闲链表(free-list)。 - 用户申请空间如果小于128字节时,将字节数扩展为8的倍数, 然后在自由链表中查找对应大小的子链表。如果找到满足需求的内存,则取出内存,返回给用户. - 如果在自由链表查找不到或者块数不够,则向内存池进行申请,一般一次申请20块。 - 如果不够分配20块,但是足够供应一个及以上的区块,则分配最多的块数给自由链表。 - 如果一块都无法提供,则把剩余的内存挂到自由链表,然后向系统heap申请空间,如果申请失败,则看看自由链表还有没有可用的块,如果也没有,则最后调用一级空间配置器。 - 如果有小额区块被释放,则由配置器回收到free-list中。 3) 二级内存池 - 二级内存池采用了16个空闲链表,这里的16个空闲链表分别管理大小为8, 16, 24120, 128的数据块。这里空闲链表节点的设计十分巧妙,这里用了一个联合体既可以表示下一个空闲数据块(存在于空闲链表中)的地址,也可以表示已经被用户使用的数据块(不存在空闲链表中)的地址。

- 空间配置函数`allocate`
	- 首先先要检查申请空间的大小,如果大于`128字节`就调用第一级配置器,小于`128字节`就检查对应的空闲链表,如果该空闲链表中有可用数据块,则直接拿来用(取空闲链表中的第一个可用数据块,然后把该空闲链表的地址设置为该数据块指向的下一个地址), 如果没有可用数据块,则调用`refill`重新填充空间。
- 空间释放函数`deallocate`
	- 首先先要检查释放数据块的大小,如果大于`128字节`就调用第一级配置器,小于`128字节`则根据数据块的大小来判断回收后的空间会被插入到哪个空闲链表。
- 重新填充空闲链表`refill`
	- 在用`allocate`配置空间时,如果空闲链表中没有可用数据块,就会调用`refill`来重新填充空间,新的空间取自内存池。缺省取`20`个数据块,如果内存池空间不足,那么能取多少个节点就取多少个。
	 - 从内存池取空间给空闲链表用是`chunk_alloc`的工作,首先根据`end_free-start_free`来判断内存池中的剩余空间是否足以调出`nobjs`个大小为`size`的数据块出去,如果内存连一个数据块的空间都无法供应,需要用`malloc`取堆中申请内存。
	- 假如山穷水尽,整个系统的堆空间都不够用了,`malloc`失败,那么`chunk_alloc`会从空闲链表中找是否有大的数据块,然后将该数据块的空间分给内存池(这个数据块会从链表中去除)。

4) 总结: - 使用allocate向内存池请求size大小的内存空间,如果需要请求的内存大小大于128bytes,直接使用malloc。 - 如果需要的内存大小小于128bytesallocate根据size找到最适合的自由链表。 - a) 如果链表不为空,返回第一个node,链表头改为第二个node。 - b) 如果链表为空,使用blockAlloc请求分配node。 - x) 如果内存池中有大于一个node的空间,分配竟可能多的node(但是最多20个),将一个node返回,其他的node添加到链表中。 - y) 如果内存池只有一个node的空间,直接返回给用户。 - z) 若果如果连一个node都没有,再次向操作系统请求分配内存。 - 分配成功,再次进行b过程。 - 分配失败,循环各个自由链表,寻找空间。 - I. 找到空间,再次进行过程b。 - II. 找不到空间,抛出异常。 - 用户调用deallocate释放内存空间,如果要求释放的内存空间大于128bytes,直接调用free。 - 否则按照其大小找到合适的自由链表,并将其插入。


14. select, epoll的区别, 原理, 性能, 限制

  • IO多路复用
    • IO复用模型阻塞IO模型上多了一个select函数select函数有一个参数是文件描述符集合,意思就是对这些的文件描述符进行循环监听,当某个文件描述符就绪的时候,就对这个文件描述符进行处理。
    • 这种IO模型是属于阻塞的IO。但是由于它可以对多个文件描述符进行阻塞监听,所以它的效率比阻塞IO模型高效。

    • IO多路复用就是我们说的selectpollepollselect/epoll的好处就在于单个process就可以同时处理多个网络连接的IO
    • 它的基本原理就是select, poll, epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
    • 当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。
    • 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
    • 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。
    • I/O多路复用阻塞I/O其实并没有太大的不同,事实上,还更差一些。
    • 因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)
    • 但是,用select的优势在于它可以同时处理多个connection
    • 所以,如果处理的连接数不是很高的话,使用select/epollweb server不一定比使用multi-threading + blocking IOweb server性能更好,可能延迟还更大。
    • select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接
    • IO 多路复用模型中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IOblock
  • select
    • select:是最初解决IO阻塞问题的方法。用结构体fd_set来告诉内核监听多个文件描述符,该结构体被称为描述符集。由数组来维持哪些描述符被置位了。对结构体的操作封装在三个宏定义中。通过轮寻来查找是否有描述符要被处理。
    • 存在的问题:
      • 内置数组的形式使得select的最大文件数受限与FD_SIZE(1024)
      • 每次调用select前都要重新初始化描述符集,将fd从用户态拷贝到内核态,每次调用select后,都需要将fd从内核态拷贝到用户态;
      • 轮寻排查当文件描述符个数很多时,效率很低;
  • poll
    • 通过一个可变长度的数组解决了select文件描述符受限的问题。
    • 数组中元素是结构体,该结构体保存描述符的信息,每增加一个文件描述符就向数组中加入一个结构体,结构体只需要拷贝一次到内核态。(感觉有问题,因该是只需要初始化一次)
    • poll解决了select重复初始化的问题。轮寻排查的问题未解决。
  • epoll
    • epoll是基于回调的(参考:epoll详解 - 胖胖的博客 - CSDN博客)
    • 轮寻排查所有文件描述符的效率不高,使服务器并发能力受限。epoll采用只返回状态发生变化的文件描述符,便解决了轮寻的瓶颈。
    • 使用红黑树管理描述符,以支持快速的增删查改
    • epoll对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)LT模式是默认模式

  • LT模式
    • LT(level triggered)是缺省的工作方式,并且同时支持blockno-block socket. 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的
  • ET模式
    • ET(edge-triggered)是高速工作方式 ,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fdIO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
    • ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  • LT模式ET模式的区别如下:
    • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
    • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
  • 参考:

15. C++11有哪些新特性?

  • C++11 最常用的新特性如下:
    • auto关键字:编译器可以根据初始值自动推导出类型。但是不能用于函数传参以及数组类型的推导
    • nullptr关键字nullptr是一种特殊类型的字面值,它可以被转换成任意其它的指针类型;而NULL一般被宏定义为0,在遇到重载时可能会出现问题。
    • 智能指针:C++11新增了std::shared_ptr, std::weak_ptr等类型的智能指针,用于解决内存管理的问题。
    • 初始化列表:使用初始化列表来对类进行初始化
    • 右值引用:基于右值引用可以实现移动语义和完美转发,消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率
    • atomic原子操作: 用于多线程资源互斥操作
    • 新增STL容器array以及tuple

  • 回答二
    • 关键字及新语法:
      • auto关键字
      • nullptr关键字
      • for循环语法
      • decltype
    • STL容器
      • stl::array
      • stl::forward_list
      • std::unordered_map
      • std::unorderd_set
    • 多线程
      • std::thread
      • std::stomic
      • std::condition_variable
    • 智慧指针内存管理
      • std::shared_ptr
      • std::weak_ptr
    • 函数
      • std::function
      • std::bind
      • lamda表达式

16. C++11中的可变参数模板、右值引用和lambda

  • 可变参数模板:
    • C++11的可变参数模板,对参数进行了高度泛化,可以表示任意数目、任意类型的参数
    • 其语法为:在classtypename后面带上省略号。
    • 例如:
        Template<class ... T>
        void func(T ... args){
            cout<<num is<<sizeof ...(args)<<endl;
        }
        func();//args不含任何参数
        func(1);//args包含一个int类型的实参
        func(1,2.0)//args包含一个int一个double类型的实参
      
    • 其中T叫做模板参数包,args叫做函数参数包
    • 省略号作用如下:
      • 声明一个包含0到任意个模板参数的参数包
      • 在模板定义得右边,可以将参数包展成一个个独立的参数
    • C++11可以使用递归函数的方式展开参数包,获得可变参数的每个值。通过递归函数展开参数包,需要提供一个参数包展开的函数和一个递归终止函数。
    • 例如:
        #include using namespace std;
        // 最终递归函数
        void print(){
            cout << "empty" << endl;
        }
        // 展开函数
        template void print(T head, Args... args){
            cout << head << ","; print(args...);
        }
        int main(){
            print(1, 2, 3, 4); return 0;
        }
      
    • 参数包Args ...在展开的过程中递归调用自己,没调用一次参数包中的参数就会少一个,直到所有参数都展开为止。当没有参数时就会调用非模板函数printf终止递归过程。

  • 右值引用:
    • C++中,左值通常指可以取地址,有名字的值就是左值,而不能取地址,没有名字的就是右值。
    • 而在C++11中,右值是由两个概念构成,将亡值纯右值。纯右值是用于识别临时变量和一些不跟对象关联的值,比如1+3产生的临时变量值2, true等,而将亡值通常是指具有转移语义的对象,比如返回右值引用T&&的函数返回值等。
    • C++11中,右值引用就是对一个右值进行引用的类型。
    • 由于右值通常不具有名字,所以我们一般只能通过右值表达式获得其引用
    • 比如:
      • T && a=ReturnRvale();
      • 假设ReturnRvalue()函数返回一个右值,那么上述语句声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
    • 基于右值引用可以实现转移语义和完美转发新特性。
  • 移动语义:
    • 对于一个包含指针成员变量的类,由于编译器默认的拷贝构造函数都是浅拷贝,所有我们一般需要通过实现深拷贝的拷贝构造函数,为指针成员分配新的内存并进行内容拷贝,从而避免悬挂指针的问题。
    • 如下列代码所示:
        #include<iostream>
        using namespace std;
        class HasPtrMem{
        public:
        HasPtrMem(): d(new int(0)){
            cout << "Construct: " << ++n_cstr <<endl;
        }
        HasPtrMem(const HasPtrMem &h): d(new int(*h.d)){
            cout << "Copy construct: " << ++n_cptr << endl;
        }
        ~HasPtrMem(){
            cout << "Destruct: " << ++n_dstr << endl;
        }
        int *d;
        static int n_cstr;
        static int n_dstr;
        static int n_cptr;
        };
        int HasPtrMem::n_cstr = 0;
        int HasPtrMem::n_dstr = 0;
        int HasPtrMem::n_cptr = 0;
        HasPtrMem GetTemp(){return HasPtrMem();}
        int main(){
        HasPtrMem a = GetTemp();
        }
      
    • 当类HasPtrMem包含一个成员函数GetTemp,其返回值类型是HasPtrMem,如果我们定义了深拷贝的拷贝构造函数,那么在调用该函数时需要调用两次拷贝构造函数。第一次是生成GetTemp函数返回时的临时变量,第二次是将该返回值赋值给main函数中的变量a。与此对应需要调用三次析构函数来释放内存。
    • 而在上述过程中,使用临时变量构造a时会调用拷贝构造函数分配对内存,而临时对象在语句结束后会释放它所使用的堆内存。这样重复申请和释放内存,在申请内存较大时会严重影响性能。因此C++使用移动构造函数,从而保证使用临时对象构造a时不分配内存,从而提高性能。
    • 如下列代码所示,移动构造函数接收一个右值引用作为参数,使用右值引用的参数初始化其指针成员变量。
        HasPtrMem(HasPtrMem &&h): d(h.d){ // 移动构造函数
        h.d = nullptr;                // 将临时值得指针成员置空
        cout << "Move construct: " << ++n_mvtr << endl;
        }
      
    • 其原理就是使用在构造对象a时,使用h.d来初始化a,然后将临时对象h的成员变量d指向nullptr,从而保证临时变量析构时不会释放对内存。 在这里插入图片描述
  • 完美转发:
    • 完美转发是指在函数模板中,完全依照模板的参数的类型,将参数传递给函数模板中调用的另一个函数,即传入转发函数的是左值对象,目标函数就能获得左值对象转发函数是右值对象,目标函数就能获得右值对象,而不产生额外的开销。
    • 因此转发函数和目标函数参数一般采用引用类型,从而避免拷贝的开销。其次,由于目标函数可能需要能够既接受左值引用,又接受右值引用,所以考虑转发也需要兼容这两种类型。
    • C++11采用引用折叠的规则,结合新的模板推导规则实现完美转发。其引用折叠规则如下:
    • 参考: 什么是右值引用,跟左值又有什么区别?
    TR类型定义 声明v的类型 v的实际类型
    T& TR A&
    T& TR& A&
    T& TR&& A&
    T&& TR A&&
    T&& TR& A&
    T&& TR&& A&&
    • 因此,我们将转发函数和目标函数的参数都设置为右值引用类型
        void IamForwording(T && t){
            IrunCodeActually(static_cast<T &&>(t));
        }
      
    • 当传入一个X类型的左值引用时,转发函数将被实例为:
        void IamForwording(X & && t){
            IrunCodeActually(static_cast<T& &&>(t));
        }
      
    • 经过引用折叠,变为:
        void IamForwording(X & t){
            IrunCodeActually(static_cast<X &>(t));
        }
      
    • 当传入一个X类型的右值引用时,转发函数将被实例为:
        void IamForwording(X&& && t){
            IrunCodeActually(static_cast<X && &&>(t));
        }
      
    • 经过引用折叠,变为:
        void IamForwording(X&& t){
            IrunCodeActually(static_cast<X &&>(t));
        }
      
    • 除此之外,还可以使用forward()函数来完成左值引用到右值引用的转换:
        template <typename T>
        void IamForwording(T && t){
            IrunCodeActually(forward(t));
        }
      
  • Lambda表达式:
  • Lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量
  • 其定义如下:
    • [capture](params)mutable->return-type{statement}
    • 其中:
      • [capture]:捕获列表,捕获上下文变量以供lambda使用。同时[]lambda寅初复,编译器根据该符号来判断接下来代码是否是lambda函数。
      • (Params):参数列表,与普通函数的参数列表一致,如果不需要传递参数,则可以连通括号一起省略。
      • mutable是修饰符,默认情况下lambda函数总是一个const函数Mutable可以取消其常量性。在使用该修饰符时,参数列表不可省略。
      • ->return-type:返回类型是返回值类型
      • {statement}:函数体,内容与普通函数一样,除了可以使用参数之外,还可以使用所捕获的变量。
      • Lambda表达式与普通函数最大的区别就是其可以通过捕获列表访问一些上下文中的数据。其形式如下:
        • [val] 表示值传递方式捕捉变量var
        • [=] 表示值传递方式捕捉所有父作用域的变量(包括this)
        • [&val]表示引用传递捕捉变量var
        • [this]表示值传递方式捕捉当前的this指针
    • Lambda的类型被定义为“闭包”的类,其通常用于STL库中,在某些场景下可用于简化仿函数的使用,同时Lambda作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。