Skip to content

ILoveU3D/EmbraceFoFeetCpp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 

Repository files navigation

C++面试题合集

  1. 说一说常用的 Linux 命令
    常用的 Linux 命令有:
    cd:切换当前目录
    ls:查看当前文件与目录
    grep:通常与管道命令一起使用,用于对一些命令的输出进行筛选加工
    cp:复制文件或文件夹
    mv:移动文件或文件夹
    rm:删除文件或文件夹
    ps:查看进程情况
    kill:向进程发送信号
    tar:对文件进行打包
    cat:查看文件内容
    top:查看操作系统的信息,如进程、CPU占用率、内存信息等
    free:查看内存使用情况
    pwd:显示当前工作目录

  2. 静态库和动态库如何制作及使用,区别是什么

  • 命名规则。静态库:libxxx.a,Windows: libxxx.lib;动态库: Linux : libxxx.so;Windows : libxxx.dll
  • 制作。静态库:1. gcc xxx.c xxx.c -c 2. 将 .o 文件打包,使用 ar 工具(archive)ar rcs libxxx.a xxx.o xxx.o;动态库:1. a.gcc 得到 .o 文件,得到和位置无关的代码 gcc -c –fpic/-fPIC a.c b.c 2. b.gcc 得到动态库 gcc -shared a.o b.o -o libcalc.so
  • 使用。编译程序时通过 “-l 库名” 参数进行编译。
  • 区别。静态库:gcc 进行链接时,会把静态库中代码打包到可执行程序中,编译时加载;发布程序时无需提供静态库,移植方便;消耗内存,更新部署发布麻烦。动态库: gcc 进行链接时,动态库的代码不会被打包到可执行程序中,运行时加载;发布程序时需要提供动态库;内存占用小,更新部署发布简单。
  1. GDB常见的调试命令 启动gdb:gdb 可执行性文件
    关闭gdb:quit/q
    查看当前文件代码:l/list 行号或者函数名
    查看非当前文件代码:l/list 文件名:行号或函数名
    设置断点:b/break
    查看断点:i/info b/break
    删除断点:d/delete 断点编号
    运行gdb:start(运行到第一行停) run(遇到断点停止)
    继续运行:c(到下一个断点停)
    向下执行一行代码(不会进入函数体):n/next
    查看堆栈信息:backtrace

  2. 进程调度算法有哪些

  • 先来先服务(FCFS)调度算法 先来先去服务调度算法是一种最简单的调度算法,也称为先进先出或严格排队方案。每次调度都是从后备作业(进程)队列中选择一个或多个最先进入该队列的作业(进程),将它们调入内存,为它们分配资源、创建进程,当每个进程就绪后,它加入就绪队列。当前正运行的进程停止执行,选择在就绪队列中存在时间最长的进程运行。
  • 短作业优先(SJF)调度算法 短作业优先(SJF)的调度算法是从后备队列中选择一个或若干个估计运行时间最短的作业(进程),将它们调入内存运行,短进程优先(SPF)调度算法从就绪队列中选择一个估计运行时间最短的进程,将处理机分配给它,使之立即执行,直到完成或者发生某件事而阻塞时,才释放处理机。
  • 优先级调度算法 优先级调度算法又称优先权调度算法,该算法既可以用于作业调度,也可以用于进程调度,该算法中的优先级用于描述作业运行的紧迫程度。在作业调度中,优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列;在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。
  • 高响应比优先调度算法 高响应比优先调度算法主要用于作业调度,该算法是对 FCFS 调度算法和 SJF 调度算法的一种综合平衡,同时考虑每个作业的等待时间和估计的运行时间。在每次进行作业调度时,先计算后备作业队列中每个作业的响应比,从中选出响应比最高的作业投入运行。
  • 时间片轮转调度算法 时间片轮转调度算法主要适用于分时系统。每次调度时,把 CPU 分配给队首进程,并令其执行一个时间片。时间片的大小从几 ms 到几百 ms。当执行的时间片用完时,由一个计时器发出时钟中断请求,调度程序便据此信号来停止该进程的执行,并将它送往就绪队列的末尾;然后,再把处理机分配给就绪队列中新的队首进程,同时也让它执行一个时间片。
  • 多级反馈队列调度算法 多级反馈队列调度算法是时间片轮转调度算法和优先级调度算法的综合和发展,通过动态调整进程优先级和时间片大小,多级反馈队列调度算法可以兼顾多方面的系统目标。
  1. 说一说什么是大端、小端,如何判断大端和小端 大端和小端指的是字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序。字节序分为大端字节序(Big-Endian) 和小端字节序(Little-Endian)。
  • 大端字节序:是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处

  • 小端字节序:是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处

  • 如何判断大端还是小端:可以定义一个联合体,联合体中有一个 short 类型的数据,有一个 char 类型的数组,数组大小为 short 类型的大小。给 short 类型成员赋值一个十六进制数 0x0102,然后输出根据数组第一个元素和第二个元素的结果来判断是大端还是小端。

#include <stdio.h> 
int main(){
    union {
        short value;
        char bytes[sizeof(short)];
    } test;
    test.value = 0x0102;
    if((test.bytes[0] == 1) && (test.bytes[1] == 2)) {
        printf("大端字节序\n");
    }else if((test.bytes[0] == 2) && (test.bytes[1] == 1)) {         printf("小端字节序\n");} 
    else {printf("未知\n");}
    return 0;
}

注意,大小端在异构通信时值得区分。

  1. 什么是孤儿进程,什么是僵尸进程,如何解决僵尸进程
  • 孤儿进程 孤儿进程是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并且由 init 进程对它们完整状态收集工作,孤儿进程一般不会产生任何危害。
  • 僵尸进程 僵尸进程是指一个进程使用 fork()函数创建子进程,如果子进程退出,而父进程并没有调用 wt() 或者wtpid() 系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。 解决僵尸进程 一般,为了防止产生僵尸进程,在 fork() 子进程之后我们都要及时在父进程中使用 wt() 或者 wtpid() 系统调用,等子进程结束后,父进程回收子进程 PCB 的资源。 同时,当子进程退出的时候,内核都会给父进程一个 SIGCHLD 信号,所以可以建立一个捕获 SIGCHLD 信号的信号处理函数,在函数体中调用 wt() 或wtpid(),就可以清理退出的子进程以达到防止僵尸进程的目的。
  1. 说一说进程通信的方式有哪些?
    1、管道(匿名管道):本质上是内核中维护的一块内存缓冲区,Linux系统中由pipe()函数创建,只能用于具有亲缘关系的进程间通信。 2、命名管道:不同于匿名管道之处在于它提供了一个路径名与之关联,进程通过访问该路径就能相互通信,适用于没有亲缘关系的进程间通信。 3、信号:一种异步通信方式,信号可以让一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。 4、消息队列:是一个消息链表且随内核持续,具有特定的格式和特定的优先级,由具有写权限的进程添加消息,由具有读权限的进程读走消息。 5、共享内存:允许两个或者多个进程共享物理内存的同一块区域(通常被称为段),无需内核介入,速度比管道快。 6、内存映射 :将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。 7、信号量:主要用来解决进程和线程间并发执行时的同步问题,信号量操作分为P操作和V操作,P将信号量值-1,V将信号量值+1。当信号量值<=0时,进行P操作进线程会被阻塞,直到另一个进程或线程执行了V操作将信号量的值大于0时。 8、 Socket 套接字:一般用于网络中不同主机上的进程之间的通信,且提供了应用层进程利用网络协议交换数据的机制。

  2. 进程有多少种状态
    进程有五种状态:创建、就绪、执行、阻塞、终止:

  • 创建:一个进程启动,首先进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
  • 就绪状态:在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
  • 运行状态:获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
  • 阻塞状态:在运行状态期间,如果进行了阻塞的操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
  • 终止状态:进程结束或者被系统终止,进入终止状态。
  1. 请你说说进程和线程的区别
  • 进程是操作系统进行资源调度和分配的基本单位,线程是操作系统可执行的最小调度和分配单位
  • 一个线程属于一个进程,一个进程可以有多个线程
  • 一个进程崩溃不影响其他进程,但是一个线程崩溃会让进程崩溃
  • 进程在执行过程中有独立的存储单元,而线程之间共享进程的内存
  • 进程之间切换系统开销大,而线程之间切换开销比进程小
  1. 请你说说线程和协程的区别
  • 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
  • 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;
  • 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
  • 线程进程都是同步机制,而协程则是异步;
  • 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
  • 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。
  1. 请你介绍一下死锁,产生的必要条件,产生的原因,怎么预防死锁?
    死锁的概念以及产生条件:死锁是两个或两个以上的进程之间由于竞争现象而导致进程都不能继续执行的现象。产生死锁的三个必要条件是互斥、不抢占和占有并等待,这些条件都具备只是有可能会造成死锁,只有第四个条件循环等待也具备的时候才会必然出现死锁。 如何预防死锁:设置某些限定条件,去破坏四个必要条件中的一个或多个,来预防死锁。比如说打破不可抢占条件:当一个进程占有一个资源时又申请另外一个资源,但不能满足时,就退出,并且返还原来占有的资源。
    产生死锁的必要条件 虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:
  • 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;
  • 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
  • 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
  • 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
  1. 请你说说分段和分页
  • 分段。将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段为单位,段与段在内存中可以不相邻接,实现了离散分配。分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
  • 分页。用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。分页主要用于实现虚拟内存,从而获得更大的地址空间。
  • 段页式。页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。
  1. 请你说说互斥锁和自旋锁
  • 互斥锁 互斥锁也称为互斥量(Mutex),是一种用来保护临界区的特殊变量, 它可以处于锁定(locked) 状态, 也可以处于解锁(unlocked) 状态: - 如果互斥锁是锁定的, 就是某个特定的线程正持有这个互斥锁 - 如果没有线程持有这个互斥锁,那么这个互斥锁就处于解锁状态 每个互斥锁内部有一个线程等待队列,用来保存等待该互斥锁的线程。当互斥锁处于解锁状态时, 如果某个线程试图获取这个互斥锁, 那么这个线程就可以得到这个互斥锁而不会阻塞;当互斥锁处于锁定状态时, 如果某个线程试图获取这个互斥锁, 那么这个线程将阻塞在互斥锁的等待队列内。
  • 自旋锁 自旋锁与互斥锁类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等(自旋)阻塞状态。自旋锁可以用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。 自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行忙循环 - 旋转 - 等待锁重新可用。
  1. 请你说说共享内存
  • 什么是共享内存 共享内存是进程间通信的一种方式。不同进程之间共享的内存通常为同一段物理内存,进程可以将同一段物理内存连接到他们自己的地址空间中,所有的进程都可以访问共享内存中的地址。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
  • 共享内存的优点 因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
  • 共享内存的缺点 共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。
  1. 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,让程序获得更多的可用内存。
  • 空间独立。由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的。这就解决了多进程之间物理地址冲突的问题。
  • 可以扩充内存。每个进程拥有自己独立的虚拟地址空间,但不需要让进程中所有的页都必须映射到物理内存中,当程序引用到不在物理内存中的页时,由硬件执行必要的内存映射,将缺失的部分装入物理内存并重新执行失败的指令,这也使得有限的内存运行大程序成为可能。
  • 安全性强。页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
  1. 缺页置换的常用算法
  • OPT 最佳置换算法(理想):将当前页面中在未来最长时间内不会被访问的页置换出去
  • 先进先出:淘汰最早调入的页面
  • 最近最久未使用 LRU:每个页面有一个t来记录上次页面被访问直到现在,每次置换时置换t值最大的页面(用寄存器或栈实现)
  • 时钟算法clock(也被称为最近未使用算法NRU):页面设置访问为,将页面链接为一个环形列表,每个页有一个访问位0/1, 1表示又一次获救的机会,下次循环指针指向它时可以免除此次置换,但是会把访问位置为0, 代表他下次如果碰到循环指针就该被置换了。页面被访问的时候访问位设为1。页面置换的时候,如果当前指针的访问位为0,置换,否则将这个值置为0,循环直到遇到访问位为0的页面。
  • 改进型Clock算法:在clock算法的基础上添加一个修改位,优先替换访问位和修改位都是0的页面,其次替换访问位为0修改位为1的页面。
  • 最少使用算法LFU:设置寄存器记录页面被访问次数,每次置换当前访问次数最少的。
  1. 请你说说条件变量
    条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。 使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作: 唤醒 再次获取互斥锁 重新评估条件

  2. 请你说说 TCP 和 UDP 的区别
    首先 UDP 协议和 TCP 协议都是运输层协议,都是为应用层程序服务,都具有复用(不同的应用层协议可以共用 UDP 协议和 TCP 协议)和分用(将数据报解析之后分发给不同的应用层程序)的功能。UDP 提供面向无连接基于数据报的不可靠传输,TCP 提供面向连接基于字节流的可靠传输。UDP 在很多实时性要求高的场景有很好的表现,而 TCP 在要求数据准确、对速度没有硬性要求的场景有很好的表现。

  • UDP协议:面向无连接(不需要三次握手和四次挥手)、尽最大努力交付、面向报文(每次收发都是一整个报文段)、没有拥塞控制不可靠(只管发不管过程和结果)、支持一对一、一对多、多对一和多对多的通信方式、首部开销很小(8字节)。优点是快,没有TCP各种机制,少了很多首部信息和重复确认的过程,节省了大量的网络资源。缺点是不可靠不稳定,只管数据的发送不管过程和结果,网络不好的时候很容易造成数据丢失。又因为网络不好的时候不会影响到主机数据报的发送速率,这对很多实时的应用程序很重要,因为像语音通话、视频会议等要求源主机要以恒定的速率发送数据报,允许网络不好的时候丢失一些数据,但不允许太大的延迟,UDP很适合这种要求。
  • TCP协议:是TCP/IP体系中非常复杂的一个协议,面向连接(需要三次握手四次挥手)、单播(只能端对端的连接)、可靠交付(有大量的机制保护TCP连接数据的可靠性)、全双工通讯(允许双方同时发送信息,也是四次挥手的原由)、面向字节流(不保留数据报边界的情况下以字节流的方式进行传输,这也是长连接的由来。)、头部开销大(最少20字节)。优点是可靠、稳定,有确认、窗口、重传、拥塞控制机制,在数据传完之后,还会断开连接用来节约系统资源。缺点是慢,效率低,占用系统资源高,在传递数据之前要先建立连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接。在要求数据准确、对速度没有硬性要求的场景有很好的表现,比如在FTP(文件传输)、HTTP/HTTPS(超文本传输),TCP很适合这种要求。
  1. 请你说说 TCP 三次握手四次挥手过程
    三次握手过程:
  • 第一次握手:客户端向服务器端发送连接请求报文段,包含自身数据通讯初始序号,进入SYN-SENT状态。
  • 第二次握手:服务器端收到连接请求报文段后,如果同意,发送应答,包含自身数据通讯初始序号,进入SYN-RECEIVED状态。
  • 第三次握手:客户端收到应答,最后向服务器端发送确认报文,进入ESTABLISHED状态,此时成功建立长连接。 四次挥手过程:
  • 第一次挥手:客户端认为数据发送完毕,需要向服务器端发送连接释放请求。
  • 第二次挥手:服务器收到连接释放请求,告诉应用层释放TCP连接。然后发送ACK包,进入CLOSE-WT状态,此时表明客户端到服务器端的连接已经释放,不再接受客户端的数据。因为TCP是全双工的,所以服务器仍可以发送数据。
  • 第三次挥手:当服务器端数据发送完毕,向客户端发送连接释放请求,进入LAST-ACK状态。
  • 第四次挥手:客户端收到连接释放请求,向服务器端发送确认应答报文,此时客户端进入TIME-WT状态,持续2倍的MSL(最长报文段寿命),若期间没有收到服务器端的数据报文,进入CLOSED状态。服务器端收到确认应答后,也进入CLOSED状态。
    附加: 以下是客户端向服务器端发起TCP连接的详细过程: 1. 客户端和服务器端刚开始都是处于CLOSED(关闭)状态。 2. 要注意的是客户端主动打开连接,而服务器端是被动打开连接的。 3. 服务器端的进程先创建TCB(传输控制块)准备接受客户端的连接请求。 4. 客户端的进程也是先创建TCB(传输控制块),然后向服务器端发出连接请求报文段,这个报文段中的同步位SYN置为1,同时选择一个初始序号seq=x。TCP协议规定了SYN=1的报文段不可以携带数据,但是要消耗掉一个序号。这个时候客户端进入SYN-SENT状态。 5. 服务器端收到连接请求报文之后,如果同意连接,就给客户端发送确认响应。在确认报文中应该将同步位SYN和ACK都置为1,而确认号是ACK+1。这时候服务器端也需要给自己选一个初始序号seq=y。值得注意的是这个确认报文也不能携带数据,同样要消耗掉一个序号。这时服务器端进入SYN-RECEIVED状态。 6. 客户端进程收到服务器端的确认报文,最后还要向服务器端给出确认。确认报文段的ACK置为1,确认号是y+1,而自己的序号seq=x+1。TCP标准规定,ACK报文段可以携带数据,但是如果不携带数据就不消耗序号。在这个情况下,下一个数据报文的序号仍然是seq=x+1。到这时,TCP连接已经成功建立,A进入ESTABLISHED(已建立连接)状态。 到此TCP连接三次握手的过程就全部结束了。但是为什么一定要三次握手而不是两次,为什么客户端最后还需要发送一次确认报文呢?其实主要是为了防止已经失效的连接请求报文突然又被传送给了服务器端,然后产生错误。假设现在有一种情况,客户端发出的第一个连接请求报文段并没有丢失而是在某些网络节点上被滞留了,直到客户端和服务器端的新连接已经释放后的某个时间点,第一个连接请求报文段才到了服务器端,这时候服务器端以为客户端又发起了一次请求,于是服务器端向客户端发起了确认连接报文段,同意连接。假设不采用三次握手,这时候连接已经建立了,但是客户端并不知道这个情况,服务器端会一直等待客户端的数据报文,这样服务器端的资源就会被浪费,占用大量的资源。所以采用三次握手可以防止这种现象,保护网络和系统资源。 TCP连接释放的过程比较复杂,客户端和服务器端都可以主动释放连接。下面是从客户端主动释放连接为例讲解四次挥手的详细过程: 1. 客户端的应用进程先向TCP发出一个连接释放报文段,然后停止发送数据报,主动关闭TCP连接。客户端需要将连接释放报文段首部的终止控制FIN置为1,序号设置为u,u相当于前面传输的数据报文段的最后一个字节的序号加1。这时候客户端进入FIN-WT-1(终止等待1)状态,等待服务器端的确认。需要注意的是,FIN报文段也是即使不携带数据,它也消耗一个序号。 2. 服务器在收到客户端发来的连接释放报文段请求之后就发出确认,确认号ack=u+1,这个报文段自己的序号是v,v相当于之前已经传送出去的最后一个报文段的序号加1。这时候服务器端进入CLOSE-WT(关闭等待)状态,这时候服务器端的TCP进程就要通知应用进程,客户端到服务器端的连接已经关闭了。需要注意的是,这个时候的TCP连接就处于一个半关闭(half-colse)的状态,尽管客户端已经没有数据要发送了,但是服务器端还是可以向客户端发送数据的,服务器端到客户端的连接并没有被释放掉。 3. 如果服务器端也没有数据要发送给客户端了,那么应用进程就通知TCP释放连接。这时候服务器端发出的连接释放报文段请求的终止指令FIN也置为1。这时候服务器端的序号已经是w了,因为在半关闭状态服务器端可能又发送了一些数据,服务器也必须重复上次已经发送过的确认号ack=u+1。这时候服务器端进入LAST-ACK(最后确认)状态,等待客户端的确认。 4. 客户端收到服务器端的连接释放请求报文段之后,必须发出确认。在确认报文段中把ACK置为1,确认号ack=w+1,而自己的序号是seq=u+1(根据TCP标准,FIN消耗了一个序号),然后进入TIME-WT(时间等待)状态,这时候连接并没有释放掉,必须等到2倍的MSL(最长报文段寿命)之后,连接才会释放。
  1. 说说OSI 七层模型
    在计算机网络中要做到正确的数据交换,就必须提前约定好相应的规则。OSI七层模型是一个协议栈,就是为了统一计算机网络标准,方便数据的交换。它自上而下依次为:
  • 应用层:应用层是体系结构中的最高层,是应用进程间通信和交互的规则,进程指计算机中运行的程序。也是用户与应用程序之间的一个接口,操作程序(软件,Web应用),进而触发更下层的服务。 协议:HTTP、HTTPS、FTP、TFTP、SMTP等
  • 表示层:对从应用层获取到的数据报文数据进行格式处理、安全处理和压缩处理。 格式:JPEG、ASCll、加密格式等
  • 会话层:对当前主机进程和目标主机进程会话的建立、管理和终止行为。
  • 传输层:对两台主机进程也就是应用层提供数据传输服务。定义了传输数据的进程端口号,负责数据包的排序、差错检验和流量控制等。 协议:UDP、TCP
  • 网络层:主要进行逻辑地址的查询。 协议: ICMP、IGMP、IP(IPv4、IPv6)
  • 数据链路层:建立相邻节点的逻辑连接,进行逻辑地址寻址、差错校验等。 协议:ARP、RARP、PPP 等
  • 物理层:物理层上数据的单位是Bit比特,数据的传输都是通过0(或1)比特流来实现的,而0(或1)比特流与电压的高低有关。负责了最底层数据传输的建立、传输和断开。
  1. 请你说说 TCP 如何实现可靠传输 可靠传输的具体实现是:
  • 应用层的数据会被分割成TCP认为最适合发送的数据块。
  • 序列号:TCP给发送的每一个包都进行编号,接收方对数据包进行排序,把有序数据传送给应用层,TCP的接收端会丢弃重复的数据。
  • 检验和:TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。
  • 确认应答:如果收到的数据报报文段的检验和没有差错,就确认收到,如果有差错,TCP就丢弃这个报文段和不确认收到此报文段。
  • 流量控制:TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。
  • 拥塞控制:当网络拥塞时,减少数据的发送。
  • 停止等待协议:它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
  • 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
  1. 请你说说 TCP 和 UDP 的使用场景
    UDP的优点是快,没有TCP各种机制,少了很多首部信息和重复确认的过程,节省了大量的网络资源。缺点是不可靠不稳定,只管数据的发送不管过程和结果,网络不好的时候很容易造成数据丢失。又因为网络不好的时候不会影响到主机数据报的发送速率,这对很多实时的应用程序很重要,因为像语音通话、视频会议等要求源主机要以恒定的速率发送数据报,允许网络不好的时候丢失一些数据,但不允许太大的延迟。DNS和ARP协议也是基于UDP实现的,要求快速获取IP、MAC地址,如果基于TCP那么对整个因特网的资源占用过大且速度慢。还有游戏应用程序也是通过UDP来传输报文段,允许出现丢帧导致的卡顿,但是对游戏的整体体验不会产生严重的影响。所以UDP在语音、视频、寻址、游戏、广播方面有很好的应用前景,实时性高,允许部分的数据丢失。 TCP的优点是面向连接提供可靠交付,即对数据有保证、无差错的进行运输。当需要数据准确无误的运输给对方时,如浏览器中需要获取服务器资源使用的HTTP/HTTPS协议,需要保证文件准确、无差错,邮件服务器中使用的SMTP协议,保证邮件内容准确无误的传递给对方,或者是大型应用程序文件,这些都要保证文件的准确、无差错的运输给对方,所以一定要基于TCP来运输,而不是UDP。

  2. 请你说说 HTTP 和 HTTPS 的区别
    由于HTTP简单快速的特性,当客户端向服务器端请求数据的时候,只需要传送请求方法和路径就可以取到结果,基于TCP,默认端口号为80,耗时可以简略计算为1RTT,传递的数据全部是明文传输,几乎没有安全性。HTTPS是基于TLS的,而TLS又基于TCP,当客户端向服务器端请求数据的时候,服务器大概率会将客户端重定向到该服务器的443端口,进行新的TCP连接,此时服务器会返回一个证书文件,而不是响应报文体。此时客户端验证证书文件紧接创建对称密钥,之后重新和服务器建立TLS连接,当服务器返回ACK确认之后,连接正式建立,此时上方整个过程耗时为3RTT,并且之后和服务器的通信数据都是通过对称密钥加密过的,几乎无法破解。
    HTTP和HTTPS的不同点总结如下:

  • HTTP是基于TCP的,而HTTPS是基于TLS的
  • HTTP的往返时间为1RTT,而HTTPS的往返时间为3RTT
  • HTTP只需要创建一次TCP连接,而HTTPS需要创建两次TCP连接
  • HTTP的默认端口号为80,而HTTPS的默认端口号为443
  • HTTP的安全性很差,而HTTPS的安全性很强。
  1. 请你说说 HTTP 状态码及其含义
  • 1xx:指定客户端相应的某些动作,代表请求已被接受,需要继续处理。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。
  • 2xx:代表请求已成功被服务器接收、理解、并接受。这系列中最常见的有200、201状态码。
  • 200(成功):服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
  • 201(已创建):请求成功并且服务器创建了新的资源。
  • 202(已接受):服务器已接受请求,但尚未处理。
  • 203(非授权信息):服务器已成功处理了请求,但返回的信息可能来自另一来源。
  • 204(无内容):服务器成功处理了请求,但没有返回任何内容。
  • 205(重置内容):服务器成功处理了请求,但没有返回任何内容。
  • 206(部分内容):服务器成功处理了部分 GET 请求。
  • 3xx:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后续的请求地址(重定向目标)在响应头Location字段中指明。这系列中最常见的有301、302状态码。
  • 300(多种选择):针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择。
  • 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
  • 302(临时移动):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  • 303(查看其他位置):请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码。
  • 304(未修改):自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
  • 305(使用代理):请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理。
  • 307(临时重定向):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
  • 4xx:表示请求错误。代表了客户端看起来可能发生了错误,妨碍了服务器的处理。常见有:401、404状态码。
  • 400(错误请求):服务器不理解请求的语法。
  • 401(未授权):请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
  • 403(禁止):服务器拒绝请求。
  • 404(未找到):服务器找不到请求的网页。
  • 405(方法禁用):禁用请求中指定的方法。
  • 406(不接受):无法使用请求的内容特性响应请求的网页。
  • 407(需要代理授权):此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理。
  • 408(请求超时):服务器等候请求时发生超时。
  • 409(冲突):服务器在完成请求时发生冲突。 服务器必须在响应中包含有关冲突的信息。
  • 410(已删除):如果请求的资源已永久删除,服务器就会返回此响应。 - 411(需要有效长度):服务器不接受不含有效内容长度标头字段的请求。
  • 412(未满足前提条件):服务器未满足请求者在请求中设置的其中一个前提条件。
  • 413(请求实体过大):服务器无法处理请求,因为请求实体过大,超出服务器的处理能力。
  • 414(请求的 URI 过长):请求的 URI(通常为网址)过长,服务器无法处理。
  • 415(不支持的媒体类型):请求的格式不受请求页面的支持。
  • 416(请求范围不符合要求):如果页面无法提供请求的范围,则服务器会返回此状态代码。
  • 417 (未满足期望值):服务器未满足"期望"请求标头字段的要求。
  • 5xx:代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。常见有500、503状态码。
  • 500(服务器内部错误):服务器遇到错误,无法完成请求。
  • 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码。
  • 502(错误网关):服务器作为网关或代理,从上游服务器收到无效响应。
  • 503(服务不可用):服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
  • 504(网关超时):服务器作为网关或代理,但是没有及时从上游服务器收到请求。
  • 505(HTTP 版本不受支持):服务器不支持请求中所用的 HTTP 协议版本。
  1. 请你说说 GET 和 POST 的区别
  • get主要用来获取数据,post主要用来提交数据。
  • get的参数有长度限制,最长2048字节,而post没有限制。
  • get的参数会附加在url之 ,以 " ? "分割url和传输数据,多个参数用 "&"连接,而post会把参数放在http请求体中。
  • get是明文传输,可以直接通过url看到参数信息,post是放在请求体中,除非用工具才能看到。
  • get请求会保存在浏览器历史记录中,也可以保存在web服务器日志中。
  • get在浏览器回退时是无害的,而post会再次提交请求。
  • get请求会被浏览器主动缓存,而post不会,除非手动设置。
  • get请求只能进行url编码,而post支持多种编码方式。
  • get请求的参数数据类型只接受ASCII字符,而post没有限制。
  1. 请你说说 TIME_WT
    TCP连接第四次挥手结束时,主动发起连接释放请求的一方进入TIME_WT状态,此时主动发起连接释放请求的一方会等待2MSL(最大报文生存期)才会回到初始状态CLOSED。

  2. 请你说说拥塞控制机制
    拥塞控制就是防止太多的数据进入到网络中,这样可以使网络中的路由器或者链路不会过载,首先要求当前的网络可以承受住现有的网络负荷,它是一个全局性的过程,拥塞控制的算法有以下四种:慢开始、拥塞避免、快重传、快恢复。

  3. 请你说说TCP 的流量控制
    如果发送方把数据发送得过快,接收方可能就来不及接受到所有的数据,中间可能会丢失数据报。流量控制就是让发送方的发送速率不要过快,让接收方来得及接收所有的数据。

  4. 请你说说 HTTPS
    HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的HTTP通道,在HTTP的基础上通过身份认证和传输加密阶段保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入TLS(Transport Layer Security 安全传输层协议)/SSL(Secure Sockets Layer 安全套接层协议),HTTPS 的安全基础是 TLS/SSL,因此加密就需要TLS/ SSL。HTTPS的特点是:内容加密、身份验证、数据完整性。

  5. 请你说说 HTTPS 加解密的过程是怎么样的?
    HTTPS数据加解密过程中数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。HTTPS协议加密的过程可以分为两个阶段,分别是:

  • 证书的认证阶段:使用非对称加解密算法对数据传送阶段的对称加解密密钥进行加密和解密。
  • 数据传送阶段:通过证书认证阶段获取到目标服务器的对称加解密密钥,对数据进行加密传送给服务器。
  1. 请你说说 TCP 粘包
    多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若发送方发送数据包的长度和接收方在缓存中读取的数据包长度不一致,就会发生粘包,发送端可能堆积了两次数据,每次100字节一共在发送缓存堆积了200字节的数据,而接收方在接收缓存中一次读取120字节的数据,这时候接收端读取的数据中就包括了下一个报文段的头部,造成了粘包。\
    解决粘包的方法:

  2. 发送方关闭Nagle算法,使用TCP_NODELAY选项关闭Nagle功能

  3. 发送定长的数据包。每个数据包的长度一样,接收方可以很容易区分数据包的边界

  4. 数据包末尾加上\r\n标记,模仿FTP协议,但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界

  5. 数据包头部加上数据包的长度。数据包头部定长4字节,可以存储数据包的整体长度 5. 应用层自定义规则

  6. UDP 怎么样可以实现可靠的传输?
    UDP不是面向连接的协议,因此资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。如果想要使用UDP还要保证数据的可靠传输,就只能通过应用层来做文章。实现的方式可以参考TCP的可靠传输机制,差别就是将TCP传输层功能,如确认机制、重传功能、流量控制、拥塞控制等功能实现在了应用层。从应用层角度考虑分别是:

  • 提供超时重传机制,能避免数据报丢失的问题。
  • 提供确认序列号,保证数据拼接时候的正确排序。
  1. 请你说说滑动窗口
    在流量控制中那些已经被客户端发送但是还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为N的窗口,随着TCP协议的运行、数据的运输,这个窗口在序号空间向前滑动,因此这个窗口被称为滑动窗口。

  2. 请你介绍一下 I/O 多路复用 I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。I/O 多路复用能够在单个线程中,通过监视多个 I/O 流的状态来同时管理多个 I/O 流,一旦检测到某个文件描述符上我们关心的事件发生(就绪),能够通知程序进行相应的处理(读写操作)。 Linux 下实现 I/O 复用的系统调用主要有 select、poll 和 epoll。 select select 的主旨思想:

  • 首先要构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中,这个文件描述符的列表数据类型为 fd_set,它是一个整型数组,总共是 1024 个比特位,每一个比特位代表一个文件描述符的状态。比如当需要 select 检测时,这一位为 0 就表示不检测对应的文件描述符的事件,为 1 表示检测对应的文件描述符的事件。
  • 调用 select() 系统调用,监听该列表中的文件描述符的事件,这个函数是阻塞的,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回,并修改文件描述符的列表中对应的值,0 表示没有检测到该事件,1 表示检测到该事件。函数对文件描述符的检测的操作是由内核完成的。
  • select() 返回时,会告诉进程有多少描述符要进行 I/O 操作,接下来遍历文件描述符的列表进行 I/O 操作。 poll poll 的原理和 select 类似,poll 支持的文件描述符没有限制。
    epoll epoll 是一种更加高效的 IO 复用技术,epoll 的使用步骤及原理如下:
  • 调用 epoll_create() 会在内核中创建一个 eventpoll 结构体数据,称之为 epoll 对象,在这个结构体中有 2 个比较重要的数据成员,一个是需要检测的文件描述符的信息 struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表);
  • 调用 epoll_ctrl() 可以向 epoll 对象中添加、删除、修改要监听的文件描述符及事件;
  • 调用 epoll_wt() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。 epoll 的两种工作模式:
  • LT 模式(水平触发) LT(Level - Triggered)是缺省的工作方式,并且同时支持 Block 和 Nonblock Socket。在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的 fd 进行 IO 操作,如果不作任何操作,内核还是会继续通知。
  • ET 模式(边沿触发) ET(Edge - Triggered)是高速工作方式,只支持 Nonblock socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
  1. 浏览器从输入 URL 开始到页面显示内容,中间发生了什么?
  • DNS解析域名,获取ip端口
  • 建立tcp链接
  • http发送请求
  • 服务器处理请求
  • 服务器端返回数据
  • 关闭tcp链接
  • 浏览器解析html
  • 浏览器布局渲染
  1. 请你说说 MySQL 的事务隔离级别 SQL 标准定义了四种隔离级别,这四种隔离级别分别是:
  • 读未提交(READ UNCOMMITTED);
  • 读提交 (READ COMMITTED);
  • 可重复读 (REPEATABLE READ);
  • 串行化 (SERIALIZABLE)。
    事务隔离是为了解决脏读、不可重复读、幻读问题,下表展示了 4 种隔离级别对这三个问题的解决程度:\
隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ 不可能 不可能 可能
SERIALIZABLE 不可能 不可能 不可能

上述4种隔离级别MySQL都支持,并且InnoDB存储引擎默认的支持隔离级别是REPEATABLE READ,但是与标准SQL不同的是,InnoDB存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock的锁算法,因此避免了幻读的产生。所以,InnoDB存储引擎在默认的事务隔离级别下已经能完全保证事务的隔离性要求,即达到SQL标准的SERIALIZABLE隔离级别;

  1. 说一说 C++ 和 C 中 struct 的区别以及和 class 的区别 C++ 和 C 中 struct 的区别: 1. C 的结构体不允许有函数存在,C++ 的结构体允许有内部成员函数,并且允许该函数是虚函数 2. C 的结构体内部成员不能加权限,默认是 public,而 C++ 的结构体内部成员权限可以是 public、protected、private,默认 public 3. C 的结构体是不可以继承,C++ 的结构体可以从其它的结构体或者类继承 4. C 中的结构体不能直接初始化数据成员,C++ 中可以 5. C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名后直接使用,而 C++ 中使用结构体可以省略 struct 关键字直接使用

  2. 说一说 static 关键字的作用 static 是一个关键字,可以用来修饰局部变量、全局变量、成员变量、函数和成员方法。主要作用有:限制数据的作用域、延长数据的生命周期、修饰成员可以被该类所有对象共享。 1. 限制数据的作用域(隐藏) 所有没有加 static 的全局变量和函数都具有全局可见性,其它源文件中也可以访问。被 static 修饰的全局变量和函数只能在当前源文件中访问,其它源文件访问不了,利用这个特性可以在不同的文件中定义同名变量和同名函数,而不必担心命名冲突。 2. 延长数据的生命周期 普通的局部变量出了作用域就会释放,而静态变量存储在静态区,知道程序运行结束才会释放。 3. 静态成员被该类所有对象共享 static 关键字可以修饰类中的成员变量和成员方法,被称为静态成员变量和静态成员方法,静态成员拥有一块单独的存储区,不管创建多少个该类的对象,所有对象都共享这一块内存。静态成员本质上属于类,可以通过类名直接访问。

  3. 说一说什么是野指针,怎么产生的,如何避免

  • 什么是野指针: 野指针是指指向的位置是随机的、不可知的、不正确的。
  • 野指针产生的原因 a. 指针变量未初始化或者随便赋值:指针变量没有初始化,其值是随机的,也就是指针变量指向的是不确定的内存,如果对它解除引用,结果是不可知的。\ b. 指针释放后未置空:有时候指针在释放后没有复制为 nullptr,虽然指针变量指向的内存被释放掉了,但是指针变量中的值还在,这时指针变量就是指向一个未知的内存,如果对它解除引用,结果是不可知的。\
    c. 指针操作超出了变量的作用域:函数中返回了局部变量的地址或者引用,因为局部变量出了作用域就释放了,这时候返回的地址指向的内存也是未知的。\
  • 如何避免野指针
    a. 指针变量一定要初始化,可以初始化为 nullptr,因为 nullptr 明确表示空指针,对 nullptr 操作也不会有问题。\ b. 释放后置为 nullptr。
  1. 说说 const 和 define 的区别
    const 在 C 语言中表示只读,编译器禁止对它修饰的变量进行修改,在 C++ 中增加了常量的语义。而 define 用于定义宏,而宏也可以用于定义常量。它们的区别有:
  • const 生效于编译阶段,而 define 生效于预处理阶段;
  • define只是简单的字符串替换,没有类型检查,而 const 有对应的数据类型,编译器要进行判断的,可以避免一些低级的错误;
  • 用 define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址的;
  • define 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝;
  • 可以对 const 常量进行调试,但是不能对宏常量进行调试。
  1. 请你说说 extern 的作用,extern变量在哪个数据段,为什么要 extern C?
    extern 的作用有: 1. extern 可以置于变量声明或者函数声明前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它文件中寻找其定义。 2. extern 变量表示声明一个变量,表示该变量是一个外部变量,也就是全局变量,所以 extern 修饰的变量保存在静态存储区(全局区),全局变量如果没有显示初始化,会默认初始化为 0,或者显示初始化为 0 ,则保存在程序的 BSS 段,如果初始化不为 0 则保存在程序的 DATA 段。 3. extern "C" 的作用是为了能够正确的实现 C++ 代码调用 C 语言代码。加上 extern "C" 后,会指示编译器这部分代码按照 C 语言(而不是 C++)的方式进行编译。由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。 这个功能十分有用处,因为在 C++ 出现以前,很多代码都是 C 语言写的,而且很底层的库也是 C 语言写的,为了更好的支持原来的 C 代码和已经写好的 C 语言库,需要在 C++ 中尽可能的支持 C,而 extern "C" 就是其中的一个策略。

  2. 请你说说 const 的用法 const 的用法有很多:

  • 用在变量身上,表示该变量只读,不能对它的值进行修改 const int a = 10; a = 20; // 编译会报错,因为 a 只读,不能对它进行修改
  • 结合指针一起使用 const int * p; // 常量指针 int * const p; // 指针常量 const int * const p; const int * p 是常量指针,表示指针变量 p 所指向的内容不能修改,指针变量 p 的内容可以修改; int * const p 是指针常量,表示指针变量 p 的内容不能修改,指针变量 p 所指向的内容可以修改; const int * const p 表示指针变量 p 的内容和所指向的内容都不可以修改。
  • const 用于函数参数 void foo(const int * p); void foo(const int & p); const 用于形参时说明形参在函数内部不能被改变,这是非常有用的,有时候函数参数传递指针或者引用,在函数内部不希望对指针和引用指向的数据进行修改,可以加上 const。
  • 在类中修饰成员方法,防止在方法中修改非 static 成员 class A { public: int a; void fun() const { a = 20; // 错误,const 修饰的成员方法中不能修改非静态成员变量 } }
  • const 修饰类的成员变量 class T { public: T() : a(10) { } private: const int a; static const int b; }; const int T::b = 20;
  1. 请你说说 C 语言里面 volatile,可以和 const 同时使用吗
    volatile 限定符是用来告诉计算机,所修饰的变量的值随时都会进行修改的。用于防止编译器对该代码进行优化。通俗的讲就是编译器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份。const 和 volatile 可以一起使用,volatile 的含义是防止编译器对该代码进行优化,这个值可能变掉的。而 const 的含义是在代码中不能对该变量进行修改。因此,它们本来就不是矛盾的。

  2. 请你说说C++引用的概念

  • 引用(Reference)是 C++ 相对于 C 语言的一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。
  • 基本语法 typename & ref = varname;
  • 使用引用的注意事项:
  • 引用必须引用合法的内存空间
  • 引用在定义时必须初始化
  • 引用一旦初始化后,就不能再引用其它数据
  • 引用在定义时需要加上 &,在使用时不能加 &,使用时加 & 表示取地址
  • 函数中不要返回局部变量的引用
  • 引用的本质是指针,低层的实现还是指针。
  1. 请你说说指针和引用的区别
  • 定义和性质不同。指针是一种数据类型,用于保存地址类型的数据,而引用可以看成是变量的别名。指针定义格式为:数据类型 *;而引用的定义格式为:数据类型 &;
  • 引用不可以为空,当被创建的时候必须初始化,而指针变量可以是空值,在任何时候初始化;
  • 指针可以有多级,但引用只能是一级;
  • 引用使用时无需解引用(*),指针需要解引用;
  • 指针变量的值可以是 NULL,而引用的值不可以为 NULL;
  • 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了;
  • sizeof 引用得到的是所指向的变量(对象)的大小,而 sizeof 指针得到的是指针变量本身的大小;
  • 指针作为函数参数传递时传递的是指针变量的值,而引用作为函数参数传递时传递的是实参本身,而不是拷贝副本;
  • 指针和引用进行++运算意义不一样。
  1. 说说内联函数和函数的区别,内联函数的作用
  • 内联函数和函数的区别:
  • 内联函数比普通函数多了关键字 inline;
  • 内联函数避免了函数调用的开销;普通函数有调用的开销;
  • 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址;
  • 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句,如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行;普通函数没有这个要求。
  • 内联函数的作用: 因为函数调用时候需要创建时间、参数传入传递等操作,造成了时间和空间的额外开销。通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题。
  1. 简述 C++ 的内存管理 C++ 的内存分区主要有:代码区、未初始化数据区(BSS)、已初始化数据区(DATA)、栈区(Stack)、堆区(Heap)
  • 代码区 加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
  • 未初始化数据区 加载的是可执行文件 BSS 段,位置可以分开也可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
  • 已初始化数据区(全局初始化数据区/静态数据区) 加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
  • 栈区 栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
  • 堆区 堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序,用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
  1. 简述一下堆和栈的区别 堆和栈主要有如下几点区别:管理方式、空间大小、是否产生内存碎片、生长方向、分配方式、分配效率。
  • 管理方式 对于栈来讲,是由编译器自动管理,无需手动控制;对于堆来说,分配和释放都是由程序员控制的。
  • 空间大小 总体来说,栈的空间是要小于堆的。堆内存几乎是没有什么限制的;但是对于栈来讲,一般是有一定的空间大小的。
  • 碎片问题 对于堆来讲,由于分配和释放是由程序员控制的(利用new/delete 或 malloc/free),频繁的操作势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的数据结构,在某一数据弹出之前,它之前的所有数据都已经弹出。
  • 生长方向 对于堆来讲,生长方向是向上的,也就是沿着内存地址增加的方向,对于栈来讲,它的生长方式是向下的,也就是沿着内存地址减小的方向增长。
  • 分配方式 堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配,静态分配是编译器完成的,比如局部变量的分配;动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器实现的,无需我们手工实现。
  • 分配效率 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率很高。堆则是 C/C++ 函数提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率要比栈底的多。
  1. 说一说什么是内存泄露,如何检测
  • 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
  • 避免内存泄露的方法主要就是要有良好的编码习惯,动态开辟内存空间,及时释放内存。也可以采用智能指针来避免内存泄露。
  • 可以采用静态分析技术、源代码插装技术等进行检测。常见的一些检测工作有:LCLink、ccmalloc、Dmalloc、Electric Fence、Leaky、LeakTracer、MEMWATCH、Valgrind、KCachegrind等等。
  1. 请你说说 malloc 的实现原理
    malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,即内存池。 malloc() 分配空间有一个数据结构,允许它来区分边界,区分已分配和空闲的空间,数据结构中包含一个头部信息和有效载荷,有效载荷的首地址就是 malloc() 返回的地址,可能在尾部还有填充,为了保持内存对齐。头部相当于该数据结构的元数据,其中包含了块大小和是否是空闲空间的信息,这样可以根据头地址和块大小的地址推出下一个内存块的地址,这就是隐式链表。
    malloc() 基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会调用 sbrk() 推进 brk 指针来申请内存空间。搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配。
  • 首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
  • 下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
  • 最佳适配:对堆进行彻底的搜索,从头开始遍历所有块,使用数据区大小大于 size 且差值最小的块作为此次分配的块。
    在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,需要将两个相邻的内存块进行合并。 还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式。在之前的有效载荷中加入了前驱和后驱的指针,也可以称为双向链表。维护空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。
  1. 请你说说 new 的实现原理,new 和 malloc 的区别
  • new 的实现原理: 如果是简单类型,则直接调用 operator new(),在 operator new() 函数中会调用 malloc() 函数,如果调用 malloc() 失败会调用 _callnewh(),如果 _callnewh() 返回 0 则抛出 bac_alloc 异常,返回非零则继续分配内存。 如果是复杂类型,先调用 operator new()函数,然后在分配的内存上调用构造函数。
  • new 和 malloc 的区别 - new 是操作符,而 malloc 是函数;
  • 使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的尺寸;
  • new 分配失败的时候会直接抛出异常,malloc 分配失败会返回 NULL;
  • 对于非简单类型,new 在分配内存后,会调用构造函数,而 malloc 不会;
  • new 分配成功后会返回对应类型的指针,而 malloc 分配成功后会返回 void * 类型;
  • malloc 可以分配任意字节,new 只能分配实例所占内存的整数倍数大小;
  • new 可以被重载,而 malloc 不能被重载;
  • new 操作符从自由存储区上分配内存空间,而 malloc 从堆上动态分配内存;
  • 使用 malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用 realloc 函数进行内存重新分配实现内存的扩充,new 没有这样直观的配套设施来扩充内存。
  1. 请你说说 delete 和 free 的区别 delete 和 free 的区别:
  • delete 是操作符,而 free 是函数;
  • delete 用于释放 new 分配的空间,free 有用释放 malloc 分配的空间;
  • free 会不会调用对象的析构函数,而 delete 会调用对象的析构函数;
  • 调用 free 之前需要检查要释放的指针是否为 NULL,使用 delete 释放内存则不需要检查指针是否为 NULL;
  1. 简述一下 C++ 的重载和重写
  • 重载
  • 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关。
  • 重载的规则 - 函数名相同 - 必须具有不同的参数列表 - 可以有不同的访问修饰符
  • 重载用来实现静态多态(函数名相同,功能不一样)。
  • 重载是多个函数或者同一个类中方法之间的关系,是平行关系。
  • 重写
  • 重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样。
  • 重写的规则: - 方法声明必须完全与父类中被重写的方法相同 - 访问修饰符的权限要大于或者等于父类中被重写的方法的访问修饰符 - 子类重写的方法可以加virtual,也可以不加
  • 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数)。
  • 重写是父类和子类之间的关系,是垂直关系。
  1. 简述一下面向对象的三大特征
  • 封装 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理:比如景区,如果人人都能随意进来,那么很容易造成问题,比如损坏公物。所以我们需要建一堵围墙将景区围起来,但是我们的目的不是不让别人进去,所以开放了售票通道,可以买票在合理的监管机制下进去游玩。 C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。 - private 修饰的成员只能在本类中访问 - protected 表示受保护的权限,修饰的成员只能在本类或者子类中访问 - public 修饰的成员是公共的,哪儿都可用访问。 封装的好处:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。
  • 继承 C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个 B 类继承于 A 类,或称从类 A 派生类 B。这样的话,类 A 成为基类(父类), 类 B 成为派生类(子类)。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。 继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。
  • 多态 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。
  1. 简述一下虚函数的实现原理
  • 虚函数的作用 C++ 中的虚函数的作用主要是实现了动态多态的机制。动态多态,简单的说就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
  • 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表(virtual function table,vtbl)。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。虚函数表类似于类的静态成员。
  1. 虚析构函数有什么作用
  • 概念 虚析构函数,是将基类的析构函数声明为 virtual class Base { public: Base() { } // 虚析构函数 virtual ~Base() { } }
  • 作用 虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。
  1. 请你说说虚函数可以是内联函数吗
  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
  • 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。
  • inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
  1. 说一说 STL 中有哪些常见的容器 STL 中容器分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下: 顺序容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。
  • vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
  • deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
  • list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。
  • set/multiset set中不允许相同元素,multiset 中允许存在相同元素。
  • map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。
  • stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。
  • queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。
  • priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。
  1. STL 容器用过哪些,查找的时间复杂度是多少,为什么? STL 中常用的容器有 vector、deque、list、map、set、multimap、multiset、unordered_map、unordered_set 等。容器底层实现方式及时间复杂度分别如下:
  • vector 采用一维数组实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
  • deque 采用双向队列实现,元素在内存连续存放,不同操作的时间复杂度为: 插入: O(N) 查看: O(1) 删除: O(N)
  • list 采用双向链表实现,元素存放在堆中,不同操作的时间复杂度为: 插入: O(1) 查看: O(N) 删除: O(1)
  • map、set、multimap、multiset 上述四种容器采用红黑树实现,红黑树是平衡二叉树的一种。不同操作的时间复杂度近似为: 插入: O(logN) 查看: O(logN) 删除: O(logN)
  • unordered_map、unordered_set、unordered_multimap、 unordered_multiset 上述四种容器采用哈希表实现,不同操作的时间复杂度为: 插入: O(1),最坏情况O(N) 查看: O(1),最坏情况O(N) 删除: O(1),最坏情况O(N) 注意:容器的时间复杂度取决于其底层实现方式。
  1. 简述 vector 的实现原理
    vector 是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问,由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步: 1. 完全弃用现有的内存空间,重新申请更大的内存空间; 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中; 3. 最后将旧的内存空间释放。 vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。一般以1.5或2倍扩容,取决于编译器。

  2. 请你说说 map 实现原理,各操作的时间复杂度是多少

  • map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AVL是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。
  • 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
  1. 请你说说红黑树的特性,为什么要有红黑树
    虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点: 1、具有二叉查找树的特点; 2、根节点是黑色的; 3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据; 4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的; 5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。

  2. 请你说说 unordered_map 实现原理
    unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。

  3. 请你说说导致哈希冲突的原因和影响因素,哈希冲突的解决方法

  • 哈希冲突产生的原因 哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,这时候就产生了哈希冲突。
  • 产生哈希冲突的影响因素 装填因子(装填因子=数据总数 / 哈希表长)、哈希函数、处理冲突的方法
  • 哈希冲突的解决方法:开放地址方法,链式地址法,再哈希法。
  1. weak_ptr 如何解决 shared_ptr 的循环引用问题?
    weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它指向一个由 shared_ptr 管理的对象而不影响所指对象的生命周期,也就是将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数,依此特性可以解决 shared_ptr 的循环引用问题。 weak_ptr 没有解引用 * 和获取指针 -> 运算符,它只能通过 lock 成员函数去获取对应的 shared_ptr 智能指针对象,从而获取对应的地址和内容。 不论是否有 weak_ptr 指向,一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。

  2. shared_ptr 怎么知道跟它共享对象的指针释放了
    share_ptr 底层是采用引用计数的方式实现的。简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1),每当有新对象使用此堆内存时,该整形值 +1;反之,每当使用此堆内存的对象被释放时,该整形值减 1。当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。仅当最后一个指针过期时,才调用 delete。

  3. 请你回答一下智能指针有没有内存泄露的情况
    智能指针有内存泄露的情况:当两个类对象中各自有一个 shared_ptr 指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄露。 为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

  4. 请你说说 unique_ptr 的实现原理及使用场景
    实现原理 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权,这就是用于 unique_ptr 的策略。 unique_ptr 中把拷贝构造函数和拷贝赋值声明为 private 或 delete,这样就不可以对指针指向进行拷贝了,也就不能产生指向同一个对象的指针。
    使用场景 如果程序不需要多个指向同一个对象的指针,则可以使用 unique_ptr; 如果使用 new 分配内存,应该选择 unique_ptr; 如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。

  5. 请你说说 C++11、C++14、C++17、C++20 都有什么新特性
    C++11 新特性:

  • static_assert 编译时断言
  • 新增加类型 long long ,unsigned long long,char16_t,char32_t,原始字符串
  • auto
  • decltype
  • 委托构造函数
  • constexpr
  • 模板别名
  • alignas
  • alignof
  • 原子操作库
  • nullptr
  • 显示转换运算符
  • 继承构造函数
  • 变参数模板
  • 列表初始化
  • 右值引用
  • Lambda 表达式
  • override、final
  • unique_ptr、shared_ptr
  • initializer_list
  • array、unordered_map、unordered_set
  • 线程支持库 C++14 新特性:
  • 二进制字面量
  • 泛型 Lambda 表达式
  • 带初始化/泛化的 Lambda 捕获
  • 变量模板
  • std::make_unique - std::shared_timed_mutex、std::shared_lock - std::quoted - std::integer_sequence - std::exchange C++17 新特性
  • 构造函数模板推导
  • 结构化绑定
  • 内联变量
  • 折叠表达式
  • 字符串转换
  • std::shared_mutex C++20 新特性
  • 允许 Lambda 捕获 [=, this]
  • 三路比较运算符
  • char8_t
  • 立即函数(consteval)
  • 协程
  • constinit
  1. 请你说说 auto 和 decltype 如何使用
    C++11 提供了多种简化声明的功能,尤其在使用模板时。
  • auto 实现自动类型推断,要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型:
auto a = 12; auto pt = &a;  
double fm(double a, int b) {  return a + b; } 
auto pf = fm;
  • 简化模板声明
for(std::initializer_list<double>::iterator p = il.begin(); p != il.end(); p++)  
    for(auto p = il.begin(); p != il.end(); p++)  

decltype decltype 将变量的类型声明为表达式指定的类型。

decltype(expression) var;  decltype(x) y;// 让y的类型与x相同,x是一个表达式
  1. 请你说说左值、右值、左值引用、右值引用、右值引用的使用场景
  • 左值 在 C++ 中可以取地址的、有名字的就是左值
int a = 10; // 其中 a 就是左值 
  • 右值 不能取地址的、没有名字的就是右值
int a = 10; // 其中 10 就是右值
  • 左值引用 左值引用就是对一个左值进行引用。 传统的 C++ 引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符 const 的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址:
int n; int * pt = new int; const int b = 101;
int & rn = n; int & rt = *pt; const int & rb = b; const int & rb = 10; 
  • 右值引用 右值引用就是对一个右值进行引用。C++ 11 新增了右值引用(rvalue reference),这种引用可指向右值(即可出现在赋值表达式右边的值),但不能对其应用地址运算符。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如 x + y 等表达式以及返回值的函数(条件是该函数返回的不是引用),右值引用使用 && 声明:
int x = 10; int y = 23;
int && r1 = 13; int && r2 = x + y;
double && r3 = std::sqrt(2.0);  
  • 右值引用的使用场景 右值引用可以实现移动语义、完美转发。
  1. 请你说说 C++ Lambda 表达式用法及实现原理
    Lambda 表达式语法: [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型 { 函数体; }; 其中各部分的含义分别为:
  • [外部变量方位方式说明符] [ ] 方括号用于向编译器表明当前是一个Lambda 表达式,其不能被省略。在方括号内部,可以注明当前 Lambda 函数的函数体中可以使用哪些“外部变量”。所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。[外部变量]的定义方式: 外部变量格式:功能 [] :空方括号表示当前 lambda 匿名函数中不导入任何外部变量。 [=]:只有一个 = 等号,表示以值传递的方式导入所有外部变量; [&]:只有一个 & 符号,表示以引用传递的方式导入所有外部变量; [val1,val2,...] :表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序; [&val1,&val2,...]:表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序; [val,&val2,...] :以上 2 种方式还可以混合使用,变量之间没有前后次序。 [=,&val1,...]:表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。 [this] : 表示以值传递的方式导入当前的 this 指针。
  • (参数) 和普通函数的定义一样,Lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;
  • mutable 此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 Lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。对于以值传递方式引入的外部变量,Lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量。
  • noexcept/throw() 可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,Lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 Lambda 函数内部可以抛出的异常类型。
  • -> 返回值类型 指明 Lambda 匿名函数的返回值类型。如果 Lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略 -> 返回值类型。 6. 函数体 和普通函数一样,Lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。
  1. 完美转发了解吗
    模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,我们希望能够在传递过程中保持它的左值或者右值的属性。 左值引用和右值引用在完成了参数传递之后,再使用时已经完全退化成了左值。在万能引用中,编译器有一个规则,如果传入的是左值,则模板类型会被推导成左值引用类型; 传入的是右值,则模板类型就是值的类型。 右值引用是左值,所以当右值进入函数后,右值变为具名对象,即左值,此时再次传递这个对象,传递的是左值。而完美转发是指让右值可以不断以右值身份传递下去。

  2. 说说Socket
    Socket是网络通信中使用的一种机制,它可以用来实现TCP和UDP协议,它可以实现两个不同的进程之间的通信。下面是Socket的通信过程:

  • 服务器端启动,绑定一个端口号,等待客户端的连接请求。
  • 客户端启动,向服务器端发送连接请求,并指定服务器的IP地址和端口号。
  • 服务器端接收到客户端的连接请求后,为该连接创建一个新的Socket对象,同时服务器端继续等待其他客户端的连接请求。
  • 客户端接收到服务器端的响应后,为该连接创建一个新的Socket对象,然后通过该Socket对象与服务器端进行通信。
  • 服务器端和客户端通过各自的Socket对象进行通信,直到通信结束。
  • 通信结束后,客户端和服务器端分别关闭自己的Socket对象,释放网络资源。
  • 在通信过程中,Socket对象扮演着重要的角色,它通过网络传输数据并完成通信。在Java中,可以使用Java Socket API实现Socket通信。
  1. 请你说说线程池的概念与实现
    线程池(thread pool): 一种线程使用模式,线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在短时间任务创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数据取决于可用的并发处理器、处理器内核、内存、网络sockets等数量。
    线程池的主要组成由三个部分构成:
  • 任务队列(Task Quene)
  • 线程池(Thread Pool)
  • 完成队列(Completed Tasks) 示例代码
  1. 请你说说 innodb 和 myisam 的区别
    InnoDB是具有事务、回滚和崩溃修复能力的事务安全型引擎,它可以实现行级锁来保证高性能的大量数据中的并发操作;MyISAM是具有默认支持全文索引、压缩功能及较高查询性能的非事务性引擎。具体来说,可以在以下角度上形成对比:
  • 事务:InnoDB支持事务;MyISAM不支持。
  • 数据锁:InnoDB支持行级锁;MyISAM只支持表级锁。
  • 读写性能:InnoDB增删改性能更优;MyISAM查询性能更优。
  • 全文索引:InnoDB不支持(但可通过插件等方式支持);MyISAM默认支持。
  • 外键:InnoDB支持外键;MyISAM不支持。
  • 存储结构:InnoDB在磁盘存储为一个文件;MyISAM在磁盘上存储成三个文件(表定义、数据、索引)。
  • 存储空间:InnoDB需要更多的内存和存储;MyISAM支持支持三种不同的存储格式:静态表(默认)、动态表、压缩表。
  • 移植:InnoDB在数据量小时可通过拷贝数据文件、备份 binlog、mysqldump工具移植,数据量大时比较麻烦;可单独对某个表通过拷贝表文件移植。
  • 崩溃恢复:InnoDB有崩溃恢复机制;MyISAM没有。
  • 默认推荐:InnoDB是MySQL5.5之后的默认引擎。
  1. 请你说说 MySQL 索引,以及它们的好处和坏处
    索引就像指向表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构; 索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种; 在大数据量的查询中,合理使用索引的优点非常明显,不仅能大幅提高匹配where条件的检索效率,还能用于排序和分组操作的加速。 当时索引如果使用不当也有比较大的坏处:比如索引必定会增加存储资源的消耗;同时也增大了插入、更新和删除操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新。

  2. 请你说说 MySQL 主从同步是如何实现的
    复制(replication)是MySQL数据库提供的一种高可用高性能的解决方案,一般用来建立大型的应用。总体来说,replication的工作原理分为以下3个步骤: 1. 主服务器(master)把数据更改记录到二进制日志(binlog)中。 2. 从服务器(slave)把主服务器的二进制日志复制到自己的中继日志(relay log)中。 3. 从服务器重做中继日志中的日志,把更改应用到自己的数据库上,以达到数据的最终一致性。 复制的工作原理并不复杂,其实就是一个完全备份加上二进制日志备份的还原。不同的是这个二进制日志的还原操作基本上实时在进行中。这里特别需要注意的是,复制不是完全实时地进行同步,而是异步实时。这中间存在主从服务器之间的执行延时,如果主服务器的压力很大,则可能导致主从服务器延时较大。复制的工作原理如下图所示,其中从服务器有2个线程,一个是I/O线程,负责读取主服务器的二进制日志,并将其保存为中继日志;另一个是SQL线程,复制执行中继日志。

  3. 请你说说数据库索引的底层数据结构
    B+树由B树和索引顺序访问方法演化而来,它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点,各叶子节点通过指针进行链接。
    B+树索引在数据库中的一个特点就是高扇出性,例如在InnoDB存储引擎中,每个页的大小为16KB。在数据库中,B+树的高度一般都在2~4层,这意味着查找某一键值最多只需要2到4次IO操作,这还不错。因为现在一般的磁盘每秒至少可以做100次IO操作,2~4次的IO操作意味着查询时间只需0.02~0.04秒。

  4. 请你说说聚簇索引和非聚簇索引
    聚簇索引是将数据与索引存储到一起,找到索引也就找到了数据;而非聚簇索引是将数据和索引存储分离开,索引树的叶子节点存储了数据行的地址。 在InnoDB中,一个表有且仅有一个聚簇索引(因为原始数据只留一份,而数据和聚簇索引在一起),并且该索引是建立在主键上的,即使没有指定主键,也会特殊处理生成一个聚簇索引;其他索引都是辅助索引,使用辅助索引访问索引外的其他字段时都需要进行二次查找。 而在MyISAM中,所有索引都是非聚簇索引,叶子节点存储着数据的地址,对于主键索引和普通索引在存储上没有区别。
    在InnoDB存储引擎中,可以将B+树索引分为聚簇索引和辅助索引(非聚簇索引)。无论是何种索引,每个页的大小都为16KB,且不能更改。 聚簇索引是根据主键创建的一棵B+树,聚簇索引的叶子节点存放了表中的所有记录。辅助索引是根据索引键创建的一棵B+树,与聚簇索引不同的是,其叶子节点仅存放索引键值,以及该索引键值指向的主键。也就是说,如果通过辅助索引来查找数据,那么当找到辅助索引的叶子节点后,很有可能还需要根据主键值查找聚簇索引来得到数据,这种查找方式又被称为书签查找。因为辅助索引不包含行记录的所有数据,这就意味着每页可以存放更多的键值,因此其高度一般都要小于聚簇索引。

  5. 只要创建了索引,就一定会走索引吗?
    不一定。 比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。 举例,假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放,索引可以搜索id、(id,name)、(id, name, age)字段组合。如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或者(name,age)组合则不能使用该索引查询。

  6. 请你说说 InnoDB 的 MVCC
    MVCC(Multi-Version Concurrency Control,多版本并发控制)是 MySQL 中实现事务隔离级别的一种技术。
    在 MVCC 中,每个事务在开始时都会获得一个唯一的事务 ID(Transaction ID,简称 XID),并且每个数据行也都有一个版本号(或时间戳)。当事务对数据行进行修改时,会将修改后的数据行保存为一个新版本,并将旧版本标记为已删除。这样,每个事务都可以看到自己启动时的数据行版本,而不会被其他并发事务所修改的版本所干扰。
    在 MVCC 中,读操作和写操作的并发性都得到了保障。读操作可以读取旧版本或新版本的数据行,而写操作则可以并发进行,因为每个事务都在自己的版本中对数据行进行修改,不会相互干扰。
    需要注意的是,MVCC 只在使用了支持事务的存储引擎(如 InnoDB)时才可用。在使用 MVCC 时,需要注意的一些问题,例如长事务可能会导致版本链过长,从而影响性能,因此需要谨慎设计应用逻辑和事务管理。

  7. 请你介绍一下数据库的 ACID
    事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。在事务中的操作,要么都执行修改,要么都不执行,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一。事务需遵循ACID四个特性:

  • A(atomicity),原子性。原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,整个事务的执行才算成功。事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
  • C(consistency),一致性。一致性指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
  • I(isolation),隔离性。事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,这通常使用锁来实现。
  • D(durability),持久性。事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。持久性保证的是事务系统的高可靠性,而不是高可用性。
  1. 说说设计模式
  • 简单工厂模式: 定义了一个创建对象的类,由这个类来封装实例化对象的行为。
public class SimplePizzaFactory {
       public Pizza CreatePizza(String ordertype) {
              Pizza pizza = null;
              if (ordertype.equals("cheese")) {
                     pizza = new CheesePizza();
              } else if (ordertype.equals("greek")) {
                     pizza = new GreekPizza();
              } else if (ordertype.equals("pepper")) {
                     pizza = new PepperPizza();
              }
              return pizza;
       }
}
  • 工厂方法模式:定义了一个创建对象的抽象方法,由子类决定要实例化的类。工厂方法模式将对象的实例化推迟到子类。
  • 抽象工厂模式:定义了一个接口用于创建相关或有依赖关系的对象族,而无需明确指定具体类。
  • 生成器模式: 封装一个复杂对象构造过程,并允许按步骤构造。
  • 原型模式: 通过复制现有实例来创建新的实例,无需知道相应类的信息。
  • 适配器模式:适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。
  • 装饰者模式:动态的将新功能附加到对象上。在对象功能扩展方面,它比继承更有弹性。
  • 代理模式:代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。隐藏了系统的复杂性,并向客户端提供了一个可以访问系统的接口。
  • 策略模式:策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。
  • 观察者模式: 定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
  1. 说说http 1.0,1.1,2.0,3.0版本区别
  • http1.0规定了浏览器与服务器只保持短暂的连接,连接无法复用,浏览器的每次请求都需要与服务器建立一个TCP连接,服务器完成请求处理后立即断开TCP连接。TCP连接的建立需要三次握手,很耗费时间,因此http1.0版本性能比较差。
  • http1.1引入了持久连接,也就是TCP连接默认不关闭,可以被多个请求复用。客户端和服务器发现对方一段时间没有活动就可以主动关闭连接或者客户端在最后一个请求时,主动告诉服务端要关闭连接。还引入了管道机制,也就是在同一个TCP连接里,客户端可以同时发送多个请求,这样就进一步改进了HTTP协议的效率,但对于服务端而言还是要顺序执行。
  • http2.0为了解决HTTP1.1仍存在的效率问题,采用了多路复用,也就是在一个连接里客户端和浏览器都可以同时发送多个请求或回应,而且不用按照顺序一一对应。前提就是HTTP2进行了二进制分帧,也就是HTTP2会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。
  • http3目的在于降低基于TCP通讯的Web延迟,但是基于UDP实现的,这种通信方式像现在使用的微信语言,它使用UDP代理了TCP,UDP是一个非连接的协议,传输数据之前源端和终端不建立连接,并且UDP是使用最大努力交付,即不保证可靠交付。
  1. 请你说说RAII机制
    C++语言的机制保证了,当创建一个类对象时,会自动调用构造函数,当对象超出作用域时会自动调用析构函数。RAII正是利用这种机制,利用类来管理资源,将资源与类对象的生命周期绑定,即在对象创建时获取对应的资源,在对象生命周期内控制对资源的访问,使之始终保持有效,最后在对象析构时,释放所获取的资源。
    在资源管理方面,智能指针(std::shared_ptr和std::unique_ptr)是RAII最具代表性的实现,使用了智能指针,可以实现自动的内存管理,再也不用担心忘记delete造成内存泄漏了。
    在状态管理方面,线程同步中使用std::unique_lock或std::lock_guard对互斥量std::mutex进行状态管理也是RAII的典型实现,通过这种方式,我们再也不用担心互斥量之间的代码出现异常而造成线程死锁。
    总结起来说,RAII的核心思想是将资源或状态与类对象的生命周期绑定,通过C++语言机制,实现资源与状态的安全管理。\

  2. vector是线程安全的吗?什么是线程安全?
    简单来说,vector并不是线程安全的。这意味着在多个线程同时对一个vector进行读写操作时,可能会导致数据竞争和不确定的结果。这是因为vector的内部实现并没有考虑到多线程并发访问的情况。
    当多个线程同时对vector进行写操作时,可能会导致数据的不一致性。例如,一个线程正在向vector中添加元素,而另一个线程正在删除元素,这样就可能导致vector的内部结构发生变化,从而导致访问错误或崩溃。
    为了解决这个问题,可以采取一些措施来保证vector的线程安全性。一种常见的方法是使用互斥锁(mutex)来保护对vector的访问。通过在每个对vector的读写操作前后加锁,可以确保同一时间只有一个线程能够对vector进行操作,从而避免了数据竞争。
    另一种方法是使用并发容器,如std::vector>,它使用智能指针来管理元素的生命周期,并提供了一些线程安全的操作接口。这样可以避免在多线程环境下对vector进行直接的读写操作,从而减少了潜在的线程安全问题。

  3. c++中的多线程如何实现?
    C++11引入了一个多线程标准库thread,可以方便地创建和管理多个线程。以下是一个简单的示例,演示如何使用thread库创建两个线程并等待它们完成:

#include <iostream>
#include <thread>
// A function to be executed by a thread
void threadFunction(int id)
{
    std::cout << "Thread " << id << " started" << std::endl;
    // Do some work here...
    std::cout << "Thread " << id << " finished" << std::endl;
}
int main()
{
    // Create two threads
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);
    // Wait for the threads to finish
    t1.join();
    t2.join();
    std::cout << "All threads finished" << std::endl;
    return 0;
}
  1. c++中如何通过锁来同步线程?
    在多线程环境中,如果多个线程尝试同时访问共享资源,可能会导致不可预测的结果。为了避免这种情况,我们需要使用同步机制,例如互斥锁。互斥锁是一种机制,可以确保在任何给定时间只有一个线程可以访问共享资源。
#include <iostream>
#include <thread>
#include <mutex>
// A shared variable
int counter = 0;
// A mutex to protect the shared variable
std::mutex mtx;
// A function to be executed by a thread
void threadFunction(int id)
{
    std::cout << "Thread " << id << " started" << std::endl;
    // Increment the shared variable
    for (int i = 0; i < 100000; ++i) {
        mtx.lock();
        ++counter;
        mtx.unlock();
    }
    std::cout << "Thread " << id << " finished" << std::endl;
}

int main()
{
    // Create two threads
    std::thread t1(threadFunction, 1);
    std::thread t2(threadFunction, 2);
    // Wait for the threads to finish
    t1.join();
    t2.join();
    std::cout << "All threads finished" << std::endl;
    std::cout << "Counter value: " << counter << std::endl;
    return 0;
}
  1. c++中如何执行异步任务?
    C++11 中提供了 std::async 函数来执行异步任务。std::async 函数返回一个 std::future 对象,可以通过它来获取异步任务的结果。
#include <iostream>
#include <future>
int calculate() {
    // 模拟计算密集型任务
    int sum = 0;
    for (int i = 0; i < 100000000; ++i) {
        sum += i;
    }
    return sum;
}
int main() {
    // 执行异步任务
    std::future<int> result = std::async(std::launch::async, calculate);
    // 程序继续执行其他操作
    std::cout << "Other work" << std::endl;
    // 获取异步任务的结果
    int sum = result.get();
    std::cout << "Sum: " << sum << std::endl;
    return 0;
}
  1. 对称加密与非对称加密的区别
    对称加密和非对称加密是两种不同的加密技术。对称加密使用同一密钥进行加密和解密,而非对称加密则使用一对密钥,即公钥和私钥。
    在对称加密中,加密和解密使用相同的密钥,这个密钥只有发送和接收方知道,因此被称为共享密钥。加密和解密过程是相对简单的,因为它们都使用相同的密钥。但由于密钥必须在发送和接收之间共享,因此需要确保密钥的安全性。如果密钥被未授权的人访问,那么加密和解密的过程就变得毫无意义。
    在非对称加密中,加密和解密使用不同的密钥。公钥可以被任何人访问,而私钥只有接收方可以访问。公钥用于加密数据,而私钥用于解密数据。这种加密方式更加安全,因为不需要共享密钥。此外,非对称加密还可以用于数字签名,以确保数据的完整性和身份认证。

  2. c++中有哪些锁?

  • 互斥锁(mutex)
  • 定时互斥锁(timed_mutex)
  • 条件变量 (condition_variable)
  • 读写锁 (shared_mutex)
  • 递归锁(recursive_mutex)
  • 自旋锁 (spinlock)
  1. C++ 中 static 成员变量为什么不能类内初始化?
    static 修饰,如果定义时直接初始化,在多次创建同一个对象的时候会非预期地修改其值

  2. dynamic_cast原理
    dynamic_cast通过C++的运行时机制(RTTI)实现,每个类会有一个类似静态变量的虚函数表,子类继承父类时会继承这张表,并添加自己的虚函数地址。因此在运行执行dynamic_cast时,在虚函数表中会存储typeid信息以供校验

  3. 请说说sizeof的用法

  • 对变量/表达式使用,输出类型所占的字节数,例如sizeof(int)为4,sizeof(char)为1
  • 对数组使用,得到数组占有的所有字节数,例如int a[10];sizeof(a)为4*10=40
  • 对字符串使用,得到原数组占用字节数+'\0'所占字节数,例如sizeof("a")为2
  • 对结构体使用,得到结构体中所有元素所占字节数之和,并考虑对齐
  • 对类使用,空类会占1字节,静态成员和函数成员不占字节数,其余成员与结构体类似。此外,虚继承时存在指向虚函数表的指针。

About

秋招面试C++临时抱佛脚

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published