C++面试笔记
目录
- 目录
- new delete free malloc 的区别
- 虚函数表与虚函数表指针的创建时机
- C++什么时候生成默认拷贝构造函数
- 面向对象的三大特征以及特性
- 线程池相关知识
- 进程和线程的区别
- 描述系统调用整个流程
- 页面置换算法有哪些
- tcp和udp的区别
- 水平触发与边缘触发的区别
- 什么是半打开半关闭状态
- 写文件时进程宕机,数据会丢失吗?
- 左值引用和右值引用区别
- 智能指针种类以及使用场景
- 什么设计模式,它解决的是什么问题
- B树与B+树与二叉搜索树
- 语法基础
- 野指针 悬空指针 空指针
- union的相关知识
- 指针相关知识
- C++ 内存对齐机制
- std::string
- const int * ptr, int * const ptr的区别是什么?
- 什么是std::ref?
- decltype, std::declval, std::decay_t 分别是什么?
- const 与 constexpr
- noexcept
- 用户自定义字面量
- mutable volatile
- explicit
- std::invoke
- stl容器里emplace_back和push_back的区别,emplace_back是不是能完美替代push_back
- 如果使用std::move(t)来构造一个对象,但是该类没有显式提供移动构造函数,那么它是使用的式默认移动构造函数已经定义的拷贝构造
- 在模板中使用 typedef的时候的注意事项
new delete free malloc 的区别
- 背景:前两者是C++里的操作符,后两者是C语言的库函数
- new在分配内存的时候是自动计算分配内存的大小,而malloc需要程序员手动指定
- new是在free store(自由存储区)分配,malloc是在堆区分配,由于new的底层实现用到了malloc所以free store默认也是堆区
- new分配失败会抛出异常,malloc分配失败会返回NULL
- new返回的指向对象类型的指针,malloc返回的是void*/,需要强制类型转换成对应类型
- new分配完内存后会执行析构函数, malloc分配内存后则不会
衍生问题
malloc是怎么分配空间的
- malloc在分配内存的时候会有一个阈值,这个值为128k,当分配内存小于128K的时候进行brk()调用申请内存,此内存在堆区,大于的时候会通过mmp()点用进行分配内存(分配在文件映射段也就是再堆区与栈区直接那个段),此内存会在销毁的时候释放,而小于128K的时候,在被销毁的时候会被先放到内存池中
malloc分配的虚拟内存还是物理内存
- malloc分配的内存是虚拟内存。
malloc分配后是否立即得到物理内存
- 不是的,他是在访问已分配的虚拟地址空间的时候,操作系统通过查找页表,发现虚拟内存对应的页没有在物理内存中,就会触发缺页中断,然后操作系统会建立虚拟内存和物理内存之间的映射关系,这样之后才会得到物理内存
free(p)是怎么得到p这个空间有多大的
- 在malloc分配内存的时候,会首先分配16个字节放在前面,这16个字节会存储有关内存的相关信息,其中就包括内存块大小,所以在执行free的时候只需要向前移16字节就能知道空间有多大了
free释放内存后,内存还在吗
- 分情况,如果内存是通过brk()调用得到存放在堆区,释放内存后会被存储在内存池中,而如果通过mmp()调用得到的内存,则会被操作系统释放
为什么不直接全用mmp()来分配,而需要分情况使用brk()呢
- 因为mmp()调用的时候每次都需要进行系统调用,而进行系统调用需要从用户态变为内核态再变回用户态需要大量时间。另外,因为 mmap 分配的内存每次释放的时候,都会归还给操作系统,于是每次 mmap 分配的虚拟地址都是缺页状态的,然后在第一次访问该虚拟地址的时候,就会触发缺页中断。也就是说,频繁通过 mmap 分配的内存话,不仅每次都会发生运行态的切换,还会发生缺页中断(在第一次访问虚拟地址后),这样会导致 CPU 消耗较大。
- 而使用brk()就能改进这两个问题,由于堆空间是连续的,所以直接预分配更大的内存来作为内存池,当内存释放的时候,就缓存在内存池中。这样就可以在内存池中,取出对应内存块,而且可能虚拟地址与物理地址映射关系还存在这样不仅减少了系统调用的次数,也减少了缺页中断的次数,这将大大降低CPU的消耗
new的执行过程
- 先执行operator new
- 寻找合适的内存空间
- 执行构造函数
delete的执行过程
- 先调用析构函数
- 执行operator delete
- 销毁空间
虚函数表与虚函数表指针的创建时机
- 背景:是用来实现多态的
- 什么时候生成的? 是在编译器编译的时候,遇到virtual关键词的时候生成的
- 存放在哪里? 可执行文件(存在磁盘), 运行的时候(在内存),在磁盘里是存储在.rodata中,在内存中是存储在代码区
虚函数表指针创建的时机
- 类对象构造的时候,会把类的虚函数表地址赋给 vptr
- 如果类没有构造函数,编译器会生成默认的构造函数(类的前八个字节就是虚函数表指针所在位置(vptr))
- 继承的情况下,虚函数表指针赋值过程?
- 调用基类构造函数,把A的虚函数表的地址赋给 vptr
- 调用子类构造函数,把B子类的虚函数表的地址赋给 vptr
虚函数表和虚函数表指针的关系
一个类只对应一个虚函数表,这个类创造的每一个对象对应的是虚函数表指针(在堆里),通常来说虚函数表指针是不一样的(涉及浅拷贝与深拷贝),需要显示写出拷贝构造函数和重载赋值运算符,虚函数表指针是指向的是虚函数表位置,大概如图所示:(vptr 在32位机上4字节,64位机上8字节)
解释这张图就是 虚函数表指针指向虚函数表(在.rodata中)的某个位置,这个位置是一个函数的地址,指向代码区里的函数对于每个对象来说,其都有一个虚函数表指针指向这个类的虚函数表,而虚函数表是与类绑定的,因此对于每个对象的虚函数表指针来说其指向的位置是相同的,即每个指针中存的地址是一样的。那么析构后仅是将对应的vptr置为nullptr,应该不会影响其他对象的vptr.
衍生问题
多态的内容
- 首先多态分为静态多态(编译时多态)与动态多态(运行时多态)
- 静态多态:在系统编译期间就可以确定程序将要执行哪个函数,比如 函数重载,通过类成员函数符指定的运算
函数重载条件:1.同一个作用域,2.同一个函数名,3.函数参数类型不同或者个数不同或者顺序不同
注意:如果只有函数的返回值不同,就不算函数重载 - 动态多态:是利用虚函数实现运行时的多态,即在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数,动态多态是在虚函数的基础上实现的,而实现的条件有:
(1) 在类中声明为虚函数
(2) 函数的函数名,返回值,函数参数个数,参数类型,全都与基类的所声明的虚函数相同(否则是函数重载的条件)
(3) 将子类对象的指针(或以引用形式)赋值给父类对象的指针(或引用),再用该指向父类对象的指针(或引用)调用虚函数
如此,便可以实现动态多态,程序会按照实际对象类型来选择要实行的函数具体时哪一个。
c++内存模型
- 栈区:用于实现函数调用。由编译器自动分配释放,存放函数的参数值和局部变量等(向下增长),每个线程都有自己的栈区,栈区的内存是线程私有的,不同线程之间的栈区不共享
- 堆区:用于存放于存放动态分配的变量。由程序员动态分配和释放,使用new和delete操作符(向上增长)
堆区碎片:随着时间的推移,动态内存的频繁分配和释放可能导致堆区出现碎片。堆区碎片是指堆中剩余的不连续、无法利用的小块内存。虽然这不会直接影响程序的正确性,但在某些情况下,可能会降低内存的利用率
线程共享:堆区内存可以在线程之间共享,多个线程可以访问和使用堆区的相同内存。这使得堆区在多线程编程中非常有用,但也需要注意同步和避免竞争条件 - 全局/静态代码区:存放全局变量,和静态变量,在程序结束是自动释放
- 分为Bss Semgent:存放未初始化的全局与静态变量(.bss),不占用实际的磁盘空间,只在编译时预留内存空间
- Data Semgent:存放初始化的全局与静态变量(.data)
- 代码区(Text Semgent):通常也被称为文本区或只读区。存放程序的二进制代码(ELF)文件和常量,代码段是只读的,可以被多个进程共享
- .text
- .rodata,里面存放只读数据,其中虚函数表就在其中
程序变为可执行文件的过程
- 预处理阶段:预处理器(cpp)根据以#开头的命令修改原始的C程序,得到的通常是以.i文件扩展名为结尾的文件,也就是说,在这个阶段主要进行的是文本替换,宏展开,删除注释这类简单工作
对应的命令:Linux> gcc -E hello.c hello.i - 编译阶段:编译器将文本文件hello.i翻译成hello.s(用汇编语言程序翻译)
对应的命令:Linux> gcc -S hello.c hello.s - 汇编阶段:将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。也就是编译原理学的东西(词法分析,语法分析,语义检查和中间代码生成,代码优化,目标代码生成,用来发现语法错误)
对应的命令:Linux> gcc -c hello.c hello.o - 连接阶段:此时hello程序调用了printf函数。 printf函数存在于一个名为printf.o的单独的预编译目标文件中。 链接器(ld)就负责处理把这个文件并入到hello.o程序中,结果得到hello文件,一个可执行文件。最后可执行文件加载到储存器后由系统负责执行
对应的命令:Linux> gcc hello.cpp
注意: gcc -o 这里的-o并不是对应连接阶段,而是用于指定要生成的结果文件,后面跟的就是结果文件名字。链接器的作用就是以一组可重定位目标文件作为输入,生成可加载和运行的可执行目标文件
静态库与动态库的区别
- 函数库一般分为静态库和动态库两种。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为.a。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,gcc在编译时默认使用动态库。
ELF文件模型
C++什么时候生成默认拷贝构造函数
- 背景:如果不提供,就是浅拷贝(位拷贝),危害(堆上的资源,文件句柄socket)
什么时候触发默认拷贝构造函数
- 赋值的时候
- 函数传参,其中参数为类对象
- 函数返回值
什么时候生成默认拷贝构造函数
- 类成员变量也是一个类,该成员类有默认拷贝构造函数
- 类继承自一个基类,该基类有默认拷贝构造函数
- 类成员中有一个或者多个虚函数,如果没有拷贝构造函数
- 基类有虚函数,子类没有
衍生问题
函数返回值的细节
- 对于以下这种情况:
1 | A func(){ |
- 在以上情况不考虑编译器优化的时候,会有一次构造函数,一次析构函数(前两者由于作用域),一次拷贝构造函数(由于返回值)
- 而如果去掉优化,并且时C++11版本后,会有以下三种情况
- 看类有没有移动构造
- 看类有没有拷贝构造
- 报错
面向对象的三大特征以及特性
- 封装:
- 目的:隐藏实现细节,实现模块化(public,protect,private)
- 特性:访问权限,属性,方法
- 继承:
- 目的:无需修改原有类的情况下通过继承实现对功能的扩展(只能继承public以及protected,因为private 访问不到,虽然它也会在子类中)
- 特性:权限继承,基类在子类中最高权限
- 怎么破除继承:1.使用友元 2.使用using 这样可以让派生类能够使用基类的私有成员
- 多态: 有静态多态与动态多态 在上面有阐述
衍生问题
继承相关知识
什么是纯虚函数?什么是抽象类?
一个函数如果虚函数并且有等于0就为虚函数,此处0填充在虚表中,这会导致纯虚函数的虚表为0项,即无法创建虚表,无法实例化
一个类只要存在至少一个纯虚函数就是抽象类
接口继承与实现继承
- 接口继承就是一个类继承一个抽象类,并且这个子类必须重写父类这个抽象类的纯虚函数,并且要确保接口继承是一个”is-a”关系,也就是public继承,并且这个接口对于子类来说是有意义的,例如企鹅是鸟类但不能飞,所以鸟这个类就不能有飞行这个纯虚函数(接口)
- 实现继承就是派生类以protected或private继承一个基类时,派生类没有继承到基类的接口,而是继承到了基类的实现。实现继承就意味着派生类与基类不是”is-a”关系,而只是复用其实现或功能。
- 对于虚函数而言是继承接口与默认实现,对于非虚函数而言是继承的接口与强制实现
那接口继承相比于实现继承有什么好处吗
- 对于某些情况就可以避免危险,例如飞机的飞行模式实现,使用接口继承就比实现继承更安全
- 如图:
- 使用纯虚函数加提供protected下的默认实现函数来代替虚函数继承
纯接口继承与接口类
- 纯接口继承是指基类只提供接口,不提供定义,即严格代码规范下,基类的所有函数都是纯虚函数,不提供具体实现,派生类需要对所有方法进行自定义,这样的类型称为纯接口类。纯接口继承完全分离了接口与实现,依赖更少,如下例所示:
- 这样的接口类有以下三个特点:一,没有非静态成员变量;二,所有成员都是public成员;三,所有成员都是纯虚函数,析构函数除外,因此在上例Interface类中存在一个有定义的虚的析构函数。纯接口继承的优点是最小化调用处的依赖,且接口与实现完全分离,这样在只有实现发生变化时,调用处不会受到任何影响。而它的缺点是不利于代码复用,如果多个派生类都要实现相差不多的方法F(),就需要重复编写多遍F()的代码。
继承的时候不要覆盖非虚函数
- 因为这样就破坏了”is-a”关系,如果子类也编写了一个与基类同名的非虚函数,这种关系叫做覆盖而不是重写,是子类函数覆盖了基类函数,当我们直接通过派生类对象调用F()与通过基类指针调用F(),会产生不一样的行为!如图:
组合
- 将std::string变成Password的一个成员,而不是Password的基类,这样仍能使用std::string的各种功能,且不需要增加一种继承关系。这种方法被称为“组合”,它是比继承更灵活的复用方法。一般在可以用组合达到目的时,要尽量避免使用实现继承
什么是菱形继承
- InputFile和OutputFile同时继承于File类,而下一层IOFile类同时继承InputFile和OutputFile,即继承了两次File,这就是菱形继承
- 使用虚继承来解决菱形继承问题
线程池相关知识
什么线程池
- 线程池是一个高效管理线程的技术,它是预先创建好一组线程,并用这些预先创建好的线程来处理工作任务,这些线程可以在需要的时候被动态地分配和重用,而不是为每一个任务创建一个新线程,这可以大大减少创建和销毁线程的开销
- 线程池由三个部分组成:
- 任务队列:用于存放需要执行的任务
- 线程池管理器:负责管理线程池的创建,销毁和线程的分配回收等,会根据需要动态调整线程数量
- 工作线程:线程池中实际执行任务的线程,它们从任务队列中取出任务并执行
为什么要用线程池
- 可以降低资源消耗:可以减少创建和销毁线程的开销
- 提高性能:对于一个任务而言不需要等待线程的创建就能被执行
- 避免过度资源竞争:线程池可以限制同时执行的线程数量,避免过度竞争,提高稳定性
- 控制并发度:可以根据资源与负载情况动态调整线程数量
- 简化了线程管理的难度
可能会出现任务队列溢出:执行完一个任务的速度远小于进入任务队列的速度
怎么用线程池
进程和线程的区别
- 本质区别:进程是资源分配的基本单位,线程是CPU调度的基本单位
- 并发性:进程切换效率低,线程切换效率高
- 内存:进程有独立的虚拟地址空间,线程没有独立虚拟地址空间,是共享同一个进程,但是线程会分配独立的栈区,PC,本地存储等
- 所属关系:一个线程只能属于一个进程,一个进程可以拥有多个线程
- 健壮性:进程的健壮性高,因为进程切换在多进程的时候不会因为一个进程的宕机而影响整体,但是线程宕机就会影响同一进程下的其它线程
衍生问题
进程切换与线程切换的区别
- 进程在进行上下文切换的时候,要保留现场的运行环境(CPU的寄存器,程序计数器,用户空间的信息,内核空间pcb),进程切换在多进程的时候不会因为一个进程的宕机而影响整体
- 线程的切换,如果线程是在不同进程里的,和进程是一样的,如果是在同一个进程里就可以少保留一些信息(CPU寄存器,程序计数器),效率就高了,但是线程需要加锁。
描述系统调用整个流程
- 宏观视角:应用程序想要访问内核里的资源,应用程序通过函数库调用函数库中的系统调用,将用户态转变为内核态,调用完后再切换回用户态
- 微观视角:想要实现系统调用,离不开中断,当程序想要获取内核数据的时候,会触发中断,操作系统会把系统调用号放在寄存器(eax)中,并且会有一个中断号(比如 int 0x80),接下来操作系统会切换堆栈,也就是从用户态切换为内核态,在内核态中通过中断号在中断向量表中找到对应的中断处理程序(比如system_call),然后通过寄存器eax中的系统调用号在系统调用表中,找到对应的处理程序(比如syscall_read),执行相应操作,之后通过iret中断返回对应值,并切换回用户态
衍生问题
系统调用是否会引起线程/进程切换
- 不一定,如果我们使用阻塞的IO且IO未就绪,就会将线程或进程切换;运行态 -> 阻塞态,如果使用的是非阻塞IO就不会引起进/线程切换
用户态切换内核态(切换堆栈)具体是啥?
- 首先会切换中断上下文,也就是先保存用户态的上下文,之后会将CPU上的用户态指令切换成内核态的指令
task_struct的组成部分
- 标识符:与进程相关的唯一标识符,用来区别正在执行的进程和其他进程。
- 状态:描述进程的状态,因为进程有挂起,阻塞,运行等好几个状态,所以都有个标识符来记录进程的执行状态。
- 优先级:如果有好几个进程正在执行,就涉及到进程被执行的先后顺序的问题,这和进程优先级这个标识符有关。
- 程序计数器:程序中即将被执行的下一条指令的地址。
- 内存指针:程序代码和进程相关数据的指针。
- 上下文数据:进程执行时处理器的寄存器中的数据。
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表等。
- 记账信息:包括处理器的时间总和,记账号等等。
页面置换算法有哪些
- 产生页面置换原因:缺页中断-> 内存已满 -> 将某些页面置换到磁盘中
- 加载一个页面,如果产生缺页中断的全过程:
- 最佳页面置换算法(置换未来最长时间不访问的页面)
- 先进先出置换算法(FIFO) (选择在内存驻留时间最长的页面)
- 最近最久未使用置换算法 (选择最长时间没有被访问的页面)
- 时钟页面置换算法/最近未用算法 (环形列表实现)
- 改进版的时钟页面置换算法
tcp和udp的区别
- 相同点:都是传输层的协议,目的都是为了给上层提供服务
- 是否面向链接:tcp是面向链接,udp是面向无链接, 面向链接可以告诉我们:他建立链接需要三次握手,释放链接需要四次挥手,它是一个端对端的链接,他是一个全双工链接(双方可以互相发送数据),面向无连接:无需三次握手与四次挥手,可以一对一,一对多,多对一,多对多
- 数据传输方式:tcp是基于字节流,UDP是基于报文,对于TCP来说,完整的用户消息可能会被分成多个tcp报文进行传输,对于接收端而言,需要处理粘包(对于一个完整数据与一个不属于这个数据的数据链接在一起的问题)问题,而对于UDP,每次收发都是完整数据
- 是否可靠:TCP是可靠的,UDP是不可靠的,不可靠的意思就是,不保证消息交付的完整性,也不保证交付顺序,不进行拥塞控制,不进行流量控制(没有接收缓冲区)
- 传输效率:tcp效率低,udp效率高,tcp由于需要实现可靠传输,以及发送相同数据时,tcp所允许的大小比UDP小,因为tcp头是20字节,UDP头只有8字节
- 应用场景:TCP 主要是对于要求数据完整,效率不需要很高的场景(文件传输),UDP则是数据允许丢失,同时实时性要求高的的场景(网络直播,)
衍生问题
TCP的MSS和IP的MTU分别是什么?
- 首先MTU为时网络层的最大传输单元,以太网最大的数据帧是1518字节,这样刨去帧头14字节和帧尾CRC校验部分4字节,那么剩下承载上层IP报文的地方最大就只有1500字节,这个值就是以太网的默认MTU值。当传输的数据的字节数超过这个数据就得分片,
- MSS通常是MTU - IP头 - TCP头得到的数据,也就是1500 - 20 - 20 = 1460;,MSS的值通常需要两端的协商,得到具体值,TCP是传输层,IP在网络层,数据从传输层到网络层就要进行封装,IP头,TCP头就是封装
既然再IP层就会分片,为什么还要再TCP那进行分段,MSS
- 当数据大于这个(MSS)最大报文段长度的时候,要进行分段,这个值是建立连接的时候双方协商的,当双方每次发数据的时候都会协商一次,如果在TCP不分段传给IP的时候,一旦ip中的一个MTU发生丢包,就得重新传一大片数据,而如果采用了MSS分段,丢包的时候只需要重传丢的那一小块MSS就行,这样是为了效率,为了避免IP进行分片(加入TCP头 与 IP头)
TCP是如何保证可靠性的?
- 重传机制:解决的是数据丢失问题,是通过序列号和确认应答机制来实现的,
- 超时重传:也就是会有一个超时定时器,当设定来回的时间之内没有收到ACK,就会重传数据,就是超时后重传
- 快速重传:就是在超时之前收到三个相同的数据包确认,直接重传丢失的数据,
- 滑动窗口:(是一种机制,为下面两个服务)
- 可以连续发送多个字节数据,而不需要返回每个字节数据的确认
- 窗口:在没有应答的情况下发送方可以发送多少数据
- 滑动:收到确认包之后在移动窗口,例如连续发了300字节数据其中第201到300数据丢失了,下次发送的时候窗口左边界会移动到201,并根据对方(接收方)发来的窗口大小来调整大小(即发送方的滑动窗口由接收方决定)
- 流量控制:
- 通过接收方的处理能力来限制发送方的发送量,即解决接收方接收缓冲区满而丢失数据的问题
- 怎么控制? 先收缩窗口,再缩小缓冲区,当窗口收缩为0的时候,发送方由于不能发送数据,会发送一个1字节的探测报文,来探测接收方滑动窗口大小是否改变
- 拥塞控制:
- 解决的是网络不好的时候,接收方继续发送大量数据而导致数据时延或丢失的问题
- 怎么控制?首先接收方会有一个cwnd(拥塞窗口),接收方接收缓冲区会有一个接收窗口,发送方还一个ssthresh值(慢启动阀值),一开始发送方从1开始执行慢开始算法(拥塞窗口以指数增长),当到达设定的慢启动阀值的时候就开始进行拥塞避免算法(cwnd慢慢 + 1),如果此时遇到了超时重传,就会使得阀值减少一半,然后cwnd变为1从新开始慢启动,如果遇到的是快重传,就会执行快恢复(阀值减半,但cwnd不会下降为1而是会变为从阀值开始执行拥塞避免)如图,2N=vRTT,探测到拥塞说明管道的容量为当前窗口C,而C=2N,因此N=(1/2)C!
为什么需要三次握手
- 为什么最后要发送ack呢,因为如果主动连接端有一次SYN连接请求超时了,也就是在网络上滞留了,主动连接方会超时重传,当这个重传的被接受了,并且建立了链接,并正确释放连接后,滞留的连接请求到达了服务端,服务端发送确认请求,并且进入established阶段,但是由于客户端时closed,不会给予响应,就会导致服务端一直处于连接状态
为什么需要四次挥手
- 因为服务端可以继续发送未发完的数据,使用三次挥手可能导致数据不完整
为什么在四次挥手的时候需要等待2个MSL时间
- 保证A发送的最后一个ACK报文段能够到达B。这个ACK报文段有可能丢失,B收不到这个确认报文,就会超时重传连接释放报文段,然后A可以在2MSL时间内收到这个重传的连接释放报文段,接着A重传一次确认,重新启动2MSL计时器,最后A和B都进入到CLOSED状态,若A在TIME-WAIT状态不等待一段时间,而是发送完ACK报文段后立即释放连接,则无法收到B重传的连接释放报文段,所以不会再发送一次确认报文段,B就无法正常进入到CLOSED状态。
- 防止已失效的连接请求报文段出现在本连接中。A在发送完最后一个ACK报文段后,再经过2MSL,就可以使这个连接所产生的所有报文段都从网络中消失,使下一个新的连接中不会出现旧的连接请求报文段。
什么是SYN攻击?
- 我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN 报文,服务端每接收到 一个 SYN 报文,就进入 SYN_RCVD 状态,但服务端发送出去的 ACK + SYN 报文,无法得到未知 IP 主机的ACK 应答,久而久之就会占满服务端的 SYN 接收队列(未连接队列),使得服务器不能为正常用户服务。
如何唯一确定一个TCP连接呢?
- TCP 四元组可以唯一的确定一个连接,四元组包括如下: 源地址 源端口 目的地址 目的端口。
- 源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
- 源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
水平触发与边缘触发的区别
- 水平触发:
- 读事件:只要有数据可读(接受缓冲区还有数据),就继续触发读事件
- 写事件:当发送缓冲区没有满的时候,也就是还能继续写入数据的时候,会继续触发写事件
边缘触发:
- 读事件:只有新数据到达缓冲区的时候才会再次出发读事件
- 写事件:只有当发送缓冲区从满变为了不满时,才会触发写事件
由于水平触发时会一直调用读或写事件,所以得关闭EPOLLOUT,如果发送的数据很多会频繁关闭,所以一般来说边缘触发的效率要比水平触发要高
什么是半打开半关闭状态
- 当连接双方两端,一端断开了连接,而另一端没有发觉,继续发送数据,保留连接状态,服务端一方未能及时检测到对端已经关闭,会一直占用资源直至超时。
- 半打开套接字会占用文件句柄和内存,影响系统的性能。因此,半打开套接字需要特别注意,应尽快检测并关闭。
如何处理半打开状态
- 采用超时重连机制或心跳包机制,定期检查连接状态,检出异常就关闭套接字
- 添加空闲超时机制,在一段时间内未收到数据就关闭套接字
- 在应用层做好异常处理
- 使用TCP keep-alive选项,对端DOWN后,TCP层会自动关闭连接。
写文件时进程宕机,数据会丢失吗?
- 背景:
- 文件的写是在stdIO库中
- stdIO有缓冲区:可以setbuf自定义,作用是减少系统调用
- page cache:的作用是为了减少磁盘IO的词是,是为了提供速度,因为从内存到磁盘需要花费大量时间,在中间引入page cache后可以异步向磁盘载入数据,缺点:用户层无法优化page cache的策略,这也是为什么数据库要维护page管理
- 两种磁盘IO的方式:
- 缓存文件IO: 是用户态的缓冲区里的数据先经过page cache,再写入磁盘
- 直接文件IO:是直接从用户态的缓存区到磁盘
分两种情况
- 写文件如果没有调用fflush(write)宕机后会丢失数据
- 写文件如果调用过fflush,也就是把数据写入了page cache,进程宕机不会丢失,但是os崩了就会
- 假设进程宕机了,系统也关闭了,如果没调用write会丢失,调用了write但是用的是缓存文件IO也会丢失,用直接文件IO不会丢失
文件相关知识
- 用户态会有一个缓冲区,在内核态会有一个高速缓存区(page cache),还有磁盘,一共有四个接口,fflush(本质调用的是write)是把用户态缓冲区内容写入page cache,fsync(指定一个fd),fdatasync,sync(把所有文件全部载入磁盘) 是把page cache写入磁盘中
如图:
什么使用直接文件IO,什么时候使用缓存文件IO
- 大数据使用直接文件IO,因为大数据使用缓存文件IO,会把page cache全部占据了并从page cache中淘汰一些数据,如果将来这些数据被频繁读取,需要从磁盘重新读,会大大减低效率
- 小数据使用缓存文件IO
左值引用和右值引用区别
- 一个是针对左值,一个是引用右值,const左值引用,可以引用右值,但是不能修改(右值引用就是解决不能修改这个问题),右值引用也可通过std::move(将左值转换为右值)来引用左值,声明出来的左值引用和右值引用都是左值
- 功能差异:
- 左值引用是为了避免对象拷贝,常用在函数传参和函数返回值(A& b = func())中
- 右值引用是为了实现移动语义move 和 完美转发
- 右值引用的移动语义是为了在对象赋值时避免资源的重新分配,移动构造以及移动赋值构造,stl应用,std::unique_ptr,function等
衍生问题
什么是左值,什么是右值
- 左值:可以放在等号左边,可以取地址,并且是具备名字的,比如(变量,返回左值引用的函数调用,前置自增自减,赋值运算符,解引用)
- 右值:只能在等号右边,不可以取地址,没有具备名字
- 纯右值:常量(字面值),返回非引用类型的函数调用,后置自增自减,逻辑表达式,算数表达式,比较表达式
- 将亡值:是C++11新引入的值类型,与移动语义息息相关,移动构造和移动赋值构造处理的就是将亡值,并进行资源转移,之后将调用析构函数
右值引用的移动语义是什么意思?
- 就是用来处理将亡值这个右值,用于函数返回值时可以减少一次拷贝构造函数,因为它是将将亡值里的资源直接”移动”到被赋值的上面
- 移动构造函数:
1 | A(A&& a){ |
什么是完美转发
- 完美转发的意思就是函数模板可以将自己的参数完美的转发给内部调用的其他函数
- 完美是指不仅能准确的转发参数的值,还能准确的转发参数的左右值属性,因为右值它经过一系列处理后会变为左值
- 我的理解是想要实现完美转发就离不开万能引用 + 引用折叠 + std::forward函数
- forward函数:,它定义于move.h中
1 | forward<int>(a);// 这个与foward<int &&>(a)结果一样,无论a是左值还是右值都转换为右值引用 |
右值转发:forward(param) —> int&& 或 forward
(param) —> int&& 左值转发:forward
(param) —> int& 如果要转发其自身的类型,使用: forward
(param)
那什么是万能引用呢?
- 它既能接收左值又能接收右值
- 基于参数模板的, T &&,他会有以下几种情况:
- T &&碰到右值int &&, T匹配成int;
- T && 遇到左值 int ,也能匹配,T此时是int &。
- T && 碰到左值const int,T匹配为 const int &。
- T &&碰到左值const int (指针类型), T匹配为const int& (下略)
- T &&碰到左值const int const(指针类型), T匹配为const intconst & (下略)
- 其中就用到了引用折叠 int & && 这种情况 会被折叠为 int &
智能指针种类以及使用场景
- 指针管理的困境:
- 资源释放了,但是指针没置空,野指针,指针悬挂
- 没有释放资源导致内存泄漏
- 多个指针指向一个资源,导致重复释放一个资源
使用RAII思想:它通过在对象的构造函数中获取资源,在对象的析构函数中释放资源,从而确保资源的正确获取和释放,利用生命周期来正确释放资源
一共三种智能指针:shared_ptr:
- 解决的是多个指针指向一个资源的问题,共享所有权
- 原理:内部维护了一个引用计数,这个数字就是指向这个资源的指针数量,只有数为0才能通过析构函数释放资源,因为不同shared_ptr指针需要共享相同的内存对象,因此引用计数的存储是在堆上的
- 使用场景:通常用于一些资源创建昂贵比较耗时的场景, 比如涉及到文件读写、网络连接、数据库连接等。当需要共享资源的所有权时,例如,一个资源需要被多个对象共享,但是不知道哪个对象会最后释放它,这时候就可以使用std::shared_ptr
- weak_ptr:
- 是一个弱引用,指向的是shared_ptr所指的对象,而不影响所指对象的生命周期(不会改变引用计数)
- weak_ptr不能解引用,所以如果要访问所指对象,就得强制转换为shared_ptr,lock()函数就实现了该功能,成功返回共享对象的shared_ptr,失败返回空的shared_ptr
- 使用场景:
- 可用于实现缓存,因为当weak_ptr所知对象被销毁是,weak_ptr会自动释放,不会成为野指针,为什么不直接用shared_ptr,因为用这个之后引用计数永远不会为0
- 避免循环引用问题,两个对象的shared_ptr互相指向对方,导致形成一个环,互相依赖,从而导致内存泄漏,解决办法就是将其中一个shared_ptr改为weak_ptr
- 用于实现单例模式,,优点是避免循环应用:避免了内存泄漏。访问控制:可以访问对象,但是不会延长对象的生命周期。可以在单例对象不被使用时,自动释放对象。
unique_ptr:
- 独享所有权,一个资源只能有一个指针指向它,使用移动语义实现
原理:
禁用了拷贝构造和赋值构造
1
2unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;std::move() 可以将一个unique_ptr转移给另一个unique_ptr或者shared_ptr。转移后,原来的unique_ptr将不再拥有对内存的控制权,将变为空指针
1
2std::unique_ptr<int> p1 = std::make_unique<int>(0);
std::unique_ptr<int> p2 = std::move(p1);
什么设计模式,它解决的是什么问题
- 设计模式是指在软件开发中,通过前人验证的,用于解决特定环境下,一种通用的,可重用的解决方案
- 解决的是如何做到修改少量代码,就可以适应需求的变化,前提是(既有稳定点,又有变化点),就好比一个整洁的房间,好动的猫,怎么保证房间的整洁
相关知识
设计模式一共有哪几大类,设计模式的六大原则是什么
- 有三大类, 创建型模式(工厂模式,抽象工厂模式,单例模式,建造者模式,原型模式),结构型模式(适配器模式,代理模式,外观模式),行为型模式(策略模式,观察者模式,责任链模式)
- 六大原则:
- 开放封闭原则:
- 原则思想:尽量通过扩展软件实体来解决需求变化,而不是通过修改已有的代码
- 描述:一个软件产品在生命周期内,都会发生变化,既然变化是一个既定的事实,我们就应该在设计的时候尽量适应这些变化,以提高项目的稳定性和灵活性。
- 里氏代换原则:
- 原则思想:使用的基类可以在任何地方使用继承的子类,完美替换基类
- 大概意思是:子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法,子类中可以增加自己特有的方法
- 依赖倒转原则:
- 核心思想是面向接口编程,它要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,依赖抽象而不依赖具体
- 接口隔离原则:
- 这个原则的大概意思是使用多个隔离的接口,也就是各个接口之间的联系要尽可能小,即要降低接口之间的耦合度
- 类间的依赖接力在最基础的接口,尽量不要使用用户选择不用的接口
- 迪米特法则(最少知道原则):
- 原则思想:一个软件实体应当尽可能少的与其他实体发生相互作用,简称类之间的解耦
- 大概意思就是一个模块修改而尽量少的影响其他模块,也就是要高内聚,低耦合
- 单一职责原则:
- 一个方法只负责一件事件
- 优点:降低类和类的耦合,提高可读性,增加可维护性和可拓展性,降低可变性的风险
- 开放封闭原则:
单例模式
什么是单例,它可以在哪些地方用到了
- 保证一个类只有一个实例,并且提供一个访问该全局的访问点
- windows的任务管理器,windows的回收站,网站的计数器,多线程的线程池设计等等
单例的优缺点
- 优点:保证所有的对象都只会访问一个实例,避免对共享资源的多重占用,一个系统只存在一个对象,因此可以节约系统资源,所以如果需要频繁创建和销毁的对象时单例模式可以提高系统的性能
- 缺点:单例类不好扩展,不适用于变化的对象
单例创建方式
饿汉式
- 类初始化的时候,会加载该对象,调用效率高,线程安全
1 |
|
懒汉式
- 类初始化时,不会初始化该对象,而是在真正需要使用的时候才会创建该对象
1 |
|
静态内部方式
- 结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。
1 |
|
注意:这种方式 在C++11之前是不安全的,得加互斥锁,但是在C++11之后是安全的
如何选择单例模式的创建方式
- 如果不需要延迟加载单例,就用饿汉式
- 如果需要用延迟加载机制,可以用静态内部类或者懒汉式
工厂模式
- 定义一个创建对象的接口,让子类决定实例化哪个类,而对象的创建统一交由工厂去生产,有良好的封装性,既做到了解耦,也保证了最少知识原则。
- 工厂模式总共分为三类,简单工厂模式,工厂方法模式,抽象工厂模式
简单工厂模式
- 特点是需要在工厂类中做判断,从而创造相应产品,当需要增加产品种类的时候,不能够扩展,而只能修改源码,不满足开发封闭原则
- 举例:有一家生产处理器核的厂家,它只有一个工厂,能够生产两种型号的处理器核。客户需要什么样的处理器核,一定要显示地告诉生产工厂。下面给出一种实现方案:
1 | //程序实例(简单工厂模式) |
- 大概如图所示:
简单工厂模式的优缺点
- 优点:可以根据需求,动态生成使用者所需的类对象,而使用者不用去知道怎么创建对象,者使得模块各司其职,降低了系统耦合性
- 缺点:违反了开放封闭原则
工厂方法模式
所谓工厂方法模式(又称多态性工厂模式),是指核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。该核心类成为一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节
举例:这家生产处理器核的产家赚了不少钱,于是决定再开设一个工厂专门用来生产B型号的单核,而原来的工厂专门用来生产A型号的单核。这时,客户要做的是找好工厂,比如要A型号的核,就找A工厂要;否则找B工厂要,不再需要告诉工厂具体要什么型号的处理器核了。下面给出一个实现方案:
1 | //程序实例(工厂方法模式) |
工厂方法模式的优缺点
- 优点:扩展性好,符合了开放封闭原则,新增的时候只需要扩展子类就行
- 缺点:如果需要很多工厂,就要继承出很多子类
抽象工厂模式
- 举例:这家公司的技术不断进步,不仅可以生产单核处理器,也能生产多核处理器。现在简单工厂模式和工厂方法模式都鞭长莫及。抽象工厂模式登场了。它的定义为提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。具体这样应用,这家公司还是开设两个工厂,一个专门用来生产A型号的单核多核处理器,而另一个工厂专门用来生产B型号的单核多核处理器,下面给出实现的代码:
1 | //程序实例(抽象工厂模式) |
抽象工厂模式的优缺点
优点: 工厂抽象类创建了多个类型的产品,当有需求时,可以创建相关产品子类和子工厂类来获取。
缺点: 扩展新种类产品时困难。抽象工厂模式需要我们在工厂抽象类中提前确定了可能需要的产品种类,以满足不同型号的多种产品的需求。但是如果我们需要的产品种类并没有在工厂抽象类中提前确定,那我们就需要去修改工厂抽象类了,而一旦修改了工厂抽象类,那么所有的工厂子类也需要修改,这样显然扩展不方便。
抽象工厂模式和工厂方法模式的区别
- 传统的工厂方法模式,一般只能有一个纯虚函数,他的子类实现这个纯虚函数,一个只能创造一种产品,且每个产品都是同一种
- 抽象工厂模式,必须有多个纯虚函数,他的子类必须实现这些纯虚函数,并创建一系列相关的对象,即一个工厂可以创造多个有相同性质又有不同的产品
策略模式
- 定义:策略模式是定义了一系列算法,把它们一个个封装起来,并且使它们可互相替换,该模式使得算法可以独立于使用它的客户端变化而变化
例如:一个商城有多种打折方式,有国庆打折方式,劳动节打折方式,春节打折方式,又例如一个会员,有初级会员,中级会员,高级会员
如果不采用策略模式,在一个商城类中,既要有国庆打折方式,劳动节打折方式,春节打折方式,当一个打折方式变化后,这个商城类整体就要变化
- 如果采用设计模式,国庆打折方式抽象出来成为一个类劳动节打折方式和春节打折方式付也抽象出来成为类,这样就会有一个商城类和 一个国庆打折方式类,一个劳动节打折方式类,一个春节打折方式类,在商城类只需要一个指针,指向对应的打折方式就行
1 | class ProStategy{ |
其中左位使用前,右为使用后
应用案例
- 日志容错恢复机制:通常情况下,日志记录在数据库中,但是如果发生了异常,数据库暂时连接不上的情况,就会先将日志记录在文件中,之后在合适的时机再写回数据库,这就可以采用策略模式,把日志记录在数据库和记录在文件中当作两种记录日志的策略,在运行其动态选择(断网了选择记入在文件,连接的上就计入在数据库中)
观察者模式
- 定义:指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
解决了什么问题
- 稳定点:一对多的依赖关系,一变化多跟着变化
- 变化点:多增加或者多减少,不能影响依赖关系,也不能影响一的
例子
- 气象站发布气象资料给数据中心,数据中心经过处理,将气象信息更新到两个不同的显示终端(a和b)
1 |
|
- 博客订阅的例子,当博主发表新文章的时候,即博主状态发生了改变,那些订阅的读者就会收到通知,然后进行相应的动作,比如去看文章,或者收藏起来
1 | //观察者 |
代码结构
- 观察者接口(抽象类)
- 实现不同的观察者
- 目标对象接口(一个单例,用容器接收不同的观察者)
- 往容器添加与删除接口
- 推送变化
如果扩展代码
- 新增一个观察者,继承自观察者接口,实现观察者的变化逻辑,进容器
- 减少就是使用出容器
应用的案例
- 游戏业务开发场景:一个用户有许多的特性,比如装备系统,vip系统,人物面板系统,当一个系统改变,其他系统也跟着改变
优缺点
- 优点
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
- 目标与观察者之间建立了一套触发机制。
- 缺点
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
责任链模式
- 定义:使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止
- 人话:客户发出一个请求,会有很多对象可以处理这个请求,并且不同对象的处理逻辑是不一样的,对于客户而言,不关心是谁处理的,只是希望处理流程可以灵活多变,处理请求的对象需要方便修改处理请求的方式或者能被替换,以适应业务功能的变化
解决了什么问题
- 稳定点: 处理流程,请求按照链条传递,需要链表和请求处理的接口,要可打断(结束继续遍历)
- 变化点:处理节点的个数,处理顺序,处理逻辑,其中处理逻辑又分为处理方式和处理条件
例子
- 背景:请假流程,3天内需要主程序批准,30天内需要项目经理批准,30天以上需要老板批准
1 | // 工厂模式加责任链模式 |
代码结构
- 从单个节点出发:
- 抽象一个处理对象接口
- 处理对象继承自该接口
- 实现处理请求功能
- 实现链条关系
- 实现功能传递
- 实现一个构造链表关系的静态接口(工厂)
代码扩展
- 如果我要扩展出一个新的处理逻辑,只需新增一个类,继承自处理请假接口,并在工厂中加入链表
- 例如我要加一个董事长
1 | // 董事长处理者 |
应用案例
- nginx 就是用了责任链模式,http请求就是要经过11个阶段一个一个处理
装饰器模式
- 定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。
- 结构及实现:通常情况下,扩展一个类的功能会使用继承方式来实现。但继承具有静态特征,耦合度高,并且随着扩展功能的增多,子类会很膨胀。如果使用组合关系来创建一个包装对象(即装饰对象)来包裹真实对象,并在保持真实对象的类结构不变的前提下,为其提供额外的功能,这就是装饰模式的目标。下面来分析其基本结构和实现方法。
优缺点
优点:
- 装饰器是继承的有力补充,比继承灵活,在不改变原有对象的情况下,动态扩展功能,即插即用
- 通过使用不用装饰类及这些装饰类的排列组合,可以实现不同效果;
- 装饰器模式完全遵守开闭原则
缺点:装饰模式会增加许多子类,过度使用会增加程序得复杂性。
代码
1 |
|
- 往咖啡里加配料 就可以使用装饰者模式,我们可以动态地为咖啡添加各种配料,而不需要修改咖啡类的代码,这使得代码更加可维护和可扩展。
代码扩展
- 新增一个类继承自coffee装饰器类,实现具体功能
B树与B+树与二叉搜索树
- 什么是二叉搜索树?二叉搜索树是一种使用了二分的思想,将小于根节点的数值放在左子树,大于根节点的放在右子树,这样便于查找数据,但是也有局限性,当根节点为最小值或者最大值的时候会退化成O(N)的复杂度
- 什么是B树?是为了解决二叉搜索树效率不稳定的弊端,它已经不属于二叉树,而是一种多叉树,运用的也是二分思想,允许一个节点有多个索引,并且每个索引都有一个指针以及对应的数据,但由于没多一层高度,就会多一次操作,在数据库里要频繁IO,所以效率也不是特别高
- 什么是B+树?B+树是B树的改良版,它只允许叶子节点有数据,并且每个叶子节点通过链表连接。其中一个节点的子节点包含根节点所表示的索引,这个索引的是半闭半开的。B+树比B树的层级更少,查找效率更稳定和快。
语法基础
野指针 悬空指针 空指针
- 野指针是没有被初始化过的指针,指向的位置是不可知的(随机的、不正确的、没有明确限制的)
- 悬空指针:指针最初指向的内存已经被释放了的一种指针。(例子:返回局部变量的地址)
- 空指针:指针的值为0,不指向任何有效数据
union的相关知识
- union 是一个共用体,union 里面的属性是共享同一个内存,所以当我们sizeof(union),输出的大小是union里面内存最高的
指针相关知识
1 |
|
C++ 内存对齐机制
- C++内存对齐机制:内存对齐的时候 看的是结构体里面最大的字节,如果最大为8 则对齐的时候是8的倍数,如果最大为4,最对齐是4的倍数
std::string
- std::string 在初始化时给多少就会有多少字节,在后续扩展的时候,是以原来的两倍扩展,std::string 内部是用char* 实现的,所以当sizeof它的时候是4
const int ptr, int const ptr的区别是什么?
- const int ptr 表示的是 ptr所指向的内容是常量,是不可变的,int const ptr; 则表示其ptr指针的指向是不能改变的
什么是std::ref?
std::ref的作用是将一个值包装为reference_Wrapper,这个对象在bind 和 thread时会被识别为引用,这样就解决了bind与thread 无法传递引用的问题(因为原本会被拷贝为右值)
大致可以这么理解:在底层 ref函数会把 一个值的地址和类型封装成reference_wrapper,当我们调用的时候触发了仿函数(),取得了该地址下的值
1 | _CONSTEXPR20 operator _Ty&() const noexcept { |
std::ref 和不同引用的区别
- std::ref只是尝试模拟引用传递,并不能真正变成引用,在非模板情况下,std::ref根本没法实现引用传递,只有模板自动推导类型或类型隐式转换时,std::ref能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型。
总结
- 我来给总结下,首先我们讲解了std::ref的一些用法,然后我们讲解std::ref是通过std::reference_wrapper实现,然后我们借助了cppreference上的实现来给大家剖析了他本质就是存放了对象的地址(类似指针的用法😁),还讲解了noexcept等语法,最后我们讲解了下std::bind为什么要使用到reference_wrapper。
- std::bind使用的是参数的拷贝而不是引用,当可调用对象期待入参为引用时,必须显示利用std::ref来进行引用绑定。
- 多线程std::thread的可调用对象期望入参为引用时,也必须显式通过std::ref来绑定引用进行传参。
decltype, std::declval, std::decay_t 分别是什么?
decltype
- decltype是一个关键字,用于从一个表达式中推导出其类型。它通常与表达式一起使用,以便在编译时确定表达式的类型
1 | int x = 5; |
std::declval
- std::declval是一个函数模板,它能返回类型 T 的右值引用,其实是一个伪实例,不会产生任何临时对象,也不会因为表达式很复杂而发生真实的计算。因为不会真正的进行构造,所以可以实现在元编程时伪构造一个没有定义默认构造函数类,还可以避开纯虚基类不能实例化的问题,说白了它就是假装创建个对象(实际没创造)用于推导类型。
std::decay_t
std::decay_t用于获取一个类型的衰变类型(decay type)。衰变类型是指将一个类型转换为最基本形式的类型,通常是将引用和顶层 const 限定符去除,并将数组类型转换为指针类型。说白了就是一个类型转换为最基本形式的类型
如果类型是数组类型,则将其转换为指向数组首元素的指针类型。
- 如果类型是函数类型,则将其转换为指向函数的指针类型。
- 如果类型是引用类型,则将其转换为对应的非引用类型。
- 如果类型是顶层 const 限定符类型,则将其转换为对应的非 const 类型
const 与 constexpr
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。
注意,获得在编译阶段计算出结果的能力,并不代表 constexpr 修饰的表达式一定会在程序编译阶段被执行,具体的计算时机还是编译器说了算。const与constexpr,这两个都是编译期就能知道确切值,在C++11后const建议用来表示只读,constexpr来表示常量,constexpr修饰的是常量表达式,它可以修饰模板函数,可以修饰函数(除了typedef,using static_assert断言),只有一个返回值且必须是常量表达式,函数必须先声明,可以修饰构造函数
noexcept
- noexcept 是一个说明符同时也是一个操作符
- noexcept 作为说明符放在函数名后面,表明次函数不会抛出异常,等同于noexcept(true)
- noexcept 作为操作符时,可以用来判断一个函数是否会抛出异常,用法为 noexcept(funcName)
- 当使用 noexcept 标记函数时,我们需要自己保证函数不会抛出异常,这样可以生成更高效的代码,他会减少编译器对于抛出异常后对象的默认虚构
- 如果标记了 noexcept函数还是抛出了异常,那么程序会直接调用 std::abort() 终止程序,try…catch都没用
- C++17后noexcept成为了一种类型的一部分
使用 noecept之前的汇编代码:
1 | entrance(): |
使用 noecept之后的汇编代码:
1 | entrance(): |
- 总而言之就是优化了个析构函数
用户自定义字面量
- 其中用户自定义后缀尽可能使用“_”下划线作为开头,否则很可能会与C++原生的表示方式冲突,如2L其实是long long 2
1 | //定义 |
- 然而用户定义字面量也不是随意定义的,有如下规则限制:
1、字面量只可以使用四种基本类型:整型、浮点型、字符、字符串
2、若字面量为整型,参数只能为unsigned long long、const char,且当unsigned long long无法容纳该字面量时,会将其转换为字符串,以’\0’结束,并调用const char 参数版本的字面量函数
3、若字面量为浮点型,参数只能为unsigned double 和const char ,当unsigned double过长时,也会调用const char 版本
4、若字面量为字符型,参数只能为一个char
5、若字面量为字符串,参数只能为const char* (注意是与,传入两个参数)size_t,即长度已知的字符串作为参数
6、operator “” [用户定义字面量后缀],注意中间必须有空格
mutable volatile
- mutable 就是使得被mutable修饰的const成员可以被修改(const_cast也可以实现去const)
- volatile 表示直接存取原始内存地址,就是编译器优化的时候为了提高效率,会把一个变量读取到一个寄存器中,如果在本线程里值没有发生改变,就会直接从寄存器里取出上一次的值,如果在别的线程里被改变,编译器是识别不出来的,所以就会使用一个与实际不一样的值,这是很致命的。
explicit
- explicit 是禁用隐式转换
std::invoke
- std::invoke 是 C++17标准库中引入的一个函数模板,它的引入就是为了解决这个问题,它提供了一种统一的调用语法,无论是调用普通函数、函数指针、类成员函数指针、仿函数、std::function、类成员还是lambda表达式,都可以使用相同的方式进行调用。
1 |
|
- invoke 只是调用可调用函数 而不是封装
invoke 和function 的区别
std::invoke 与 std::function 是 C++ 标准库中不同的概念,它们有不同的作用和用途:
std::invoke:
是一个模板函数,用于调用可调用对象(函数指针、成员函数指针、仿函数等)。
它是一个通用的工具函数,用于在运行时动态调用不同类型的可调用对象。
不会持有可调用对象,只是调用它并返回结果。
在使用时需要指定要调用的可调用对象的类型,例如成员函数指针需要使用 &ClassName::member_function 的形式。std::function:
是一个模板类,用于封装可调用对象,使其表现得像一个函数。
它可以持有任何可调用对象,包括函数指针、成员函数指针、函数对象、Lambda 表达式等。
提供了一种统一的接口来处理不同类型的可调用对象。
可以在运行时动态改变持有的可调用对象。
总的来说,std::invoke 用于调用可调用对象,而 std::function 用于封装和管理可调用对象。它们的主要区别在于功能和使用方式。
stl容器里emplace_back和push_back的区别,emplace_back是不是能完美替代push_back
- 当加入一个已经存在的值的时候两个是一样的
1 |
|
- 当移动一个已经存在的对象的时候也是一样的
1 | Item item1(1, "car1"); |
- 当创建并加入的时候也是一样的
1 | Item item1(1, "car1"); // Item(int , string) |
- 直接构造一个匿名对象的时候 emplace_back效率更高,因为它是直接在vector容器的尾部执行构造函数
1 | // Item(int , string) |
- 这样笔误可能会让你找不出错误
如果使用std::move(t)来构造一个对象,但是该类没有显式提供移动构造函数,那么它是使用的式默认移动构造函数已经定义的拷贝构造
- 他会调用已经定义的拷贝构造,而不是默认的移动构造
在模板中使用 typedef的时候的注意事项
- typedef typename:
在模板中,typename 关键字用于指示其后的标识符是一个类型名字(type name)而不是变量名或者其他东西。这在模板的实例化过程中是非常重要的,因为编译器需要知道该标识符代表的是类型还是变量等。
通常情况下,当模板中使用了依赖于模板参数的类型名字时,需要在前面加上 typename 关键字,以告诉编译器这是一个类型。
- typedef:
typedef 关键字用于给一个类型定义一个别名,可以为已有的类型或者复杂的类型表达式定义一个简单的别名。
在模板中,typedef 用于给类型起一个别名,可以方便后续使用,但没有 typename 关键字那样特别用于模板的语法。
综上所述,typedef typename 用于模板中,指示其后的标识符是一个类型名字;而 typedef 则用于给类型定义别名,无论在模板中还是非模板中都可以使用。
什么叫做依赖于模板参数呢?
就是你定义的时候用到了模板嵌套类或者是你上一次typedef的的别名,如下
1 |
|
typedef typename bucket_data::iterator bucket_iterator;:在这里,bucket_iterator 是由 bucket_data 类型的迭代器类型决定的,而 bucket_data 是依赖于模板参数的,因此需要加上 typename。
又如下:
1 | template<typename Key, typename Value, typename Hash = std::hash<Key>> |
- 这用到了嵌套类 所以得用typename