八股文随记

文章目录


前言

随手记录一些八股,待整理

一、计算机网络

1.介绍一下TCP/IP模型和OSI模型的区别

OSI模型是ISO国际标准化组织制定的一个用于计算机或者通信系统间互联的标准体系,将计算机网络通信分为七层。
虽然OSI在理论上更全面,但TCP/IP在实际的通信中更为实用。TCP/IP模型分为四层:

  1. 应用层
    该层与OSI模型的应用层、表示层、会话层类似。
    提供与用户应用程序交互的接口,为网络上的各种应用提供服务。如电子邮件(SMTP)、网页浏览(HTTP)、文件传输(FTP)等。
  2. 传输层
    该层对应OSI模型的传输层。
    负责端到端的数据传输,提供可靠的、无连接的数据传输服务。主要的协议包括TCP,提供可靠的数据连接,保证数据的完整性和准确性;UDP是无连接的协议,用于不需要确保可靠性的传输,如实时的音频视频。
  3. 网络层
    该层对应OSI模型的网络层。
    主要的协议是IP,它负责数据包的路由和转发。选择最佳路径将数据从源主机传输到目标主机。IP协议使用IP地址来标识主机和网络,并进行逻辑地址寻址。
  4. 网络接口层
    该层对应OSI模型的数据链路层和物理层。负责物理传输媒介上的传输,例如以太网、WiFi。也包含硬件地址(MAC地址)的管理。

2.从输入 URL 到页面展示到底发生了什么?

  • 解析URL信息,准备发送HTTP请求
  • 检查浏览器缓存是否有缓存该资源,如果有直接返回,没有就发送网络请求
  • 网络请求前,根据域名进行DNS域名解析,获取域名的IP地址。DNS解析顺序为:本地浏览器缓存->本地Host文件->路由器缓存->DNS服务器->根DNS服务器。直到找到为止(如果请求协议是HTTPS,还需要建立TLS连接)
  • 浏览器与服务器IP建立TCP连接,三次握手
  • 建立连接后,客户端发送HTTP请求。浏览器构建请求行、请求头等信息,并把域名相关的cookie等数据加入请求头,向服务器构建请求信息(如果是HTTPS的话,还涉及到HTTPS的加解密流程)
  • 服务器处理请求并返回响应数据
  • 浏览器与服务器断开TCP连接,四次挥手
  • 浏览器解析响应并渲染界面

3.HTTP请求报文和响应报文是怎样的,有哪些常见的字段?

HTTP报文分为请求报文和响应报文:

  1. 请求报文主要由请求行、请求头、空行、请求体构成
  • 请求行包括
    方法: 执行指定的操作,如GET、POST、PUT、DELETE 等
    资源路径:请求的资源的URL
    使用的HTTP版本:如 HTTP/1.1 或 HTTP/2.0
  • 请求头的字段较多,常用的包含以下几个:
    Host:请求的服务器的域名
    Accept:客户端能够处理的媒体类型
    Authorization:用于认证的凭证信息,比如token数据
    Content-Length:请求体的长度
    Content-Type:请求体的媒体类型
    Cookie:存储在客户端的cookie数据
    Connection:管理连接的选项,如 keep-alive
  • 接下来是空行,用于分割请求头和请求体。
  • 而请求体通常用于 POST 和 PUT 请求,包含发送给服务器的数据。
  1. 响应报文是服务端向客户端返回的数据形式,主要包含状态行、响应头、空行、响应体
  • 状态行包含HTTP版本、状态码和状态消息。例如:HTTP/1.1 200 OK
  • 响应头也是以键值对的形式提供的额外信息,类似于请求头,用于告知客户端有关响应的详细信息,包括
    Content-Type:指定响应主体的媒体类型
    Content-Length:指定响应主体的长度(字节数
    Server:指定服务器的信息
    Expires: 响应的过期时间,之后内容被认为是过时的
    ETag: 响应体的实体标签,用于缓存和条件请求
    Last-Modified: 资源最后被修改的日期和时间
    Location:在重定向时指定新的资源位置
    Set-Cookie:在响应中设置Cookie
  • 空行、在响应头和响应体之间,表示响应头的结束。
  • 而响应体是服务端实际传输的数据,可以是文本、HTML页面、图片、视频等,也可能为空。

4.HTTP有哪些请求方式?

  1. GET:请求指定的资源
  2. POST:向指定资源提交数据进行处理请求(例如表单提交、上传文件),数据被包含在请求体中
  3. PUT:更新指定资源
  4. DELETE:请求服务器删除指定资源
  5. HEAD:类似于 GET 请求,只不过不返回报文主体,只获取报文首部
  6. OPTIONS:允许客户端查看服务器的性能
  7. PATCH:对 PUT 方法的补充,对资源进行部分更新

5.GET请求和POST请求的区别

  1. GET是获取数据、POST是提交数据进行处理
  2. GET请求将参数附加在URL之后,POST请求将数据放在请求体中
  3. GET请求由于参数暴露在URL中,安全性较低;POST相对更安全
  4. GET请求受到URL长度限制,数据量有限;POST请求理论上没有大小限制
  5. GET请求是幂等的,即多次执行相同的GET请求,资源的状态不会改变;POST请求不是幂等的,因为每次提交都可能改变资源状态
  6. GET请求被浏览器主动缓存,POST默认不会被缓存

幂等性是指一次和多次请求某一个资源应该具有同样的副作用。简单来说意味着对同一URL的多个请求应该返回同样的结果。

6.HTTP中常见的状态码有哪些?

  • 1xx 信息(接收的请求正在处理)
    100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
  • 2xx 成功(请求正常处理完毕)
    200 OK:表示客户端请求成功
    204 No Content :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
    206 Partial Content :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。
  • 3xx 重定向(需要进行附加操作以完成请求)
    301 Moved Permanently :永久性重定向
    302 Found :临时性重定向
    303 See Other :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
    304 Not Modified :请求的内容没有修改过,所以服务器返回此响应时,不会返回网页内容,而是使用缓存。就是如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
  • 4xx 客户端错误(服务器无法处理请求)
    400 Bad Request :请求报文中存在语法错误。
    401 Unauthorized :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
    403 Forbidden :请求被拒绝。请求的对应资源禁止被访问。
    404 Not Found:服务器无法找到对应资源
  • 5xx 服务器错误(服务器处理请求出)
    500 Internal Server Error :服务器正在执行请求时发生错误。
    503 Service Unavailable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
    在这里插入图片描述

7.什么是强缓存和协商缓存

强缓存和协商缓存是HTTP缓存机制的两种类型,它们用于减少服务器的负担和提高网页加载速度。

  1. 强缓存:客户端在没有向服务器发送请求的情况下,直接从本地缓存中获取资源。

    • Expires强缓存:设置一个强缓存时间,此时间范围内,从内存中读取缓存并返回。但是因为Expires判断强缓存过期的机制是获取本地时间戳,与之前拿到的资源文件中的Expires字段的时间做比较来判断是否需要对服务器发起请求。这里有一个巨大的漏洞:“如果我本地时间不准咋办?”所以目前已经被废弃了。
    • Cache-Control强缓存:目前使用的强缓存是通过HTTP响应头中的Cache-Control字段实现,通过max-age来告诉浏览器在指定时间内可以直接使用缓存数据,无需再次请求。
  2. 协商缓存:当强缓存失效时,浏览器会发送请求到服务器,通过ETag或Last-Modified等HTTP响应头与服务器进行验证,以确定资源是否被修改。如果资源未修改,服务器返回304 Not Modified状态码,告知浏览器使用本地缓存;如果资源已修改,则返回新的资源,浏览器更新本地缓存。这种方式需要与服务器通信,但可以确保用户总是获取最新的内容。
    基于Last-Modified的协商缓存

    • Last-Modified 是资源的最后修改时间,服务器在响应头部中返回。
      当客户端读取到Last-modified的时候,会在下次的请求标头中携带一个字段:If-Modified-Since,而这个请求头中的If-Modified-Since就是服务器第一次修改时候给他的时间
      服务器比较请求中的 If-Modified-Since 值与当前资源的 Last-Modified 值,如果比对的结果是没有变化,表示资源未发生变化,返回状态码 304 Not Modified。如果比对的结果说资源已经更新了,就会给浏览器正常返回资源,返回200状态。
      但是这样的协商缓存有两个缺点:
      因为是更改文件修改时间来判断的,所以在文件内容本身不修改的情况下,依然有可能更新文件修改时间(比如修改文件名再改回来),这样,就有可能文件内容明明没有修改,但是缓存依然失效了。
      当文件在极短时间内完成修改的时候(比如几百毫秒)。因为文件修改时间记录的最小单位是秒,所以,如果文件在几百毫秒内完成修改的话,文件修改时间不会改变,这样,即使文件内容修改了,依然不会返回新的文件。

    • 基于ETag的协商缓存:将原先协商缓存的比较时间戳的形式修改成了比较文件指纹(根据文件内容计算出的唯一哈希值)。
      ETag 是服务器为资源生成的唯一标识符(文件指纹),可以是根据文件内容计算出的哈希值,服务端将其和资源一起放回给客户端。
      客户端在请求头部的 If-None-Match 字段中携带上次响应的 ETag 值。
      服务器比较请求中的 If-None-Match 值与当前资源的 ETag 值,如果匹配,表示资源未发生变化,返回状态码 304 Not Modified。如果两个文件指纹不吻合,则说明文件被更改,那么将新的文件指纹重新存储到响应头的ETag中并返回给客户端
      在这里插入图片描述

8.HTTP1.0和HTTP1.1的区别

  1. 持久连接:HTTP/1.1 默认支持持久连接,允许在一个TCP连接上发送多个HTTP请求和响应,减少了连接建立和关闭的开销。而HTTP/1.0 默认为短连接,每次请求都需要建立一个TCP连接,并通过Connection: keep-alive头来实现持久连接。
  2. 管道化:HTTP/1.1 支持管道化(不是默认开启),允许客户端在第一个请求的响应到达之前发送多个请求,这可以减少等待时间,提高效率。HTTP/1.0不支持管道化。
  3. 缓存控制:HTTP1.0主要使用If-Modified-Since/Expires来做为缓存判断的标准,而HTTP1.1则引入了更多的缓存控制策略例如Etag / If-None-Match等更多可供选择的缓存头来控制缓存策略。
  4. 错误处理:HTTP/1.1 增加了一些新的HTTP状态码,如100 Continue,用于增强错误处理和请求的中间响应。
  5. Host 头:HTTP/1.1 引入了Host头,允许客户端指定请求的主机名,这使得在同一台服务器上托管多个域名成为可能。HTTP/1.0没有这个头字段。
  6. 带宽优化 :HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能, 而HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content)

9.HTTP2.0与HTTP1.1的区别

  1. 二进制协议:HTTP/2.0 采用二进制格式传输数据,而非HTTP/1.1 的文本格式,使得解析更高效,减少了解析时间。
  2. 多路复用:HTTP/2.0 支持多路复用,允许在单个TCP连接上并行交错发送多个请求和响应,解决了HTTP/1.1 中的队头阻塞问题。
  3. 头部压缩:HTTP/2.0 引入了HPACK 压缩算法,对请求和响应的头部信息进行压缩,减少了冗余头部信息的传输,提高了传输效率。
  4. 服务器推送:HTTP/2.0 允许服务器主动推送资源给客户端,而不需要客户端明确请求,这可以减少页面加载时间。
  5. 优先级和依赖:HTTP/2.0 允许客户端为请求设置优先级,并表达请求之间的依赖关系,资源加载更加有序。

10.HTTP3.0有了解过吗

HTTP/3是HTTP协议的最新版本,它基于QUIC协议,具有以下特点:

  1. 无队头阻塞: QUIC 使用UDP协议来传输数据。一个连接上的多个stream之间没有依赖, 如果一个stream丢了一个UDP包,不会影响后面的stream,不存在 队头阻塞问题。
  2. 零 RTT 连接建立:QUIC 允许在首次连接时进行零往返时间连接建立,从而减少了连接延迟,加快了页面加载速度。
  3. 连接迁移:QUIC 允许在网络切换(如从 Wi-Fi 到移动网络)时,将连接迁移到新的 IP 地址,从而减少连接的中断时间。
  4. 向前纠错机制:每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传。
  5. 安全性:HTTP/3默认使用TLS加密,确保了数据传输的安全性。

11.HTTPS和HTTP有哪些区别

两者的区别主要在于安全性和数据加密:

  1. 加密层:HTTPS 在HTTP 的基础上增加了SSL/TLS 协议作为加密层,确保数据传输的安全性。而HTTP 数据传输是明文的,容易受到攻击。
  2. HTTP 连接建立相对简单, TCP 三次握手之后便可进行 HTTP 的报文传输。而 HTTPS 在 TCP 三次握手之后,还需进行 SSL/TLS 的握手过程,才可进入加密报文传输。
  3. 端口:HTTPS 通常使用端口443 ,而HTTP 使用端口80。
  4. HTTPS 协议需要向 CA 申请数字证书,来保证服务器的身份是可信的。

12.HTTPS的工作原理(HTTPS建立连接的过程)

HTTPS 主要基于SSL/TLS 协议,确保了数据传输的安全性和完整性, 其建立连接并传输数据的过程如下:

  1. 密钥交换:客户端发起HTTPS请求后,服务器会发送其公钥证书给客户端。
  2. 证书验证:客户端会验证服务器的证书是否由受信任的证书颁发机构(CA )签发,并检查证书的有效性。
  3. 加密通信:一旦证书验证通过,客户端会生成一个随机的对称加密密钥,并使用服务器的公钥加密这个密钥,然后发送给服务器。
  4. 建立安全连接:服务器使用自己的私钥解密得到对称加密密钥,此时客户端和服务器都有了相同的密钥,可以进行加密和解密操作。
  5. 数据传输:使用对称加密密钥对所有传输的数据进行加密,确保数据在传输过程中的安全性。
  6. 完整性校验:SSL/TLS协议还包括消息完整性校验机制,如消息认证码,确保数据在传输过程中未被篡改。
  7. 结束连接:数据传输完成后,通信双方会进行会话密钥的销毁,以确保不会留下安全隐患。

13.TCP和UDP的区别

  1. TCP是面向连接的协议,需要在数据传输前建立连接;UDP是无连接的,不需要建立连接。
  2. TCP提供可靠的数据传输,保证数据包的顺序和完整性;UDP不保证数据包的顺序或完整性。
  3. TCP具有拥塞控制机制,可以根据网络状况调整数据传输速率;UDP没有拥塞控制,发送速率通常固定。
  4. TCP通过滑动窗口机制进行流量控制,避免接收方处理不过来;UDP没有流量控制。
  5. TCP能够检测并重传丢失或损坏的数据包;UDP不提供错误恢复机制。
  6. TCP有复杂的报文头部,包含序列号、确认号等信息,有20字节;UDP的报文头部相对简单,8字节。
  7. 由于TCP的连接建立、数据校验和重传机制,其性能开销通常比UDP大;UDP由于简单,性能开销小。
  8. 每⼀条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  9. TCP是面向字节流的,它把上面应用层交下来的数据看成无结构的字节流会发送,可以想象成流水形式的,发送方TCP会将数据放入“蓄水池”(缓存区),等到可以发送的时候就发送,不能发送就等着TCP会根据当前网络的拥塞状态来确定每个报文段的大小。
    UDP是面向报文的,发送方的UDP对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层,无论应用层交给UDP多长的报文,它统统发送,一次发送一个。而对接收方,接到后直接去除首部,交给上面的应用层就完成任务了。因此,它需要应用层控制报文的大小
  10. 适用场景:TCP适用于需要可靠传输的应用,如网页浏览、文件传输等;UDP适用于对实时性要求高的应用,如语音通话、视频会议等。

14.TCP连接如何确保可靠性

TCP通过差错控制(序列号、确认应答、数据校验)、超时重传、流量控制、拥塞控制等机制,确保了数据传输的可靠性和效率。
序列号:每个TCP段都有一个序列号,确保数据包的顺序正确。

  1. 数据校验:TCP使用校验和来检测数据在传输过程中是否出现错误,如果检测到错误,接收方会丢弃该数据包,并等待重传。
  2. 确认应答:接收方发送ACK确认收到的数据,如果发送方在一定时间内没有收到确认,会重新发送数据。
  3. 超时重传:发送方设置一个定时器,如果在定时器超时之前没有收到确认,发送方会重传数据。
  4. 流量控制:TCP通过滑动窗口机制进行流量控制,确保接收方能够处理发送方的数据量。
  5. 拥塞控制:TCP通过算法如慢启动、拥塞避免、快重传和快恢复等,来控制数据的发送速率,防止网络拥塞。
  6. 数据合理分片和排序:tcp会按最大传输单元(MTU)合理分片,接收方会缓存未按序到达的数据,重新排序后交给应用层。

15.既然提到了拥塞控制,那你能说说说拥塞控制是怎么实现的嘛

TCP拥塞控制可以在网络出现拥塞时动态地调整数据传输的速率,以防止网络过载。TCP拥塞控制的主要机制包括以下几个方面:

  1. 慢启动(Slow Start): 初始阶段,TCP发送方会以较小的发送窗口开始传输数据。随着每次成功收到确认的数据,发送方逐渐增加发送窗口的大小,实现指数级的增长,这称为慢启动。这有助于在网络刚开始传输时谨慎地逐步增加速率,以避免引发拥塞。
  2. 拥塞避免(Congestion Avoidance): 一旦达到一定的阈值(通常是慢启动阈值),TCP发送方就会进入拥塞避免阶段。在拥塞避免阶段,发送方以线性增加的方式增加发送窗口的大小,而不再是指数级的增长。这有助于控制发送速率,以避免引起网络拥塞。
  3. 快速重传(Fast Retransmit): 如果发送方连续收到相同的确认,它会认为发生了数据包的丢失,并会快速重传未确认的数据包,而不必等待超时。这有助于更快地恢复由于拥塞引起的数据包丢失。
  4. 快速恢复(Fast Recovery): 在发生快速重传后,TCP进入快速恢复阶段。在这个阶段,发送方不会回到慢启动阶段,而是将慢启动阈值设置为当前窗口的一半,并将拥塞窗口大小设置为慢启动阈值加上已确认但未被快速重传的数据块的数量。这有助于更快地从拥塞中恢复。

16.TCP流量控制是怎么实现的?

流量控制就是让发送方发送速率不要过快,让接收方来得及接收。利用滑动窗口机制就可以实施流量控制,主要方法就是动态调整发送方和接收方之间数据传输速率。

  • 滑动窗口大小: 在TCP通信中,每个TCP报文段都包含一个窗口字段,该字段指示发送方可以发送多少字节的数据而不等待确认。这个窗口大小是动态调整的。
  • 接收方窗口大小: 接收方通过TCP报文中的窗口字段告诉发送方自己当前的可接收窗口大小。这是接收方缓冲区中还有多少可用空间。
  • 流量控制的目标: 流量控制的目标是确保发送方不要发送超过接收方缓冲区容量的数据。如果接收方的缓冲区快满了,它会减小窗口大小,通知发送方暂停发送,以防止溢出。
  • 动态调整: 发送方会根据接收方的窗口大小动态调整发送数据的速率。如果接收方的窗口大小增加,发送方可以加速发送数据。如果窗口大小减小,发送方将减缓发送数据的速率。
  • 确认机制: 接收方会定期发送确认(ACK)报文,告知发送方已成功接收数据。这也与流量控制密切相关,因为接收方可以通过ACK报文中的窗口字段来通知发送方它的当前窗口大小。

17.UDP怎么实现可靠传输

UDP它不属于连接型协议,因而具有资源消耗小,处理速度快的优点,所以通常音频、视频和普通数据在传送时使用UDP较多,因为它们即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。传输层无法保证数据的可靠传输,只能通过应用层来实现了。实现的方式可以参照tcp可靠性传输的方式,只是实现不在传输层,实现转移到了应用层。关键在于两点,从应用层角度考虑:
(1)提供超时重传,能避免数据报丢失。
(2)提供确认序列号,可以对数据报进行确认和排序。

  • 本端:首先在UDP数据报定义一个首部,首部包含确认序列号和时间戳,时间戳是用来计算RTT(数据报传输的往返时间),计算出合适的RTO(重传的超时时间)。然后以等-停的方式发送数据报,即收到对端的确认之后才发送下一个的数据报。当时间超时,本端重传数据报,同时RTO扩大为原来的两倍,重新开始计时。
  • 对端:接受到一个数据报之后取下该数据报首部的时间戳和确认序列号,并添加本端的确认数据报首部之后发送给对端。根据此序列号对已收到的数据报进行排序并丢弃重复的数据报。

18.TCP连接三次握手的过程,为什么是三次,可以是两次或者更多吗?

(1) 三次握手的过程
刚开始客户端处于 Closed 的状态,服务端处于 Listen 状态,进行三次握手:

  • 第一次握手:客户端向服务器发送一个SYN (同步序列编号)报文,请求建立连接,客户端进入SYN_SENT 状态。
  • 第二次握手:服务器收到SYN 报文后,如果同意建立连接,则会发送一个SYN-ACK (同步确认)报文作为响应,同时进入SYN_RCVD 状态。
  • 第三次握手:客户端收到服务器的SYN-ACK 报文后,会发送一个ACK (确认)报文作为最终响应,之后客户端和服务器都进入ESTABLISHED 状态,连接建立成功。
    在这里插入图片描述

(2)为什么需要三次握手
通过三次握手,客户端和服务器都能够确认对方的接收和发送能力。
第一次握手确认了客户端到服务器的通道是开放的;
第二次握手确认了服务器到客户端的通道是开放的;
第三次握手则确认了客户端接收到服务器的确认,从而确保了双方的通道都是可用的。
而如果仅使用两次握手,服务器可能无法确定客户端的接收能力是否正常,比如客户端可能已经关闭了连接,但之前发送的连接请求报文在网络上延迟到达了服务器,服务器就会主动去建立一个连接,但是客户端接收不到,导致资源的浪费。而四次握手可以优化为三次。

19.TCP连接四次挥手的过程,为什么是四次?

(1)四次挥手的过程
刚开始双方都处于 ESTABLISHED 状态,假如是客户端先发起关闭请求。四次挥手的过程如下:

  • 第一次挥手:客户端发送一个FIN报文给服务端,表示自己要断开数据传送,报文中会指定一个序列号 (seq=x)。然后,客户端进入FIN-WAIT-1 状态。
  • 第二次挥手:服务端收到FIN报文后,回复ACK报文给客户端,且把客户端的序列号值+1,作为ACK报文的序列号(seq=x+1)。然后,服务端进入CLOSE-WAIT(seq=x+1)状态,客户端进入FIN-WAIT-2状态。
  • 第三次挥手:服务端也要断开连接时,发送FIN报文给客户端,且指定一个序列号(seq=y+1),随后服务端进入LAST-ACK状态。
  • 第四次挥手:客户端收到FIN报文后,发出ACK报文进行应答,并把服务端的序列号值+1作为ACK报文序列号(seq=y+2)。此时客户端进入TIME-WAIT状态。服务端在收到客户端的ACK 报文后进入CLOSE 状态。如果客户端等待2MSL没有收到回复,才关闭连接。
    在这里插入图片描述

(2)为什么需要四次挥手
因为当服务端收到客户端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。但是关闭连接时,当服务端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉客户端,“你发的FIN报文我收到了”。只有等到我服务端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四次挥手。

20.HTTP的Keep-Alive是什么?TCP 的 Keepalive 和 HTTP 的 Keep-Alive 是一个东西吗?

  1. HTTP 的 Keep-Alive,是由应用层实现的,称为 HTTP 长连接
    每次请求都要经历这样的过程:建立 TCP连接 -> HTTP请求资源 -> 响应资源 -> 释放连接,这就是HTTP短连接,但是这样每次建立连接都只能请求一次资源,所以HTTP 的 Keep-Alive实现了使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,就就是 HTTP 长连接。通过设置HTTP头Connection: keep-alive来实现。
  2. TCP 的 Keepalive,是由TCP 层(内核态)实现的,称为 TCP 保活机制,是一种用于在 TCP 连接上检测空闲连接状态的机制
    当TCP连接建立后,如果一段时间内没有任何数据传输,TCP Keepalive会发送探测包来检查连接是否仍然有效。

21.DNS查询过程

DNS 用来将主机名和域名转换为IP地址, 其查询过程一般通过以下步骤:

  1. 本地DNS缓存检查:首先查询本地DNS缓存,如果缓存中有对应的IP地址,则直接返回结果。
  2. 如果本地缓存中没有,则会向本地的DNS服务器(通常由你的互联网服务提供商(ISP)提供, 比如中国移动)发送一个DNS查询请求。
  3. 如果本地DNS解析器有该域名的ip地址,就会直接返回,如果没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的DNS服务器继续查询。
  4. 本地DNS解析器接着向指定的顶级域名DNS服务器发出查询请求。顶级域DNS服务器也不负责具体的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下一步的信息。
  5. 本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找"example.com"域名对应的IP地址,并将结果返回给本地DNS解析器。
  6. 本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应。
  7. 浏览器发起连接: 本地DNS解析器已经将IP地址返回给您的计算机,您的浏览器可以使用该IP地址与目标服务器建立连接,开始获取网页内容。

22.CDN是什么,有什么作用?

CDN是一种分布式网络服务,通过将内容存储在分布式的服务器上,使用户可以从距离较近的服务器获取所需的内容,从而加速互联网上的内容传输。

  • 就近访问:CDN 在全球范围内部署了多个服务器节点,用户的请求会被路由到距离最近的 CDN 节点,提供快速的内容访问。
  • 内容缓存:CDN 节点会缓存静态资源,如图片、样式表、脚本等。当用户请求访问这些资源时,CDN 会首先检查是否已经缓存了该资源。如果有缓存,CDN 节点会直接返回缓存的资源,如果没有缓存所需资源,它会从源服务器(原始服务器)回源获取资源,并将资源缓存到节点中,以便以后的请求。通过缓存内容,减少了对原始服务器的请求,减轻了源站的负载。
  • 可用性:即使某些节点出现问题,用户请求可以被重定向到其他健康的节点。

23.Cookie和Session是什么?有什么区别?

(1) Cookie和Session是什么?
Cookie 和 Session 都用于管理用户的状态和身份, Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。

  1. Cookie
    Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据。
    服务器在接收到来自客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从而动态生成与该客户端相对应的内容。
  2. Session
    客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。Session 主要用于维护用户登录状态、存储用户的临时数据和上下文信息等。服务器为每个用户分配一个唯一的Session ID,通常存储在Cookie中。

(2) Cookie和Session的区别?

  • 存储位置:Cookie 数据存储在用户的浏览器中,而 Session 数据存储在服务器上。
  • 数据容量:Cookie 存储容量较小,一般为几 KB。Session 存储容量较大,通常没有固定限制,取决于服务器的配置和资源。
  • 安全性:由于 Cookie 存储在用户浏览器中,因此可以被用户读取和篡改。相比之下,Session 数据存储在服务器上,更难被用户访问和修改。
  • 生命周期:Cookie可以设置过期时间,Session 依赖于会话的持续时间或用户活动。
  • 传输方式:Cookie 在每次 HTTP 请求中都会被自动发送到服务器,而 Session ID 通常通过 Cookie 或 URL 参数传递。

二、操作系统

1.进程和线程之间有什么区别

进程是资源分配和调度的基本单位。
线程是程序执行的最小单位,线程是进程的子任务,是进程内的执行单元。 一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存。

  • 资源开销:
    进程:由于每个进程都有独立的内存空间,创建和销毁进程的开销较大。进程间切换需要保存和恢复整个进程的状态,因此上下文切换的开销较高。
    线程:线程共享相同的内存空间,创建和销毁线程的开销较小。线程间切换只需要保存和恢复少量的线程上下文,因此上下文切换的开销较小。
  • 通信与同步:
    进程:由于进程间相互隔离,进程之间的通信需要使用一些特殊机制,如管道、消息队列、共享内存等。
    线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加方便。
  • 安全性:
    进程:由于进程间相互隔离,一个进程的崩溃不会直接影响其他进程的稳定性。
    线程:由于线程共享相同的内存空间,一个线程的错误可能会影响整个进程的稳定性。

在这里插入图片描述

2.并行和并发有什么区别

并行是在同一时刻执行多个任务。
并发是在相同的时间段内执行多个任务,任务可能交替执行,通过调度实现。
并行的多个任务可以同时进行,每个任务都在不同的处理单元(如多个CPU核心)上执行。需要硬件进行支持。在并行系统中,多个处理单元可以同时处理独立的子任务,从而加速整体任务的完成。
并发是指在相同的时间段内执行多个任务,这些任务可能不是同时发生的,而是交替执行,通过时间片轮转或者事件驱动的方式。操作系统通过引入进程和线程,使得程序能够并发运行。

3.解释一下用户态和核心态,什么场景下,会发生内核态和用户态的切换?

  1. 用户态和内核态的区别
    用户态和内核态是操作系统为了保护系统资源和实现权限控制而设计的两种不同的CPU运行级别,可以控制进程或程序对计算机硬件资源的访问权限和操作范围。
  • 用户态:在用户态下,进程或程序只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源。
  • 核心态:核心态是操作系统的特权级别,允许进程或程序执行特权指令和访问操作系统的核心部分。在核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存、文件系统等操作。
  1. 在什么场景下,会发生内核态和用户态的切换
  • 系统调用:当用户程序需要请求操作系统提供的服务时,会通过系统调用进入内核态。
  • 异常:当程序执行过程中出现错误或异常情况时,CPU会自动切换到内核态,以便操作系统能够处理这些异常。
  • 中断:外部设备(如键盘、鼠标、磁盘等)产生的中断信号会使CPU从用户态切换到内核态。操作系统会处理这些中断,执行相应的中断处理程序,然后再将CPU切换回用户态。

4.进程调度算法你了解多少

  • 先来先服务:非抢占式的调度算法,按照请求的顺序进行调度。 这种调度方式简单,有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业需要执行很长时间,造成了短作业等待时间过长。
  • 最短作业优先:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 但是如果一直有短作业到来,那么长作业永远得不到调度,造成长作业“饥饿”现象。
  • 最短剩余时间优先:基于最短作业优先改进,抢占式,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
  • 优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
  • 时间片轮转:为每个进程分配一个时间片,进程轮流执行,时间片用完后切换到下一个进程。
  • 多级队列:时间片轮转调度算法和优先级调度算法的结合。 将进程分为不同的优先级队列,每个队列有自己的调度算法。

5.进程间有哪些通信方式

  • 管道:是一种半双工的通信方式,数据只能单向流动而且只能在具有父子进程关系的进程间使用。
  • 命名管道: 类似管道,也是半双工的通信方式,但是它允许在不相关的进程间通信。
  • 消息队列:允许进程发送和接收消息,而消息队列是消息的链表,可以设置消息优先级。
  • 信号:用于发送通知到进程,告知其发生了某种事件或条件。
  • 信号量:是一个计数器,可以用来控制多个进程对共享资源的访问,常作为一种锁机制,主要作为进程间以及同一进程内不同线程之间的同步手段。
  • 共享内存:就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的进程通信方式,
  • Socket套接字:是支持TCP/IP 的网络通信的基本操作单元,主要用于在客户端和服务器之间通过网络进行通信。

6.解释一下进程同步和互斥,以及如何实现进程同步和互斥

进程同步是指多个并发执行的进程之间协调和管理它们的执行顺序,以确保它们按照一定的顺序或时间间隔执行。
互斥指的是在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。

解决进程同步和互斥的问题有很多种方法,其中一种常见的方法是使用信号量和 PV 操作。信号量是一种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是一种对信号量进行增加或者减少的操作,它们可以用来控制进程之间的同步或者互斥。

  • P操作:相当于“检查”信号量,如果资源可用,就减少计数,然后使用资源。
  • V操作:相当于“归还”资源,增加信号量的计数,并可能唤醒等待的进程。

除此之外,下面的方法也可以解决进程同步和互斥问题:

  • 临界区:将可能引发互斥问题的代码段称为临界区,里面包含了需要互斥访问的资源。进入这个区域前需要先获取锁,退出临界区后释放该锁。这确保同一时间只有一个进程可以进入临界区。
  • 互斥锁(Mutex):互斥锁是一种同步机制,用于实现互斥。每个共享资源都关联一个互斥锁,进程在访问该资源前需要先获取互斥锁,使用完后释放锁。只有获得锁的进程才能访问共享资源。
  • 条件变量:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

7.什么是死锁,如何避免死锁?

死锁是系统中两个或多个进程在执行过程中,因争夺资源而造成的一种僵局。当每个进程都持有一定的资源并等待其他进程释放它们所需的资源时,如果这些资源都被其他进程占有且不释放,就导致了死锁。

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件:一个进程占用了某个资源时,其他进程无法同时占用该资源。
  • 请求保持条件:一个进程因为请求资源而阻塞的时候,不会释放自己的资源。
  • 不可剥夺条件:资源不能被强制性地从一个进程中剥夺,只能由持有者自愿释放。
  • 循环等待条件:多个进程之间形成一个循环等待资源的链,每个进程都在等待下一个进程所占有的资源。

避免死锁:通过破坏死锁的四个必要条件之一来预防死锁。比如破坏循环等待条件,让所有进程按照相同的顺序请求资源。 检测死锁:通过检测系统中的资源分配情况来判断是否存在死锁。例如,可以使用资源分配图或银行家算法进行检测。 解除死锁:一旦检测到死锁存在,可以采取一些措施来解除死锁。例如,可以通过抢占资源、终止某些进程或进行资源回收等方式来解除死锁。

8.介绍一下几种典型的锁

  • 互斥锁:互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源。
  • 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到获取为止。如果别的线程长时期占有锁,那么自旋就是在浪费CPU做无用功,但是自旋锁一般应用于加锁时间很短的场景,这个时候效率比较高。

其他的锁都是基于这两个锁的

  • 读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。
  • 悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁
  • 乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作。
  • 条件变量:条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,他常和互斥锁一起使用,以免出现竞态条件。当条件不满足时,线程往往解开相应的互斥锁并阻塞线程然后等待条件发生变化。一旦其他的某个线程改变了条件变量,他将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。总的来说互斥锁是线程间互斥的机制,条件变量则是同步机制。

9.讲一讲你理解的虚拟内存

虚拟内存是指在每一个进程创建加载的过程中,会分配一个连续虚拟地址空间,它不是真实存在的,而是通过映射与实际物理地址空间对应,这样就可以使每个进程看起来都有自己独立的连续地址空间,并允许程序访问比物理内存RAM更大的地址空间, 每个程序都可以认为它拥有足够的内存来运行。

需要虚拟内存的原因:

  • 内存扩展: 虚拟内存使得每个程序都可以使用比实际可用内存更多的内存,从而允许运行更大的程序或处理更多的数据。
  • 内存隔离:虚拟内存还提供了进程之间的内存隔离。每个进程都有自己的虚拟地址空间,因此一个进程无法直接访问另一个进程的内存。
  • 物理内存管理:虚拟内存允许操作系统动态地将数据和程序的部分加载到物理内存中,以满足当前正在运行的进程的需求。当物理内存不足时,操作系统可以将不常用的数据或程序暂时移到硬盘上,从而释放内存,以便其他进程使用。
  • 页面交换:当物理内存不足时,操作系统可以将一部分数据从物理内存写入到硬盘的虚拟内存中,这个过程被称为页面交换。当需要时,数据可以再次从虚拟内存中加载到物理内存中。这样可以保证系统可以继续运行,尽管物理内存有限。
  • 内存映射文件:虚拟内存还可以用于将文件映射到内存中,这使得文件的读取和写入可以像访问内存一样高效。

10.你知道的线程同步的方式有哪些?

线程同步机制是指在多线程编程中,为了保证线程之间的互不干扰,而采用的一种机制。常见的线程同步机制有以下几种:

  1. 互斥锁:互斥锁是最常见的线程同步机制。它允许只有一个线程同时访问被保护的临界区(共享资源)
  2. 条件变量:条件变量用于线程间通信,允许一个线程等待某个条件满足,而其他线程可以发出信号通知等待线程。通常与互斥锁一起使用。
  3. 读写锁: 读写锁允许多个线程同时读取共享资源,但只允许一个线程写入资源。
  4. 信号量:用于控制多个线程对共享资源进行访问的工具。

11.有哪些页面置换算法

常见页面置换算法有最佳置换算法(OPT)、先进先出(FIFO)、最近最久未使用算法(LRU)、时钟算法(Clock) 等。

  1. 先进先出FIFO算法:也就是最先进入内存的页面最先被置换出去。虽然实现简单,但是该算法与进程实际运行时的规律不适应,因为先进入的页面也有可能最经常被访问。因此,算法性能差
  2. 最近最久未使用算法LRU :LRU算法基于页面的使用历史,通过选择最长时间未被使用的页面进行置换。(该算法的实现需要专门的硬件⽀持,虽然算法性能好,但是实现困难,开销大
  3. 最不经常使用LFU :淘汰访问次数最少的页面,考虑页面的访问频率。
  4. 时钟算法CLOCK:Clock算法的核心思想是通过使用一个指针(称为时钟指针)在环形链表上遍历,检查页面是否被访问过, 当需要进行页面置换时,Clock算法从时钟指针的位置开始遍历环形链表。 如果当前页面的访问位为0,表示该页面最久未被访问,可以选择进行置换。将访问位设置为1,继续遍历下一个页面。 如果当前页面的访问位为1,表示该页面最近被访问过,它仍然处于活跃状态。将访问位设置为0,并继续遍历下一个页面如果遍历过程中找到一个访问位为0的页面,那么选择该页面进行置换。
  5. 最佳置换算法: 该算法根据未来的页面访问情况,选择最长时间内不会被访问到的页面进行置换。但操作系统无法预测未来要访问什么页面,所以这种算法只是一种理想情况下的置换算法,通常是无法实现的。

12.熟悉哪些Linux命令

  1. 文件操作:
    • ls:列出目录内容。
    • cd:改变当前目录。
    • pwd:显示当前工作目录。
    • cp:复制文件或目录。
    • mv:移动或重命名文件。
    • rm:删除文件或目录。
    • touch:创建空文件或更新文件时间戳。
  2. 文件内容查看:
    • cat:查看文件内容。
    • head:查看文件的前几行。
    • tail:查看文件的后几行,常用于查看日志文件。
  3. 文件编辑:
    • vi 或 vim:强大的文本编辑器。
  4. 权限管理:
    • chmod:更改文件或目录的访问权限。
    • chown:更改文件或目录的所有者和/或所属组。
  5. 磁盘管理:
    • df:查看磁盘空间使用情况。
  6. 网络管理:
    • ifconfig 或 ip addr:查看和配置网络接口。
    • ping:测试网络连接。
    • netstat:查看网络状态和统计信息。
    • ssh:安全远程登录。
  7. 进程管理:
    • ps:查看当前运行的进程。
    • kill:发送信号给进程。
  8. 软件包管理(根据Linux发行版不同,命令可能有所不同):
    • apt-get(Debian/Ubuntu):安装、更新和删除软件包。

13.Linux中如何查看一个进程,如何杀死一个进程,如何查看某个端口有没有被占用

  1. 查看进程: 用 ps 命令查看当前运行的进程,比如 ps aux 可以列出所有进程及其详细信息。
  2. 杀死进程: 首先用 ps 或 top 命令找到进程的PID(进程ID)。 然后用 kill 命令加上进程ID来结束进程,例如 kill -9 PID。“-9” 是强制杀死进程的信号。
  3. 查看端口占用: 使用 lsof -i:端口号 可以查看占用特定端口的进程。 或者用 netstat -tulnp | grep 端口号,这会显示监听在该端口的服务及其进程ID。

14.说一下 select、poll、epoll

I/O多路复用通常通过select、poll、epoll等系统调用来实现。

  1. select: select是委托内核监视多个文件描述符的可读、可写和错误状态。当有文件描述符对应的缓冲区发生改变,就返回,返回值为就绪的描述符数量。
    缺点:每次调用select都需要把fd集合从用户态拷贝到内核态,同时每次调用都要在内核遍历所有传递进来的fd,它的效率可能随着监视的文件描述符数量的增加而降低。并且支持的文件描述符数量太小,默认为1024。
  2. poll: poll是select的一种改进,它使用轮询方式来检查多个文件描述符的状态。改进了 select 的文件描述符数组的数量大小限制以及不能重用的问题,因为 poll 用的不再是数组,而是结构体封装了文件描述符及处理事件,我们可以自定义其数量以及重用文件描述符。但对于大量的文件描述符,poll的性能也可能变得不足够高效。
  3. epoll: epoll是Linux特有的I/O多路复用机制,相较于select和poll,它在处理大量文件描述符时更加高效。epoll使用事件通知的方式,只有在文件描述符就绪时才会通知应用程序,而不需要应用程序轮询。
    epoll实例是一个 event_poll类型的结构体,结构体中我们重点理解的是 红黑树和双链表,红黑树用来保存需要检测的文件描述符,双链表用来保存检测到的有变化的文件描述符(传出)。
    检测添加到红黑树中描述符和事件,检测到有变化的描述符就在链表中做记录,最终会输出到传入传出参数 epoll_event的数组中,并且返回发生变化的文件描述符的个数。

三、MySQL

1.一条SQL查询语句是如何执行的?

在这里插入图片描述

  1. 客户端请求->
  2. 连接器:负责跟客户端建立连接、获取权限、维持和管理连接。->
  3. 查询缓存:先到查询缓存看看,之前是不是执行过这条语句。执行过的语句及其结果可能会以key-value 对的形式,被直接缓存在内存中。存在缓存则直接返回,不存在则执行后续操作 ->
  4. 分析器:对SQL进行词法分析和语法分析操作 ->
  5. 优化器:对执行的SQL优化选择最优的执行方案,决定使用哪个索引和各个表的连接顺序 ->
  6. 执行器:执行时会先看用户是否有执行权限,有才去使用这个引擎提供的接口,开始执行语句 ->
  7. 去引擎层获取数据返回(如果开启查询缓存则会缓存查询结果)

2.事务的四大特性有哪些?

事务的四大特性通常被称为ACID特性

  1. 原子性:确保事务的所有操作要么全部执行成功,要么全部失败回滚,不存在部分成功的情况。
  2. 一致性:事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。数据库的完整性没有被破坏。表示写入的资料必须完全符合所有的预设规则。
  3. 隔离性:多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。
  4. 持久性:一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。

3.数据库的事务隔离级别有哪些?

  1. 读未提交(Read Uncommitted):
    允许一个事务读取另一个事务尚未提交的数据修改。
    最低的隔离级别,存在脏读、不可重复读和幻读的问题。
  2. 读已提交(Read Committed):
    一个事务只能读取已经提交的数据。其他事务的修改在该事务提交之后才可见。
    解决了脏读问题,但仍可能出现不可重复读和幻读。
  3. 可重复读(Repeatable Read):
    事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。
    解决了不可重复读问题,但仍可能出现幻读。
  4. 序列化(Serializable):
    最高的隔离级别,确保事务之间的并发执行效果与串行执行的效果相同,即不会出现脏读、不可重复读和幻读。
  • 脏读: 在第一个修改事务和读取事务进行的时候,读取事务读到的数据为100,这是修改之后的数据,但是之后该事务满足一致性等特性而做了回滚操作,那么读取事务得到的结果就是脏数据了。
  • 不可重复读: T2 读取一个数据,然后T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同
  • 幻读: 一般是T1在某个范围内进行修改操作(增加或者删除),而T2读取该范围导致读到的数据是修改之间的了,强调范围。

4.MySQL的执行引擎有哪些?

MySQL的执行引擎主要负责查询的执行和数据的存储, 其执行引擎主要有InnoDB、MyISAM、Memery 等。

  • InnoDB
    • 是目前MySQL的默认事务型存储引擎
    • 实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ)。
    • 主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
    • 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。
    • 支持真正的在线热备份,支持外键,支持行级锁
  • MyISAM
    • 引擎是早期的默认存储引擎,支持全文索引,但是不支持事务
    • 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它
    • 不支持行级锁,只能对整张表加锁,读取时会对需要读到的所有表加共享锁,写⼊时则对表加排它锁。
    • 但在表有读取操作的同时,也可以往表中插入新的记录,这被称为并发插入(CONCURRENT INSERT)。
    • 设计简单,数据以紧密格式存储。对于只读数据,或者表⽐较⼩、可以容忍修复操作,则依然可以使⽤它。
  • Memery
    • Memery就是将数据放在内存中,访问速度快,但数据在数据库服务器重启后会丢失。

5.MySQL为什么使用B+树来作索引

MySQL中的数据一般是放在磁盘中的,B+tree的磁盘读写代价更低,并且查询效率更加稳定

  • 单点查询:B 树和hash进行单个索引查询时,最快可以在 O(1) 的时间代价内就查到。平均来看,会比 B+ 树稍快一些。但是 B 树的查询波动会比较大,因为每个节点既存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。B+树的非叶子节点不存放实际的记录数据,仅存放索引,所以数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。并且在查询多条记录的时候,B+比hash更快。
  • 插入和删除效率:B+ 树有大量的冗余节点,删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,删除非常快。B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。B 树没有冗余节点,删除节点的时候非常复杂,可能涉及复杂的树的变形。
  • 范围查询:B+ 树所有叶子节点间有一个链表进行连接,而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。存在大量范围检索的场景,适合使用 B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如nosql的MongoDB。

6.说一下索引失效的场景?

索引失效意味着查询操作不能有效利用索引进行数据检索,从而导致性能下降,下面一些场景会发生索引失效。

  • 使用OR条件:当使用OR连接多个条件,并且每个条件用到不同的索引列时,索引可能不会被使用。
  • 使用非等值查询:当使用!=或<>操作符时,索引可能不会被使用,特别是当非等值条件在WHERE子句的开始部分时。
  • 对列进行类型转换: 如果在查询中对列进行类型转换,例如将字符列转换为数字或日期,索引可能会失效。
  • 使用LIKE语句:以通配符%开头的LIKE查询会导致索引失效。
  • 函数或表达式:在列上使用函数或表达式作为查询条件,通常会导致索引失效。
  • 表连接中的列类型不匹配: 如果在连接操作中涉及的两个表的列类型不匹配,索引可能会失效。例如,一个表的列是整数,另一个表的列是字符,连接时可能会导致索引失效。

7.什么是慢查询?原因是什么?可以怎么优化?

数据库查询的执行时间超过指定的超时时间时,就被称为慢查询。

原因:

  • 查询语句比较复杂:查询涉及多个表,包含复杂的连接和子查询,可能导致执行时间较长。
  • 查询数据量大:当查询的数据量庞大时,即使查询本身并不复杂,也可能导致较长的执行时间。
  • 缺少索引:如果查询的表没有合适的索引,需要遍历整张表才能找到结果,查询速度较慢。
  • 数据库设计不合理:数据库表设计庞大,查询时可能需要较多时间。
  • 并发冲突:当多个查询同时访问相同的资源时,可能发生并发冲突,导致查询变慢。
  • 硬件资源不足:如果MySQL服务器上同时运行了太多的查询,会导致服务器负载过高,从而导致查询变慢

优化:

  • 运行语句,找到慢查询的sql
  • 查询区分度最高的字段
  • explain:显示mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引、写出更优化的查询语句
  • order by limit形式的sql语句,让排序的表优先查
  • 考虑建立索引原则

8.undo log、redo log、binlog 有什么用?

  • undo log
    • undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的 SQL语句,他需要记录你要回滚的相应日志信息。
    • 当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据
    • 当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作
    • 当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作
    • undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
  • redo log
    • redo log是物理日志,记录了某个数据页做了什么修改,每当执行一个事务就会产生一条或者多条物理日志。
    • 就是说当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。
    • 当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。
    • 当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
  • binlog
    • binlog(归档日志)是Server 层生成的日志,主要用于数据备份和主从复制。

9.MySQL和Redis的区别是什么

  • Redis基于键值对,支持多种数据结构;而MySQL是一种关系型数据库,使用表来组织数据。
  • Redis将数据存在内存中,通过持久化机制将数据写入磁盘,MySQL通常将数据存储在磁盘上。
  • Redis不使用SQL,而是使用自己的命令集,MySQL使用SQL来进行数据查询和操作。
  • Redis以高性能和低延迟为目标,适用于读多写少的应用场景,MySQL 适用于需要支持复杂查询、事务处理、拥有大规模数据集的场景。
  • Redis 更适合处理高速、高并发的数据访问,以及需要复杂数据结构和功能的场景,在实际应用中,很多系统会同时使用 MySQL 和 Redis。

10.Redis有什么优缺点?为什么用Redis查询会比较快

(1) Redis有什么优缺点?

  • Redis 是一个基于内存的数据库,读写速度非常快,通常被用作缓存、消息队列、分布式锁和键值存储数据库
  • 它支持多种数据结构,如String,list,set,sorted set,hash等,
  • Redis 还提供了分布式特性,可以将数据分布在多个节点上,以提高可扩展性和可用性
  • 但是Redis 受限于物理内存的大小,不适合存储超大量数据,并且需要大量内存,相比磁盘存储成本更高

(2)为什么Redis查询快

  • 基于内存操作: 传统的磁盘文件操作相比减少了IO,提高了操作的速度。
  • 高效的数据结构:Redis专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。
  • 单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。
  • I/O多路复用:采用非阻塞I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。

11.Redis的数据类型有哪些?

Redis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)。

  • 字符串STRING:存储字符串数据,最基本的数据类型。
  • 哈希表HASH:存储字段和值的映射,用于存储对象。后续操作的时候,可以直接仅修改这个对象中的某个字段的值。
  • 列表LIST:存储有序的字符串元素列表。为一个双向链表,即可以反向查找和遍历,不过带来了部分额外的内存开销。
  • 集合SET:存储唯一的字符串元素,无序。特殊之处在于可以自动排重的。
  • 有序集合ZSET:类似于集合,但每个元素都关联一个权重参数,可以按分数进行排序。

Redis版本更新,又增加了几种数据类型:

  • BitMap: 存储位的数据结构,可以用于处理一些位运算操作。
  • HyperLogLog:用于基数估算的数据结构,用于统计元素的唯一数量。
  • GEO: 存储地理位置信息的数据结构。
  • Stream:专门为消息队列设计的数据类型。

12.Redis是单线程的还是多线程的,为什么?

Redis在其传统的实现中是单线程的(网络请求模块使用单线程进行处理,其他模块仍用多个线程),这意味着它使用单个线程来处理所有的客户端请求。
Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)
这样的设计选择有几个好处:

  • 简化模型:单线程模型简化了并发控制,避免了复杂的多线程同步问题。
  • 性能优化:由于大多数操作是内存中的,单线程避免了线程间切换和锁竞争的开销。
  • 原子性保证:单线程执行确保了操作的原子性,简化了事务和持久化的实现。
  • 顺序执行:单线程保证了请求的顺序执行。

但是Redis的单线程模型并不意味着它在处理客户端请求时不高效。实际上,由于其操作主要在内存中进行,Redis能够提供极高的吞吐量和低延迟的响应。
此外,Redis 6.0 引入了多线程的功能,用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。

13.说一说Redis持久化机制有哪些

很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。或者是为了防止系统故障而将数据备份到一个远程位置。
以下有两种持久化机制

  • 快照(snapshotting)持久化(RDB持久化)
    Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。
    Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
  • AOF(append-only file)持久化
    与快照持久化相比,AOF持久化的实时性更好,因此已成为主流的持久化方案。
    开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令以追加的方式写入硬盘中的AOF文件。
  • 混合持久化方式
    Redis 4.0 新增 RDB 和 AOF 的混合持久化。
    如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
    当然缺点也是有的, AOF 里面的RDB部分是压缩格式不再是 AOF 格式,可读性较差。

14.介绍一下Redis缓存雪崩和缓存穿透等问题,如何解决这些问题?

  1. 缓存雪崩
  • 是指在某个时间点,大量缓存同时失效,所以,后面的请求都会落到数据库上,增加了系统负载。
  • 解决方案:
    事前:尽量保证整个 Redis 集群的高可用性,发现机器宕机尽快补上,选择合适的内存淘汰策略。
    事中: 通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
    事后:利⽤ Redis 持久化机制保存的数据尽快恢复缓存
  1. 缓存穿透
  • 是指查询一个在缓存和数据库都不存在的数据,这个数据始终无法被缓存,导致每次请求都直接访问数据库,增加数据库的负载。典型的情况是攻击者可能通过构造不存在的 key 大量访问缓存,导致对数据库的频繁查询。
  • 解决方案:
    布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
    缓存空对象:当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取
  1. 缓存击穿
  • 是指一个key非常热点,当这个key在失效的瞬间,持续的大量并发请求查询这个缓存不存在的数据时,导致请求直接访问数据库,增加数据库的负载。
  • 解决方案:
    热门key缓存设置为永不过期
    采用互斥锁
  1. 缓存预热
  • 是指系统上线后,将相关的缓存数据直接加载到缓存系统。可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户会直接查询事先被预热的缓存数据
  1. 缓存更新
  • 除了缓存服务器自带的缓存失效策略之外,还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
    (1)定时去清理过期的缓存;定时删除和惰性删除
    (2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
  • 优劣
    第一种的缺点是维护大量缓存的key是比较麻烦的,
    第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对复杂
  1. 缓存降级
  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据⼀些关键数据进行自动降级,也可以配置开关实现人工降级。
    降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
  • 目的
    为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户

15.如何保证数据库和缓存的一致性

  1. Cache Aside
  • 原理:先从缓存中读取数据,如果没有就再去数据库里面读数据,然后把数据放回缓存中,如果缓存中可以找到数据就直接返回数据;更新数据的时候先把数据持久化到数据库,然后再让缓存失效。
  • 问题:假如有两个操作一个更新一个查询,第一个操作先更新数据库,还没来及删除数据库,查询操作可能拿到的就是旧的数据;更新操作马上让缓存失效了,所以后续的查询可以保证数据的一致性;还有的问题就是有一个是读操作没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,也会造成脏数据。
  • 可行性:出现上述问题的概率其实非常低,需要同时达成读缓存时缓存失效并且有并发写的操作。数据库读写要比缓存慢得多,所以读操作在写操作之前进入数据库,并且在写操作之后更新,概率比较低。
  1. Read/Write Through
  • 原理:Read/Write Through原理是把更新数据库(Repository)的操作由缓存代理,应用认为后端是一个单一的存储,而存储自己维护自己的缓存。
  • Read Through:就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside策略是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对调用方是透明的。
  • Write Through:当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由缓存自己更新数据库(这是一个同步操作)。
  1. Write Behind
  • 原理:在更新数据的时候,只更新缓存,不更新数据库,而缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作非常快,带来的问题是,数据不是强一致性的,而且可能会丢。
  • 第二步失效问题:这种可能性极小,缓存删除只是标记一下无效的软删除,可以看作不耗时间。如果会出问题,一般程序在写数据库那里就没有完成:故意在写完数据库后,休眠很长时间再来删除缓存。

16.主键、索引、外键

  • 定义:
    主键:唯⼀标识⼀条记录,不允许重复,不允许为空,通常⽤来保证数据的唯⼀性和⽤于在表中查找特定的⾏
    外键:外键是⼀个表中的字段,其值是另⼀个表的主键,⽤于建⽴两个表之间的关系。
    索引:没有重复值,但可以有⼀个空值 ,⽤于快速查询到数据。
  • 作⽤:
    主键:⽤于唯⼀标识表中每⼀⾏的字段
    外键:主要⽤于和其它表建⽴联系
    索引:为了提⾼查询排序的速度
  • 区别:
    外键是⼀个表中的字段,它与另⼀个表的主键形成关联,⽤于建⽴表之间的关系。
    主键和外键通常都与索引有关,但索引不⼀定是主键或外键。

17.为什么使用索引?

  • 通过创建唯⼀性索引,可以保证数据库表中每⼀⾏数据的唯⼀性。
  • 可以⼤⼤加快数据的检索速度,这也是创建索引的最主要的原因。
  • 帮助服务器避免排序和临时表
  • 将随机IO变为顺序IO。
  • 可以加速表和表之间的连接,特别是在实现数据的参考完整性⽅⾯特别有意义。

18.怎么建立有效的索引

  1. 索引应该针对在查询条件中经常使用的列创建。
  2. 对于频繁更新的表,避免对所有列创建索引,因为更新操作会增加索引维护的开销。
  3. 为那些在WHERE子句中经常出现的列创建索引。
  4. 对于那些只有少数不同值的列,不适合创建索引,因为这些列不会提高查询效率。
  5. 对于连接表的列,创建索引,因为这些列在JOIN操作中经常使用。
  6. 考虑使用复合索引,其中包含多个列,以满足多个查询条件。
  7. 创建索引的SQL语法如下:CREATE INDEX index_name ON table_name(column1, column2, …);

19.MySQL有哪些锁?作用是什么?

  1. 全局锁
    全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样,加上全局锁,意味着整个数据库都是只读状态。
  2. 表级锁
  • 元数据锁(MDL):对数据库表进行操作时,会自动给这个表加上元数据锁,为了保证当用户对表执行 CRUD 操作时,其他线程对这个表结构做了变更。元数据锁在事务提交后才会释放。
  • 意向锁:对某些记录加上共享锁之前,需要先在表级别加上一个意向共享锁,对某些纪录加上独占锁之前,需要先在表级别加上一个意向独占锁。普通的 select 是不会加行级锁的,普通的 select 语句是利用 MVCC 实现一致性读,是无锁的。
  • AUTO-INC 锁:表里的主键通常都会设置成自增的,之后可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值通过 AUTO-INC 锁实现的。在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时字段的值是连续递增的。
  1. 行锁
  • 记录锁:锁住的是一条记录,记录锁分为排他锁和共享锁。
  • 间隙锁:只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。间隙锁之间是兼容的,两个事务可以同时持有包含共同间隙范围的间隙锁,并不存在互斥关系。
  • Next-Key Lock:Next-Key Lock临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。next-key lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。
  • 插入意向锁:一个事务在插入一条记录的时候,需要判断插入位置是否已被其他事务加了间隙锁(next-key lock 也包含间隙锁)。如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止,在此期间会生成一个插⼊意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

四、C++

1.静态变量和全局变量、局部变量的区别、在内存上是怎么分布的

  1. 静态局部变量
    • 作用域: 限定在定义它的函数内。
    • 生命周期: 与程序的生命周期相同,但只能在定义它的函数内部访问。
    • 关键字: 使用 static 关键字修饰。
    • 初始化: 仅在第一次调用函数时初始化,之后保持其值。
    • 内存分布:静态局部变量存储在全局/静态存储区。
    • 当希望在函数调用之间保留变量的值,并且不希望其他函数访问这个变量时,可以使用静态局部变量
  2. 全局变量
    • 作用域: 整个程序。
    • 生命周期: 与程序的生命周期相同。
    • 关键字: 定义在全局作用域,不使用特定关键字。
    • 内存分布:全局变量也存储在全局/静态存储区。
    • 当多个函数需要共享相同的数据时,可以使用全局变量。
  3. 局部变量
    • 作用域: 限定在定义它的块(大括号内)。
    • 生命周期: 在块结束时销毁。
    • 关键字: 定义在函数、语句块或类的成员函数中。
    • 内存分布:局部变量存储在栈上,与它们所在的作用域(如函数)相关联。
    • 当变量只在某个特定作用域内有效,并且不需要其他作用域访问时,可以使用局部变量。

2.指针和引用的区别

指针存放某个对象的地址,其本身就是变量(命了名的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变
引用就是变量的别名,从一而终,不可变,必须初始化

  • 定义和声明
    指针是一个变量,其值是另一个变量的地址。声明指针时,使用 * 符号。
    引用是一个别名,它是在已存在的变量上创建的。在声明引用时,使用 & 符号。
  • 使用和操作
    指针可以通过解引用操作符 * 来访问指针指向的变量的值,还可以通过地址运算符 & 获取变量的地址。
    引用在声明时被初始化,并在整个生命周期中一直引用同一个变量。不需要使用解引用操作符,因为引用本身就是变量的别名
    指针可以有多级,引用只能一级
  • 空值和空引用
    指针可以为空(nullptr)表示不指向任何有效的地址。
    引用必须在声明时初始化,并且不能在后续改变引用的绑定对象。因此,没有空引用的概念
    不存在指向空值的引用,但是存在指向空值的指针
  • 可变性
    指针可以改变指针的指向,使其指向不同的内存地址。
    引用⼀旦引用被初始化,它将⼀直引用同一个对象,不能改变绑定。
  • 用途
    需要返回函数内局部变量的内存的时候用指针。使用指针传参需要开辟内存,用完要记得释放指针,不然会内存泄漏。而返回局部变量的引用是没有意义的
    对栈空间大小比较敏感(比如递归)的时候使用引用。使用引用传递不需要创建临时变量,开销要更小
    类对象作为参数传递的时候使用引用,这是C++类对象传递的标准方式

3.C++内存分区

很多情况下,提到 C++ 程序的内存分区时,会简化为下面五个主要区域

  1. 栈(Stack): 用于存储局部变量和函数调用的上下文。栈的内存分配是自动的,由编译器管理。
  2. 堆(Heap): 用于动态内存分配。程序员可以使用 new、malloc 等操作符或函数从堆上分配内存,并使用 delete、free 释放内存。
  3. 全局/静态存储区(Global/Static Storage): 存储全局变量和静态变量,生命周期是整个程序运行期间。在程序启动时分配,程序结束时释放。包括:
    • 数据段:存储初始化的全局变量和静态变量。
    • BSS 段 :存储未初始化的全局变量和静态变量。
  4. 常量存储区(Constant Data): 也称为只读区。存储程序中的常量数据,如字符串字面量。
  5. 代码段(Code Segment 或 Text Segment): 存储程序的可执行代码和函数的二进制指令。

4.static关键字和const关键字的作用

  1. static 关键字: 用于控制变量和函数的生命周期、作用域和访问权限。

    • 声明静态变量:静态变量的生命周期直到程序结束。当在函数内部声明静态变量时,即使函数执行完了也不会释放它,下次调用该函数时会保留上次的状态。
    • 在类中,被static声明的成员被称为静态成员。
    • 静态成员变量:在类中使用static关键字修饰的成员变量,表示该变量属于类而不是类的实例,所有实例共享同一份数据
    • 静态成员函数:在类内使用static关键字修饰的成员函数,所有对象共享同一个函数;静态成员函数只能访问静态成员变量;静态成员函数调用可以不需要通过创建类的实例,而是直接通过类名调用。
    • static变量如果被多个线程访问,需要特别注意线程安全问题。
  2. const: 关键字用于定义常量,即一旦初始化后其值不能被修改:

    • 常量变量:声明变量,使变量的值不能修改(只读)
    • 常量成员函数,表示该函数不会修改对象的成员变量
    • 常量指针:可以指向一个 const 类型的值,或者是一个指向 const 值的指针,表明指针指向的值不能通过这个指针被修改。
    • const变量由于其不可变性,天然具有线程安全性。

有时候static和const 可以组合使用,如static const变量,表示一个静态的常量。
总结来说,static关键字用于创建类的静态成员,而const 关键字用于定义常量。
在这里插入图片描述

5.常量指针和指针常量之间有什么区别

  • 常量指针(底层const)
    是指定义了一个指针,这个指针指向一个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是指针对其所指对象的不可改变性。
  • 指针常量(顶层const)
    指针常量是指定义了一个指针,这个指针的值只能在定义时初始化,其他地方不能改变。指针常量强调的是指针的不可改变性。

也可以同时使用 const 关键字来定义一个指针,既是常量指针又是指针常量,即它所指向的数据不能被修改,同时指针本身的值也不能改变。

6.结构体和类之间有什么区别

  • 通常, struct 用于表示一组相关的数据,而 class 用于表示一个封装了数据和操作的对象。
  • struct结构体中的成员默认是公有的(public)。类中的成员默认是私有的(private)。
  • struct 继承时默认使用公有继承。class 继承时默认使用私有继承。
  • class 可以用于定义模板函数,而 struct 不行。
  • 在实际使用中,可以根据具体的需求选择使用 struct 或 class 。如果只是用来组织一些数据,而不涉及复杂的封装和继承关系, struct 可能更直观;如果需要进行封装、继承等面向对象编程的特性,可以选择使用 class 。

7.什么是智能指针,C++有哪几种智能指针

智能指针是C++中用来自动管理动态分配内存的一种模板类,维护着一个指向动态分配对象的指针,并在智能指针对象被销毁时,自动释放该内存,从而避免内存泄漏。
C++有以下几种智能指针:

  • std::unique_ptr:独占式拥有指针,保证同一时间只有一个unique_ptr指向特定内存,适用于不需要共享所有权的场景,如栈上对象的管理。
  • std::shared_ptr:多个shared_ptr可以共享同一内存,使用引用计数机制来管理内存的生命周期,适用于多个对象需要共享同一个资源的场景。
  • std::weak_ptr:弱引用,用于解决shared_ptr可能导致的循环引用问题,它不拥有所指向的对象。

8.智能指针的实现原理是什么

  1. std::unique_ptr
    unique_ptr代表独占所有权的智能指针,同一时间只能有一个unique_ptr实例指向特定资源。
    它通过析构函数来管理资源的释放。当unique_ptr超出作用域时,会自动调用删除操作符来释放其指向的内存。
    std::unique_ptr 通过删除复制构造函数和复制赋值运算符来确保所有权的唯一性,但提供移动构造函数和移动赋值运算符,允许所有权的转移。
  2. std::shared_ptr
    shared_ptr允许多个指针实例共享对同一资源的所有权,使用引用计数机制来跟踪有多少个shared_ptr指向同一资源。
    内部维护一个控制块,通常包含引用计数和资源的原始指针。每当创建一个新的shared_ptr或将一个shared_ptr赋值给另一个时,引用计数增加。
    当shared_ptr被销毁或通过标准库提供的reset成员函数重置时,引用计数减少。当引用计数降到零时,控制块会释放资源并自我销毁。
  3. std::weak_ptr
    std::weak_ptr 是一种不拥有对象的智能指针,它观察 std::shared_ptr 管理的对象,但不增加引用计数。
    它用于解决 std::shared_ptr 之间可能产生的循环引用问题,因为循环引用会导致引用计数永远不会达到零,从而造成内存泄漏。

9.new和malloc有什么区别

new和malloc在C++中都用于动态内存分配,但它们之间有几个关键的区别:

  1. 语法层面:
    new是C++的操作符,可以直接用来分配对象或数组。
    malloc是一个函数,通常需要包含头文件<cstdlib>,并且只分配原始内存。
  2. 类型安全:
    new是类型安全的,它会根据分配的对象类型进行正确的内存分配和构造函数调用。
    malloc 不是类型安全的,它只分配原始内存,不调用构造函数。返回类型是 void*,需要强制类型转换为具体的指针类型。
  3. 构造与析构:
    使用 new 分配的对象在对象生命周期结束时需要使用 delete 来释放,delete 会自动调用对象的析构函数。
    使用 malloc 分配的内存需要使用 free 来释放,free 不会自动调用析构函数,因此如果分配的是对象数组,需要手动调用析构函数。
  4. 异常安全性:
    new在分配失败时会抛出std::bad_alloc异常。
    malloc在分配失败时返回NULL指针。
  5. 管理机制:
    C++中的new和delete通常由编译器实现,可能包含一些额外的内存管理机制。
    C语言的malloc和free由C标准库提供,与编译器无关。

总结来说,new和malloc都是动态内存分配的手段,但new提供了类型安全和构造/析构的自动化,而malloc则提供了更底层的内存分配方式,需要手动管理构造和析构。在C++中,推荐使用new来分配对象,以保持类型安全和自动化的资源管理。

10.delete 和 free 有什么区别?

delete和free都是用来释放动态分配的内存,但它们有不同的使用方式:

  1. 语法:
    delete是C++中的关键字,用于释放由new分配的对象。
    free是C语言中的函数,通常包含在<stdlib.h>头文件中,用于释放由malloc分配的内存。
  2. 对象销毁:
    当使用 delete 释放对象内存时,C++ 编译器会自动调用对象的析构函数,释放与对象相关的资源,并执行对象的清理工作。
    free 仅释放内存,不调用析构函数。因此,如果使用 malloc 分配了 C++ 对象的内存,需要手动调用析构函数后再调用 free。
  3. 数组处理:
    如果是数组,C++提供了delete[]来释放整个数组的内存,而C语言中仍然使用free,没有区分单个对象和数组。
  4. 返回值:
    free 没有返回值,即使内存释放失败,也不会反馈任何信息。
    delete 之后,指针会自动置为 nullptr
  5. 类型检查:
    delete 进行类型检查,确保删除的对象类型与 new 分配时的类型一致。
    free 不进行类型检查,因为它只处理 void* 类型的指针。

总结来说,delete和free都是用来释放动态内存的,但它们分别用于C++和C语言中的内存管理。delete适用于C++对象,会自动调用析构函数;而free适用于C语言分配的内存,不涉及对象的析构。

11.堆区和栈区的区别

堆 (Heap) 和栈 (Stack) 是程序运行时两种不同的内存分配区域

  1. 内存分配:

  • 是由编译器自动管理的,用于存储局部变量和函数调用的上下文信息。
    栈上的对象在定义它们的块或函数调用结束后自动销毁。
    栈的内存分配和释放速度很快,因为栈的内存管理是连续的,不需要搜索空闲内存。
    栈向下增长

  • 由程序员手动管理的,用于存储动态分配的对象。
    堆上的对象需要程序员手动释放,否则可能导致内存泄漏。
    堆的内存分配和释放速度通常比栈慢,因为可能需要搜索合适的内存块,并且涉及内存碎片整理。
    堆向上增长
  1. 大小限制:
    栈的大小通常有限制,远小于堆的大小,且在不同系统和编译器中可能不同。
    堆的大小通常很大,受限于系统可用内存。
  2. 使用场景:
    栈主要用于存储函数参数、局部变量等。
    堆用于存储生存期不受限于单个函数调用的对象,如使用 new 或 malloc 分配的对象。

12.什么是内存泄漏, 如何检测和防止?

如果程序的某一段代码在内存池中动态申请了一块内存而没有及时将其释放,就会导致那块内存一直处于被占用的状态而无法使用,造成了资源的浪费。内存泄漏并不是说物理上的消失掉了,是因为无法使用该区域,在外界看来这块内存就好像被泄漏了一样。

  • 什么操作会导致内存泄漏:

    • 忘记释放内存:使用 new 或 malloc 等分配内存后,没有使用 delete 或 free 释放内存。
    • 子类继承父类时,没有将基类的析构函数定义为虚函数。
    • 野指针:指针被赋值为 nullptr 或重新赋值后,丢失了对先前分配内存的引用,导致无法释放。
    • 循环引用:在使用引用计数的智能指针(如 std::shared_ptr)时,循环引用会导致引用计数永远不会归零,从而无法释放内存。
    • 使用不匹配的内存释放函数: 使用 delete 释放由 new[] 分配的内存,或使用 delete[] 释放由 new 分配的内存,这可能导致未定义行为。
    • 资源未关闭:对于文件、网络连接等资源,如果没有正确关闭,虽然不直接导致内存泄漏,但会占用系统资源,可能导致资源耗尽。
  • 如何避免

    • 使用智能指针:优先使用 std::unique_ptr、std::shared_ptr 等智能指针来自动管理内存。
    • 确保资源释放: 对于手动分配的内存,确保在不再需要时使用 delete 或 free 释放。
    • 内存泄漏检测工具: 在开发和测试阶段,使用如Valgrind、AddressSanitizer或Visual Studio的诊断工具来检测内存泄漏。

13.什么是野指针?如何避免?

  • 什么是野指针
    野指针是指“指向已经被释放的或无效的内存地址的指针”。在 C 和 C++ 这类允许直接操作内存地址的语言中,如果指针没有被正确初始化,或者指针所指向的内存已经被释放,那么这个指针就成为了野指针。使用野指针可能会导致程序崩溃、数据损坏或者其他一些不可预测的行为。

  • 在什么情况下会产生野指针?

    • 在释放后没有置空指针: 使用 delete 或 free 释放了内存后,没有将指针设置为 nullptr,指针仍然指向已释放的内存地址。
    • 返回局部变量的指针 : 如果函数返回了指向其局部变量的指针,一旦函数返回,这些局部变量的生命周期结束,返回的指针成为野指针。
    • 越界访问:指针访问的内存超出了其合法的内存块边界。
    • 函数中的参数指针被释放。
  • 如何避免野指针

    • 在释放内存后将指针置为 nullptr 。
    • 避免返回局部变量的指针。
    • 使用智能指针(如 std::unique_ptr 和 std::shared_ptr )。
    • 注意函数参数的生命周期,避免在函数内释放调用方传递的指针,或者通过引用传递指针。

14.C++面向对象三大特性

面向对象编程是C++的核心特性之一。OOP是一种编程范式,通过它,程序可以以对象的形式组织数据和功能,并通过对象之间的交互来实现任务。面向对象编程具有封装、继承和多态三个主要特性:

  • 封装:将客观事物封装成为抽象的类, 类把自己数据和方法进行隐藏,仅对外公开接口来和对象进行交互,防止外界干扰或不确定性访问。
  • 继承:指一个类(称为子类或派生类)可以从另一个类(称为父类或基类)中继承属性和行为的能力。通过继承,子类可以重用父类的代码,并且可以在不修改父类的情况下添加新的功能或修改已有的功能。继承使得代码具有层次性和可扩展性,能够建立起类之间的层次关系。
  • 多态:多态是指同一个操作作用于不同的对象时,可以有不同的解释和行为。多态性允许以统一的方式处理不同类型的对象,从而提高了代码的可扩展性和可维护性。在C++中,多态性通常通过虚函数(virtual functions)来实现。通过基类中定义虚函数,并在派生类中重新定义该函数,可以实现运行时的动态绑定。

15.简述一下 C++的重载和重写,以及它们的区别和实现方式

重载和重写是两种不同的概念,它们都用于实现多态性,但方式和使用场景有所不同。

  • 重载:在同一个类或命名空间中,声明多个同名函数, 但是参数列表不同。编译器根据参数的类型、数量或顺序来区分不同的函数。
  • 重写:重写发生在继承体系中,在子类中,声明一个与父类中虚函数具有相同名称、相同参数列表和相同返回类型的函数,并在子类函数前加上 override 关键字。

区别:

  • 作用域:重载发生在同一个作用域内,而重写发生在继承体系中。
  • 参数列表:对于重载的函数,参数列表必须不同;对于重写的函数,参数列表必须与被重写的函数完全相同。
  • 返回类型:重载函数的返回类型可以不同,但重写函数的返回类型必须与被重写的函数相同(或与之兼容,C++中称为协变返回类型)。
  • 关系:重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
  • 虚函数:重写通常与虚函数一起使用,以实现运行时多态性;而重载是编译时多态性,由编译器在编译期间确定调用哪个函数。
  • 关键字:重写函数可以使用 override 关键字,明确指出该函数是对父类虚函数的重写。

16.C++怎么实现多态

C++ 的多态分为编译时多态(也被称为静态多态)和运行时多态(也被称为动态动态)

  1. 编译时多态
    编译时多态在编译期间就确定了具体的函数或类型,由编译器根据函数的签名或模板实例化来选择正确的函数或实例。
    • 函数重载(Function Overloading):允许在同一作用域内声明多个同名函数,只要它们的参数列表不同。
    • 运算符重载(Operator Overloading): 允许为自定义类型定义或修改运算符的行为。
    • 模板(Templates): 允许创建泛型类和函数,它们可以在多种数据类型上使用。
  2. 运行时多态:
    运行时多态主要通过虚函数和抽象类实现,父类中定义声明虚函数,子类实现对虚函数的重写。由虚函数表和虚函数指针在运行时确定调用哪个函数。
    • 虚函数(Virtual Functions): 虚函数在基类定义,派生类可以重写这些虚函数。
    • 抽象类(Abstract Classes): 包含至少一个纯虚函数的类,不能被实例化,但可以作为其他类的基类。
  3. 运行时多态实现原理:
    运行时多态涉及到虚函数表(vtable)和虚函数指针(vptr)。当类包含虚函数时,编译器会自动为该类创建一个虚函数表,表中包含类中所有虚函数的地址。
    子类的虚函数表继承了父类的虚函数表,但会使用自己重写的虚函数将虚函数表中对应的虚函数进行覆盖。
    当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型在运行时查找正确的函数地址并调用相应的函数,实现多态。

17.虚函数和纯虚函数的区别

虚函数和纯虚函数都用于实现多态。

  1. 虚函数
    虚函数是在普通函数之前加一个 virtual 关键字
    虚函数是在基类中声明的,并且可以在派生类中被重写。
    虚函数可以有实现,也就是说,基类中的虚函数可以有一个定义,派生类可以选择提供自己的实现,也可以使用基类的实现。
    通过虚函数,可以在基类指针或引用中实现动态绑定,即在运行时确定调用哪个类中的函数实现。
  2. 纯虚函数
    纯虚函数是在虚函数后面加一个 =0
    纯虚函数也是在基类中声明的,但它没有实现,只有声明。
    当一个类包含至少一个纯虚函数时,它就成为了一个抽象类,这意味着你不能实例化这样的类,但可以声明这种类型的指针或引用。
  3. 区别
  • 是否实现:
    虚函数提供函数声明和实现,即提供虚函数的默认实现。
    纯虚函数没有函数具体实现,只提供函数声明。
  • 派生类是否实现
    派生类可以选择是否覆盖虚函数的默认实现。
    当一个类包含至少一个纯虚函数时,派生类必须提供具体实现,否则他们也变成抽象类。
  • 实例化:
    包含纯虚函数的类是抽象类,不能被实例化;
    而包含虚函数的类不一定是抽象类,可以被实例化,除非它也包含纯虚函数。
  • 目的:
    虚函数用于提供一个可以在派生类中被重写的方法实现;
    通过纯虚函数,抽象类提供一种接口规范,要求派生类必须提供具体实现。
    动态绑定:
  • 虚函数支持动态绑定,
    纯虚函数由于没有实现,它们本身不参与动态绑定,但可以作为接口的一部分,影响整个类的多态性。

18.虚函数是怎么实现的

虚函数的实现依赖于一种称为虚函数表的机制。

  1. 虚函数表的创建: 当一个类包含虚函数时,编译器会自动为这个类创建一个虚函数表。这个表是一个函数指针数组,每个指针指向一个虚函数的实现。
  2. 虚函数表指针: 编译器会在对象的内存布局中添加一个隐式的虚函数表指针(通常是一个指向 vtable 的指针),这样每个对象都可以通过这个指针访问到类的虚函数表。
  3. 虚函数的声明: 在类中声明虚函数时,可以使用 virtual 关键字。如果一个函数被声明为虚函数,编译器会在类的 vtable 中为这个函数分配一个入口。
  4. 重写虚函数: 当从基类继承并创建派生类时,可以在派生类中重写基类的虚函数。重写的函数会替换掉 vtable 中对应的基类实现。
    动态绑定: 当通过基类指针或引用调用虚函数时,程序会使用对象的虚函数表指针来查找正确的函数实现。这个过程称为动态绑定或晚期绑定。
  5. 调用虚函数: 程序运行时,当调用一个虚函数时,会先通过对象的虚函数表指针找到 vtable,然后在 vtable 中查找对应的函数指针,并调用该函数。

简短来说,每个类都有一个虚表,里面有这个类的虚函数地址;每个对象都有指向它的类的虚表的指针,这个指针称为虚指针。 当调用虚函数时,编译器会调用对象的虚指针查找虚表,通过虚函数的地址来执行相应的虚函数。

19.虚函数表是什么

  1. 虚函数表是 C++ 中实现运行时多态(动态绑定)的关键机制之一。
  2. 虚函数表是一个或多个函数指针的集合,它存储了类中所有虚函数的地址。当类包含虚函数时,编译器会自动为这个类创建一个虚函数表。
  3. 虚函数表的主要目的是在运行时能够确定通过基类指针或引用调用的是哪个派生类中的虚函数实现,从而实现动态绑定。
  4. 原理
  • 创建虚函数表:当类声明至少一个虚函数时,编译器会为这个类生成一个虚函数表。
  • 虚函数表指针:编译器会为包含虚函数的类的对象添加一个隐藏的虚函数表指针(通常是一个指针或引用),指向类的虚函数表。
  • 调用虚函数:当通过基类指针或引用调用虚函数时,程序会使用对象的虚函数表指针来查找并调用正确的函数实现。

20.什么是构造函数和析构函数?构造函数、析构函数可以是虚函数吗

  1. 构造函数
    构造函数是创建对象时自动调用的成员函数,它的作用是初始化成员变量,为对象分配资源,执行必要的初始化操作。
    特点
  • 函数名必须与类名相同,且没有返回类型;
  • 可以有多个构造函数;
  • 如果没有为类定义一个构造函数,编译器会自动生成一个默认构造函数,它没有参数,也可能执行一些默认的初始化操作。
  • 构造函数不能是虚函数。
  1. 析构函数
    特点:
  • 函数名为 ~类名 ;
  • 没有参数;
  • 如果没有为类定义一个析构函数,编译器会自动生成一个默认析构函数,执行简单的清理操作。
  • 析构函数可以是虚函数。
  1. 关于是否可以为虚函数
  • 构造函数在对象创建时被调用,此时不涉及多态性。

  • 虚函数对应一个虚表,这个表存在对象的内存空间,如果此时构造函数是虚函数,对象还没实例化没有分配内存空间,也就无法调用;

  • 虚函数是用在信息不全的情况下,能使重载的函数使用。但构造函数本身就是要初始化对象,因此没必要是虚函数。

  • 析构函数是对象生命周期结束时自动调用的函数,它的作用是释放对象占用的资源,执行一些必要的清理操作。

  • 虚析构函数可以在运行时实现多态性;

  • 如果基类的析构函数不是虚函数,当通过基类指针去删除派生类对象时,不会调用派生类的析构函数。可能会导致派生类的资源未被正确释放,从而造成资源泄漏

21.C++构造函数有几种,分别什么作用

在C++中,构造函数有几种不同的类型,每种都有其特定的作用:

  • 默认构造函数:没有参数的构造函数,用于创建对象的默认实例,不需要显式提供任何初始化参数。
  • 参数化构造函数:带参数的构造函数,允许在创建对象时初始化成员变量。
  • 拷贝构造函数:以同一类的实例为参数的构造函数,用于复制已有对象。
  • 移动构造函数:以同一类的实例的右值引用为参数,用于利用即将销毁的对象的资源。
  • 转换构造函数:允许将其他类型或值隐式转换为当前类类型的实例。
  • 委托构造函数:一个构造函数调用另一个构造函数来完成初始化,可以是同一个类的其他构造函数。
  • 初始化列表构造函数:使用成员初始化列表来初始化成员变量,这是最高效的初始化方式。
  • 常量构造函数:声明为const的构造函数,可以用于创建常量对象。
  • constexpr构造函数:允许在编译时初始化对象,用于定义和初始化字面量类型的对象。

22.STL 容器了解哪些

  1. 序列容器
  • std::vector: 动态数组,提供快速随机访问。
  • std::deque: 双端队列,提供从两端快速插入和删除的能力。
  • std::list: 双向链表,提供高效的元素插入和删除。
  • std::forward_list: 单向链表,每个元素只存储下一个元素的引用。
  • std::array: 固定大小的数组,具有静态分配的内存。
  1. 关联容器:
  • std::set: 基于红黑树,存储唯一元素的集合, 会默认按照升序进行排序。
  • std::multiset: 允许容器中有多个相同的元素。
  • std::map: 基于红黑树,存储键值对的有序映射。
  • std::multimap: 允许映射中有多个相同的键。
  • std::unordered_set: 基于哈希表,提供平均时间复杂度为 O(1) 的查找。
  • std::unordered_map: 基于哈希表,存储键值对的无序映射。
  1. 容器适配器(Container Adapters):
  • std::stack: 后进先出(LIFO)的栈。
  • std::queue: 先进先出(FIFO)的队列。
  • std::priority_queue: 优先队列,元素按优先级排序。

23.深拷贝与浅拷贝的区别

  1. 浅拷贝
  • 定义:浅拷贝仅复制对象本身,不复制对象所指向的动态分配的内存。换句话说,它只复制内存中的对象副本,而不复制对象内部指向的任何动态分配的资源。
  • 实现:通常通过复制构造函数或赋值运算符实现。
  • 特点:
    速度快,因为只涉及基本数据类型的复制。
    如果原始对象包含指针,浅拷贝会导致两个对象尝试管理相同的动态内存,这可能导致多重释放和悬空指针问题。
  1. 深拷贝
  • 定义:深拷贝不仅复制对象本身,还递归地复制对象所指向的所有动态分配的内存。这意味着每个对象都有自己的独立资源副本。
  • 实现:通常需要自定义复制构造函数或赋值运算符来确保所有动态分配的资源都被正确复制。
  • 特点:
    速度慢,因为需要递归地复制所有资源。
    可以安全地使用复制出的对象,而不担心资源管理问题。

24.vector和list的区别

  1. vector
  • 基于动态数组:std::vector 基于可以动态扩展的数组实现,这意味着它在内存中连续存储元素。
  • 随机访问:提供快速的随机访问能力,可以通过索引快速访问任何元素。
  • 内存分配:通常在内存分配上更紧凑,因为元素紧密排列,没有额外的空间用于链接或指针。
  • 时间复杂度:
    元素访问:O(1),即常数时间复杂度。
    插入和删除:在 vector 的末尾是 O(1),但如果需要在中间插入或删除元素,则可能需要 O(n),因为可能需要移动后续所有元素。
    内存管理:使用连续内存分配,可以利用缓存的优势,提高访问速度。
  • 使用场景:
    当你需要快速随机访问元素时。
    当你需要在末尾快速添加或删除元素时。
    当你关心内存使用效率时。
  1. list
  • 基于双向链表:std::list 是基于双向链表的容器,每个元素通过节点链接到前一个和后一个元素。
  • 非连续存储:元素在内存中不是连续存储的,每个元素包含指向前一个和后一个元素的指针。
  • 时间复杂度:
    元素访问:O(n),需要从头开始遍历到所需位置。
    插入和删除:非常快速,特别是当需要在列表中间插入或删除元素时,操作是 O(1),前提是已经拥有指向待插入或删除元素的迭代器。
    内存管理:由于元素间通过指针链接,内存分配可能更分散,但插入和删除操作不需要移动其他元素。
  • 使用场景:
    当你需要在列表中间高效地插入或删除元素时。
    当你不需要随机访问元素时。
    当你需要一个灵活的容器,可以动态地添加和删除元素而不会引起大量的内存复制或移动。

25.vector 底层原理和扩容过程

  1. 底层原理
  • vector 是 C++ 标准库中的一个容器,可以看作是一个动态数组,它的大小可以根据元素的增加而增长。它通过在堆上分配一段连续的内存空间存放元素,支持时间复杂度为 O(1 ) 的随机访问。
  • vector 底层维护了三个迭代器和两个变量,这三个迭代器分别指向对象的起始字节位置,最后一个元素的末尾字节和整个 vector 分配空间的末尾字节。两个变量分别是 size 和 capacity ,Size 表示当前存储元素的数量,capacity 表示当前分配空间的大小。当创建一个 vector 对象时,会分配一个初始化大小的空间存放元素,初始化空间可以通过构造函数的参数指定,缺省情况下为 0。当对 vector 容器进行增加和删除元素时,只需要调整末尾元素指针,而不需要移动整个内存块。
  1. 扩容机制
  • 当添加元素的数量达到当前分配空间的大小时,vector 会申请一个更大的内存块,然后将元素从旧的内存块拷贝到新的内存块中,并释放旧的内存块。 扩容可能导致原有迭代器和指针失效,扩容完成后,容器返回指向新内存区域的迭代器或指针。
  • vector 扩容的机制分为固定扩容和加倍扩容。
    固定扩容就是在每次扩容时在原容量的基础上增加固定容量。但是固定扩容可能会面临多次扩容(扩容的不够大)的情况,时间复杂度较高。
    加倍扩容就是在每次扩容时原容量翻倍,优点是使得正常情况下扩容的次数大大减少,时间复杂度低,缺点是空间利用率低。

26.push_back()和emplace_back()的区别

push_back()和emplace_back()都是C++标准库容器(如std::vector)中用来添加元素的方法,但它们在添加元素的方式上有所不同:

  1. push_back():
    语法是container.push_back(value);,传入的是一个值或对象的副本/移动版本。
    它接受一个元素的副本或移动该元素,然后将其添加到容器的末尾。
    这个方法需要先构造一个元素的副本或移动构造一个临时对象,然后再将其添加到容器中。
  2. emplace_back():
    语法是container.emplace_back(args…);,传入的是构造新元素所需的参数列表。
    它使用就地构造(emplace)的方式,直接在容器的内存空间中构造新元素。
    这个方法避免了元素的复制或移动操作,因为它直接在容器的末尾空间构造新元素。
    emplace_back()通常比push_back()更高效,因为它避免了额外的复制或移动操作。当构造函数需要大量资源时,emplace_back()的优势更加明显。

27.map dequeu list的实现原理

  1. std:: map
  • 基于红黑树:std::map 基于一种自平衡的二叉搜索树——红黑树实现。
  • 有序容器:元素按照键的顺序自动排序,通常是按照小于(<)运算符定义的顺序。
  • 唯一键:每个键都是唯一的,不允许有重复的键。
  • 时间复杂度:提供对数时间复杂度 (O(log n)) 的查找、插入和删除操作。
  • 迭代器:由于 std::map 是基于树的,迭代器在遍历时是有序的。
  1. std::list
  • 基于双向链表:std::list 是一个双向链表,每个元素都持有指向前一个和后一个元素的指针。
  • 无序容器:元素在容器中没有特定的顺序。
  • 插入和删除:提供高效的插入和删除操作,特别是当需要在容器中间插入或删除元素时。
  • 时间复杂度:提供线性时间复杂度 (O(n)) 的查找操作,但插入和删除可以在 O(1) 时间内完成,前提是已经拥有指向待插入或删除元素的迭代器。
  • 迭代器:由于 std::list 是线性结构,迭代器在遍历时是顺序的,但不支持随机访问。
  1. std::deque
  • 基于动态数组:std::deque 是一个基于动态数组的序列容器,可以高效地从两端添加或删除元素。
  • 允许序列操作:可以快速地在队列的前端和后端添加或删除元素。
  • 时间复杂度:提供常数时间复杂度 (O(1)) 的前端和后端插入和删除操作。中间插入或删除操作可能需要 O(n) 时间。

28.map 和 unordered_map的区别和实现机制

  1. map
    基于红黑树:std::map 基于一种自平衡的二叉搜索树(通常是红黑树)实现,可以保持元素有序。
    有序容器:元素按照键的顺序自动排序,可以通过键值进行有序遍历。
    元素访问:提供对元素的快速查找、插入和删除操作,时间复杂度为 O(log n)。
    唯一键:每个键都是唯一的,不允许有重复的键。
    迭代器稳定性:由于基于树结构,迭代器在遍历时是稳定的,即使容器发生插入或删除操作,迭代器指向的元素也不会改变,除非该元素被删除。
  2. unordered_map
    基于哈希表:std::unordered_map 基于哈希表实现,通过哈希函数将键分布到数组的槽位中。
    无序容器:元素在容器中是无序的,不能按键的顺序进行遍历。
    元素访问:理想情况下,提供平均时间复杂度为 O(1) 的快速查找、插入和删除操作。最坏情况下,性能可能下降到 O(n)。
    允许重复键:实际上,std::unordered_map 不允许有重复的键,因为哈希表的设计不允许两个元素具有相同的哈希值。如果发生哈希冲突,会通过某种方式(如链表或开放寻址)解决。
    迭代器稳定性:由于基于哈希表,迭代器的稳定性不如 std::map。在发生哈希表的重新哈希 (rehashing) 时,迭代器可能会失效。
    遍历顺序与创建该容器时输入元素的顺序是不一定一致的,遍历是按照哈希表从前往后依次遍历的。
  3. 使用场景
    当需要元素有序且对性能有较高要求时,应选择 std::map。
    当元素的顺序不重要,且需要快速访问元素时,应选择 std::unordered_map。
  4. 实现机制
    std::map 的实现依赖于红黑树的旋转和颜色变换来保持树的平衡,确保操作的时间复杂度。
    std::unordered_map 的实现依赖于一个良好的哈希函数来最小化冲突,并通过解决冲突的机制(如链表或开放寻址)来存储具有相同哈希值的元素。

29.C++11新特性有哪些

  1. 类型推导:
    auto关键字,允许编译器根据初始化表达式推导变量类型。
    decltype 分析表达式并得到它的类型,却不实际计算表达式的值。
  2. 基于范围的 for 循环:提供了一种更简洁的遍历容器的方法。
  3. lambda 表达式:允许在需要的地方定义匿名函数。
  4. 智能指针(如 std::unique_ptr 和 std::shared_ptr):提供了自动内存管理。
  5. 右值引用和移动语义:优化资源的移动操作,高效的将资源从一个对象转移到另一个对象,减少拷贝的开销。
  6. nullptr: 空指针,明确表示空指针的关键字

30.移动语义有什么作用,原理是什么

  1. 移动语义是 C++11 引入的一项特性,对于大型对象或包含资源的对象(如动态分配的内存、文件句柄等),复制构造函数可能会非常昂贵。移动语义允许对象的资源被“移动”到新对象,而不是复制,从而节省资源和时间。其主要作用是优化资源的利用,特别是在对象的复制操作中。
  2. 移动语义通过移动构造函数和移动赋值运算符实现。在移动构造或移动赋值过程中,源对象的资源被“拿走”,并转移到目标对象,源对象变为无效状态。

31.左值引用和右值引用的区别

在C++中,左值和右值是两种不同类型的表达式,它们分别对应着不同的引用方式:

  1. 左值引用:
  • 左值引用使用&符号
  • 左值引用绑定到左值表达式上,即那些具有持久存储期的表达式,如变量或者对象。
  • 它们在内存中有一个持久的地址,可以被赋值和修改。
  1. 右值引用:
  • 右值引用使用两个&符号
  • 右值引用绑定到右值表达式上,通常是临时对象或即将销毁的对象,它们没有持久的存储期。
  • 右值引用的主要目的是通过移动语义来利用这些临时资源,避免不必要的复制。
  1. 区别总结
  • 绑定对象:左值引用绑定到具有持久状态的对象,而右值引用绑定到临时或即将销毁的对象。
  • 生命周期:左值引用延长了它所引用对象的生命周期,右值引用则表示对一个临时值的引用。
  • 可修改性:左值引用可以被用来修改其所引用的对象,而右值引用通常用于移动语义,不涉及修改。
  • 标准库支持:C++11 标准库中的某些函数和算法(如 std::move)特别设计来与右值引用配合使用。

32.说一下lambda函数

lambda 表达式(也称为匿名函数)是在 C++11 标准中引入的一种方便的函数编写方式。Lambda 允许你在需要一个函数对象的地方快速定义函数的行为,而不需要按照传统方式定义一个完整的函数。

我们主要出于这些情况用 lambda 函数:

  • API:Lambda 函数可以简化回调函数的编写,特别是在使用 STL 时。
  • 简洁:在需要临时使用一个函数但不想定义一个完整函数的情况下。
  • 闭包:Lambda 可以捕获外部变量,形成闭包,使得函数可以访问和操作外部作用域的变量。

33.C++如何实现一个单例模式

在C++中实现单例模式,主要是确保一个类只有一个实例,并提供一个全局访问点, C++实现单例模式需要满足以下几点要求:

  • 私有化构造函数:将类的构造函数定义为私有,防止外部通过new关键字创建多个实例。
  • 静态实例:在类内部提供一个静态私有实例,这个实例将作为整个程序的唯一实例。
  • 公有访问方法:提供一个公有的静态方法,通常称为getInstance,用于获取类的唯一实例。
  • 删除拷贝构造函数和赋值操作符:为了防止通过拷贝或赋值来创建新的实例,需要将拷贝构造函数和赋值操作符定义为私有或删除。

单例模式有懒汉式和饿汉式两种实现。

  • 懒汉式 类实例只有在第一次被使用时才会创建,这个时候需要注意多线程下的访问,需要利用互斥锁来加以控制。
//最简单的一种懒汉式单例模式
public class SingleTest {
    //定义一个私有类变量来存放单例,私有的目的是指外部无法直接获取这个变量,而要使用提供的公共方法来获取。
    private static SingleTest instance; 
    public SingleTest () {}  
    //定义一个公共的公开的方法来返回该类的实例。
    public static SingleTest getInstance() {
        //第一次访问去创建实例
        if (instance == null) {
            instance = new SingleTest ();
        }
        //否则直接返回实例
        return instance;
    }
}
  • 饿汉式 类实例在类被加载时就进行创建。
    饿汉式单例性能优于懒汉式单例,但过早的实例化可能会导致资源浪费
    如果初始化本身依赖于一些其他数据,那么也就很难保证其他数据会在它初始化之前准备好
//最简单的一种懒汉式单例模式
//饿汉式单例实现

public class SingleTest {

    private static final SingleTest INSTANCE = new SingleTest();

    private SingleTest() {}

    public static SingleTest getInstance() {
        return INSTANCE;
    }
}

34.什么是菱形继承

多继承体系中,当两个派生类继承同一个基类,然后有一个最派生类同时继承这两个派生类时,就形成了菱形继承的结构。
这种结构会导致基类的成员在最派生类中出现两次,因为两个派生类各自继承了基类的成员,而最派生类又继承了这两个派生类。菱
形继承如果没有适当处理,会导致二义性问题,比如基类的构造函数和析构函数调用顺序问题。
为了解决这个问题,可以使用虚继承。虚继承确保了基类只被继承一次,无论在继承链中出现多少次。

35.C++中的多线程同步机制

  1. 互斥锁(Mutex) 互斥锁是最常用的同步机制之一,它可以确保在任意时刻只有一个线程能够访问共享资源。通过调用std::mutex类的lock()和unlock()方法,可以将对共享资源的访问保护起来。
  2. 条件变量(Condition Variable) 条件变量是一种机制,用于线程间的通信和同步。它允许一个或多个线程等待某个特定条件的发生,并在条件满足时被唤醒。
  3. 原子操作 原子操作是一种不可分割的操作,不会被中断。C++11引入了原子操作库,其中定义了一些原子类型,如std::atomic_int。
  4. 读写锁(Reader-Writer Lock):通过std::shared_mutex和std::shared_lock、std::unique_lock提供,用于在读多写少的情况下提高并发性。

36.如何在C++中创建和管理线程?

在C++中,可以使用标准库中的头文件来创建和管理线程,创建和管理线程步骤如下。

  1. 包含头文件
  2. 定义线程函数,作为线程的入口点。
  3. 创建线程对象,使用std::thread来创建线程。你可以将线程函数传递给std::thread的构造函数来指定线程执行的任务
  4. 启动线程:使用join()方法或detach()方法
  5. 等待线程结束(可选):使用join()方法启动线程,主线程将会阻塞直到新线程执行完毕,如果调用detach()方法,新线程将会变成分离状态,主线程不再与其同步,并且不再需要等待线程的结束。

五、手撕

1.快速排序

排序问题参考:https://zhuanlan.zhihu.com/p/692147006

  1. 选择基准值: 从数组中选择一个元素作为基准值。通常选择最后一个元素作为基准值,但也可以选择随机位置或者中间位置的元素。
  2. 划分数组: 将数组分成两个子数组,其中一个子数组的所有元素都小于基准值,另一个子数组的所有元素都大于或等于基准值。
  3. 递归排序: 递归地对两个子数组进行快速排序。
  4. 合并结果: 由于快速排序是原地排序算法,所以不需要显式合并子数组。整个数组在递归调用结束后已经被排序好了。

快速排序的平均时间复杂度为 O(n log n),最坏情况下的时间复杂度为 O(n^2),其中 n 是数组的大小。虽然最坏情况下的时间复杂度较高,但是快速排序通常被认为是最快的排序算法之一,特别是对于大规模数据集。

#include <iostream>
#include <vector>
using namespace std;

int partition(vector<int>& arr, int low, int high){
	int key = low;
	int left = low;
	int right = high;

	//right先走,left后走,right找小,left找大
	while (left < right){
		while (left < right && arr[right] >= arr[key])
			right--;
		while (left < right && arr[left] <= arr[key])
			left++;
		swap(arr[left], arr[right]);
	}
	// 将基准值放到正确的位置上
	swap(arr[key], arr[left]);
	return left;
}


void quickSort(vector<int>& arr, int low, int high) {
    if (low < high) {
        // 划分数组,获取基准值的位置
        int pi = partition(arr, low, high);
        
        // 递归地对基准值左右两侧的子数组进行排序
        quickSort(arr, low, pi - 1);
        quickSort(arr, pi + 1, high);
    }
}

int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    
    cout << "原始数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;
    
    quickSort(arr, 0, arr.size() - 1);
    
    cout << "排序后的数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;
    
    return 0;
}

2.堆排序(topK问题)

堆排序是一种基于二叉堆数据结构的排序算法。它利用了堆的性质:在一个最大堆(或最小堆)中,父节点的值总是大于等于(或小于等于)其子节点的值。堆排序的基本思想可以概括为以下几个步骤:

  1. 构建最大堆: 将待排序的数组看作一个完全二叉树,并将其转换为最大堆。这一步通常从最后一个非叶子节点开始,依次向上调整,使得每个节点都满足堆的性质。
  2. 交换堆顶元素和末尾元素: 将堆顶元素(即最大值)与堆的最后一个元素进行交换,然后将堆的大小减1,即排除最大值所在的节点。
  3. 调整堆: 将交换后的堆重新调整为最大堆,即调用函数对堆进行下沉操作,以维持堆的性质。
  4. 重复步骤2和3: 重复执行步骤2和3,直到堆的大小减小为1,即所有元素都已经有序。

堆排序的时间复杂度为 O(n log n),其中 n 是数组的大小。虽然堆排序不是稳定的排序算法,但是它是一种原地排序算法,不需要额外的存储空间。

#include <iostream>
#include <vector>
using namespace std;

// 调整堆,使以节点 i 为根的子树满足最大堆性质
void maxHeapify(vector<int>& arr, int n, int i) {
    int largest = i; // 初始化根节点为最大值
    int left = 2 * i + 1; // 左孩子节点的索引
    int right = 2 * i + 2; // 右孩子节点的索引

    // 找出根节点、左孩子节点和右孩子节点中的最大值
    if (left < n && arr[left] > arr[largest])
        largest = left;
    if (right < n && arr[right] > arr[largest])
        largest = right;

    // 如果最大值不是根节点,则交换根节点和最大值节点,然后递归调整交换后的子树
    if (largest != i) {
        swap(arr[i], arr[largest]);
        maxHeapify(arr, n, largest);
    }
}

// 堆排序
void heapSort(vector<int>& arr) {
    int n = arr.size();

    // 构建最大堆
    for (int i = n / 2 - 1; i >= 0; i--)
        maxHeapify(arr, n, i);

    // 依次取出堆顶元素(最大值),然后调整堆
    for (int i = n - 1; i > 0; i--) {
        swap(arr[0], arr[i]); // 将堆顶元素(最大值)放到数组末尾
        maxHeapify(arr, i, 0); // 调整堆,保持堆的性质
    }
}

int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};

    cout << "原始数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;

    heapSort(arr);

    cout << "排序后的数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;

    return 0;
}

3.归并排序

  1. 分解: 将数组递归地分解为越来越小的子数组,直到子数组的长度为1,此时认为这些子数组都是有序的。
  2. 合并: 将两个有序的子数组合并成一个有序的数组。合并过程中,创建一个临时数组,然后按照顺序比较两个子数组的元素,将较小的元素放入临时数组中。
  3. 递归合并: 重复以上步骤,直到所有子数组都合并为一个完整的有序数组。

归并排序的时间复杂度为 O(n log n),其中 n 是数组的大小。归并排序是一种稳定的排序算法,适用于各种数据规模,并且相对于其他复杂度为 O(n log n) 的排序算法而言,归并排序有着较好的稳定性和可靠性。

#include <iostream>
#include <vector>
using namespace std;

void merge(vector<int>& arr, int left, int mid, int right) {
    int n1 = mid - left + 1;
    int n2 = right - mid;

    // 创建临时数组来存储左右子数组
    vector<int> L(n1), R(n2);
    
    // 将数据复制到临时数组中
    for (int i = 0; i < n1; i++)
        L[i] = arr[left + i];
    for (int j = 0; j < n2; j++)
        R[j] = arr[mid + 1 + j];

    // 合并临时数组
    int i = 0, j = 0, k = left;
    while (i < n1 && j < n2) {
        if (L[i] <= R[j]) {
            arr[k] = L[i];
            i++;
        } else {
            arr[k] = R[j];
            j++;
        }
        k++;
    }

    // 处理剩余的元素
    while (i < n1) {
        arr[k] = L[i];
        i++;
        k++;
    }
    while (j < n2) {
        arr[k] = R[j];
        j++;
        k++;
    }
}

void mergeSort(vector<int>& arr, int left, int right) {
    if (left < right) {
        int mid = left + (right - left) / 2;
        // 递归地对左右两部分进行排序
        mergeSort(arr, left, mid);
        mergeSort(arr, mid + 1, right);
        // 合并已排序的左右两部分
        merge(arr, left, mid, right);
    }
}

int main() {
    vector<int> arr = {64, 34, 25, 12, 22, 11, 90};
    
    cout << "原始数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;
    
    mergeSort(arr, 0, arr.size() - 1);
    
    cout << "排序后的数组为: ";
    for (int num : arr)
        cout << num << " ";
    cout << endl;
    
    return 0;
}

4.冒泡排序

  1. 比较相邻元素: 从数组的第一个元素开始,依次比较相邻的两个元素。
  2. 交换位置: 如果前面的元素大于后面的元素(如果是升序排序),则交换它们的位置,将较大的元素向后移动。
  3. 重复步骤1和2: 继续执行以上步骤,直到数组末尾,这样一次遍历后,数组中最大的元素将会放置到末尾位置。
  4. 减少遍历范围: 在第一次遍历完成后,最大的元素已经就位,接下来的遍历不需要考虑已经排序好的元素,因此每次遍历时可以减少遍历的范围。
  5. 重复多次遍历: 重复执行以上步骤,每次遍历后,未排序部分的范围缩小,直到整个数组都被排序。

这样通过多次遍历数组,每次遍历都会将一个最大(或最小)的元素放到合适的位置,直到整个数组有序。
最坏时间复杂度:O(N^2)

void bubbleSort(vector<int>& vec) {
    int n = vec.size();
    for (int i = 0; i < n-1; i++) {
        for (int j = 0; j < n-i-1; j++) {
            if (vec[j] > vec[j+1]) {
                // 交换 vec[j] 和 vec[j+1]
                int temp = vec[j];
                vec[j] = vec[j+1];
                vec[j+1] = temp;
            }
        }
    }
}

5.LRU (最近最少使用) 缓存

力扣146

  1. 先定义一个双向链表的节点
  2. 建立LRU类:在这个类里定义两个节点指向头和尾,来维护链表,该链表用于记录使用的情况;记录capacity和size;定义一个map来存放kv对。
  3. 定义私有函数:addTohead用于新增节点后加入头部;removeNode用于删除节点(方便后两个);moveTohead用于使用节点后加入头部;removeTail用于超出capacity时候删除最少使用节点
  4. 定义put和get方法。注意操作后再链表移动到头部,维护size当超容量了要删除尾节点

六、场景

1.我现在有一个IP换成新的了,在运营商只有一个我的旧的IP,问现在一个用户访问原来的旧IP访问不到,我可以采用什么方案来解决?

  1. DNS记录更新:如果你使用了域名来访问你的服务,确保你的域名的DNS记录已更新到新的IP地址。
  2. DNS缓存清理:用户可能还在缓存中保存着旧的IP地址。建议用户清理本地DNS缓存。
  3. 设置旧IP地址的重定向:如果你可以访问旧的IP地址的服务器,可以在旧IP上设置一个重定向或转发,指向新的IP地址。这可以帮助在过渡期间引导流量到新的IP地址。
  4. 网络中继/转发:如果可能的话,在旧的IP地址上设置一个网络中继或转发,将流量从旧IP地址转发到新的IP地址。这种方法需要你对网络设备有控制权限。

待续


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值