C++闯关手册 - 面试实例:第二轮面试

第二轮面试深入综合考察,可能会更深入提问,也可能会涉及到更复杂的编程问题和项目经验,或者C++之外的操作系统和网络原理。面试官可能会要求你解释一下你在项目中是如何使用C++的,或者让你解决一些更复杂的编程问题。面试官通常是你入职后的小组长,或者能力等级更高点的平级同事。本文包含高频面试题目及解析,注意问的和回答的越深度底层,越凸显更高的技能。

C++闯关手册 - 面试实例:第二轮面试

C++ 闯关手册 - 面试实例

第二面:综合考察

1)自我介绍

每一个面试都会有个自我介绍,这里省略,基本面试几次自己都会说的比较溜了。面试官是有可能根据你的介绍内容进行提问的,这里介绍的时候可以根据情况引导面试官。

2)问项目,深挖项目中遇到的问题

这里写简历时候或面试前可以提前准备的项目问题,简历里提高点要提前想想可能会挖哪些点,提前有个预期。

很可能问到,你遇到过的难题是如何解决的?

提前挑一个项目中遇到的困难问题或用了很长时间才解决的问题:讲述问题表象,如何从各个方面排查,最后是如何确定到问题点,并用了什么方式解决的。

不一定是很大的问题,也可能最后就是一个很小的点,因为但这就是你的经验,知道了经历过了就觉得很简单,但是不知道的人就大概率要重复踩坑的。

3)TCP三次握手

TCP三次握手是TCP/IP协议用于在两个网络设备之间建立一个可靠的连接的过程。它确保两端的发送和接收能力都是可用的。以下是三次握手的步骤:

  1. 客户端发送SYN包:
    • 客户端选择一个随机序列号x,并发送一个SYN包(同步序列编号)到服务器,表示客户端尝试建立连接。
  2. 服务器响应SYN-ACK包:
    • 服务器接收到客户端的SYN包后,会选择自己的一个随机序列号y,并向客户端发送一个SYN-ACK包。这个包中包含服务器的序列号y,和客户端序列号x的确认号(即x+1),表示服务器也准备好建立连接了。
  3. 客户端发送ACK包:
    • 客户端收到服务器的SYN-ACK包后,会发送一个ACK包给服务器,确认号为y+1,表示客户端也准备好了,双方可以开始数据传输。

伪代码描述如下:

客户端                                           服务器
  |                                               |
  |-----发送SYN包(x)------------------------------>|
  |                                               |
  |<----------------发送SYN-ACK包(y, 确认号x+1)-----|
  |                                               |
  |-----发送ACK包(确认号y+1)----------------------->|
  |                                               |

这个过程确保了双方都确认了对方的接收和发送能力,从而建立了一个可靠的连接。

扩展:TCP四次挥手

TCP四次挥手是TCP/IP协议用于在两个网络设备之间终止一个已建立的连接的过程。这个过程确保了双方都能完成数据的发送和接收。以下是四次挥手的步骤:

  1. 客户端发送FIN包:
    • 客户端决定关闭连接,发送一个FIN包给服务器,表示客户端已经没有数据发送了。
  2. 服务器响应ACK包:
    • 服务器接收到FIN包后,发送一个ACK包给客户端,确认号为客户端的序列号加1。此时,服务器可能还有数据发送给客户端。
  3. 服务器发送FIN包:
    • 服务器发送完所有剩余的数据后,发送一个FIN包给客户端,表示服务器也没有数据要发送了。
  4. 客户端响应ACK包:
    • 客户端接收到服务器的FIN包后,发送一个ACK包给服务器,确认号为服务器的序列号加1。此时,连接的这一方关闭。

伪代码描述如下:

客户端                                           服务器
  |                                               |
  |-------发送FIN包------------------------------->|
  |                                               |
  |<------------------------------发送ACK包--------|
  |                                               |
  |<------------------------------发送FIN包--------|
  |                                               |
  |-------发送ACK包------------------------------->|
  |                                               |

在四次挥手过程中,连接的双方都需要发送FIN包和接收ACK包,以确保双方都能够完成数据的发送和接收,最终安全地关闭连接。

注意:TCP四次挥手过程中,谁先发送FIN包是由应用程序的逻辑决定的,而不是由TCP协议强制规定的,这里的客户端和服务端是相对的说法,一般情况下理解由客户端先发起,实际过程中的服务端是可以先发送FIN包的;

TCP状态图.png

扩展:TCP四次挥手过程中,为什么需要等待2倍的MSL时间才能完全关闭连接?

TCP四次挥手过程中,等待2倍的最大报文段生存时间(Maximum Segment Lifetime, MSL)是为了确保最后一个确认报文(ACK)能够安全到达对方。这个等待时间被称为TIME_WAIT状态或2MSL等待状态。原因包括:

  1. 确保最后一个ACK报文能够到达: 如果最后一个发送的ACK报文丢失,那么另一端没有收到这个ACK的确认,会重新发送FIN报文。等待2MSL时间可以确保这个重传的FIN报文能够到达,并且有足够的时间进行响应。这样可以避免连接被过早关闭,而对方还在等待ACK的确认。
  2. 等待足够的时间以确保所有重复的报文都消失: 网络中可能存在重复的报文段。等待2MSL时间可以确保网络中所有的报文段都已经消失,这样新的连接就不会接收到旧连接的任何延迟报文,从而避免可能的混淆。
  3. 允许可靠的连接重启: 等待2MSL时间还可以确保连接双方足够的时间来处理旧的或延迟的报文段,使得在同一对端口上可靠地启动新的连接,而不会与之前的连接混淆。

总的来说,等待2倍的MSL时间是一个保守的措施,用来确保TCP连接可靠地终止,同时避免可能的问题,如旧数据的干扰和连接状态的不一致。

4)TCP如何保证可靠传输?

TCP(传输控制协议)通过以下机制保证数据的可靠传输:

  1. 序列号和确认应答
    • TCP为每个发送的数据包分配一个序列号,并要求接收方对收到的数据包发送确认应答(ACK)。接收方在ACK中指明下一个期望接收的序列号,这样发送方就知道哪些数据已被成功接收。
  2. 数据重传
    • 如果发送方在超时时间内没有收到对发送数据的确认应答,它会重新发送该数据。这确保了数据在丢失或被破坏时能够被恢复。
  3. 流量控制
    • TCP使用窗口机制来进行流量控制,接收方通过告知其可接收的数据量(窗口大小),来防止发送方发送过多数据导致接收方处理不过来。
  4. 拥塞控制
    • TCP通过减慢数据发送速率来响应网络拥塞的迹象(如数据包丢失)。常见的拥塞控制算法包括慢启动、拥塞避免、快速重传和快速恢复。
  5. 有序数据传输
    • TCP保证数据按照发送顺序到达接收方。如果数据包到达顺序不正确,TCP会在交付给应用层之前重新排序。
  6. 错误检测
    • TCP头部和数据部分都包含校验和(checksum),用于检测数据在传输过程中的任何变化。如果检测到错误,受影响的数据包将被丢弃,并通过重传机制恢复。
  7. 连接管理
    • TCP通过三次握手过程建立连接,以确保双方都准备好进行数据传输。通过四次挥手过程安全地关闭连接,确保双方都完成了数据传输。

通过这些机制,TCP能够提供一种可靠的数据传输服务,在数据丢失、顺序错乱、重复或错误的情况下能够进行纠正。

5)TCP重传机制

TCP重传机制是TCP协议保证数据可靠传输的关键特性之一。当TCP数据包在网络中丢失、损坏或未在预定时间内被确认时,重传机制确保这些数据能够被重新发送。以下是TCP重传机制的基本工作原理:

  1. 超时重传(Timeout-based Retransmission)
    • 每当TCP发送一个数据包,它都会启动一个定时器,等待接收方的确认应答(ACK)。如果在定时器到期之前没有收到ACK,TCP会认为该数据包丢失,并将其重新发送。
  2. 快速重传(Fast Retransmission)
    • 如果发送方收到三个或更多的重复ACK(即接收方收到了一个序列号更高的数据包,而中间的某些数据包尚未到达),它会立即重新发送丢失的数据包,而不是等待定时器超时。这是因为重复ACK是网络中存在丢包的明确迹象。
  3. 选择确认(Selective Acknowledgment, SACK)
    • 这是一种更高效的错误恢复机制,允许接收方明确告知发送方哪些数据包已经成功接收,哪些需要重传。这样,发送方只需重传那些未被确认接收的数据包,而不是重传自最后一个被确认的数据包以来的所有数据。
  4. 拥塞控制
    • 重传机制与TCP的拥塞控制紧密相关。当检测到丢包时,TCP会认为网络拥塞,并减少其数据发送速率(例如,通过减少拥塞窗口大小)。这有助于减轻网络拥塞并避免进一步的数据包丢失。
  5. 定时器调整
    • TCP使用RTT(往返时间)估计来动态调整其重传定时器的超时时间。这确保了重传定时器既不会过于宽松(导致不必要的延迟)也不会过于紧张(导致不必要的重传)。

通过这些机制,TCP能够在各种网络条件下有效地处理数据包的丢失和错误,从而提供可靠的数据传输服务。

扩展:TCP滑动窗口机制

TCP滑动窗口机制是TCP协议中用于流量控制和拥塞控制的关键技术,它通过动态调整窗口大小来优化数据传输性能并防止网络拥塞。以下是TCP滑动窗口相关的重点知识总结:

  1. 窗口大小的影响因素
    • 接收方的接收缓冲区大小。
    • 网络的拥塞程度。
    • 往返时间(RTT)。
  2. 拥塞控制算法
    • 慢启动(Slow Start):窗口大小从1个MSS开始,每收到一个ACK,窗口大小翻倍,直到达到慢启动阈值(ssthresh)或发生丢包。
    • 拥塞避免(Congestion Avoidance):达到ssthresh后,窗口大小以线性方式增长,以避免网络拥塞。
    • 快速重传(Fast Retransmit):接收到三个重复的ACK时,立即重传丢失的报文段。
    • 快速恢复(Fast Recovery):在快速重传后,调整窗口大小为ssthresh加上3个MSS,然后根据进一步接收的重复ACK线性增加窗口大小,直到接收到新的ACK。
  3. 窗口大小调整的影响
    • 吞吐量:较大的窗口允许发送更多的数据,提高网络吞吐量。
    • 延迟:合适的窗口大小可以减少因等待ACK而产生的空闲时间,减少总体延迟。
    • 网络拥塞:过大的窗口可能导致网络拥塞,引起数据包丢失和重传,降低数据传输效率。
    • 资源利用率:合适的窗口大小可以提高发送方和接收方资源的利用率。
  4. 动态调整机制
    • TCP通过拥塞控制算法动态调整滑动窗口大小,以响应网络条件的变化,实现高效且可靠的数据传输。

TCP滑动窗口机制的设计旨在平衡网络吞吐量、延迟、拥塞控制和资源利用率,通过动态调整窗口大小来适应网络条件的变化,优化数据传输性能。

6)数据库锁

数据库锁是一种机制,用于管理对数据库中数据的并发访问,以保证数据的一致性和完整性。锁可以在不同的级别上应用,如行级锁、表级锁和数据库级锁,不同的数据库系统可能支持不同类型的锁。以下是数据库锁的一些关键点:

  1. 目的:防止数据在并发操作中出现读写冲突,确保事务的ACID属性(原子性、一致性、隔离性、持久性)。
  2. 类型
    • 共享锁(Shared Lock):允许事务读取一条记录,其他事务也可以获取同一数据的共享锁来读取,但不能修改。
    • 排他锁(Exclusive Lock):允许事务读取并修改一条记录,在该锁定期间,其他事务不能读取或修改此记录。
  3. 级别
    • 行级锁:最细粒度的锁,只锁定单个行记录,管理开销大,但并发度高。
    • 表级锁:锁定整个表,管理开销小,但并发度低。
    • 页面锁:锁定数据库页,介于行级锁和表级锁之间。
    • 数据库级锁:锁定整个数据库,最粗粒度的锁。
  4. 锁策略
    • 乐观锁:通常通过记录版本号实现,每次更新记录时检查版本号是否变化,适用于读多写少的场景。
    • 悲观锁:假设冲突很可能发生,事务在操作数据前先加锁,适用于写多读少的场景。
  5. 死锁
    • 当两个或多个事务相互等待对方释放锁时,会发生死锁。数据库管理系统通常通过死锁检测和解决机制来处理死锁,如超时、死锁检测算法等。
  6. 锁升级
    • 为了减少锁的开销,一些数据库系统支持锁升级,例如从行级锁升级为表级锁,当一个事务锁定了足够多的行时触发。

数据库锁是数据库管理中的一个复杂但重要的部分,合理的锁策略和锁管理可以显著提高数据库的并发性能和系统的稳定性。

扩展:实际开发使用

在实际的数据库应用开发中,开发者通常不需要直接管理锁。数据库系统(无论是关系型数据库如MySQL、PostgreSQL,还是非关系型数据库如MongoDB)都会自动管理锁,以确保数据的一致性和隔离性。开发者应该关注的是设计高效的数据访问模式和查询,以减少锁的竞争和提高应用性能。这包括:

  1. 优化查询:确保查询尽可能高效,通过合理的索引来加速数据的查找和访问。
  2. 控制事务大小:避免长事务,因为它们会持有锁更长时间,增加锁竞争的可能性。
  3. 读写分离:在可能的情况下,使用读写分离的架构来减轻主数据库的负担,将读操作分发到从数据库。
  4. 限制数据操作的范围:尽量减少每次操作影响的数据量,避免大批量的数据更新操作。
  5. 监控和分析:使用数据库提供的工具监控锁等待时间和锁争用情况,及时调整策略。

通过关注这些方面,可以有效减少锁竞争,提高数据库操作的性能和应用的响应速度。

7)内核态和用户态

内核态(Kernel Mode)和用户态(User Mode)是操作系统中两种不同的执行模式,它们定义了代码执行时的权限级别。

  1. 用户态
    • 在用户态下,运行的是用户进程或应用程序,这些程序不能直接访问操作系统内核数据结构和硬件设备。
    • 用户态提供了一个受限的执行环境,确保用户程序不能直接影响系统的稳定性和安全性。
    • 当应用程序需要进行文件操作、网络通信等需要操作系统介入的操作时,必须通过系统调用(System Call)切换到内核态,由操作系统内核代为执行。
  2. 内核态
    • 在内核态下,代码具有执行任何CPU指令和访问所有内存地址的权限。操作系统的核心部分在内核态下运行,处理系统调用、中断处理、硬件设备管理等任务。
    • 内核态提供了完全的访问权限,以便操作系统能够控制硬件和执行任务管理、内存管理等核心功能。
    • 由于内核态代码具有高权限,任何错误或恶意的内核态代码都可能导致系统崩溃或安全问题。

切换机制

  • 从用户态切换到内核态通常发生在应用程序执行系统调用请求操作系统服务时,或当发生硬件中断或异常时。
  • 当系统调用或中断处理完成后,操作系统将控制权返回给用户态程序,继续执行用户级别的操作。

这种设计是为了保护系统的稳定性和安全性,防止用户程序直接操作硬件和关键数据结构,同时也是实现操作系统多任务和资源管理的基础。

8)内核态用户态切换过程

内核态与用户态之间的切换过程涉及到操作系统的底层机制,主要包括以下步骤:

  1. 触发切换
    • 系统调用、中断或异常是触发从用户态切换到内核态的常见原因。
    • 系统调用是由用户程序主动请求操作系统服务而触发的。
    • 中断是由外部事件(如硬件设备的信号)触发的。
    • 异常是由程序执行错误(如除零、访问非法内存)触发的。
  2. 保存上下文
    • 在切换到内核态之前,操作系统会保存当前用户态程序的上下文(包括程序计数器、寄存器等状态),以便之后能够恢复到当前状态。
  3. 权限提升和状态切换
    • CPU将权限级别从用户态提升到内核态,这通常涉及到改变程序状态寄存器中的特定位。
  4. 执行内核代码
    • 根据触发切换的原因(系统调用、中断或异常),CPU跳转到相应的内核处理程序执行。
  5. 恢复上下文和权限降低
    • 内核态的操作完成后,操作系统将CPU的权限级别从内核态降低回用户态,并恢复之前保存的用户态程序的上下文,继续执行用户程序。
  6. 返回用户态
    • 用户程序继续从中断点继续执行,直到下一次需要内核介入的操作发生。
扩展:系统调用的触发情况

系统调用是操作系统提供给用户程序的一种接口,用于请求操作系统提供的服务。用户程序需要在以下情况下使用系统调用:

  1. 文件操作:创建、读取、写入、删除文件和目录等操作需要通过系统调用来实现,因为这些操作涉及到文件系统的管理,需要操作系统的介入。
  2. 进程控制:创建新进程、结束进程、进程间通信(IPC)、获取进程状态等操作都需要系统调用。这些操作直接关系到操作系统的进程管理和调度。
  3. 内存管理:分配或释放内存空间通常需要通过系统调用来完成,因为操作系统负责管理系统的内存资源。
  4. 设备管理:对硬件设备的访问(如读写磁盘、网络通信、控制外设等)需要通过系统调用,以确保对设备的访问是安全和有效的。
  5. 用户与任务通信:获取用户输入、在屏幕上显示信息等操作,通常需要通过系统调用来实现,因为这些操作涉及到用户界面和外部设备的控制。
  6. 访问系统信息:获取系统时间、系统配置、性能指标等信息通常需要通过系统调用。

系统调用提供了一种安全的机制,允许用户程序在受控的环境中执行这些操作,同时保护操作系统的稳定性和安全性。

9)程序在开始运行的时候,内核态和用户态都发生了什么?

程序从开始运行到执行过程中,内核态和用户态发生的主要事件可以概括如下:

  1. 程序加载

    • 用户通过命令或点击图标启动程序时,操作系统(内核态)首先接收到这个请求。
    • 操作系统负责将程序的代码和数据从磁盘加载到内存中。这一步涉及文件系统操作和内存管理,都在内核态下完成。
  2. 进程创建

    • 操作系统为新程序创建一个进程。这包括分配进程标识符(PID)、设置进程状态、分配内存空间等。这些操作都在内核态进行。
    • 初始化进程的上下文环境,包括寄存器、程序计数器等,为程序的执行做准备。
  3. 执行权限转换

    • 一旦进程和资源准备就绪,操作系统将CPU的执行权限从内核态切换到用户态,程序开始在用户态下执行其代码。
扩展回答
  1. 系统调用和服务请求

    • 程序在执行过程中,可能会请求操作系统提供的服务,如文件读写、网络通信、内存分配等。这些服务请求通过系统调用实现。
    • 每次执行系统调用时,程序必须从用户态切换到内核态,由操作系统接管控制,完成请求的服务。
    • 服务完成后,操作系统将控制权和执行权限返回给用户程序,继续在用户态下执行。
  2. 中断和异常处理

    • 在程序执行过程中,可能会发生硬件中断(如键盘输入、网络数据到达)或软件异常(如除零错误、非法访问内存)。这些事件都需要操作系统介入处理。
    • 处理这些事件时,执行权限会临时从用户态切换到内核态,操作系统负责处理中断或异常。
    • 处理完成后,控制权返回用户程序,继续在用户态执行。
  3. 程序结束

    • 程序执行完成后,或者接收到结束信号时,会再次通过系统调用通知操作系统。
    • 操作系统回收程序使用的资源,如内存、文件描述符等,并关闭进程。这些清理工作在内核态完成。
    • 最后,操作系统将CPU的控制权转移给其他进程或返回到操作系统,等待下一次用户请求。

整个过程中,内核态和用户态的切换是频繁且必要的,以确保系统的稳定性和安全性,同时提供丰富的操作系统服务给用户程序。

10)算法题

给定一个网址,输出倒序网址和每个点为坐标的倒序

举例:www.baidu.com

写第一个函数,输出 www.udiab.moc

写第二个函数,输出 com.baidu.www

原地反转解法

第一种情况(www.udiab.moc)可以概括为:以.为分割为part,part内倒序,part间正序

第二种情况(com.baidu.www)可以概括为:以.为分割为part,part内正序,part间倒序

要实现原地反转字符串,同时考虑以.为分隔符的情况,我们可以采用以下步骤:

  1. 完整反转字符串:首先,反转整个字符串。这一步可以通过双指针方法实现,一个指针从字符串开始,另一个从字符串结束,交换这两个指针指向的字符,然后向中间移动,直到两个指针相遇。改变了字符串整体的大顺序;
  2. 反转每个部分:由于整个字符串已经被反转,每个部分(以.为分隔符的子字符串)也被反转了。为了恢复每个部分内字符的正确顺序,我们需要再次对每个部分进行反转。这可以通过搜索.字符来确定每个部分的边界,然后对这些部分使用同样的双指针反转方法。只改变部分区间内的字符串顺序。
  3. 经过前两步可以实现第二个函数的功能;而第2步其实就是第一个函数要实现的功能。

以下是题目解法的代码:

#include <iostream>
#include <string>

void reverseString(std::string& s, int start, int end) {
    while (start < end) {
        std::swap(s[start++], s[end--]);
    }
}

// 第一个函数:以.为分割为part,part内倒序,part间正序
void reverseUrlInnerDomain(std::string& url) {
  // 反转.分割每个部分
  int start = 0;
  for (int i = 0; i <= url.length(); ++i) {
    if (i == url.length() || url[i] == '.') {
      reverseString(url, start, i - 1);
      start = i + 1;
    }
  }
}

// 第二个函数:以.为分割成part,part内正序,part间倒序
void reverseUrlDomains(std::string& url) {
  // 反转整个字符串
  reverseString(url, 0, url.length() - 1);
  // 反转以.分割每个部分
  reverseUrlInnerDomain(url);
}

int main() {
  std::string url = "www.baidu.com";

  std::cout << "原地解法:" << std::endl;
  reverseUrlInnerDomain(url);
  std::cout << url << std::endl;  // 输出: www.udiab.moc

  url = "www.baidu.com";
  reverseUrlDomains(url);
  std::cout << url << std::endl;  // 输出: com.baidu.www

  return 0;
}
  1. 这题目里有个坑点,“www” 的反转还是 “www”,所以整体上看都是反转操作,局部反转和全局反转;如果开始认为 “www” 是保持了原样,没有进行反转,和其他部分区别来看,就会 “掉坑” 造成思路混乱。
  2. 注意,原地操作数组会改变原有数组内容,如要求不改变原内容,可以复制一份再执行上述解法;或者用其他方式实现边操作边复制内容到新的字符串,但实现可能不够简洁高效;

11)反问环节

重复环节:一般可以问些岗位、团队相关的,如:面试的岗位会大概负责什么方向?团队在公司的定位及未来的方向?这也是要求职者本质需要关注的问题,团队的问负责人会更好。


💡
恭喜你,到这里你顺利通过了C++面试的第二面,已经成功了一大半了,接下来请准备进入第三轮面试吧!