2024年秋招热门面试八股文(C++)

内容整理自网络,侵权联系删除

网络通信

1、TCP 和 UDP 的区别

UDP:语音、视频、寻址、游戏、广播;
TCP:邮件、远程登陆、超文本、文件、身份信息、重要内容 ;
  UDP 协议和 TCP 协议都是运输层协议,都为应用层程序服务,都有复用(不同的应用层协议可以共用 UDP 协议和 TCP 协议)和分用(将数据报解析之后分发给不同的应用层程序)的功能。

  • UDP协议:面向无连接(不需要三次握手和四次挥手)、尽最大努力交付、面向报文(每次收发都是一整个报文段)、没有拥塞控制不可靠(只管发不管过程和结果)、支持一对一、一对多、多对一和多对多的通信方式、首部开销很小(8字节)。优点是快,少了很多首部信息和重复确认的过程,节省了大量的网络资源。缺点是不可靠不稳定,只管数据的发送不管过程和结果。语音通话、视频会议等要求源主机要以恒定的速率发送数据报,允许丢失一些数据,不允许太大的延迟。
  • TCP协议:面向连接(需要三次握手四次挥手)、单播(只能端对端的连接)、可靠交付(有大量的机制保护TCP连接数据的可靠性)、全双工通讯(允许双方同时发送信息,也是四次挥手的原由)、面向字节流(不保留数据报边界的情况下以字节流的方式进行传输,这也是长连接的由来)、头部开销大(最少20字节)。优点是可靠、稳定,有确认、窗口、重传、拥塞控制机制,在数据传完之后,还会断开连接用来节约系统资源。缺点是慢,占用系统资源高,在传递数据之前要先建立连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接。在要求数据准确、对速度没有硬性要求的场景有很好的表现,比如在FTP(文件传输)、HTTP/HTTPS(超文本传输)。

2、TCP 三次握手四次挥手过程

  第一、二次分别包含数据通讯初始序号。第三次是必须的,为了防止已经失效的连接请求报文突然又被传送给了服务器端,然后产生错误。TCP是全双工通讯,客户端和服务器端都需要释放连接和接受确认,所以必须是四次挥手。

  • 三次握手过程:
     第一次:客户端向服务器端发送连接请求报文段,包含自身数据通讯初始序号,进入SYN-SENT状态。
     第二次:服务器端收到连接请求报文段后,如果同意,发送应答,包含自身数据通讯初始序号,进入SYN-RECEIVED状态。
     第三次:客户端收到应答,最后向服务器端发送确认报文,进入ESTABLISHED状态,成功建立长连接。
  • 客户端向服务器端发起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连接释放的过程比较复杂,客户端和服务器端都可以主动释放连接。
  • 四次挥手过程:
     第一次:客户端认为数据发送完毕,需要向服务器端发送连接释放请求。
     第二次:服务器收到连接释放请求,告诉应用层释放TCP连接。然后发送ACK包,进入CLOSE-WT状态,此时表明客户端到服务器端的连接已经释放,不再接受客户端的数据。因为TCP是全双工的,所以服务器仍可以发送数据。
     第三次:当服务器端数据发送完毕,向客户端发送连接释放请求,进入LAST-ACK状态。
     第四次:客户端收到连接释放请求,向服务器端发送确认应答报文,此时客户端进入TIME-WT状态,持续2倍的MSL(最长报文段寿命),若期间没有收到服务器端的数据报文,进入CLOSED状态。服务器端收到确认应答后,也进入CLOSED状态。
  • 客户端主动释放连接为例四次挥手的详细过程
    请添加图片描述
      FIN_WAIT_1: FIN_WAIT_1FIN_WAIT_2状态的含义都是表示等待对方的FIN报文。而这两种状态的区别是:FIN_WAIT_1状态实际上是当SOCKET在ESTABLISHED状态时,它想主动关闭连接,向对方发送了FIN报文,此时该SOCKET即进入到FIN_WAIT_1状态。而当对方回应ACK报文后,则进入到FIN_WAIT_2状态,当然在实际的正常情况下,无论对方何种情况下,都应该马上回应ACK报文,所以FIN_WAIT_1状态一般是比较难见到的,而FIN_WAIT_2状态还有时常常可以用netstat看到。(主动方)
      FIN_WAIT_2FIN_WAIT_2状态下的SOCKET,表示半连接,也即有一方要求close连接,但另外还告诉对方,我暂时还有点数据需要传送给你(ACK信息),稍后再关闭连接。(主动方)
      CLOSE_WAIT:这种状态的含义其实是表示在等待关闭。当对方close一个SOCKET后发送FIN报文给自己,你系统毫无疑问地会回应一个ACK报文给对方,此时则进入到CLOSE_WAIT状态。接下来呢,实际上你真正需要考虑的事情是察看你是否还有数据发送给对方,如果没有的话,那么你也就可以 close这个SOCKET,发送FIN报文给对方,也即关闭连接。所以你在CLOSE_WAIT状态下,需要完成的事情是等待你去关闭连接。(被动方)
      LAST_ACK: 它是被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,也即可以进入到CLOSED可用状态了。(被动方)
      TIME_WAIT: 表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了。如果FINWAIT1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME_WAIT状态,而无须经过FIN_WAIT_2状态。(主动方)
      CLOSED: 表示连接中断。
      (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(最长报文段寿命)之后,连接才会释放。
      那四次挥手又是为何呢?TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

3、GET 和 POST 的区别

 1. get主要用来获取数据,post主要用来提交数据;
 2. get的参数有长度限制,最长2048字节,而post没有限制;
 3. get的参数会附加在url之 ,以 " ? "分割url和传输数据,多个参数用 "&"连接,而post会把参数放在http请求体中;
 4. get是明文传输,可以直接通过url看到参数信息,post是放在请求体中,除非用工具才能看到;
 5. get请求会保存在浏览器历史记录中,也可以保存在web服务器日志中;
 6. get在浏览器回退时是无害的,而post会再次提交请求;
 7. get请求会被浏览器主动缓存,而post不会,除非手动设置;
 8. get请求只能进行url编码,而post支持多种编码方式;
 9. get请求的参数数据类型只接受ASCII字符,而post没有限制。

4、浏览器从输入 URL 开始到页面显示内容,中间发生了什么

 1.输入地址,浏览器查找域名的 IP地址;
 2.浏览器向该 IP地址的web服务器发送一个HTTP请求,在发送请求之前浏览器和服务器建立TCP的三次握手,判断是否是HTTP缓存,如果是强制缓存且在有效期内,不再向服务器发请求,如果是HTTP协商缓存向后端发送请求且和后端服务器对比,在有效期内,服务器返回304,直接从浏览器获取数据,如果不在有效期内服务器返回200,返回新数据。
 3.请求发送出去服务器返回重定向,浏览器再按照重定向的地址重新发送请求。
 4.如果请求的参数有问题,服务器端返回404,如果服务器端挂了返回500。
 5.如果有数据一切正常,当浏览器拿到服务器的数据之后,开始渲染页面同时获取HTML页面中图片、音频、视频、CSS、JS,在这期间获取到JS文件之后,会直接执行JS代码,阻塞浏览器渲染,因为渲染引擎和JS引擎互斥,不能同时工作,所以通常把Script标签放在body标签的底部。
 6.渲染过程就是先将HTML转换成dom树,再将CSS样式转换成stylesheet,根据dom树和stylesheet创建布局树,对布局树进行分层,为每个图层生成绘制列表,再将图层分成图块,紧接着光栅化将图块转换成位图,最后合成绘制生成页面。
在这里插入图片描述

5、HTTP 状态码及其含义

  1xx代表服务器端已经接受了请求。2xx代表请求已经被服务器端成功接收,最常见的有200、201状态码。3xx代表路径被服务器端重定向到了一个新的URL,最常见的有301、302状态码。4xx代表客户端的请求发生了错误,最常见的有401、404状态码。5xx代表服务器端的响应出现了错误
 1xx:指定客户端相应的某些动作,代表请求已被接受,需要继续处理。由于 HTTP/1.0 协议中没有定义任何 1xx 状态码,所以除非在某些试验条件下,服务器禁止向此类客户端发送 1xx 响应。

 2xx:代表请求已成功被服务器接收、理解、并接受。
200(成功):服务器已成功处理了请求。 通常,这表示服务器提供了请求的网页。
201(已创建):请求成功并且服务器创建了新的资源。
202(已接受):服务器已接受请求,但尚未处理。

 3xx:代表需要客户端采取进一步的操作才能完成请求,这些状态码用来重定向,后续的请求地址(重定向目标)在响应头Location字段中指明。
301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置。
302(临时移动):服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。

 4xx:表示请求错误。代表了客户端看起来可能发生了错误,妨碍了服务器的处理。
401(未授权):请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。
403(禁止):服务器拒绝请求。
404(未找到):服务器找不到请求的网页。
408(请求超时):服务器等候请求时发生超时。

 5xx:代表了服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理。
500(服务器内部错误):服务器遇到错误,无法完成请求。
503(服务不可用):服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。

6、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的安全性很强。
HTTPS虽然在安全方面有很大的优势,但是缺点也很明显,如下:
 HTTPS握手阶段耗费时间,几乎是HTTP的数倍,会延长页面的首次绘制时间和增加耗电;
 HTTPS的效率没有HTTP高,如果部分数据内容实际上并不需要加密,会浪费计算机资源;
 HTTPS的证书需要购买,功能越强大的证书价格更高;
 HTTPS的加密并不能阻止某些网络攻击,如黑客攻击、拒绝服务攻击等。

  • HTTPS
    HTTPS(Hyper Text Transfer Protocol over SecureSocket Layer),是以安全为目标的HTTP通道,在HTTP的基础上通过身份认证和传输加密阶段保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入TLS(安全传输层协议)/SSL(Secure Sockets Layer 安全套接层协议),HTTPS 的加密就需要TLS/ SSL。 SSL是为网络通信提供安全及数据完整性的一种安全协议。SSL协议在1994年被Netscape发明,后来各个浏览器均支持SSL。 TLS是SSL3.0的后续版本。在TLS与SSL3.0之间存在着显著的差别,主要是它们所支持的加密算法不同,所以TLS与SSL3.0不能互操作。虽然TLS与SSL3.0在加密算法上不同,但是在我们理解HTTPS的过程中,我们可以把SSL和TLS看做是同一个协议。 在HTTPS数据传输的过程中,需要用TLS/SSL对数据进行加密,然后通过HTTP对加密后的密文进行传输,可以看出HTTPS的通信是由HTTP和TLS/SSL配合完成的。 HTTPS的特点:
     (1)内容加密:混合加密方式,对称加密和非对称加密。
     (2)验证身份:通过证书认证客户端访问的是正确的服务器。
     (3)数据完整性:防止传输的数据被中间人篡改。

7、TCP 如何实现可靠传输

 可靠传输就是通过TCP连接传送的数据是没有差错、不会丢失、不重复并且按序到达的。TCP是通过序列号、检验和、确认应答信号、重发机制、连接管理、窗口控制、流量控制、拥塞控制一起保证TCP传输的可靠性的。具体实现是:
 1. 应用层的数据会被分割成TCP认为最适合发送的数据块。
 2. 序列号:TCP给发送的每一个包都进行编号,接收方对数据包进行排序,把有序数据传送给应用层,TCP的接收端会丢弃重复的数据。
 3. 检验和:TCP将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。
 4. 确认应答:如果收到的数据报报文段的检验和没有差错,就确认收到,如果有差错,TCP就丢弃这个报文段和不确认收到此报文段。
 5. 流量控制:TCP 连接的每一方都有固定大小的缓冲空间,TCP的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。
 6. 拥塞控制:当网络拥塞时,减少数据的发送。
 7.停止等待协议:它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
 8. 超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。

  • TCP超时重传机制时间是多少
     影响超时重传机制协议效率的一个关键参数是重传超时时间(RTO)。RTO的值被设置过大过小都会对协议造成不利影响。如果RTO设置过大将会使发送端经过较长时间的等待才能发现报文段丢失,降低了连接数据传输的吞吐量。若RTO过小,发送端尽管可以很快地检测出报文段的丢失,但也可能将一些延迟大的报文段误认为是丢失,造成不必要的重传。TCP协议使用自适应算法以适应互联网分组传输时延的变化。这种算法的基本要点是TCP监视每个连接的性能(即传输时延),由此每一个TCP连接推算出合适的RTO值,当连接时延性能变化时,TCP也能够相应地自动修改RTO的设定,以适应这种网络的变化。 TCP协议采用自适应算法记录数据包的往返时延,并根据往返时延设定RTO的取值。一般来说,RTO的取值会略大于RTT以保证数据包的正常传输。RFC[2988]中建议RTO的计算方式为: RTO = RTTs + 4xRTTd 其中RTTs为加权平均往返时间,RTTd是偏差的加权平均值。 第一次测量往返时间时,SRTT值就取所测量到的RTT样本值,但以后每测量到一个新的往返时间样本,就按下面的式子重新计算一次平滑往返时间SRTT: SRTT = α ×(旧SRTT)+(1-α)×(新RTT)。

8、TIME_WT和 CLOSE_WT

  1. TIME_WT TCP连接第四次挥手结束时,主动发起连接释放请求的一方进入TIME_WT状态,此时主动发起连接释放请求的一方会等待2MSL(最大报文生存期)才会回到初始状态CLOSED
      产生TIME_WT的原因主要是为了实现TCP全双工连接的可靠释放,当主动发起连接释放请求的一方最后发送ACK确认数据包在网络中丢失时,由于TCP的重传机制,被动关闭的一方会重新发送FIN,在FIN到达主动关闭的一方之前,主动关闭的一方需要维持这条连接,也就是主动的一方TCP资源不可以释放,直到被动关闭一方的FIN到达之后,主动关闭方重新发送ACK确认数据包,经过2MSL时间周期没有再收到被动关闭一方的FIN之后,才会恢复到CLOSED状态,如果没有TIME_WT这个状态,当FIN到达时,主动方会用RST来响应,在被动关闭的一方看来似乎是一个错误,实际上是正常的连接释放过程。
  2. CLOSE_WT 在TCP四次挥手阶段,当对方提出连接释放请求时,自身给予响应ACK确认应答,但是TCP连接是全双工的,也需要自身发送连接释放请求,即FIN。但是自身并没有立即发送FIN,进入CLOSE_WT状态。产生CLOSE_WT的原因一般是对方关闭了连接,但是自身还在读取数据或者传输数据,没有关闭连接。需要查看代码是否书写规范,是否向对方发送了FIN,一般是出现CLOSE_WT的一方出现问题。

9、TCP/IP 五层模型

 五层协议体系结构结合了OSI模型和TCP/IP模型的优点。在计算机网络中要做到正确的数据交换,就必须提前约定好相应的规则。它是一个协议栈,就是为了统一计算机网络标准,方便数据的交换。
 1. 应用层:应用层是体系结构中的最高层,定义了应用进程间通信和交互的规则。本层任务就是通过应用进程间的信息数据流通完成特定的网络应用(软件、Web应用等)。因为不同的应用程序都需要不同的应用层协议,所以应用层协议较多,如万维网应用的HTTP协议、电子邮件的SMTP协议、文件传送的DTP协议等。请将应用层交互的数据称为报文,以免产生概念的混淆。 协议:HTTP、HTTPS、FTP、TFTP、SMTP等。
 2. 运输层:运输层的任务是负责向两个计算机中进程之间的通信提供一种通用的数据传输服务,应用层通过运输层可以传输报文。通用是指不会针对特定的应用层协议进行详细的划分,多种应用层协议公用同一个运输层服务,所以运输层有复用的功能。当然也有分发的功能,指将接受到的信息分别交付到应用层不同的进程中。 协议:UDP、TCP
 3. 网络层:网络层的任务是负责为网络上不同的主机提供通信服务。在发送数据时,网络层将运输层产生的报文段或者用户数据报封装成分组或者包(packet)进行传送。由于网络层使用IP协议,所以分组或包(packet)也叫IP数据报。网络层还需要寻找合适的路由路线,让源主机运输层发送下来的数据报能通过路由器找到目的主机。 协议:ICMP、IGMP、IP(IPv4、IPv6)、ARP、RARP
 4. 数据链路层:数据链路层简称链路层。两个节点传输数据时,链路层将网络层交下来的数据报组装成帧,在链路上传送帧。每一帧都包括数据和控制信息(同步信息、地址信息、差错控制等)。
 5. 物理层:物理层上数据的单位是Bit比特,数据的传输都是通过0(或1)比特流来实现的,而0(或1)比特流与电压的高低有关。物理层中比特流的传输不再加控制信息,需要注意的是比特流应从首部开始传送。

10、TCP粘包

 多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,发送方发送数据包的长度和接收方在缓存中读取的数据包长度不一致,就会发生粘包,发送端可能堆积了两次数据,每次100字节一共在发送缓存堆积了200字节的数据,而接收方在接收缓存中一次读取120字节的数据,这时候接收端读取的数据中就包括了下一个报文段的头部,造成了粘包。
解决粘包的方法:
 1. 发送方关闭Nagle算法,使用TCP_NODELAY选项关闭Nagle功能;
 2. 发送定长的数据包。每个数据包的长度一样,接收方可以很容易区分数据包的边界;
 3. 数据包末尾加上\r\n标记,模仿FTP协议,但问题在于如果数据正文中也含有\r\n,则会误判为消息的边界;
 4. 数据包头部加上数据包的长度。数据包头部定长4字节,可以存储数据包的整体长度;
 5. 应用层自定义规则;
 造成粘包的因素有很多,有可能是发送方造成的,也有可能是接收方造成的。比如接收方在接收缓存中读取数据不及时,在下一个数据包到达之前没有读取上一个,可能也会造成读取到超过一个数据包的情况。

11、滑动窗口

  在流量控制中那些已经被客户端发送但是还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为N的窗口,随着TCP协议的运行、数据的运输,这个窗口在序号空间向前滑动,因此这个窗口被称为滑动窗口。
  定义一个基序号为最早未确认分组的序号,将下一个序号定义为最小的未使用序号(即下一个待分发的分组),那么就可以将整个报文段分为四组,即:已被确认的分组、已发送但未被确认的分组、下一个可以分发的分组、超出窗口长度之后的待使用的分组。

12、OSI 七层模型

 在计算机网络中要做到正确的数据交换,就必须提前约定好相应的规则。OSI七层模型是一个协议栈,就是为了统一计算机网络标准,方便数据的交换。它自上而下依次为:
 应用层,管理应用进程间的通信规则。
 表示层,对数据进行处理。
 会话层,用来管理进程。
 传输层,提供数据的传输服务。
 网络层,进行逻辑地址的查询。
 数据链路层,建立节点的连接和信息校验。
 物理层,负责最底层的数据传输。

 1. 应用层:应用层是体系结构中的最高层,是应用进程间通信和交互的规则,进程指计算机中运行的程序。也是用户与应用程序之间的一个接口,操作程序(软件,Web应用),进而触发更下层的服务。 协议:HTTP、HTTPS、FTP、TFTP、SMTP等 ;
 2. 表示层:对从应用层获取到的数据报文数据进行格式处理、安全处理和压缩处理。 格式:JPEG、ASCll、加密格式等;
 3. 会话层:对当前主机进程和目标主机进程会话的建立、管理和终止行为;
 4. 传输层:对两台主机进程也就是应用层提供数据传输服务。定义了传输数据的进程端口号,负责数据包的排序、差错检验和流量控制等。 协议:UDP、TCP ;
 5. 网络层:主要进行逻辑地址的查询。 协议: ICMP、IGMP、IP(IPv4、IPv6) ;
 6. 数据链路层:建立相邻节点的逻辑连接,进行逻辑地址寻址、差错校验等。 协议:ARP、RARP、PPP 等 ;
 7. 物理层:物理层上数据的单位是Bit比特,数据的传输都是通过0(或1)比特流来实现的,而0(或1)比特流与电压的高低有关。

13、DNS解析过程以及DNS劫持

 DNS查询的过程简单描述就是:主机向本地域名服务器发起某个域名的DNS查询请求,如果本地域名服务器查询到对应IP,就返回结果,否则本地域名服务器直接向根域名服务器发起DNS查询请求,要么返回结果,要么告诉本地域名服务器下一次的请求服务器IP地址,下一次的请求服务器可能是顶级域名服务器也可能还是根域名服务器,然后继续查询。循环这样的步骤直到查询到结果,本地域名服务器拿到结果返回给主机。 在完成整个域名解析的过程之后,并没有收到本该收到的IP地址,而是接收到了一个错误的IP地址。比如输入的网址是百度,但是却进入了奇怪的网址,并且地址栏依旧是百度。在这个过程中,攻击者一般是修改了本地路由器的DNS地址,从而访问了一个伪造的DNS服务器。
 预防DNS劫持可以通过以下几种方法:
 (1)准备多个域名,当某个域名被劫持时,暂时使用另一个 ;
 (2)手动修改DNS,在地址栏输入http://192.168.1.1,进入路由器配置,填写主DNS服务器为114.114.114.114,填写备用DNS服务器为8.8.8.8;
 (3)修改路由器密码;

TCP 的流量控制

 如果发送方把数据发送得过快,接收方可能就来不及接受到所有的数据,中间可能会丢失数据报。流量控制就是让发送方的发送速率不要过快,让接收方来得及接收所有的数据。
 利用滑动窗口这个机制可以很方便的实现在TCP连接上控制对方发送数据报的速率。例如:客户端和服务器端建立TCP连接的时候,客户端告诉服务器“我的接收窗口,rwnd= 400”,这时候服务器端的发送窗口发送的数据报总大小不能超过客户端给出的接收窗口的数值,这里要注意的是,这个数值的单位是字节,而不是报文段。当客户端可以继续接收新的数据报时,发送ACK=1, ack=(上一个报文段序号)+1, rwnd = 100,再接收100个字节的数据报。

ARP协议,协议是怎么实现的,是怎么找到MAC地址的

 地址解析协议,即ARP(Address Resolution Protocol),是根据IP地址获取物理地址的一个TCP/IP协议。主机发送信息时将包含目标IP地址的ARP请求广播到局域网络上的所有主机,并接收返回消息,以此确定目标的物理地址。收到返回消息后将该IP地址和物理地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。
 ARP提供了将IP地址转换为链路层地址的机制,而且只为在同一个子网上的主机和路由器接口解析IP地址。ARP寻址的具体过程如下:
 (1) 发送数据到子网中,每台主机都有一个IP对应MAC地址的映射表,每个映射都有一个TTL值,即寿命。
 (2)主机发送一个数据报,该数据报要IP寻址到本子网上另一台主机或路由器。发送主机需要获得给定IP地址的目的主机的MAC地址。如果发送方的ARP表具有该目的结点的表项,这个任务是很容易完成的。如果ARP表中当前没有该目的主机的表项,在这种情况下,发送方用ARP协议来解析这个地址。首先,发送方构造一个称为ARP分组(ARP packet)的特殊分组。一个ARP分组有几个字段,包括发送和接收IP地址及MAC地址。ARP查询分组和响应分组都具有相同的格式。ARP查询分组的目的是询问子网上所有其他主机和路由器,以确定对应于要解析的IP地址的那个MAC地址。
 (3) 主机向它的适配器传递一个ARP查询分组,并且指示适配器应该用MAC广播地址(即FF-FF-FF-FF-FF-FF)来发送这个分组。适配器在链路层帧中封装这个ARP分组,用广播地址作为帧的目的地址,并将该帧传输进子网中。包含该ARP查询的帧能被子网上的所有其他适配器接收到,并且(由于广播地址)每个适配器都把在该帧中的ARP分组向上传递给ARP模块。这些ARP模块中的每个都检查它的IP地址是否与ARP分组中的目的IP地址相匹配。与之匹配的一个给查询主机发送回一个带有所希望映射的响应ARP分组。然后查询主机能够更新它的ARP表,并发送它的IP数据报,该数据报封装在一个链路层帧中,并且该帧的目的MAC就是对先前ARP请求进行响应的主机或路由器的MAC地址。
ARP过程详解
在这里插入图片描述
      destination MAC=FF:FF:FF:FF:FF:FF
目标MAC地址字段,FF:FF:FF:FF:FF:FF表明这是一个广播MAC地址,可以让交换机以广播的形式发出去;
      source MAC=02:00:00:00:00:1A
源MAC地址字段,它是源主机的MAC地址;
      OP=1 (表明它是一条ARP请求报文)
      target IP=192.168.0.200
目标主机的IP地址,它是为了到达主机后,目标主机ARP协议层程序判断是否发给自己的依据,它是必须要有的,因为目标MAC地址是广播地址,意味着所有主机在数据链路层都能接收,只有通过和它比较,让不是目标主机的其他主机不回复,让目标主机回复;
      sender IP=192.168.0.100
      sender MAC=02:00:00:00:00:1A
源主机的IP地址和MAC地址,这是让目标主机存入自己IP-MAC映射所使用的字段,也可以利用它们伪造ARP报文;
      target MAC=00:00:00:00:00:00
目标主机的MAC地址,ARP请求报文就是为了请求目标主机MAC地址,所以它是全0的;
通过上面的ARP广播请求报文各字段的定义,得知:
 ARP请求报文在送达交换机时,交换机根据目标MAC地址是广播地址,在除了发送方端口外的其他所有端口,都复制一份ARP请求报文,发出去给所有主机;
 接收到ARP请求报文的主机首先在数据链路层比较destination MAC,由于是广播MAC地址,可以接收,然后比较target IP;
 目标主机比较target IP发现一致后,首先会把ARP请求报文的sender IP和sender MAC存入自己的ARP缓存表
 然后目标主机的ARP协议程序会给源主机自动回一个ARP响应报文
在这里插入图片描述
      destination MAC=02:00:00:00:00:1A
目标MAC地址字段,02:00:00:00:00:1A是主机A的MAC地址,说明这是以单播的形式发送出去的;
      source MAC=02:00:00:00:00:1B
源MAC地址字段,它是源主机的MAC地址;
       OP=2 (表明它是一条ARP响应报文)
      target IP=192.168.0.100
目标主机的IP地址,主机A收到ARP响应后并不会比较它;
      sender IP=192.168.0.200
      sender MAC=02:00:00:00:00:1B
源主机的IP地址和MAC地址,目标主机收到后,把它们存入自己的ARP缓存表;
      target MAC=02:00:00:00:00:1A
目标主机的MAC地址。
总结:
 (1)ARP协议是针对IPv4的寻址协议,IPv6有自己的寻址协议;
 (2)ARP请求报文和ARP响应报文都是不能跨网段的;
 (3)ARP请求报文以广播形式发送,ARP响应报文以单播形式发送;
 (4)ARP请求报文和响应报文的接收方都会把senderIP和senderMAC存入自己的缓存表内,也就是说,请求报文和响应报文都可以让自己主机的IP和mac被对方记录,不同的是,请求报文还会让对方自动给我回一个ARP响应报文,而响应报文只会让对方存入自己的IP和mac,而不会有回复(不是完全准确,免费ARP响应是会有回复的)
 (6)只有ARP请求报文和ARP响应报文具有缓存IP和mac的功能,其他协议的message虽然也带有源mac和IP,但是并不能让目标主机把源IP和mac存入ARP缓存表内(当然ARP请求和响应报文存入IP和mac还需要判断其他条件)

16、对称加密和非对称加密

 对称加密:加密和解密使用同一个秘钥,所以叫做对称加密。常见的对称加密算法有:DES、AES、3DES等。
 非对称加密:加密和解密使用不同的秘钥,一把作为公开的公钥,另一把作为私钥。公钥加密的信息,只有私钥才能解密。私钥加密的信息,只有公钥才能解密。常见的非对称加密算法:RSA,ECC等。
  对称加密和非对称加密相比安全性低,因为加密和解密是同一个密钥,数据包被拦截之后不安全。而非对称加密中,公钥用来加密,私钥用来解密。公钥可以公开给任何用户进行加密,私钥永远在服务器或某个客户端手里,非常安全,数据被拦截也没用,因为私钥未公开就永远无法打开数据包。

17、UDP 怎么样可以实现可靠的传输

 想要使用UDP还要保证数据的可靠传输,就只能通过应用层来做文章。实现的方式可以参考TCP的可靠传输机制,差别就是将TCP传输层功能,如确认机制、重传功能、流量控制、拥塞控制等功能实现在了应用层。
在应用层实现可靠传输关键点有两个,从应用层角度考虑分别是:
 1. 提供超时重传机制,能避免数据报丢失的问题。
 2. 提供确认序列号,保证数据拼接时候的正确排序。 请求端:首先在UDP数据报定义一个首部,首部包含确认序列号和时间戳,时间戳是用来计算RTT(数据报传输的往返时间),计算出合适的RTO(重传的超时时间)。然后以等-停的方式发送数据报,即收到对端的确认之后才发送下一个的数据报。当时间超时,本端重传数据报,同时RTO扩大为原来的两倍,重新开始计时。 响应端:接受到一个数据报之后取下该数据报首部的时间戳和确认序列号,并添加本端的确认数据报首部之后发送给对端。根据此序列号对已收到的数据报进行排序并丢弃重复的数据报。

18、HTTPS 加解密的过程

 HTTPS数据加解密过程中数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。HTTPS协议加密的过程可以分为两个阶段,分别是:
证书的认证阶段:使用非对称加解密算法对数据传送阶段的对称加解密密钥进行加密和解密。
数据传送阶段:通过证书认证阶段获取到目标服务器的对称加解密密钥,对数据进行加密传送给服务器。
 HTTPS为了兼顾安全与效率,同时使用了对称加密和非对称加密。数据是被对称加密传输的,对称加密过程需要客户端的一个密钥,为了确保能把该密钥安全传输到服务器端,采用非对称加密对该密钥进行加密传输。总的来说,对数据进行对称加密,对称加密所要使用的密钥通过非对称加密传输。 在整个HTTPS数据传输的过程中一共会涉及到四个密钥:
 (1)CA机构的公钥,用来验证数字证书是否可信任 ;
 (2)服务器端的公钥 ;
 (3)服务器端的私钥;
 (4)客户端生成的随机密钥。

 一个HTTPS请求可以分为两个阶段,证书认证阶段和数据传送阶段。又可以细分为六个步骤:
 (1)客户端第一次向服务器发起HTTPS请求,连接到服务器的443(默认)端口。
 (2)服务器端有一个密钥对,公钥和私钥。用来进行非对称加密使用,服务器端保存私钥,不能泄露,公钥可以发送给任何人。服务器将自己的数字证书(包含公钥)发送给客户端。
 (3)客户端收到服务器端的数字证书之后,会对数字证书进行检查,验证合法性。如果发现数字证书有问题,那么HTTPS传输就中断。如果数字证书合格,那么客户端生成一个随机值,这个随机值是数据传输阶段时给数据对称加密的密钥,然后用数字证书中的公钥加密这个随机值密钥,这样就生成了加密数据使用的密钥的密文。到这时,HTTPS中的第一次HTTP请求就结束了。
 (4)客户端第二次向服务器发起HTTP请求,将对称加密密钥的密文发送给服务器。
 (5) 服务器接收到客户端发来的密文之后,通过使用非对称加密中的私钥解密密文,得到数据传送阶段使用的对称加密密钥。然后对需要返回给客户端的数据通过这个对称加密密钥加密,生成数据密文,最后将这个密文发送给客户端。
 (6)客户端收到服务器端发送过来的密文,通过本地密钥对密文进行解密,得到数据明文。到这时,HTTPS中的第二次HTTP请求结束,整个HTTPS传输完成。

操作系统

死锁产生的原因&怎么预防

 1. 死锁 两个或两个以上的进程在执行过程中,因争夺共享资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。这些永远在互相等待的进程称为死锁进程。
 2. 产生死锁的必要条件
 (1)互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放;
 (2) 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放;
 (3) 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放;
 (4) 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一个 P1 占用的资源;P1 正在等待 P2 占用的资源,……,Pn 正在等待已被 P0 占用的资源。
 3. 产生死锁的原因:竞争资源 、进程间推进顺序非法。
 4. 预防死锁
 (1) 有序资源分配法:设系统中有m类资源,n个进程,分别用R1,R2,…,Rm(1,2,…,m可看作资源编号)和P1,P2,…,Pn表示。根据有序资源分配法可知,进程申请资源时必须按照资源编号的升序进行,即任何进程在占有了Ri类资源后,再申请的资源Rj的编号j一定大于i。因此在任一时刻,系统中至少存在一个进程Pk,它占有了较高编号的资源Rh,且它继续请求的资源必然是空闲的,因而Pk可以一直向前推进直至完成。
 (2) 银行家算法:银行家算法的基本思想是分配资源之前,判断系统是否是安全的;若是,才分配。每分配一次资源就测试一次是否安全。我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源相当于用户向银行家贷款。操作系统按照银行家制定的规则为进程分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。当进程在执行中继续申请资源时,先测试该进程本次申请的资源数是否超过了该资源所剩余的总量。若超过则拒绝分配资源,若能存在安全状态,则按当前的申请量分配资源,否则也要推迟分配。

C++线程中的几类锁

 多线程中的锁主要有五类:互斥锁、条件锁、自旋锁、读写锁、递归锁。一般而言,所得功能与性能成反比。而且一般不使用递归锁(C++提供std::recursive_mutex),这里不做介绍。
  互斥锁:用于控制多个线程对它们之间共享资源互斥访问的一个信号量。也就是说为了避免多个线程在某一时刻同时操作一个共享资源。在某一时刻只有一个线程可以获得互斥锁,在释放互斥锁之前其它线程都不能获得互斥锁,以阻塞的状态在一个等待队列中等待。
头文件:#include <mutex>
类型:std::std::mutex、std::lock_guard
 用法:在C++中,通过构造std::mutex的实例创建互斥单元,调用成员函数lock()来锁定共享资源,调用unlock()来解锁。不过一般不使用这种解决方案,更多的是使用C++标准库中的std::lock_guard类模板,实现了一个互斥量包装程序,提供了一种方便的RAII风格的机制在作用域块中。
  条件锁:条件锁就是所谓的条件变量,当某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态,一旦条件满足则以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见的就是在线程池中,初始情况下因为没有任务使得任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒该线程来处理这个任务。
  自旋锁:互斥锁和条件锁都是比较常见的锁,比较容易理解。接下来用互斥锁和自旋锁的原理相互比较,来理解自旋锁。
 假设我们有一台计算机,该计算机拥有两个处理器core1和core2。现在在这台计算机上运行两个线程:T1和T2,且T1和T2分别在处理器core1和core2上面运行,两个线程之间共享一份公共资源Public。
 首先我们说明互斥锁的工作原理,互斥锁是一种sleep-waiting的锁。假设线程T1访问公共资源Public并获得互斥锁,同时在core1处理器上运行,此时线程T2也想要访问这份公共资源Public(即想要获得互斥锁),但是由于T1正在使用Public使得T2被阻塞。当T2处于阻塞状态时,T2被放入等待队列中,处理器core2会去处理其它的任务而不必一直等待(忙等)。也就是说处理器不会因为线程被阻塞而空闲,它会去处理其它事务。
 然后我们说明自旋锁的工作原理,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用Public,而T2也想使用Public,此时T2肯定是得不到这个自旋锁的。与互斥锁相反,此时运行T2的处理器core2会一直不断地循环检查Public使用可用(自旋锁请求),直到获得到这个自旋锁为止。
 从“自旋锁”的名称也可以看出,如果一个线程想要获得一个被使用的自旋锁,那么它会一直占用CPU请求这个自旋锁使得CPU不能去做其它的事情,直到获取这个锁为止,这就是“自旋”的含义。当发生阻塞时,互斥锁可以让CPU去处理其它的事务,但自旋锁让CPU一直不断循环请求获取这个锁
  读写锁:计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。

进程通信(IPC)的方式

 1. 管道 管道也叫无名(匿名)管道,所有的 UNIX 系统都支持这种通信机制。管道本质其实是内核中维护的一块内存缓冲区,Linux 系统中通过 pipe() 函数创建管道,会生成两个文件描述符,分别对应管道的读端和写端。无名管道只能用于具有亲缘关系的进程间的通信。
 2. 命名管道 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道(FIFO),也叫命名管道、FIFO文件。FIFO不同于匿名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
 3. 信号 信号是事件发生时对进程的通知机制,有时也称之为软件中断,它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
 4. 消息队列 消息队列就是一个消息的链表,可以把消息看作一个记录,具有特定的格式以及特定的优先级,对消息队列有写权限的进程可以向消息队列中按照一定的规则添加新消息,对消息队列有读权限的进程则可以从消息队列中读走消息,消息队列是随内核持续的。
 5. 共享内存 共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会称为一个进程用户空间的一部分,因此这种 IPC 机制无需内核介入。所有需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。与管道等要求发送进程将数据从用户空间的缓冲区复制进内核内存和接收进程将数据从内核内存复制进用户空间的缓冲区的做法相比,这种 IPC技术的速度更快。
 优点:因为所有进程共享同一块内存,共享内存在各种进程间通信方式中具有最高的效率。访问共享内存区域和访问进程独有的内存区域一样快,并不需要通过系统调用或者其它需要切入内核的过程来完成。同时它也避免了对数据的各种不必要的复制。
 缺点:共享内存没有提供同步机制,这使得我们在使用共享内存进行进程之间的通信时,往往需要借助其他手段来保证进程之间的同步工作。
 6. 内存映射 内存映射(Memory-mapped I/O)是将磁盘文件的数据映射到内存,用户通过修改内存就能修改磁盘文件。
 7. 信号量 信号量主要用来解决进程和线程间并发执行时的同步问题,进程同步是并发进程为了完成共同任务采用某个条件来协调它们的活动。对信号量的操作分为 P 操作和 V 操作,P 操作是将信号量的值减 1,V 操作是将信号量的值加 1。当信号量的值小于等于 0 之后,再进行 P 操作时,当前进程或线程会被阻塞,直到另一个进程或线程执行了 V 操作将信号量的值增加到大于 0 之时。
 8. Socket 一个套接字就是网络上进程通信的一端,提供了应用层进程利用网络协议交换数据的机制。Socket 一般用于网络中不同主机上的进程之间的通信。

3、进程和线程的区别

 1. 进程有独立的地址空间,线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间;
 2. 进程和线程切换时,需要切换进程和线程的上下文,进程的上下文切换时间开销远远大于线程上下文切换时间,效率要差一些;
 3. 进程的并发性较低,线程的并发性较高;
 4. 每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制;
 5. 系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了 CPU 外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源;
 6. 一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。

4、进程有多少种状态,如何转换

  进程有五种状态:创建、就绪、执行、阻塞、终止:
创建:一个进程启动,首先进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
就绪状态:在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
运行状态:获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
阻塞状态:在运行状态期间,如果进行了阻塞的操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
终止状态:进程结束或者被系统终止,进入终止状态。
在这里插入图片描述

5、介绍一下 I/O 多路复用

   I/O 多路复用是一种使得程序能同时监听多个文件描述符的技术,从而提高程序的性能。I/O 多路复用能够在单个线程中,通过监视多个 I/O 流的状态来同时管理多个 I/O 流,一旦检测到某个文件描述符上我们关心的事件发生(就绪),能够通知程序进行相应的处理(读写操作)。
Linux 下实现 I/O 复用的系统调用主要有 select、poll 和 epoll
 1. select 首先构造一个文件描述符的列表,将要监听的文件描述符添加到该列表中,这个文件描述符的列表数据类型为 fd_set,它是一个整型数组,总共是 1024 个比特位,每一个比特位代表一个文件描述符的状态。比如当需要 select 检测时,这一位为 0 就表示不检测对应的文件描述符的事件,为 1 表示检测对应的文件描述符的事件。
 调用 select() 系统调用,监听该列表中的文件描述符的事件,这个函数是阻塞的,直到这些描述符中的一个或者多个进行 I/O 操作时,该函数才返回,并修改文件描述符的列表中对应的值,0 表示没有检测到该事件,1 表示检测到该事件。函数对文件描述符的检测的操作是由内核完成的。
select() 返回时,会告诉进程有多少描述符要进行 I/O 操作,接下来遍历文件描述符的列表进行 I/O 操作。 select 缺点:
 (1)每次调用select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
 (2)同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
 (3)select 支持的文件描述符数量太小了,默认是 1024(由 fd_set 决定);
 (4)文件描述符集合不能重用,因为内核每次检测到事件都会修改,所以每次都需要重置;
 (5)每次 select 返回后,只能知道有几个 fd 发生了事件,但是具体哪几个还需要遍历文件描述符集合进一步判断。
 2. poll poll 的原理和 select 类似,poll 支持的文件描述符没有限制。
 3. epoll epoll 是一种更加高效的 IO 复用技术,epoll 的使用步骤及原理如下:
 调用 epoll_create() 会在内核中创建一个 eventpoll 结构体数据,称之为 epoll 对象,在这个结构体中有 2 个比较重要的数据成员,一个是需要检测的文件描述符的信息 struct_root rbr(红黑树),还有一个是就绪列表struct list_head rdlist,存放检测到数据发送改变的文件描述符信息(双向链表);
 调用 epoll_ctrl() 可以向 epoll 对象中添加、删除、修改要监听的文件描述符及事件;
 调用 epoll_wt() 可以让内核去检测就绪的事件,并将就绪的事件放到就绪列表中并返回,通过返回的事件数组做进一步的事件处理。
epoll 的两种工作模式:
 LT 模式(水平触发) 缺省的工作方式,并且同时支持 Block(阻塞)和 Nonblock(非阻塞)Socket。在这种做法中,内核检测到一个文件描述符就绪了,然后可以对这个就绪的 fd 进行 IO 操作,如果不作任何操作,内核还是会继续通知。
 ET 模式(边沿触发) 高速工作方式,只支持 Nonblock socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 检测到。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个 fd 进行 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。 ET 模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件描述符的阻塞读/阻塞写操作把处理多个文件描述符的任务卡死。

6、多路IO复用技术

  1. 基于select的IO复用
      select的IO复用原理很简单。每个文件描述符在Linux系统下就是一个整数,比如现在有4个应用客户端与服务器建立连接,其套接字分别为fd0,fd1,fd2,fd3,select的原理就是将这些套接字集中起来管理,采用fd_set数组,fd_set数组每一位代表一个套接字状态,当把fd0,fd1,fd2,fd3装入fd_set时,其状态如图所示:
      在这里插入图片描述
    将某个套接字添加到fd_set数组后,将其全部置0。调用select函数时,发生事件(如fd2的套接字接收到客户端发来的消息),对应的数组位就会从0变成1,此时检测fd_set数组中从0变成1的那些位,就是发生变化的描述符。因此,使用select函数流程如下:
    (1) 设置文件描述符,即声明fd_set变量;
    (2) 将要监视的描述符加入fd_set数组;
    (3) 将数组清零;
    (4) 设置超时时间(select函数是阻塞型的,因此设置超时时间,超时还没有fd_set数组变化就返回);
    (5) 调用select,监视变化,并进行后续处理。
    select函数的具体用法
#include <sys/select.h>
#include <time.h>

int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, 
			const struct timeval* timeout);
maxfd:监视的文件描述符个数
readset:记录“是否存在待读取的文件描述符”的变量
writeset:记录“是否存在待写的文件描述符”的变量
exceptset:记录所有异常的文件描述符的变量,
timeout:设置超时的结构体
struct timeval{
    long tv_sec; //秒
    long tv_usec;  //毫秒
}

关于fd_set的添加,清零,检测变化的函数如下:
FD_SET(int fd, fd_set* fdset); //将文件描述符fd注册到fdset中
FD_CLR(int fd, fd_set* fdset); //将文件描述符fd从fdset中删除
FD_ZERO(fd_set* fdset); //将fdset清零
FD_ISSET(int fd, fd_set* fdset); //判断fd是否发生变化,即是否有事件来临
  基于多进程的回声服务器的服务端改为基于select IO的IO复用

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <sys/time.h>

void error_handle(const char* msg){
    fputs(msg,stderr);
    fputc('\n',stderr);
    exit(1);
}

int main(int argc,char* argv[]){
    //服务器建立连接
    int servsock,clntsock;
    struct sockaddr_in servaddr,clntaddr;
    char message[50];
    socklen_t clntlen;

    if(argc!=2)
        error_handle("Please input port number");

    servsock=socket(PF_INET,SOCK_STREAM,0);  //1.建立套接字

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_addr.s_addr=htonl(INADDR_ANY);  //默认本机IP地址
    servaddr.sin_port=htons(atoi(argv[1]));

    if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
        error_handle("bind error");  //2.建立连接

    if(listen(servsock,10)==-1) //3.监听建立
        error_handle("listen() error");
    //到此为止的代码与上一节相同,均为服务器socket的建立

    //设置fd_set
    int fdmax,fd_num;
    struct timeval timeout; //设置超时时间
    fd_set readset,copyset;
    FD_ZERO(&readset);
    FD_SET(servsock,&readset);
    fdmax=servsock;  //因为Linux系统下分配的描述符都是从0开始递增,
    				//因此监视的描述符数量等于最新申请到的描述符+1,即fdmax+1
    while(1){
        copyset=readset;
        timeout.tv_sec=3;
        timeout.tv_usec=500;  //设置超时时间3.5s

        int fd_num=select(fdmax+1,&copyset,0,0,&timeout); //只有读取事件监视
        if(fd_num<0)
            error_handle("select() error");
        else if(fd_num==0){
            printf("timeout\n");
            continue;
        }
        else{  //发生了监听事件
            for(int i=0;i<fdmax+1;i++){ //遍历所有监视的文件描述符
                if(FD_ISSET(i,&copyset)){
                    if(i==servsock){ //有客户端来建立连接
                        clntlen=sizeof(clntaddr);
                        //接收连接请求
                        if((clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen))==-1) 
                            printf("accept() error\n");
                        printf("connecting\n");
                        FD_SET(clntsock,&readset); //新的描述符添加进监视
                        if(fdmax<clntsock)
                            fdmax=clntsock; //新的最大文件描述符
                    }
                    else{  //说明是已建立的客户端发来的消息
                        int str_len=read(i,message,sizeof(message));
                        if(str_len<0)
                            printf("read() error\n");
                        else if(str_len==0){ //客户端断开连接
                            FD_CLR(i,&readset); //清除该描述符
                            close(i);
                        }
                        else
                            write(i,message,str_len);
                    }
                }
            }
        }
    }
    close(servsock);
    return 0;
}

  ps:每个循环内都要重置timeout结构体并将初始时的readset复制到copyset,并用copyset调用select函数。因为timeout结构体的值随着计时改变而改变,即定时结束时,值变成了0,需要重置;而每次复制初始的readset是因为select会改变数组的内容,必须保存原始数组内容。
select主要缺点:
(1) 套接字或文件描述符是属于OS所有。因此每次select函数实际上都向操作系统重新传递了一遍文件描述符,从应用程序向OS传递数据是很耗时的;
(2) 因为发生文件描述符变化时,不知道具体是哪个发生变化。因此对于fd_set内管理的所有文件描述符,都需要遍历以找到变换的描述符(对应每次for循环)。
以上两个原因造成select在大规模多客户端时非常耗时。
select也有优点
(1) 几乎所有操作系统都支持select函数,这使得基于select的编程移植性强。因此对于连接少,断开不频繁的操作,select也有其优越性。

  1. 基于pselect的IO复用
    pselect的基本用法和功能跟select非常相似,函数原型如下:
#include <sys/select.h>
int pselect(int maxfd,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,
			const struct timespec* tsptr,const sigset_t* sigmask);

跟select的主要不同在于:
(1) select的超时结构体为timeval,其中用秒(tv_sec)和微秒(tv_usec)表示时间;pselect的超时结构体为timespec,用秒(tv_sec)和纳秒(tv_nsec,long类型)表示时间;
(2) pselect的超时设置结构体为const,一直不会改变,这样就不用每次调用pselect的时候都重置timespec,只需要初始化一次即可;
(3) pselect最后一个参数为可选信号屏蔽字,如果置为NULL,则与select调用结果相同。

  1. 基于poll的IO复用
    poll()实现原理跟select基本一样,poll函数的原型如下:
    #include <poll.h>
    
    int poll(struct pollfd fdarray[],nfds_t nfds,int timeout); //timeout单位为毫秒
    struct pollfd{
        int fd;
        short events; //要监视的事件类型
        short revents; //发生的事件类型
    }
    
  2. 基于epoll的IO复用
    epoll函数克服了select函数的相关缺点,其优点如下:
    (1) 只需向OS注册一次文件描述符集合,不用每次循环传递;
    (2) epoll函数会将发生变化的文件描述符单独集中起来,这样每次遍历时只需要遍历发生变化的文件描述符。
    (3) 相对于select同时监听的数量有限制,epoll监听数量一般远大于select,这对于多连接的服务器至关重要。
    epoll用来集中通知变化的文件描述符结构体如下:
    struct epoll_event{
        __uint32_t events;  //用来注册是什么事件需要关注,如输入/输出
        epoll_data_t data;
    }
    
    typedef union epoll_data{
        void* ptr;
        int fd; //发生变化的文件描述符
        __uint32_t u32;
        __uint64_t u64;
    } epoll_data_t;
    
    //可以看到,常用的为events, fd两个
    
    epoll相关的函数总共有3个:
    #include <sys/epoll.h>
    
    //向OS申请创建管理所有文件描述符的epoll例程,返回该例程的文件描述符
    int epoll_create(int size); 
    size:可能注册的最大监视事件,仅供OS参考
    
    //成功返回0,失败-1,用于添加/删除/修改某个文件描述符
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event); 
    epfd:epoll_create返回的该epoll例程的描述符
    op:具体的操作,如添加/删除,常用以下三种模式
        EPOLL_CTL_ADD:将fd的描述符注册到epfd,等价于FD_SET
        EPOLL_CTL_DEL:将fd的描述符从epfd移出,等价于FD_CLR
        EPOLL_CTL_MOD:修改fd所指描述符的监听类型fd: 待添加的的socket
    event:struct epoll_event结构体,内有一个event变量,指明需要监听的具体类型
        EPOLLiN:输入事件
        EPOLLOUT:输出事件
        EPOLLET:以边缘触发方式接收事件通知(稍后详述)
        
    //类似于select,调用后监听发生变化的描述符,成功时返回发生事件的个数
    int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout); 
    epfd:epoll例程描述符
    events:动态申请的结构体数组,用于保存,通知发生变化的文件描述符
    maxevents:监视的最大事件数目,即events数组的大小
    timeout:设置超时,单位为ms。-设置-1为无限等待
    
    回声服务器服务器端改写为epoll形式的IO复用
 1 #include <stdlib.h>
 2 #include <stdio.h>
 3 #include <string.h>
 4 #include <sys/socket.h>
 5 #include <arpa/inet.h>
 6 #include <unistd.h>
 7 #include <sys/epoll.h>
 8 #define EPOLL_SIZE 30 //定义监视事件的数组数量
 9 
10 void error_handle(const char* msg){
12     fputs(msg,stderr);
13     fputc('\n',stderr);
14     exit(1);
15 }
16 
17 int main(int argc,char* argv[]){
19     //服务器建立连接
20     int servsock,clntsock;
21     struct sockaddr_in servaddr,clntaddr;
22     char message[50];
23     socklen_t clntlen;
24     
25     if(argc!=2)
26         error_handle("Please input port number");
27     
28     servsock=socket(PF_INET,SOCK_STREAM,0);  //1.建立套接字
29     
30     memset(&servaddr,0,sizeof(servaddr));
31     servaddr.sin_family=AF_INET;
32     servaddr.sin_addr.s_addr=htonl(INADDR_ANY);  //默认本机IP地址
33     servaddr.sin_port=htons(atoi(argv[1]));  
34     
35     if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)
36         error_handle("bind error");  //2.建立连接
37     
38     if(listen(servsock,10)==-1) //3.监听建立
39         error_handle("listen() error");
41     //到此的代码为socket创建过程,与前两节相同
42     
43     //关于epoll的相关函数
44     epoll_event event;
45     event.events=EPOLLIN; //输入监听
46     event.data.fd=servsock;
47     int epfd=epoll_create(20); //创建epoll例程
48     epoll_ctl(epfd,EPOLL_CTL_ADD,servsock,&event); //向epfd添加fd的输入监听事件
49     struct epoll_event* events;
50     events=(epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //申请存放发生事件的数组
51     while(1){
53         int event_cnt=epoll_wait(epfd,events,EPOLL_SIZE,-1); //设置无限等待
54         if(event_cnt==-1)
55             printf("epoll_wait() error");
56         for(int i=0;i<event_cnt;i++){
58             if(events[i].data.fd==servsock) { //说明是新的客户端请求
60                 clntlen=sizeof(clntaddr);
61                 clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clntlen);
62                 if(clntsock==-1){ //accept错误
64                     close(clntsock);
65                     continue;
66                 }
67                 event.events=EPOLLIN;
68                 event.data.fd=clntsock;
69                 epoll_ctl(epfd,EPOLL_CTL_ADD,clntsock,&event); //将新申请的连接加入epfd
70                 printf("connecting\n");
71             }
72             else{//客户端发来消息
74                 int strlen=read(events[i].data.fd,message,sizeof(message));
75                 if(strlen==0){  //关闭连接请求
76                     epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL); //从epfd中删除
77                     close(events[i].data.fd);
78                 }
79                 else
80                     write(events[i].data.fd,message,strlen);
81             }
82         }
83     }
84     close(epfd); //关闭epoll例程
85     close(servsock);
86     return 0;
87 }

7、epoll,select,poll的区别

 1. select和poll的不同
(1)select和poll的原理和用法基本上是一样的,其内部实现机制也差不多,主要区别在于注册的结构体不一样。select采用fd_set结构体,每次文件描述符发生改变,相应的fd_set结构体就被改变了,因此在每次调用select之前要重置fd_set结构体。而poll采用的pollfd数组,结构为:

struct pollfd{
    int fd;
    short events;
    short revents;
}

注册的事件和发生的事件分别用events和revents表示,所以只需置一次pollfd数组。简言之,select每次都要重置fd_set,poll只需要置一次pollfd结构体。
(2)poll可以注册的事件类型没有限制,而select注册事件有限制(FD_SETSIZE宏定义的值,32位机通常是1024,64位机通常是2048)。

 2. select(poll)和epoll的区别
select和epoll实现机制不一样。下面的图说明了select的原理
在这里插入图片描述epoll与select的主要不同就在于:
(1) epoll只在初始时完成一次文件描述符的注册,拷贝到内核,即在调用epoll_ctl()指定EPOLL_CTL_ADD时;
(2)epoll在内核态采用回调函数的形式,只有相应的文件描述符发生变化,回调函数才被调用,将发生变化的描述符集中到新的epoll_event的数组中,时间复杂度O(1);
(3)返回的epoll_event数组只包含发生变换的文件描述符,所以不用遍历所有的文件描述符,时间复杂度O(1)。
详细的比较如下表:

系统调用selectpollepoll
事件集合三个fd_set数组,分别记录可读,可写和异常事件,内核修改fd_set数组来通知状态改变。每次调用之前都要重置fd_set数组。一个pollfd结构体数组,统一记录所有的可读,可写及异常事件,通过fdarray[i].events注册事件,fdarray[i].revents通知事件改变直接调用epoll_ctl将事件类型和描述符添加到内核,只需要一次添加即可
应用程序索引就绪文件描述符的事件复杂度O(n)O(n)O(1)
最大支持文件描述符数量有限制6553565535
工作模式LTLTLT/ET(条件触发/边缘触发)
内核实现方式及时间复杂度轮询检测就绪事件O(n)轮询检测就绪事件O(n)回调函数形式检测就绪事件O(1)

8、浅拷贝和深拷贝

浅拷贝和深拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是“引用”。浅拷贝和深拷贝一般在拷贝构造函数和赋值运算符重载函数中涉及到。
 1. 浅拷贝又称为值拷贝,将源对象的值拷贝到目标对象中,如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅仅拷贝的是这个指针变量的值,也就是在目标对象中该指针类型数据和源对象中的该成员指向的是同一块堆空间。这样会带来一个问题,就是在析构函数中释放该堆区数据,会被释放多次。默认的拷贝构造函数和默认的赋值运算符重载函数都是浅拷贝。
 2. 深拷贝先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样指针成员就指向了不同的内存位置。并且里面的内容是一样的,两个对象先后去调用析构函数,分别释放自己指针成员所指向的内存。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

9、拥塞控制机制

 拥塞控制就是防止太多的数据进入到网络中,这样可以使网络中的路由器或者链路不会过载,首先要求当前的网络可以承受住现有的网络负荷,它是一个全局性的过程,拥塞控制的算法有以下四种:慢开始、拥塞避免、快重传、快恢复
 在计算机网络中,宽带、每个路由器节点中的缓存和处理机等,都是网络资源。当在某个时间段中,某一个网络资源的需求量超过了这个资源所能提供的量,网络性能就会变差,这种情况就是拥塞。网络拥塞是由许多的因素引起的,比如当某个节点的缓存容量太小时、或者处理机处理的速率太慢等。拥塞控制就是防止太多的数据进入到网络中,这样可以使网络中的路由器或者链路不会过载,拥塞控制的算法有以下四种:
慢启动(slow-start):当客户端发送数据的时候,如果一次性把大量的数据字节发送到网络中,就有可能引起网络拥塞,因为并不清楚网络的负荷状态。所以较好的方法是先探测一下,由小到大逐渐增大发送窗口,也就是慢慢地增大窗口数值。
拥塞避免(congestion avoidance):让拥塞窗口cwnd缓缓地增大,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍,让拥塞窗口按照线性规律慢慢增长,比慢开始算法的拥塞窗口增长速率慢很多。
快重传(fast retransmit):要求接收方每收到一个失序的报文段之后就立即发出重复确认而不是等待自己发送数据时捎带确认,为的就是让发送方能尽早地知道有报文段没有到达接收方。
快恢复(fast recovery):两个要点,一是当发送方连续收到三个重复确认时,就执行”乘法减小“算法,把慢开始门限ssthresh减半,这是为了预防网络发生拥塞。二是发送方认为网络很可能没有发生阻塞,因此不会执行慢开始算法,而是把cwnd值设置成慢开始门限ssthresh减半之后的数值,然后执行拥塞避免算法,使拥塞窗口呈线性增长。

10、堆和栈的区别

 1. 管理方式 栈是由编译器自动管理,无需手动控制;堆分配和释放都是由程序员控制的。
 2. 空间大小 栈的空间小于堆。堆内存几乎是没有限制;栈一般是有一定的空间大小的。
 3. 碎片问题 堆分配和释放是由程序员控制的(利用new/delete 或 malloc/free),频繁的操作势必会造成内存空间的不连续,从而造成大量的内存碎片,使程序效率降低。栈则不会存在这个问题,因为栈是先进后出的数据结构,在某一数据弹出之前,它之前的所有数据都已经弹出。
 4. 生长方向 堆生长方向是向上,也就是沿着内存地址增加的方向,栈生长方式是向下的,也就是沿着内存地址减小的方向增长。
 5. 分配方式 堆都是动态分配的,没有静态分配的堆。栈有两种分配方式:静态分配和动态分配,静态分配是编译器完成的,比如局部变量的分配;动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器实现的,无需我们手工实现。
 6. 分配效率 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率很高。堆则是 C/C++ 函数提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。

线程和协程的区别

 1. 线程是操作系统的资源,线程的创建、切换、停止等都非常消耗资源,而创建协程不需要调用操作系统的功能,编程语言自身就能完成,所以协程也被称为用户态线程,协程比线程轻量很多;
 2. 线程在多核环境下是能做到真正意义上的并行,而协程是为并发而产生的;
 3. 一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行;
 4. 线程进程都是同步机制,而协程则是异步;
 5. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力;
  6. 操作系统对于线程开辟数量限制在千的级别,而协程可以达到上万的级别。

缓存穿透、击穿、雪崩的区别

缓存穿透:是指客户端查询了根本不存在的数据,使得这个请求直达存储层,导致其负载过大甚至造成宕机。这种情况可能是由于业务层误将缓存和库中的数据删除造成的,当然也不排除有人恶意攻击,专门访问库中不存在的数据导致缓存穿透。
  可以通过缓存空对象的方式和布隆过滤器两种方式来解决这一问题。缓存空对象是指当存储层未命中后,仍然将空值存入缓存层 ,当客户端再次访问数据时,缓存层直接返回空值。还可以将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。
缓存击穿:当一份访问量非常大的热点数据缓存失效的瞬间,大量的请求直达存储层,导致服务崩溃。
  缓存击穿可以通过热点数据不设置过期时间来解决,这样就不会出现上述的问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。除了永不过期的方式,我们也可以通过加互斥锁的方式来解决缓存击穿,即对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。
缓存雪崩:是指当某一时刻缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。
  缓存雪崩的解决方式有三种;第一种是在设置过期时间时,附加一个随机数,避免大量的key同时过期。第二种是启用降级和熔断措施,即发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。第三种是构建高可用的Redis服务,也就是采用哨兵或集群模式,部署多个Redis实例,这样即使个别节点宕机,依然可以保持服务的整体可用。

13、进程调度算法有哪些

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

14、什么是内存泄露,如何检测

 1. 内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
 2. 避免内存泄露的方法主要就是要有良好的编码习惯,动态开辟内存空间,及时释放内存。也可以采用智能指针来避免内存泄露。
 3. 可以采用静态分析技术、源代码插装技术等进行检测。常见的一些检测工具有:
LCLink、ccmalloc、Dmalloc、Electric Fence、Leaky、LeakTracer、MEMWATCH、Valgrind、KCachegrind等等。

15、线程的通信方式

  因为线程间可以共享一份全局内存区域,其中包括初始化数据段、未初始化数据段,以及堆内存段等,所以线程之间可以方便、快速地共享信息。不过,要考虑线程的同步和互斥,应用到的技术有:
 1. 信号 Linux 中使用 pthread_kill() 函数对线程发信号。
 2. 互斥锁、读写锁、自旋锁
 互斥锁 确保同一时间只能有一个线程访问共享资源,当锁被占用时试图对其加锁的线程都进入阻塞状态(释放 CPU 资源使其由运行状态进入等待状态),当锁释放时哪个等待线程能获得该锁取决于内核的调度。
 读写锁 当以写模式加锁而处于写状态时任何试图加锁的线程(不论是读或写)都阻塞,当以读状态模式加锁而处于读状态时“读”线程不阻塞,“写”线程阻塞。读模式共享,写模式互斥
 自旋锁 上锁受阻时线程不阻塞而是在循环中轮询查看能否获得该锁,没有线程的切换因而没有切换开销,不过对 CPU 的霸占会导致 CPU 资源的浪费。 自旋锁可以用于以下情况:锁被持有的时间短,而且线程并不希望在重新调度上花费太多的成本。 自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个已经被持有的自旋锁,那么该线程就会一直进行忙循环 - 旋转 - 等待锁重新可用。
 3. 条件变量 条件变量可以以原子的方式阻塞进程,直到某个特定条件为真为止。对条件的测试是在互斥锁的保护下进行的,条件变量始终与互斥锁一起使用。
   条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。 使用条件变量可以以原子方式阻塞线程,直到某个特定条件为真为止。条件变量始终与互斥锁一起使用,对条件的测试是在互斥锁(互斥)的保护下进行的。如果条件为假,线程通常会基于条件变量阻塞,并以原子方式释放等待条件变化的互斥锁。如果另一个线程更改了条件,该线程可能会向相关的条件变量发出信号,从而使一个或多个等待的线程执行以下操作: 唤醒 再次获取互斥锁 重新评估条件。
 4. 信号量 信号量实际上是一个非负的整数计数器,用来实现对公共资源的控制。在公共资源增加的时候,信号量就增加;公共资源减少的时候,信号量就减少;只有当信号量的值大于0的时候,才能访问信号量所代表的公共资源。

16、虚拟内存与物理内存

 1. 物理内存 以前,还没有虚拟内存概念的时候,程序寻址用的都是物理地址。程序能寻址的范围是有限的,这取决于 CPU 的地址线条数。比如在 32 位平台下,寻址的范围是 2^32 也就是 4G。并且这是固定的,如果没有虚拟内存,且每次开启一个进程都给 4G 物理内存,就可能会出现很多问题:
 因为物理内存是有限的,当有多个进程要执行的时候,都要给 4G 内存,很显然内存不够,这很快就分配完了,于是没有得到分配资源的进程就只能等待。当一个进程执行完了以后,再将等待的进程装入内存。这种频繁的装入内存的操作效率很低。
 由于指令都是直接访问物理内存的,那么任何进程都可以修改其他进程的数据,甚至会修改内核地址空间的数据,这是不安全的。
 2. 虚拟内存 虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存(一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换。

分段和分页

  分段 将用户程序地址空间分成若干个大小不等的段,每段可以定义一组相对完整的逻辑信息。存储分配时,以段 为单位,段与段在内存中可以不相邻接,实现了离散分配。分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。
  分页 用户程序的地址空间被划分成若干固定大小的区域,称为“页”,相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配。分页主要用于实现虚拟内存,从而获得更大的地址空间。
  段页式 页式存储管理能有效地提高内存利用率(解决内存碎片),而分段存储管理能反映程序的逻辑结构并有利于段的共享。将这两种存储管理方法结合起来,就形成了段页式存储管理方式。
  段页式存储管理方式即先将用户程序分成若干个段,再把每个段分成若干个页,并为每一个段赋予一个段名。在段页式系统中,为了实现从逻辑地址到物理地址的转换,系统中需要同时配置段表和页表,利用段表和页表进行从用户地址空间到物理内存空间的映射。
  系统为每一个进程建立一张段表,每个分段有一张页表。段表表项中至少包括段号、页表长度和页表始址,页表表项中至少包括页号和块号。在进行地址转换时,首先通过段表查到页表始址,然后通过页表找到页帧号,最终形成物理地址。
在这里插入图片描述

乐观锁和悲观锁

乐观锁:乐观锁总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。
悲观锁:悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
 两种锁的使用场景
乐观锁: GIT,SVN,CVS等代码版本控制管理器,就是一个乐观锁使用很好的场景,例如:A、B程序员,同时从SVN服务器上下载了code.html文件,当A完成提交后,此时B再提交,那么会报版本冲突,此时需要B进行版本处理合并后,再提交到服务器。这其实就是乐观锁的实现全过程。如果此时使用的是悲观锁,那么意味者所有程序员都必须一个一个等待操作提交完,才能访问文件,这是难以接受的。
悲观锁: 悲观锁的好处在于可以减少并发,但是当并发量非常大的时候,由于锁消耗资源、锁定时间过长等原因,很容易导致系统性能下降,资源消耗严重。因此一般我们可以在并发量不是很大,并且出现并发情况导致的异常用户和系统都很难以接受的情况下,会选择悲观锁进行。

孤儿进程,什么是僵尸进程

 1. 孤儿进程 是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为1)所收养,并且由 init 进程对它们完整状态收集工作,孤儿进程一般不会产生任何危害。
 2. 僵尸进程 僵尸进程是指一个进程使用 fork() 函数创建子进程,如果子进程退出,而父进程并没有调用 wt() 或者wtpid() 系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
 3. 解决僵尸进程 一般,为了防止产生僵尸进程,在 fork() 子进程之后我们都要及时在父进程中使用 wt() 或者 wtpid() 系统调用,等子进程结束后,父进程回收子进程 PCB 的资源。 同时,当子进程退出的时候,内核都会给父进程一个 SIGCHLD 信号,所以可以建立一个捕获 SIGCHLD 信号的信号处理函数,在函数体中调用 wt() 或 wtpid(),就可以清理退出的子进程以达到防止僵尸进程的目的。

野指针

 1. 什么是野指针 野指针是指指向的位置是随机的、不可知的、不正确的。
 2. 野指针产生的原因
  (1)指针变量未初始化或者随便赋值:指针变量没有初始化,其值是随机的,也就是指针变量指向的是不确定的内存,如果对它解除引用,结果是不可知的。
  (2)指针释放后未置空:有时候指针在释放后没有复制为 nullptr,虽然指针变量指向的内存被释放掉了,但是指针变量中的值还在,这时指针变量就是指向一个未知的内存,如果对它解除引用,结果是不可知的。
  (3)指针操作超出了变量的作用域:函数中返回了局部变量的地址或者引用,因为局部变量出了作用域就释放了,这时候返回的地址指向的内存也是未知的。
 3. 如何避免野指针
  (1)指针变量一定要初始化,可以初始化为 nullptr,因为 nullptr 明确表示空指针,对 nullptr 操作也不会有问题。
  (2)释放后置为 nullptr。

如何判断大端和小端

 大端和小端指的是字节序,就是大于一个字节类型的数据在内存中的存放顺序。0x123456在内存中的存储方式
大端模式  低地址 -----> 高地址
     0x12 | 0x34 | 0x56
小端模式 低地址 -----> 高地址
      0x56 | 0x34 | 0x12
为什么会有大小端之分
  一开始是由于不同架构的CPU处理多个字节数据的顺序不一样,比如x86的是小段模式,KEIL C51是大端模式。但是后来互联网流行,TCP/IP协议规定为大端模式,为了跨平台通信,还专门出了网络字节序和主机字节序之间的转换接口(ntohs、htons、ntohl、htonl)
  大小端模式各有优势:小端模式强制转换类型时不需要调整字节内容,直接截取低字节即可;大端模式由于符号位为第一个字节,很方便判断正负。
 1. 大端字节序:是指一个整数的最高位字节(23 ~ 31 bit)存储在内存的低地址处,低位字节(0 ~ 7 bit)存储在内存的高地址处。
 2. 小端字节序:是指整数的高位字节存储在内存的高地址处,而低位字节则存储在内存的低地址处。
 3. 如何判断大端还是小端:可以定义一个联合体,联合体中有一个 short 类型的数据,有一个 char 类型的数组,数组大小为 short 类型的大小。给 short 类型成员赋值一个十六进制数 0x0102,然后输出根据数组第一个元素和第二个元素的结果来判断是大端还是小端。

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

写时拷贝

 写时拷贝顾名思义就是“写的时候才分配内存空间”。传统的 fork() 系统调用直接把所有的资源复制给新创建的进程,这种实现过于简单并且效率低下,因为它拷贝的数据或许可以共享,或者有时候 fork() 创建新的子进程后,子进程往往要调用一种 exec函数以执行另一个程序。而 exec函数会用磁盘上的一个新程序替换当前子进程的正文段、数据段、堆段和栈段,如果之前 fork() 时拷贝了内存,则这时被替换了,这是没有意义的。 Linux 的 fork() 使用写时拷贝(Copy-on-write)页实现。写时拷贝是一种可以推迟甚至避免拷贝数据的技术。内核此时并不复制整个进程的地址空间,而是让父子进程共享同一个地址空间。只用在需要写入的时候才会复制地址空间,从而使各个进行拥有各自的地址空间。也就是说,资源的复制是在需要写入的时候才会进行,在此之前,只有以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候,大大提高了效率。

  • fork()函数
     一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
     一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。在调用fork后,fork函数后面的所有代码会执行两遍。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char const *argv[]){
    pid_t pid;
    if ((pid=fork()) < 0)
        perror("fork error");
    else if (pid == 0)//子进程
         printf("child getpid()=%d\n", getpid());
    else if(pid > 0)//父进程
        printf("parent getpid()=%d\n", getpid());
    return 0;
}
//结果如下:
//    parent getpid()=13725
//    child getpid()=13726

 在fork函数()执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。
 引用一位网友的话来解释fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
 fork出错可能有两种原因:
(1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
(2)系统内存不足,这时errno的值被设置为ENOMEM。
 创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
 每个进程都有一个互不相同的进程标识符,可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。

动态库静态库的区别和优缺点

 1. 命令方式不同:
静态库命名 Linux : libxxx.a
 lib : 前缀(固定); xxx : 库的名字;  .a : 后缀(固定)。
  Windows : libxxx.lib
动态库命名 Linux : libxxx.so
 lib : 前缀(固定); xxx : 库的名字;  .so : 后缀(固定)
 Windows : libxxx.dll
 2. 链接时间和方式不同:静态库的链接是将整个函数库的所有数据在编译时都整合进了目标代码;动态库的链接是程序执行到哪个函数链接哪个函数的库 。
 (1)静态库优缺点:优点:发布程序时无需提供静态库,移植方便,运行速度相对快些;缺点:静态链接生成的可执行文件体积较大,消耗内存,如果所使用的静态库发生更新改变,程序必须重新编译,更新麻烦。
 (2)动态库优缺点:优点:更加节省内存并减少页面交换,动态库改变并不影响使用的程序,动态函数库升级比较方便;缺点:发布程序时需要提供动态库

C++分配内存的4种方式

calloc 函数void *calloc(unsigned int num, unsigned int size)
按照所给的数据个数和数据类型所占字节数,分配一个 num * size 连续的空间。calloc申请内存空间后,会自动初始化内存空间为 0,但是malloc不会进行初始化,其内存空间存储的是一些随机数据。
malloc 函数void *malloc(unsigned int size)
在内存的动态分配区域中分配一个长度为size的连续空间,如果分配成功,则返回所分配内存空间的首地址,否则返回NULL,申请的内存不会进行初始化。
realloc 函数void *realloc(void *ptr, unsigned int size)
动态分配一个长度为size的内存空间,并把内存空间的首地址赋值给ptr,把ptr内存空间调整为size。申请的内存空间不会进行初始化。
new是动态分配内存的运算符,自动计算需要分配的空间,在分配类类型的内存空间时,同时调用类的构造函数,对内存空间进行初始化,即完成类的初始化工作。动态分配内置类型是否自动初始化取决于变量定义的位置,在函数体外定义的变量都初始化为0,在函数体内定义的内置类型变量都不进行初始化。

C++五个内存分区

1.  由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
2.  由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
3.自由存储区  由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。
4.全局/静态存储区  全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分。
5.常量存储区  里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改)。

程序设计

简述 C++ 的多态

  在面向对象中,多态是指通过基类的指针或者引用。
静态多态 静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
动态多态 动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。
 动态多态行为的表现效果为:同样的调用语句在实际运行时有多种不同的表现形态。
 实现动态多态的条件:
  (1)要有继承关系,要有虚函数重写(被 virtual 声明的函数叫虚函数);
  (2)要有父类指针(父类引用)指向子类对象 。
 动态多态的实现原理 当类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构, 虚函数表是由编译器自动生成与维护的。virtual 成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr 指针)。在多态调用时, vptr 指针就会根据这个对象在对应类的虚函数表中查找被调用的函数,从而找到函数的入口地址。

简述一下什么是面向对象

 1. 面向过程思想 具体的每一步都需要我们去实现和操作。这些步骤相互调用和协作,从而完成需求。在上面的每一个具体步骤中我们都是参与者,并且需要面对具体的每一个步骤和过程,这就是面向过程最直接的体现。
 2. 面向对象思想 面向对象的思想是尽可能模拟人类的思维方式,使得软件的开发方法与过程尽可能接近人类认识世界、解决现实问题的方法和过程,把客观世界中的实体抽象为问题域中的对象。面向对象以对象为核心,该思想认为程序由一系列对象组成。 面向对象思想的特点: 是一种更符合人类思维习惯的思想,可以将复杂的问题简单化 ,将我们从执行者变成了指挥者。但是面向对象还是基于面向过程的。

简述一下面向对象的三大特征

 1. 封装 将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理, C++通过 private、protected、public 三个关键字来控制成员变量和成员函数的访问权限,private 修饰的成员只能在本类中访问,protected 表示受保护的权限,修饰的成员只能在本类或者子类中访问,public 修饰的成员是公共的,哪儿都可用访问。
封装的好处:隐藏实现细节,提供公共的访问方式;提高代码的复用性;提高安全性。
 2. 继承 C++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类),类B成为派生类(子类)。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。
继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。
 3. 多态 在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向 父类对象时,就调用父类中定义的虚函数;即当父类指针(引用)指向 子类对象时,就调用子类中定义的虚函数。
多态的好处:多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。

4、多进程并发

 并发程序设计中,主要有三种并发方式:多进程并发,基于IO复用并发,多线程并发
 进程是具有独立功能的程序关于某个数据集合的一次运行活动,是OS为正在运行的程序建立的管理实体,是系统资源管理与分配的基本单位。一个进程有五部分:操作系统管理该进程的数据结构(PCB),内存代码,内存数据,程序状态字PSW,通用寄存器信息。一个进程在OS中有四个基本状态。
在这里插入图片描述
 1. 多进程并发
 进程是程序运行的基本单位,对于多内核的计算机,多个进程可以在多内核上同时运行,提高程序的并发性。如对于C/S类型的模型,客户端每发起一次通信,服务器开辟一个进程于其连接。这样实现服务器同时服务多个客户端。。
 Linux系统的每个进程都有一个标志号,称为进程ID,其值大于2(1要分配给系统启动后的首个进程,用于协助操作系统)。在Linux系统中,创建一个进程采用fork函数

#include <unistd.h>
pid_t fork(void);  //pid_t为返回的ID号

 调用fork函数之后,子进程创建,子进程会复制父进程的所有信息,然后从fork调用之后开始执行。那么怎么让父子进程执行不同的程序路径呢?这是通过主程序判断实现的,父进程调用fork函数,返回的是子进程的ID;而子进程的fork函数返回0,通过此返回值区别父子进程,从而控制fork函数之后的执行流。
 2. 僵尸进程
 父进程fork子进程后,两个进程按各自的程序执行。父子进程结束时通过以下两种操作返回值并结束。
(1) 通过调用return语句返回;
(2) 通过exit()函数返回。
 此返回值会保存至OS。但是子进程结束后,其返回值返回给操作系统(OS),此时OS并不会回收分配给子进程的所有资源。所以当父进程没执行完而子进程执行完成时,子进程资源没被回收,此时的子进程即为僵尸进程。僵尸进程会造成系统资源浪费。那么什么时候子进程资源会被回收呢?
(1) 当父进程结束之后;
(2) 当父进程向OS请求子进程返回值时。
因此为了结束僵尸进程,需要父进程主动向OS请求子进程的返回值。通过以下两种方式实现:
(1)父进程结束之前调用wait()函数

#include <sys/wait.h>
int status; //保存返回时的状态信息
wait(&status);
if(WIFEXITED(status)//WIFEXITED()在子进程正常终止时返回真
    printf("Child return:%d",WEXITSTATUS(status)); //WEXITSTATUS获取子进程的返回值

(2) 调用waitpid()

#include <sys/wait.h>
pid_t waitpid(pid_t pid,int* statloc,int options);
pid:等待的进程ID,若-1,则等待任一进程终止
statloc:用于保存返回状态的变量,与wait函数的参数一致
options:一般传递WNOHANG,意为非阻塞。

waitpid为非阻塞状态。
  由于进程之间只共享文件表,没有共享用户地址空间。进程之间的调度开销比较大,而且进程通信必须采用显式的IPC机制。常见的就是管道。因此多进程并发耗资源,耗时,通信不方便,较少采用。

5、new 的实现原理,new 和 malloc 的区别

 1. new 的实现原理:如果是简单类型,则直接调用 operator new(),在 operator new() 函数中会调用 malloc() 函数,如果调用 malloc() 失败会调用 _callnewh(),如果 _callnewh() 返回 0 则抛出 bac_alloc 异常,返回非零则继续分配内存。 如果是复杂类型,先调用 operator new()函数,然后在分配的内存上调用构造函数。
 2. new 和 malloc 的区别
 (1)new 是操作符,而 malloc 是函数;
 (2)使用 new 操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算,而 malloc 则需要显式地指出所需内存的尺寸;
 (3)new 分配失败的时候会直接抛出异常,malloc 分配失败会返回 NULL;
 (4)对于非简单类型,new 在分配内存后,会调用构造函数,而 malloc 不会;
 (5)new 分配成功后会返回对应类型的指针,而 malloc 分配成功后会返回 void * 类型;
 (6)malloc 可以分配任意字节,new 只能分配实例所占内存的整数倍数大小;
 (7)new 可以被重载,而 malloc 不能被重载;
 (8)new 操作符从自由存储区上分配内存空间,而 malloc 从堆上动态分配内存;
 (9)使用 malloc 分配的内存后,如果在使用过程中发现内存不足,可以使用 realloc 函数进行内存重新分配实现内存的扩充,new 没有这样直观的配套设施来扩充内存。

6、 STL 中有哪些常见的容器

 STL 中容器分为顺序容器、关联式容器、容器适配器三种类型,三种类型容器特性分别如下:
 1. 顺序容器 容器并非排序的,元素的插入位置同元素的值无关,包含 vector、deque、list。
 vector:动态数组 元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
 deque:双向队列 元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于 vector )。在两端增删元素具有较佳的性能(大部分情况下是常数时间)。
 list:双向链表 元素在内存不连续存放。在任何位置增删元素都能在常数时间完成。不支持随机存取。
 2. 关联式容器 元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树的方式实现,包含set、multiset、map、multimap。
set/multiset set中不允许相同元素,multiset 中允许存在相同元素。
map/multimap map 与 set 的不同在于 map 中存放的元素有且仅有两个成员变,一个名为 first,另一个名为 second,map 根据 first 值对元素从小到大排序,并可快速地根据 first 来检索元素。map 和multimap 的不同在于是否允许相同 first 值的元素。
 无序关联式容器 unordered_map、unordered_multimap、unordered_set、unordered_multiset
 3. 容器适配器 封装了一些基本的容器,使之具备了新的函数功能,包含 stack、queue、priority_queue。
stack:栈 栈是项的有限序列,并满足序列中被删除、检索和修改的项只能是最进插入序列的项(栈顶的项),后进先出。
queue:队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出。
priority_queue:优先级队列 内部维持某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列。

STL 容器用过哪些,查找的时间复杂度是多少

  注意:容器的时间复杂度取决于其底层实现方式。

容器底层实现插入查看删除
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. 定义和性质不同。指针是一种数据类型,用于保存地址类型的数据,引用可以看成是变量的别名。指针定义格式为:数据类型 *;引用的定义格式为:数据类型 &;
 2. 引用不可为空,创建时必须初始化,而指针变量可空,在任何时候初始化;
 3. 指针可以有多级,但引用只能是一级;
 4. 引用使用时无需解引用,指针需要解引用;
 5. 指针变量的值可以是 NULL,引用的值不可以为 NULL;
 6. 指针的值在初始化后可以改变,指向其它的存储单元,引用初始化后就不会再改变了;
 7. sizeof 引用是所指向的变量(对象)的大小,而 sizeof 指针是指针变量本身的大小;
 8. 指针作为函数参数时传递的是指针变量的值,而引用传递的是实参本身;
 9. 指针和引用进行++运算意义不一样。

  • 引用的概念
     1. 引用(Reference)是 C++ 相对于 C 语言的一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于 Windows 中的快捷方式,一个可执行程序可以有多个快捷方式;
     2. 基本语法 typename & ref = varname;
     3. 使用引用的注意事项:引用必须引用合法的内存空间、引用在定义时必须初始化 、引用一旦初始化后,就不能再引用其它数据、引用在定义时需要加上 &,在使用时不能加 &,使用时加 & 表示取地址、函数中不要返回局部变量的引用;
     4. 引用的本质是指针,底层的实现还是指针。

三种智能指针实现原理和使用场景,以及其线程安全

 1. 智能指针实现原理 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权。这就是用于 auto_ptrunique_ptr 的策略,但 unique_ptr 的策略更严格,unique_ptr 能够在编译期识别错误。 跟踪引用特定对象的智能指针计数,这称为引用计数(reference counting)。例如,赋值时,计数将加 1,而指针过期时,计数将减 1. 仅当最后一个指针过期时,才调用 delete。这是 shared_ptr 采用的策略。
 2. 使用场景 如果程序要使用多个指向同一个对象的指针,应该选择 shared_ptr; 如果程序不需要多个指向同一个对象的指针,则可以使用 unique_ptr; 如果使用 new [] 分配内存,应该选择 unique_ptr; 如果函数使用 new 分配内存,并返回指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。
 3. 线程安全 shared_ptr 智能指针的引用计数在手段上使用了 atomic 原子操作,只要 shared_ptr 在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所有 shared_ptr 智能指针在多线程下引用计数也是安全的,也就是说 shared_ptr 智能指针在多线程下传递使用时引用计数是不会有线程安全问题的。 但是指向对象的指针不是线程安全的,使用 shared_ptr 智能指针访问资源不是线程安全的,需要手动加锁解锁。智能指针的拷贝也不是线程安全的。

  • 智能指针有没有内存泄露的情况
     智能指针有内存泄露的情况:当两个类对象中各自有一个 shared_ptr 指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄露。为了解决循环引用导致的内存泄漏,引入了弱指针weak_ptr,weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。
  • 智能指针和指针的区别是什么
     (1) 智能指针 使用 new 从堆(自由存储区)分配内存,不需要时,应用 delete 释放。C++ 引入了智能指针 auto_ptr,帮助自动完成这个过程。基于程序员的编程体验和 BOOST 库提供的解决方案,C++11 摒弃了 auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr 和 weak_ptr。所有新增的智能指针都能与 STL 容器和移动语义协同工作。
     (2) 指针 指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。
     (3) 智能指针实际上是对普通指针加了一层封装机制,它负责自动释放所指的对象。指针是一种数据类型,用于保存内存地址,而智能指针是类模板。

10、C++11、C++14、C++17、C++20 都有什么新特性

  C++11 新 static_assert 编译时断言;新增加类型 long long;unsigned long long;char16_t;char32_t;原始字符串;auto - decltype ;委托构造函数;
constexpr表达式是指值不会改变并且在编译过程就能得到计算结果的表达式,声明为constexpr的变量一定是一个const变量,而且必须用常量表达式初始化;
模板别名;alignas;alignof ;原子操作库;nullptr;显示转换运算符;继承构造函数;变参数模板;列表初始化;右值引用 ;Lambda 表达式;override;final;unique_ptr;
shared_ptr;initializer_list;array;unordered_map;unordered_set;线程支持库。
 C++14 新 二进制字面量;泛型 Lambda 表达式;带初始化/泛化的 Lambda 捕获;变量模板;[[deprecated]]属性;std::make_unique;std::shared_timed_mutex、std::shared_lock;std::quoted;std::integer_sequence ;std::exchange。
  C++17 新 - 构造函数模板推导 - 结构化绑定 - 内联变量 - 折叠表达式 - 字符串转换 - std::shared_mutex 4.   C++20 新特新 - 允许 Lambda 捕获 [=, this] - 三路比较运算符 - char8_t - 立即函数(consteval) - 协程 - constinit

11、单例设计模式

  1. 概念 单例设计模式属于创建型模式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:单例类只能有一个实例、单例类必须自己创建自己的唯一实例、单例类必须给所有其他对象提供这一实例。
 2. 单例设计模式的优缺点
 优点:单例模式可以保证内存里只有一个实例,减少了内存的开销;可以避免对资源的多重占用;单例模式设置全局访问点,可以优化和共享资源的访问。
 缺点:单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则; 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象;单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
 3. C++ 单例设计模式的实现:
(1) 私有化构造函数、拷贝构造函数、赋值函数;
(2)定义一个私有的本类的静态对象成员;
(3)定义一个公共的访问该示例静态成员方法,返回该静态对象成员 。
 4. 单例设计模式的种类
(1)懒汉式:获取该类的对象时才创建该类的实例;
(2)饿汉式:获取该类的对象之前已经创建好该类的实例

12、C++ 的重载和重写

重载
(1) 重载是指不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关;
(2) 重载的规则 - 函数名相同 - 必须具有不同的参数列表 - 可以有不同的访问修饰符;
(3) 重载用来实现静态多态(函数名相同,功能不一样);
(4) 重载是多个函数或者同一个类中方法之间的关系,是平行关系。
重写
(1)重写(也叫覆盖)是指在派生类中重新对基类中的虚函数重新实现。即函数名和参数都一样,只是函数的实现体不一样;
(2)重写的规则:
 方法声明必须完全与父类中被重写的方法相同;
 访问修饰符的权限要大于或者等于父类中被重写的方法的访问修饰符;
 子类重写的方法可以加virtual,也可以不加;
(3) 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数);
(4) 重写是父类和子类之间的关系,是垂直关系。

 1. C++中重载的实现 采用命名倾轧技术,编译时会将同名的函数或方法根据某种规则生成不同的函数或方法名(因为函数或方法的特征标不一样)。
 2. C++中重写的实现 C++中重写可以用来实现动态多态,父类中需要重写的方法要加上 virtual 关键字。 虚函数实现的原理是采用虚函数表,多态中每个对象内存中都有一个指针,被称为虚函数指针,这个指针指向虚函数表,表中记录的是该类的所有虚函数的入口地址,所以对象能够根据它自身的类型调用不同的函数。

13、重载,复写,隐藏的区别

 1. 重载:在同一作用域中,同名函数的形式参数(参数个数、类型或者顺序)不同时,构成函数重载,与返回值类型无关。
 2. 隐藏:指不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部函数。
 隐藏的实质是:在函数查找时,名字查找先于类型检查。如果派生类中成员和基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字一样的则停止查找。
 3.重写:派生类中与基类同返回值类型、同名和同参数的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。

作用域有无virtual函数名形参列表返回值类型
重载相同无关相同不同无关
重写不同相同相同相同
隐藏不同无关相同无关无关

14、虚函数的实现原理

 1. 虚函数的作用 C++ 中.主要是实现了动态多态的机制。动态多态就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。
 2. 虚函数实现原理 编译器处理虚函数时,给每个对象添加一个隐藏的成员。隐藏的成员是一个指针类型的数据,指向的是函数地址数组,这个数组被称为虚函数表。虚函数表中存储的是类中的虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址,如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。
 使用虚函数时,对于内存和执行速度方面会有一定的成本:
 (1)每个对象都会变大,变大的量为存储虚函数表指针;
 (2)对于每个类,编译器都会创建一个虚函数表;
 (3)对于每次调用虚函数,都需要额外执行一个操作,就是到表中查找虚函数地址。

15、 C++ 和 C 中 struct 的区别以及和 class 的区别

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

struct student{ 
	int age; 
	string name; 
} 
typedef struct student student2; //C中取别名
struct student stu1; // C 中正常使用 
student2 stu2; // C 中通过取别名的使用 
student stu3; // C++ 中使用,C 中直接使用编译不通过 

struct 和 class 的区别:
 1. struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
 2. struct 中默认访问控制权限是 public,而 class 中默认的访问控制权限是 private;
 3. 在继承关系中,struct 默认是公有继承,而 class 是私有继承;
 4. class 关键字可以用于定义模板参数,而 struct 不能

template<class t=""> int func(const T& t, const Y& y) { }</class>

16、各数据类型 sizeof 是多少,sizeof 指针是多少,sizeof 原理

 1. 各数据类型 sizeof 的结果其实就是该数据类型的字节数,不同类型的数据 sizeof 的结果是不一样的,并且不同的操作系统和编译器下同一数据类型的 sizeof 的结果也不一样,具体看编译器是如何实现的。以下是Microsoft C++ 中对常见内置类型 sizeof 的代码:

#include <iostream> 
using namespace std; 
int mn() 
{ 
	cout << sizeof(bool) << endl;//1
	cout << sizeof(char) << endl;//1
	cout << sizeof(short) << endl;//2
	cout << sizeof(int) << endl;//4
	cout << sizeof(long) << endl;//4
	cout << sizeof(long long) << endl;//8 
	return 0; 
} 

 2. 对指针变量进行 sizeof 运算,获得的是指针变量的大小,而无论是什么类型的指针,在同一平台下结果都是一样的。在 32 位平台下是 4 个字节,在 64 位平台下是 8 个字节。
 3. sizeof 的原理:sizeof 是在编译的时候,查找符号表,判断类型,然后根据基础类型来取值。如果 sizeof 运算符的参数是一个不定长数组,则该需要在运行时计算数组长度。

17、为什么将析构函数设置成虚函数

virtual class Base { 
	public: 
	Base() { }
	virtual ~Base() { } // 虚析构函数
} 

  虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄露。如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时就会造成内存泄露。

18、malloc 的实现原理

 malloc() 的整体思想是先向操作系统申请一块大小适当的内存,然后自己管理,即内存池。 malloc() 分配空间有一个数据结构,允许它来区分边界,区分已分配和空闲的空间,数据结构中包含一个头部信息和有效载荷,有效载荷的首地址就是 malloc() 返回的地址,可能在尾部还有填充,为了保持内存对齐。头部相当于该数据结构的元数据,其中包含了块大小和是否是空闲空间的信息,这样可以根据头地址和块大小的地址推出下一个内存块的地址,这就是隐式链表。 malloc() 基本的实现原理就是维护一个内存空闲链表,当申请内存空间时,搜索内存空闲链表,找到适配的空闲内存空间,然后将空间分割成两个内存块,一个变成分配块,一个变成新的空闲块。如果没有搜索到,那么就会申请内存空间。
 搜索空闲块最常见的算法有:首次适配,下一次适配,最佳适配
 首次适配:第一次找到足够大的内存块就分配,这种方法会产生很多的内存碎片。
 下一次适配:也就是说等第二次找到足够大的内存块就分配,这样会产生比较少的内存碎片。
 最佳适配:对堆进行彻底的搜索,从头开始遍历所有块,使用数据区大小大于 size 且差值最小的块作为此次分配的块。 在释放内存块后,如果不进行合并,那么相邻的空闲内存块还是相当于两个内存块,会形成一种假碎片。所以当释放内存后,需要将两个相邻的内存块进行合并。
 还有一种实现方式则是采用显示空闲链表,这个是真正的链表形式。在之前的有效载荷中加入了前驱和后驱的指针,也可以称为双向链表。维护空闲链表的的方式第一种是用后进先出(LIFO),将新释放的块放置在链表的开始处。另一种方法是按照地址的顺序来维护。

19、delete 和 free 的区别

 1. delete是操作符,而 free是函数;
 2. delete释放 new 分配的空间,free释放 malloc 分配的空间;
 3. free会不会调用对象的析构函数,而 delete会调用对象的析构函数;
 4. 调用 free之前需要检查要释放的指针是否为 NULL,使用 delete 释放内存则不需要检查指针是否为 NULL。

20、vector 和 list 的区别,分别适用于什么场景

 1. 区别:
 vector 底层实现是数组,list 是双向链表;
 vector 支持随机访问,list 不支持;
 vector 是顺序内存,list 不是;
 vector 在中间节点进行插入删除会导致内存拷贝,list 不会;
 vector 一次性分配好内存,不够时才进行扩容,list 每次插入新节点都会进行内存申请;
 vector 随机访问性能好,插入删除性能差,list 随机访问性能差,插入删除性能好。
 2. 适用场景:vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用 vector0。
 list 拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

21、C语言里面 volatile,可以和 const 同时使用吗

 volatile 限定符是用来告诉计算机,所修饰的变量的值随时都会进行修改的。用于防止编译器对该代码进行优化。通俗的讲就是编译器在用到这个变量时必须每次都小心地从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份。
 const 和 volatile 可以一起使用,volatile 的含义是防止编译器对该代码进行优化,这个值可能变掉的。而 const 的含义是在代码中不能对该变量进行修改。因此,它们本来就不是矛盾的。

  • const 的用法:
     1. 用在变量身上,表示该变量只读,不能对它的值进行修改
     2. 结合指针一起使用
     const int * p 是常量指针,表示指针变量 p 所指向的内容不能修改,指针变量 p 的内容可以修改;
     int * const p 是指针常量,表示指针变量 p 的内容不能修改,指针变量 p 所指向的内容可以修改;
     const int * const p 表示指针变量 p 的内容和所指向的内容都不可以修改;
     3. const 用于函数参数
    void foo(const int * p);
    void foo(const int & p);
     const 用于形参时说明形参在函数内部不能被改变,这是非常有用的,有时候函数参数传递指针或者引用,在函数内部不希望对指针和引用指向的数据进行修改,可以加上 const;
     4. 在类中修饰成员方法,防止在方法中修改非 static 成员
 class A { 
 public: 
	 int a; 
	 void fun() const 
	 { 
		 a = 20; // 错误,const 修饰的成员方法中不能修改非静态成员变量 
     } 
 }

  5. const 修饰类的成员变量

 class T { 
 public: 
	 T() : a(10) { } 
 private: 
 	const int a; 
 	static const int b; 
 }; 
 const int T::b = 20; 

类的成员变量可以分为静态的和非静态的,如果 const 修饰的是静态的成员变量,可以在构造函数中对该变量进行初始化;如果 const 修饰的是静态的成员变量,则需要在类外对该变量进行初始化。

22、static 关键字的作用

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

23、const 和 define 的区别

 const 在 C 语言中表示只读,在 C++ 中增加了常量的语义。而 define 用于定义宏,而宏也可以用于定义常量。它们的区别有:
 1. const 生效于编译阶段,而 define 生效于预处理阶段;
 2. define只是简单的字符串替换,没有类型检查,而 const 有对应的数据类型,编译器要进行判断的;
 3. define 定义的常量是不可以用指针变量去指向的,用 const 定义的常量是可以用指针去指向该常量的地址的;
 4. define 不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const 在静态存储区中分配空间,在程序运行过程中内存中只有一个拷贝;
 5. 可以对 const 常量进行调试,但是不能对宏常量进行调试。

24、unique_ptr 的实现原理及使用场景

 1. 实现原理 建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权,这就是用于 unique_ptr 的策略。 unique_ptr 中把拷贝构造函数和拷贝赋值声明为 private 或 delete,这样就不可以对指针指向进行拷贝了,也就不能产生指向同一个对象的指针。 原理:
(1)构造时传入托管对象的指针,析构时delete对象;
(2)禁用赋值函数;

public:
    unique_ptr(const unique_ptr&) = delete;
    unique_ptr& operator=(const unique_ptr&) = delete;

使用"=delete"修饰,表示函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则编译就会出错。虽然赋值禁用,但允许交出控制权,交给赋值的人。

unique_ptr(unique_ptr&& move) noexcept
{
    std::cout << "construct for unique_ptr&&" << std::endl;
	move.swap(*this);
}
unique_ptr& operator=(unique_ptr&& move) noexcept
{
	move.swap(*this);
	return *this;
}

这参数move的类型,是一个右值引用,其实就是std::move的返回值。

unique_ptr<Test> tPtr1(new Test());
unique_ptr<Test> tPtr3(std::move(tPtr1));

这种情况下,是可以编译过的,只不过tPtr1的资源,如其成员变量,将无法再调用,用的话就崩溃。因为它已经全部交给tPtr3。

25、C++ Lambda 表达式用法及实现原理

Lambda 表达式完整的格式如下:

[捕获列表] (形参列表) mutable 异常列表-> 返回类型
{
    函数体
}

各项的含义:
捕获列表:捕获外部变量,捕获的变量可以在函数体中使用(可省略,即不捕获外部变量)
[]:默认不捕获任何变量;
[=]:默认以值捕获所有变量;
[&]:默认以引用捕获所有变量;
[x]:仅以值捕获x,其它变量不捕获;
[&x]:仅以引用捕获x,其它变量不捕获;
[=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
[&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
[this]:通过引用捕获当前对象(其实是复制指针);
[*this]:通过传值方式捕获当前对象;
形参列表:和普通函数的形参列表一样(可省略,即无参数列表)
mutable:mutable 关键字,如果有,则表示在函数体中可以修改捕获变量
异常列表:noexcept / throw(…),和普通函数的异常列表一样,可省略,即代表可能抛出任何类型的异常。
返回类型:和函数的返回类型一样。可省略,如省略,编译器将自动推导返回类型。
函数体:代码实现。可省略,但是没意义。
  lambda表达式因为禁用了赋值操作符,所以不可以直接赋值,即使他们形参和返回值完全一样,但是lambda表达式没有禁用复制构造函数,所以你仍然可以用一个lambda表达式去初始化另外一个lambda表达式而产生副本,并且lambda表达式也可以赋值给相对应的函数指针,这也使得你完全可以把lambda表达式看成对应函数类型的指针。
 另外,lambda表达式不能有默认参数,不支持可变参数。

int a = 1;
int b = 2;
auto lambda = [a, b](int x, int y)mutable throw() -> bool
{
    return a + b > x + y;
};
bool ret = lambda(3, 4);

编译器实现 lambda 表达式大致分为一下几个步骤:
 创建 lambda匿名类,实现构造函数,使用 lambda 表达式的函数体重载 operator()(所以 lambda 表达式 也叫匿名函数对象);
 创建 lambda 对象;
 通过对象调用 operator()。
所以编译器将 lambda 表达式翻译后的代码:

class lambda_x
{
private:
    int a;
    int b;
public:
    lambda_x(int _a, int _b) :a(_a), b(_b)
    {
    }
    bool operator()(int x, int y) throw()
    {
        return a + b > x + y;
    }
};
void LambdaDemo()
{
    int a = 1;
    int b = 2;
    lambda_x lambda = lambda_x(a, b);
    bool ret = lambda.operator()(3, 4);
}

类名 lambda_x 的 x是为了防止命名冲突加上的。
 1. lambda 表达式中的捕获列表,对应 lambda_x类的 private 成员;
 2. lambda 表达式中的形参列表,对应 lambda_x类成员函数 operator() 的形参列表;
 3. lambda 表达式中的 mutable,表明 lambda_x类成员函数 operator() 的是否具有常属性 const,即是否是 常成员函数;
 4. lambda 表达式中的返回类型,对应 lambda_x类成员函数 operator() 的返回类型;
 5. lambda 表达式中的函数体,对应 lambda_x类成员函数 operator() 的函数体;
另外,lambda 表达 捕获列表的捕获方式,也影响 对应 lambda_x类的 private 成员的类型
 a. 值捕获:private 成员的类型与捕获变量的类型一致;
 b. 引用捕获:private 成员 的类型是捕获变量的引用类型。

26、虚函数和纯虚函数的区别

 1. 格式 :虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) {}
    纯虚函数的定义格式为:virtual 返回值类型 函数名(参数列表) = 0;
 2. 特点 :虚函数可以有具体的实现,纯虚函数没有具体的实现。 对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。 有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数才可以实例化,否则也是抽象类。
 3. 作用:虚函数是 C++ 中用于实现动态多态的机制。

  • 纯虚函数
     概念:纯虚函数是一种特殊的虚函数。
     作用:很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。例如猫类和狗类的基类是动物类,动物类中有一个吃饭的函数 eat(),那这个 eat() 函数可以是纯虚函数,因为并不能够确定动物吃的东西是什么,具体吃的内容由不同的派生类去实现。
     3. 特点:如果一个类中有纯虚函数,那么这个类也被称为抽象类。这种类不能实例化对象。除非在派生类中完全实现基类中所有的纯虚函数,否则派生类也是抽象类,不能实例化对象。

27、shared_ptr 怎么知道跟它共享对象的指针释放了

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

28、extern 的作用,extern变量在哪个数据段,extern C

 1. extern 可以置于变量声明或者函数声明前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其它文件中寻找其定义。
 2. extern 变量表示声明一个变量,表示该变量是一个外部变量,也就是全局变量,所以 extern 修饰的变量保存在静态存储区(全局区),全局变量如果没有显示初始化,会默认初始化为 0 ,则保存在程序的 BSS 段,如果初始化不为 0 则保存在程序的 DATA 段。
 3. extern “C” 的作用是为了实现 C++ 代码调用 C 语言代码。加上 extern “C” 后,会指示编译器这部分代码按照 C 语言(而不是 C++)的方式进行编译。由于 C++ 支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译 C 语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

29、C++ 中的内存对齐

 1. 什么是内存对齐 计算机中内存空间都是按照字节(byte)划分,理论上讲对任何类型的变量的访问可以从任何地址开始,但计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是某个数 k(通常它为4或8)的倍数,这就是所谓的内存对齐。
 2. 内存对齐的原因
(1)平台原因(移植原因):某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2)性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问,而对齐的内存访问仅需要一次访问。
 3. 内存对齐的规则
(1)每个特定平台上的编译器都有自己的默认“对齐系数”。可通过预编译命令 #pragma pack(n),n = 1,2,4,8,16 来改变这一系数。
(2)有效对齐值:是给定值 #pragma pack(n) 和结构体中最长数据类型长度中较小的那个,有效对齐值也叫对齐单位。
(3)结构体第一个成员的偏移量(offset)为0,以后每个成员相对于结构体首地址的 offset 都是该成员大小与有效对齐值中较小那个的整数倍,如有需要编译器会在成员之间加上填充字节。
(4)结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。

30、C++ 中的四种类型转换

 1. static_cast 静态转换 用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。上行转换(把派生类的指针或引用转换成基类表示)是安全的,下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。用于基本数据类型之间的转换,如把 int 转换成 char,把 char 转换成 int;
 2. dynamic_cast 动态转换 dynamic_cast 主要用于类层次间的上行转换和下行转换,在类层次间进行上行转换时,dynamic_cast 和 static_cast 的效果是一样的。在进行下行转换时,dynamic_cast 具有类型检查的功能,比static_cast 更安全;
 3. const_cast 常量转换 常量指针被转化成非常量指针,并且仍然指向原来的对象,常量引用被转换成非常量引用,并且仍然指向原来的对象。注意:不能直接对非指针和非引用的变量使用 const_cast 操作符;
 4. reinterpret_cast 重新解释转换 最不安全的一种转换机制。将一种数据类型从一种类型转换为另一种类型,它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。

31、内联函数的作用

 1. 区别:内联函数比普通函数多了关键字 inline;
  内联函数避免了函数调用的开销;普通函数有调用的开销;
  普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址;
  内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句,如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行;普通函数没有这个要求。
 2. 内联函数的作用: 因为函数调用时候需要创建时间、参数传入传递等操作,造成了时间和空间的额外开销。通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题。

  • 虚函数可以是内联函数吗
      (1)虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
     (2)内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联。
     (3)inline virtual 唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。

32、auto 和 decltype 如何使用

C++11 提供了多种简化声明的功能,尤其在使用模板时。
 1. 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++) 

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

decltype(expression) var; 
decltype(x) y; // 让y的类型与x相同,x是一个表达式 
double x; 
decltype(&x) pd; 
template<typename t=""> void ef(T t, U u) 
{ 
	decltype(T*U) tu; 
}

33、C++11 中的可变参数模板新特性

 在 C++11 之前,类模板和函数模板只能含有固定数量的模板参数。C++11 增强了模板功能,它对参数进行了高度泛化,允许模板定义中包含 0 到任意个、任意类型的模板参数,这就是可变参数模板。可变参数模板的加入使得 C++11 的功能变得更加强大,能够很有效的提升灵活性。
 1. 可变参数函数模板语法:
template<typename… t=“”> void fun(T…args)
{
  // 函数体
}
模板参数中, typename(或者 class)后跟 … 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包。fun() 函数中,args 参数的类型用 T… 表示,表示 args 参数可以接收任意个参数,又称函数参数包。
 2. 可变参数类模板语法: template <typename… types=“”> class test。
 3. 展开参数包的方式:
 (1)可变参数函数模板可以采用递归方式、逗号表达式 + 初始化列表的方式展开参数包;
 (2)可变参数类模板可以采用递归+继承的方式展开参数包。
  C++ 11 标准提供的 tuple 元组类就是一个典型的可变参数模板类,它的定义如下: template <typename… types=“”> class tuple;

34、typdef和define区别

#define 是预处理命令,在预处理是执行简单的替换,不做正确性的检查;
typedef 是在编译时处理的,它是在自己的作用域内给已经存在的类型一个别名。

typedef    (int*)    pINT;
#define    pINT2   int*
pINT a,b;//效果同int *a; int *b;表示定义了两个整型指针变量;
pINT2 a,b;//效果同int *a, b;表示定义了一个整型指针变量a和整型变量b。

35、指针数组和数组指针

 指针数组和字符数组我看为同一个类型,也就是数组的元素为指针。
指针数组:表示的是一个由指针变量组成的数组,也就是说其中的元素都是指针变量。 例如: int *p[5];
数组指针:表示的是这是个指向数组的指针,那么该指针变量存储的地址就必须是数组的首地址,得是个指向行的地址,如 a[2][3] 数组中的 a,a+1 等,不能是具体的指向列的地址,如 &a[0][1], &a[1][1] 这类地址。例如:

int (*p2)[5]int arr[5]={12345};
int (*p1)[5] = &arr;    //正确的赋值
int (*p2)[5] = arr; //错误的

数据结构

1、B 树和 B+ 树

 它们都是平衡多路查找树,是在二叉查找树基础上的改进数据结构。在二叉查找树上查找一个数据时,最坏情况的查找次数为树的深度,当数据量很大时,查询次数可能还是很大,造成大量的磁盘IO,从而影响查询效率;
 为了减少磁盘IO的次数,必须降低树的深度,因此在二叉查找树基础上将树改成了多叉加上一些限制条件,就形成了B树;
B+树是B树的变种,区别主要是:对于k阶的B树,每个中间节点只存k-1个值k个指针,而B+树存k个值和k个指针;B+树的非叶子节点不保存具体的数据,而只保存关键字的索引;B树中所有节点中值的总集是全部关键字集合,而B+树中所有叶子节点值的总集就是全部关键字集合*;B+树为所有叶子节点增加了链接,从而实现了快速的范围查找;
 B+树由B树和索引顺序访问方法演化而来,它是为磁盘或其他直接存取辅助设备设计的一种平衡查找树,在B+树中,所有记录节点都是按键值的大小顺序存放在同一层的叶子节点,各叶子节点通过指针进行链接。B+树索引在数据库中的一个特点就是高扇出性,例如在InnoDB存储引擎中,每个页的大小为16KB。在数据库中,B+树的高度一般都在2~4层,这意味着查找某一键值最多只需要2到4次IO操作。

2、哈希冲突的原因和影响因素

 1. 哈希冲突产生的原因 哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,这时候就产生了哈希冲突。
 2. 产生哈希冲突的影响因素 装填因子(装填因子=数据总数 / 哈希表长)、哈希函数、处理冲突的方法 。
 3. 哈希冲突的解决方法:
a.开放地址方法、b.链式地址法 、c.建立公共溢出区、d.再哈希法。

3、map,unordered_map 的区别

 1. 导入的头文件不一样
 2. 原理及特点
 map:内部实现了一个红黑树,该结构具有自动排序的功能,红黑树的每一个节点都代表着 map 的一个元素,因此,对于 map 进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行这样的操作,故红黑树的效率决定了 map 的效率。
 unordered_map:内部实现了一个哈希表,因此其元素的排列顺序是杂乱的,无序的。

  • map 实现原理
     1. map 实现原理 map 内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而 AV L是严格平衡二叉搜索树),红黑树有自动排序的功能,因此 map 内部所有元素都是有序的,红黑树的每一个节点都代表着 map 的一个元素。因此,对于 map 进行的查找、删除、添加等一系列的操作都相当于是对红黑树进行的操作。map 中的元素是按照二叉树(又名二叉查找树、二叉排序树)存储的,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值,使用中序遍历可将键值按照从小到大遍历出来。
     2. 各操作的时间复杂度 插入: O(logN) 查看: O(logN) 删除: O(logN)
  • unordered_map 实现原理
     unordered_map 容器和 map 容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。但由于 unordered_map 容器底层采用的是哈希表存储结构,容器内部不会自行对存储的键值对进行排序。底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是“链地址法”(又称“开链法”)。整个存储结构如下图(其中,Pi 表示存储的各个键值对):
    在这里插入图片描述可以看到,当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。
     在 C++ STL 标准库中,将图中的各个链表称为桶(bucket),每个桶都有自己的编号(从 0 开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:
     1. 将该键值对中键的值带入设计好的哈希函数,会得到一个哈希值(一个整数,用 H 表示);
     2. 将 H 和无序容器拥有桶的数量 n 做整除运算(即 H % n),该结果即表示应将此键值对存储到的桶的编号;
     3. 建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上。
     另外,哈希表存储结构还有一个重要的属性,称为负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程序,即负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,这无疑会降低容器查找目标键值对的效率。
     默认情况下,无序容器的最大负载因子为 1.0。如果操作无序容器过程中,使得最大复杂因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此来减小负载因子的值。需要注意的是,此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这也就解释了,为什么我们在操作无序容器过程中,键值对的存储顺序有时会“莫名”的发生变动。

4、红黑树的特性,为什么要有红黑树

 虽然平衡树解决了二叉查找树退化为近似链表的缺点,能够把查找时间控制在 O(logn),不过却不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于 1,这个要求实在是太严了,导致每次进行插入/删除节点的时候,几乎都会破坏平衡树的第二个规则,进而我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。显然,如果在那种插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,于是有了红黑树,红黑树具有如下特点:
1、具有二叉查找树的特点;
2、根节点是黑色的;
3、每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据;
4、任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的;
5、每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点。(相同的黑色高度)
在这里插入图片描述一些说明:
 (1)约束4和5,保证了红黑树的大致平衡:根到叶子的所有路径中,最长路径不会超过最短路径的2倍。
 (2) 使得红黑树在最坏的情况下,也能有 O(log​N)的查找效率
 (3) 黑色高度为3时,最短路径:黑色 → 黑色→ 黑色,
        最长路径:黑色→ 红色 → 黑色→ 红色 → 黑色
 最短路径的长度为2(不算Nil的叶子节点),最长路径为4
 (4) 默认新插入的节点为红色:因为父节点为黑色的概率较大,插入新节点为红色,可以避免颜色冲突

5、vector 的扩容机制

 当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:
 1. 完全弃用现有的内存空间,重新申请更大的内存空间;
 2. 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
 3. 最后将旧的内存空间释放。 因为 vector 扩容需要申请新的空间,所以扩容以后它的内存地址会发生改变。
 vector 扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。

6、deque 的实现原理

 deque 是由一段一段的定量的连续空间构成。一旦有必要在 deque 前端或者尾端增加新的空间,便配置一段连续定量的空间,串接在 deque 的头端或者尾端。deque 最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。
 既然 deque 是分段连续内存空间,那么就必须有中央控制,维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
 deque 采取一块所谓的 map(不是 STL 的 map 容器)作为主控,这里所谓的 map 是一小块连续的内存空间,其中每一个元素(此处成为一个结点)都是一个指针,指向另一段连续性内存空间,称作缓冲区。缓冲区才是 deque的存储空间的主体。

7、set 的实现原理

 set 底层使用红黑树实现,一种高效的平衡检索二叉树。 set 容器中每一个元素就是二叉树的每一个节点,对于 set 容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。 对 set 进行插入删除操作 都不会引起迭代器的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对 set的插入删除并不会改变原有内存中节点的改变。 set 中的元素都是唯一的,而且默认情况下会对元素进行升序排列。不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。

8、迭代器失效原因

 STL 中某些容器调用了某些成员方法后会导致迭代器失效。例如 vector 容器,如果调用 reserve() 来增加容器容量,之前创建好的任何迭代器(例如开始迭代器和结束迭代器)都可能会失效,这是因为,为了增加容器的容量,vector 容器的元素可能已经被复制或移到了新的内存地址。
 1. 序列式容器迭代器失效 对于序列式容器,例如 vector、deque,由于序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为 vector、deque都是连续存储的一段空间,所以当对其进行 erase 操作时,其后的每一个元素都会向前移一个位置。解决:erase 返回下一个有效的迭代器。
 2. 关联式容器迭代器失效 对于关联容器,例如如 map、 set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在 erase 时,递增当前迭代器即可。这是因为 map 之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他点造成影响。erase 迭代器只是被删元素的迭代器失效,但是返回值为 void,所以要采用 erase(iter++) 自增方式删除迭代器。

9、数组和链表的区别

 1. 数组是将元素在内存中连续存放,由于每个元素占用内存相同,可以通过下标迅速访问数组中任何元素。但是如果要在数组中增加一个元素,需要移动大量元素,在内存中空出一个元素的空间,然后将要增加的元素放在其中。同样的道理,如果想要删除一个元素,同样需要移动大量元素去填掉被移动的元素,如果应用需要快速访问数据,很少或不插入和删除元素,就应该用数组。
 2. 链表恰好相反,链表中的元素在内存中不是顺序存储的,而是通过存在元素中的指针联系到一起。比如:上一个元素有个指针指到下一个元素,以此类推,直到最后一个元素。如果要访问链表中的一个元素,需要从第一个元素开始遍历,一直找到需要的元素位置。但是增加和删除一个元素对于链表数据结构就非常简单,只要删除元素中的指针就可以了。如果应用需要经常插入和删除元素,就需要用链表数据结构了。
从逻辑角度看:
(1)数组必须事先定义固定的长度(元素个数),不能适应数据动态的增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费。
(2)链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其他数据项)。
从内存角度看:
(1)(静态)数组从栈中分配空间,对于程序员方便快速,但自由度小;
(2)链表从堆中分配空间,自由度大,但申请管理比较麻烦。

10、

数据库

1、MySQL 索引,以及它们的好处和坏处

 索引就像指向表行的指针,是一种允许查询操作快速确定哪些行符合WHERE子句中的条件,并检索到这些行的其他列值的数据结构;
 索引主要有普通索引、唯一索引、主键索引、外键索引、全文索引、复合索引几种; 在大数据量的查询中,合理使用索引的优点非常明显,不仅能大幅提高匹配where条件的检索效率,还能用于排序和分组操作的加速。
 当时索引如果使用不当也有比较大的坏处:比如索引必定会增加存储资源的消耗;同时也增大了插入、更新和删除操作的维护成本,因为每个增删改操作后相应列的索引都必须被更新。
 只要创建了索引,就一定会走索引吗? 不一定。 比如,在使用组合索引的时候,如果没有遵从“最左前缀”的原则进行搜索,则索引是不起作用的。 举例,假设在id、name、age字段上已经成功建立了一个名为MultiIdx的组合索引。索引行中按id、name、age的顺序存放,索引可以搜索id、(id,name)、(id, name, age)字段组合。如果列不构成索引最左面的前缀,那么MySQL不能使用局部索引,如(age)或者(name,age)组合则不能使用该索引查询。

2、数据库的 ACID

 事务可由一条非常简单的SQL语句组成,也可以由一组复杂的SQL语句组成。在事务中的操作,要么都执行修改,要么都不执行,这就是事务的目的,也是事务模型区别于文件系统的重要特征之一。
事务需遵循ACID四个特性:
 A(atomicity),原子性。原子性指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,整个事务的执行才算成功。事务中任何一个SQL语句执行失败,那么已经执行成功的SQL语句也必须撤销,数据库状态应该退回到执行事务前的状态。
 C(consistency),一致性。一致性指事务将数据库从一种状态转变为另一种一致的状态。在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。
 I(isolation),隔离性。事务的隔离性要求每个读写事务的对象与其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,这通常使用锁来实现。
 D(durability) ,持久性。事务一旦提交,其结果就是永久性的,即使发生宕机等故障,数据库也能将数据恢复。持久性保证的是事务系统的高可靠性,而不是高可用性。
事务可以分为以下几种类型:
 扁平事务:是事务类型中最简单的一种,而在实际生产环境中,这可能是使用最为频繁的事务。在扁平事务中,所有操作都处于同一层次,其由BEGIN WORK开始,由COMMIT WORK或ROLLBACK WORK结束。处于之间的操作是原子的,要么都执行,要么都回滚。
 带有保存点的扁平事务:除了支持扁平事务支持的操作外,允许在事务执行过程中回滚到同一事务中较早的一个状态,这是因为可能某些事务在执行过程中出现的错误并不会对所有的操作都无效,放弃整个事务不合乎要求,开销也太大。保存点(savepoint)用来通知系统应该记住事务当前的状态,以便以后发生错误时,事务能回到该状态。 - 链事务:可视为保存点模式的一个变种。链事务的思想是:在提交一个事务时,释放不需要的数据对象,将必要的处理上下文隐式地传给下一个要开始的事务。注意,提交事务操作和开始下一个事务操作将合并为一个原子操作。这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的。
 嵌套事务:是一个层次结构框架。有一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务被称为子事务(subtransaction),其控制每一个局部的变换。
 分布式事务:通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务,同样需要满足ACID特性,要么都发生,要么都失效。
 对于MySQL的InnoDB存储引擎来说,它支持扁平事务、带有保存点的扁平事务、链事务、分布式事务。对于嵌套事务,MySQL数据库并不是原生的,因此对于有并行事务需求的用户来说MySQL就无能为力了,但是用户可以通过带有保存点的事务来模拟串行的嵌套事务。

3、InnoDB 的 MVCC

 全称 Multi-Version Concurrency Control ,即多版本并发控制,逻辑是维持一个数据的多个版本,使得读写操作没有冲突。MVCC主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。 它是一种用来解决读-写冲突的无锁并发控制机制。在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能,还可以解决脏读、幻读、不可重复读等事务隔离问题,但不能解决更新丢失问题。 实现原理主要是依赖记录中的 3个隐式字段、undo日志 、Read View。
  InnoDB默认的隔离级别是RR(REPEATABLE READ),RR解决脏读、不可重复读、幻读等问题,使用的是MVCC。MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。它最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
 1. 隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id、指向undo log的指针等。
 2. 基于undo log的版本链:每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链。
 3. ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本。但是具体要恢复到哪个版本,则需要根据ReadView来确定。所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。

4、数据库为什么不用红黑树而用 B+ 树

 首先,红黑树是一种近似平衡二叉树(不完全平衡),结点非黑即红的树,它的树高最高不会超过 2*log(n),因此查找的时间复杂度为 O(log(n)),无论是增删改查,它的性能都十分稳定; 但是,红黑树本质还是二叉树,在数据量非常大时,需要访问+判断的节点数还是会比较多,同时数据是存在磁盘上的,访问需要进行磁盘IO,导致效率较低; 而B+树是多叉的,可以有效减少磁盘IO次数;同时B+树增加了叶子结点间的连接,能保证范围查询时找到起点和终点后快速取出需要的数据。
红黑树做索引底层数据结构的缺陷 试想一下,以红黑树作为底层数据结构在面对在些表数据动辄数百万数千万的场景时,创建的索引它的树高得有多高? 索引从根节点开始查找,而如果我们需要查找的数据在底层的叶子节点上,那么树的高度是多少,就要进行多少次查找,数据存在磁盘上,访问需要进行磁盘IO,这会导致效率过低; 那么红黑树作为索引数据结构的弊端即是:树的高度过高导致查询效率变慢。

5、Redis 如何与数据库保持双写一致性

双写一致性要求:
 缓存不能读到脏数据;
 缓存可能会读到过期数据,但要在可容忍时间内实现最终一致;
 这个可容忍时间尽可能的小。
对于缓存和数据库一致性的问题,有一个很经典的解决方案就是Cache Aside Pattern
 1 命中:程序先从缓存中读取数据,如果命中,则直接返回;
 2 失效:程序先从缓存中读取数据,如果没有命中,则从数据库中读取,成功之后放到缓存;
 3 更新:程序先更新数据库,在删除缓存。
但是对于更新的这个操作,有很多种情况,哪一种情况更合适
(1)先更新缓存,再更新数据库
(2)先更新数据库,再更新缓存
(3)先删除缓存,再更新数据库
(4)先更新数据库,再删除缓存
先更新缓存的优点是每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。
删除缓存的优点是操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次查询容易出现未命中的情况,那么这时就需要再次读取数据库。 那么对比而言,删除缓存无疑是更好的选择。
先删除缓存再操作数据库的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据。
先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致。 出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。 所以我们得到结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题

 首先可以明确的是,这4种更新的解决方案,第4种(先更新数据库,再删除缓存)比前3种都要合适。先来看下前3种的缺点:
 第一种:先更新缓存,再更新数据库
 首先第一个是明显可以pass掉的,如果先更新缓存,再更新数据库,那么缓存更新成功了,再更新数据库的时候失败了,那么不仅数据库中的数据需要回滚,那么缓存中的数据也需要回滚,而且数据库的持久化比缓存的持久化做的要好,所以一般都不会先做更新缓存的操作。
 第二种:先更新数据库,在更新缓存
 刚刚说了一般都不会采用先更新缓存,再更新数据库的这样一种方式,那么先更新数据库再更新缓存是否可以呢,其实这种方式也会存在一定的问题。如果在多线程环境下,如果线程A先拿到了CPU执行权更新数据库(value=1),此时线程B抢到了CPU执行权(进行更新数据库value=2的操作,并且将数据更新进了缓存),但是这个时候线程A又将CPU执行权抢回来了,又将线程value=1的数据更新进了缓存,此时数据库中的数据是value=2,缓存中的数据是value=1,此时数据库和缓存中的数据就不一样了,这样做也有一定的风险。
请求A(更新数据库value=1)->
线程B(更新数据库value=2)->
线程B(更新缓存value=2)->
线程A(更新缓存value=1)。
 第三种:先删除缓存,再更新数据库
 如果先删除缓存,再更新数据库,假如一个请求A删除缓存A=1,此时请求B进来读取缓存,发现不存在,然后从数据库中读取到旧的数据A=1,这是线程A将一个新值写入数据库A=2,然后请求B将旧值A=1写入缓存,此时缓存中和数据库中的值不一致。
请求A(删除缓存value=1)->
请求B(读取缓存,不存在,从数据库读旧值value=1)->
请求A(写入数据库value=2)->
请求B(写入缓存value=1)。
 第四种:先更新数据库,再删除缓存
缓存刚好失效,value=2
请求A(从数据库读旧值value=1)->
请求A(将旧值写入缓存value=1)->
请求B(删除缓存)。

6、数据库引擎有哪些,各自有什么区别

 InnoDB 引擎是 MySQL 的事务安全(ACID 兼容)存储引擎,具有提交、回滚和崩溃恢复功能来保护用户数据;行级锁定读取增加了多用户并发性和性能;将用户数据存储在聚集索引中,以减少基于主键的常见查询的 I/O;还支持 FOREIGN KEY 维护数据完整性。
 MyISAM引擎的表占用空间较小,表级锁定限制了读/写工作负载的性能,因此它通常用于只读或以读取为主的场景。
 Memory引擎是将所有数据存储在 RAM 中,以便在需要快速查找非关键数据的环境中进行快速访问,以前被称为 HEAP 引擎。
 Archive引擎非常适合存储大量的独立的,作为历史记录的数据,因为它们不经常被读取。它 拥有高效的插入速度,但其对查询的支持相对较差。
 Cluster/NDB是高冗余的存储引擎,用多台数据机器联合提供服务以提高整体性能和安全性。适合数据量大,安全和性能要求高的应用。
 Federated引擎提供连接单独的 MySQL 服务器,从多个物理服务器创建一个逻辑数据库的能力,非常适合分布式或数据集市环境。

7、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隔离级别;
READ UNCOMMITTED: 它是性能最好、也最野蛮的方式,因为它压根儿就不加锁,所以根本谈不上什么隔离效果,可以理解为没有隔离。
SERIALIZABLE: 读的时候加共享锁,其他事务可以并发读,但是不能写。写的时候加排它锁,其他事务不能并发写也不能并发读。
REPEATABLE READ & READ COMMITTED: 为了解决不可重复读,MySQL 采用了 MVVC (多版本并发控制) 的方式。 我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。

8、MySQL主从同步

主从同步:当master(主)库的数据发生变化的时候,变化会实时的同步到slave(从)库。
好处:(1)水平扩展数据库的负载能力。
   (2)容错,高可用。Failover(失败切换)/High Availability
   (3)数据备份。
原理:不管是delete、update、insert,还是创建函数、存储过程,所有的操作都在master上。当master有操作的时候,slave会快速的接收到这些操作,从而做同步。
实现:在master机器上,主从同步事件会被写到特殊的log文件中(binary-log);在slave机器上,slave读取主从同步事件,并根据读取的事件变化,在slave库上做相应的更改。如此,就实现了主从同步了。
主从同步事件有哪些
在master机器上,主从同步事件会被写到特殊的log文件中(binary-log);
主从同步事件有3种形式:statement、row、mixed。
(1)statement:会将对数据库操作的sql语句写入到binlog中。
(2)row:会将每一条数据的变化写入到binlog中。
(3)mixed:statement与row的混合。Mysql决定什么时候写statement格式的,什么时候写row格式的binlog。
具体实现
 (master机器上的操作)当master上的数据发生改变的时候,该事件(insert、update、delete)变化会按照顺序写入到binlog中。当slave连接到master的时候,master机器会为slave开启binlog dump线程。当master 的 binlog发生变化的时候,binlog dump线程会通知slave,并将相应的binlog内容发送给slave。
 (在slave机器上的操作)当主从同步开启的时候,slave上会创建2个线程。
(1)I/O线程。该线程连接到master机器,master机器上的binlog dump线程会将binlog的内容发送给该I/O线程。该I/O线程接收到binlog内容后,再将内容写入到本地的relay log。
(2)SQL线程。该线程读取I/O线程写入的relay log。并且根据relay log的内容对slave数据库做相应的操作。

9、

Linux系统

1、常用的 Linux 命令

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

2、GDB 常见的调试命令

gdb 常见的调试命令:
启动和退出:gdb 可执行程序
      quit/q
给程序设置参数/获取设置参数:set args 10 20 show args
GDB 使用帮助:help
查看当前文件代码:list/l (从默认位置显示)list/l 行号 (从指定的行显示)list/l 函数名(从指定的函数显示)
查看非当前文件代码: list/l 文件名:行号 list/l 文件名:函数名
设置显示的行数:show list/listsize set list/listsize 行数
设置断点:b/break 行号 b/break 函数名 b/break 文件名:行号 b/break 文件名:函数
查看断点:i/info b/break 断点编号
删除断点:d/del/delete 断点编号
设置断点无效:dis/disable 断点编号
设置断点生效:ena/enable 断点编号
设置条件断点(一般用在循环的位置) b/break 10 if i==5
运行GDB程序:start(程序停在第一行) run(遇到断点才停)
继续运行,到下一个断点停:c/continue
向下执行一行代码(不会进入函数体):n/next
变量操作:p/print 变量名(打印变量值) ptype 变量名(打印变量类型)
向下单步调试(遇到函数进入函数体):s/step finish(跳出函数体)
自动变量操作:display 变量名
(自动打印指定变量的值) i/info display undisplay 编号
查看当前调试环境中有多少个进程:info inferiors
切换到指定 ID 编号的进程对其进行调试:inferior id
其它操作:
set var 变量名=变量值 (循环中用的较多)
(跳出循环)

3、ROS系统整体架构

从文件系统级理解ROS架构
在这里插入图片描述(1)工作空间
 工作空间是一个包含功能包、可编辑源文件和编译包的文件夹,当你想同时编译不同的功能包时非常有用,并且可以保存本地开发包。当然,用户可以根据自己的需要创建多个工作空间,在每个工作空间中开发不同用途的功能包。
src源文件空间:这个文件夹放置各个功能包和一个用于这些功能包的CMake配置文件CMakeLists.txt。这里做一下说明,由于ROS中的源码采用catkin工具进行编译,而catkin工具又是基于cmake技术的,所以我们会在src源文件空间和各个功能包中都会见到一个文件CMakeLists.txt,这个文件就是起编译配置的作用。
build编译空间:这个文件夹放置CMake和catkin编译功能包时产生的缓存、配置、中间文件等。
devel开发空间:这个文件夹放置编译好的可执行程序。

(2)功能包
  功能包是ROS中软件组织的基本形式,一个功能包具有用于创建ROS程序的最小结构和最少内容,它可以包含ROS运行的进程(节点)、配置文件等。一个功能包中主要包含这几个文件:
  CMakeLists.txt功能包配置文件:用于这个功能包cmake编译时的配置文件。
  package.xml功能包清单文件:用xml的标签格式标记这个功能包的各类相关信息,比如包的名称、依赖关系等。主要作用是为了更容易的安装和分发功能包。
  include<package_name>功能包头文件目录:你可以把你的功能包程序包含的*.h头文件放在这里,include下之所以还要加一级路径<package_name>是为了更好的区分自己定义的头文件和系统标准头文件,<package_name>用实际功能包的名称替代。不过这个文件夹不是必要项,比如有些程序没有头文件的情况。
  msg非标准消息定义目录:消息是ROS中一个进程(节点)发送到其他进程(节点)的信息,消息类型是消息的数据结构,ROS系统提供了很多标准类型的消息可以直接使用,如果你要使用一些非标准类型的消息,就需要自己来定义该类型的消息,并把定义的文件放在这里。不过这个文件夹不是必要项,比如程序中只使用标准类型的消息的情况。
  srv服务类型定义目录:服务是ROS中进程(节点)间的请求/响应通信过程,服务类型是服务请求/响应的数据结构,服务类型的定义放在这里。如果要调用此服务,你需要使用该功能包名称和服务名称。不过这个文件夹不是必要项,比如程序中不使用服务的情况。
  scripts可执行脚本文件存放目录:这里用于存放bash、python或其他脚本的可执行文件。不过这个文件夹不是必要项,比如程序中不使用可执行脚本的情况。
  launch文件目录:这里用于存放*.launch文件,*.launch文件用于启动ROS功能包中的一个或多个节点,在含有多个节点启动的大型项目中很有用。不过这个文件夹不是必要项,节点也可以不通过launch文件启动。
  src功能包中节点源文件存放目录:一个功能包中可以有多个进程(节点)程序来完成不同的功能,每个进程(节点)程序都是可以单独运行的,这里用于存放这些进程(节点)程序的源文件,你可以在这里再创建文件夹和文件来按你的需求组织源文件,源文件可以用c++、python等来书写。

(3)消息
 消息是ROS中一个进程(节点)发送到其他进程(节点)的信息,消息类型是消息的数据结构,ROS系统提供了很多标准类型的消息可以直接使用,如果你要使用一些非标准类型的消息,就需要自己来定义该类型的消息。
 ROS使用了一种精简的消息类型描述语言来描述ROS进程(节点)发布的数据值。通过这种描述语言对消息类型的定义,ROS可以在不同的编程语言(如c++、python等)书写的程序中使用此消息。不管是ROS系统提供的标准类型消息,还是用户自定义的非标准类型消息,定义文件都是以*.msg作为扩展名。消息类型的定义分为两个主要部分:字段的数据类型和字段的名称,简单点说就是结构体中的变量类型和变量名称。
 在大多数情况下,我们都可以使用ROS系统提供的标准类型的消息来完成任务,这得益于ROS系统提供了丰富的标准类型的消息。经常用到的类型包括:基本类型(std_msgs)、通用类型(sensor_msgs、geometry_msgs、nav_msgs、actionlib_msgs

(4)服务
 服务是ROS中进程(节点)间的请求/响应通信过程,服务类型是服务请求/响应的数据结构。服务类型的定义借鉴了消息类型的定义方式,所以这里就不在赘述了。区别在于,消息数据是ROS进程(节点)间多对多广播式通信过程中传递的信息;服务数据是ROS进程(节点)间点对点的请求/响应通信过程传递的信息。

从计算图级理解ROS架构
 ROS会创建一个连接所有进程(节点)的网络,其中的任何进程(节点)都可以访问此网络,并通过该网络与其他进程(节点)交互,获取其他进程(节点)发布的信息,并将自身数据发布到网络上,这个计算图网络中的节点(node)、主题(topic)、服务(server)等都要有唯一的名称做标识。
请添加图片描述(1)节点
 节点是主要的计算执行进程,功能包中创建的每个可执行程序在被启动加载到系统进程中后,该进程就是一个ROS节点,如图中的node1、node2、node3等都是节点(node)。节点都是各自独立的可执行文件,能够通过主题(topic)、服务(server)或参数服务器(parameter server)与其他节点通信。ROS通过使用节点将代码和功能解耦,提高了系统的容错力和可维护性。所以你最好让每一个节点都具有特定的单一的功能,而不是创建一个包罗万象的大节点。节点如果用c++进行编写,需要用到ROS提供的库roscpp;节点如果用python进行编写,需要用到ROS提供的库rospy。
rosnode info <node_name>:用于输出当前节点信息。
rosnode kill <node_name>:用于杀死正在运行节点进程来结束节点的运行。
rosnode list:用于列出当前活动的节点。
rosnode machine <hostname>:用于列出指定计算机上运行的节点。
rosnode ping <node_name>:用于测试节点间的网络连通性。
rosnode cleanup:用于将无法访问节点的注册信息清除。

(2)消息
 节点通过消息(message)完成彼此的沟通。消息包含一个节点发送给其他节点的信息数据。
rosmsg show <message_type>:用于显示一条消息的字段。
rosmsg list:用于列出所有消息。
rosmsg package <package _name>:用于列出功能包的所有消息。
rosmsg packages:用于列出所有具有该消息的功能包。
rosmsg users <message_type>:用于搜索使用该消息类型的代码文件。
rosmsg md5 <message_type>:用于显示一条消息的MD5求和结果。

(3)主题
 每个消息都必须发布到相应的主题(topic),通过主题来实现在ROS计算图网络中的路由转发。当一个节点发送数据时,我们就说该节点正在向主题发布消息;节点可以通过订阅某个主题,接收来自其他节点的消息。通过主题进行消息路由不需要节点之间直接连接,这就意味着发布者节点和订阅者节点之间不需要知道彼此是否存在,这样就保证了发布者节点与订阅者节点之间的解耦合。同一个主题可以有多个订阅者也可以有多个发布者,不过要注意必须使用不同的节点发布同一个主题。每个主题都是强类型的,不管是发布消息到主题还是从主题中订阅消息,发布者和订阅者定义的消息类型必须与主题的消息类型相匹配。
rostopic bw </topic_name>:用于显示主题所使用的带宽。
rostopic echo </topic_name>:用于将主题中的消息数据输出到屏幕。
rostopic find <message_type>:用于按照消息类型查找主题。
rostopic hz </topic_name>:用于显示主题的发布频率。
rostopic info </topic_name>:用于输出活动主题、发布的主题、主题订阅者和服务的信息。
rostopic list:用于列出当前活动主题的列表。
rostopic pub </topic_name> <message_type> <args>:用于通过命令行将数据发布到主题。
rostopic type </topic_name>:用于输出主题中发布的消息类型。

(4)服务
 在一些特殊的场合,节点间需要点对点的高效率通信并及时获取应答,这个时候就需要用服务的方式进行交互。提供服务的节点叫服务端,向服务端发起请求并等待响应的节点叫客户端,客户端发起一次请求并得到服务端的一次响应,这样就完成了一次服务通信过程。服务通信过程中服务的数据类型需要用户自己定义,与消息不同,节点并不提供标准服务类型。服务类型的定义文件都是以*.srv为扩展名,并且被放在功能包的srv/文件夹下。
rosservice call </service_name> <args>:用于通过命令行参数调用服务。
rosservice find <service_type>:用于根据服务类型查询服务。
rosservice info </service_name>:用于输出服务信息。
rosservice list:用于列出活动服务清单。
rosservice type </service_name>:用于输出服务类型。

(5)节点管理器
 节点管理器(master)用于节点的名称注册和查找等,也负责设置节点间的通信。如果在你的整个ROS系统中没有节点管理器,就不会有节点、消息、服务之间的通信。由于ROS本身就是一个分布式的网络系统,所以你可以在某台计算机上运行节点管理器,在这台计算机和其他台计算机上运行节点。
roscore:用于启动节点管理器,加载ROS节点管理器和其他ROS核心组件。
rosrun需要一个一个打开节点,在节点较多的时候比较麻烦,这时候roslaunch就派上用场了。我们把需要启动的节点写在.launch文件中,.launch脚本会自动帮我们运行需要的节点,而不需要手动逐个开启,但是launch脚本开启节点的顺序是不确定的。

(6)参数服务器
 参数服务器(parameter server)能够使数据通过关键词存储在一个系统的核心位置。通过使用参数,就能够在节点运行时动态配置节点或改变节点的工作任务。参数服务器是可通过网络访问的共享的多变量字典,节点使用此服务器来存储和检索运行时的参数。
rosparam list:用于列出参数服务器中的所有参数。
rosparam get <parameter_name>:用于获取参数服务器中的参数值。
rosparam set <parameter_name> <value>:用于设置参数服务器中参数的值。
rosparam delete <parameter_name>:用于将参数从参数服务器中删除。
rosparam dump <file>:用于将参数服务器的参数保存到一个文件。
rosparam load <file>:用于从文件将参数加载到参数服务器。

(7)消息记录包
 消息记录包(bag)是一种用于保存和回放ROS消息数据的文件格式。消息记录包是一种用于存储数据的重要机制,它可以帮助记录一些难以收集的传感器数据,然后通过反复回放数据进行算法的性能开发和测试。ROS创建的消息记录包文件以*.bag为扩展名,通过播放、停止、后退操作该文件,可以像实时会话一样在ROS中再现情景,便于算法的反复调试。
rosbag <args>:用来录制、播放和执行操作。

4、

参考资料:https://www.cnblogs.com/yuanwebpage/p/12916424.html
参考资料:https://www.nowcoder.com/exam/interview/
参考资料:https://blog.csdn.net/wjz110201/article/details/115111117
参考资料:https://blog.csdn.net/newchenxf/article/details/116274506
参考资料:https://blog.csdn.net/Chiang2018/article/details/122221182

  • 15
    点赞
  • 126
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值