分享

蚂蚁(CTO线)C++研发工程师一面凉经

 深度Linux 2024-05-13 发布于湖南

大家好,我是深度。今天呢,给大家分享一位小伙伴的C++研发工程师一面凉经,要不还得是蚂蚁集团公司,真的难。。。我也看了原题目,又多又难,对于一个校招生还是相当于难度的,当然薪资也不会少。

结:只有一两个问没回答好,其他的都回答出来了,手撕代码也过了,但挂了。第一次大厂面试,从此之后我意识到,如果我的简历想进大厂的话,八股文要全答对才有机会。

1、项目相关

  • 项目概述:简要介绍项目的背景和目标。说明该项目是什么以及为什么它是重要的。

  • 角色和职责:解释你在项目中扮演的角色和负责的具体任务。强调你在团队中所承担的职责,并提及与其他成员合作的经验。

  • 技术栈和工具:列出你在项目中使用的主要技术栈和工具。包括编程语言、框架、数据库等方面的信息。

  • 实施过程:描述你在项目中采用的方法和流程。讨论如何规划、设计和实施项目,并解决遇到的挑战或问题。

  • 成果和收获:强调项目取得了什么成果,例如交付的功能、性能改进或用户反馈。并分享你从这个项目中学到了什么技术或团队合作方面的经验。

  • 总结和教训:总结整个项目经历,并指出你从中学到了哪些教训和经验教训。强调自己对持续学习和不断改进的态度。

按照以上步骤,详细介绍,我就不一一列举了

2、 Reactor 模型(为什么用Reactor,Reactor解决了什么实际问题,假设线程池有100个线程,但有1000个用户同时使用,Reactor的具体表现,前端用户的体验是什么样的)和 Proactor模型

Reactor模型和Proactor模型都是常见的事件驱动编程模型,用于解决高并发网络应用程序的实际问题。它们主要用于处理I/O操作,如网络通信。

Reactor模型:Reactor模型基于事件循环机制,其中一个或多个线程(通常是单线程)负责监听和分发来自客户端的请求。当有新的请求到达时,Reactor会将其派发给对应的处理器进行处理。这样可以充分利用系统资源,并提供高并发性能。

通过使用Reactor模型,可以解决以下实际问题:

  • 提供高并发处理能力:通过事件循环和非阻塞I/O操作,可以支持大量并发连接。

  • 减少线程开销:相比为每个连接创建一个线程来处理,使用有限数量的线程集中处理所有连接更加高效。

  • 降低资源占用:减少上下文切换和内存占用。

在假设线程池有100个线程但有1000个用户同时使用的情况下,使用Reactor模型可以实现以下表现:

  • Reactor负责监听所有请求,并将其分派给空闲的工作线程进行处理。

  • 当新请求到达时,在没有空闲工作线程时可能需要等待。

  • 如果工作线程忙碌,可能会导致一些用户请求的响应时间较长。

  • 由于线程数有限,处理请求的速度可能会受到限制,从而影响用户体验。

Proactor模型是Reactor模型的一种扩展,它将I/O操作的主动性交给操作系统来处理,以提高并发性能。在Proactor模型中,应用程序提交I/O请求后可以继续执行其他任务,在操作系统完成相应的I/O操作后通知应用程序进行后续处理。这样可以减少阻塞和等待时间,并提高吞吐量和响应性能。

3、IO多路复用的流程和原理

  • 创建文件描述符集合:应用程序创建一个存放需要监听的文件描述符(通常是套接字)的集合。

  • 将文件描述符添加到集合中:将需要监听的文件描述符添加到集合中,使其可以被监视。这通常使用系统调用如select()poll()epoll()等来完成。

  • 等待事件发生:应用程序进入阻塞状态,等待任何一个或多个文件描述符上发生可读、可写或异常等事件。当有事件发生时,操作系统会唤醒应用程序。

  • 检测就绪事件:应用程序检查哪些文件描述符上有就绪事件发生,并进行相应处理。这可能包括读取数据、发送数据、关闭连接等操作。

  • 重复步骤2和3:应用程序重新将所有需要监听的文件描述符添加到集合中,并再次进入阻塞状态等待新的事件发生。

IO多路复用的原理主要依赖于操作系统提供的特性和系统调用:

  • select(): 是传统而通用的IO多路复用系统调用,它通过轮询方式检查文件描述符上的事件,并返回就绪的文件描述符集合。

  • poll(): 类似于select(),但使用更简单和更高效的数据结构来管理文件描述符集合。

  • epoll(): 是Linux系统特有的IO多路复用机制,使用事件驱动模型,在内核中维护一个事件表来跟踪注册的文件描述符。

4、epoll、poll、select特点和区别

epoll、poll和select是三种常见的IO多路复用机制,它们有一些相似之处,但也存在一些区别。

(1)select:

  • 特点:select是最早的IO多路复用函数,可以同时监听多个文件描述符的读写事件。它使用位图来表示文件描述符集合,在每次调用时需要将所有待检测的文件描述符集合从用户空间复制到内核空间。

  • 限制:在Windows系统下支持的最大文件描述符数量有限,通常为1024。

(2)poll:

  • 特点:poll与select类似,也可以同时监听多个文件描述符。它通过一个数据结构(数组)来存储待监视的文件描述符,并且在每次调用时不需要重新拷贝整个集合到内核中。

  • 改进:相较于select,poll解决了最大文件描述符数量限制的问题。

(3)epoll:

  • 特点:epoll是Linux特有的高性能IO多路复用机制。它引入了事件驱动模型,并使用红黑树来管理文件描述符集合。在每次调用时只需要将发生变化(就绪)的文件描述符传递给内核即可。

  • 高效性:相较于select和poll,在大规模连接数下具有更好的扩展性和性能表现。

  • 工作方式:epoll提供三个操作模式:EPOLL_CTL_ADD(添加文件描述符)、EPOLL_CTL_MOD(修改文件描述符的事件类型)和EPOLL_CTL_DEL(删除文件描述符)。

5、有没有考虑程序崩溃情景,项目程序崩溃了怎么办

  • 异常处理:合理地捕获和处理异常情况可以帮助避免程序崩溃。使用try-catch块来捕获异常,并进行适当的错误处理和资源释放操作。

  • 日志记录:在代码中添加日志记录功能,将关键信息、错误堆栈等输出到日志文件中。这样,在程序崩溃时可以通过查看日志文件来定位问题所在,并快速修复。

  • 代码审查:进行严格的代码审查,尽量避免潜在的bug和安全漏洞。通过团队内部的代码审核机制,及时发现并修复可能导致程序崩溃的问题。

  • 备份与恢复:定期备份数据和系统状态,以便在程序崩溃后能够快速恢复。备份策略应根据项目需求进行设计,并确保备份数据可靠性与完整性。

  • 监控与报警:设置监控系统来实时监测程序运行状态和资源消耗情况。当出现异常或超过阈值时,及时发送报警通知给相应人员,以便能够快速响应并解决问题。

  • 自动重启:在程序崩溃后,使用自动化的监控脚本或工具来检测并尝试重新启动崩溃的进程。这可以帮助保证系统的可用性和稳定性。

  • 异常情况处理:根据具体项目需求,在程序崩溃时设置合适的恢复机制,例如数据回滚、任务重启等,以确保系统在异常情况下能够正确地继续运行。

6、项目具体应用场景,为什么做这个项目

对于项目的具体应用场景和目的,需要根据具体项目来确定。不同的项目有不同的应用领域和目标。以下是一些常见的项目应用场景和原因:

  • 电子商务平台:为了满足在线购物的需求,开发一个电子商务平台可以方便用户购买商品,并提供订单管理、支付系统、客户服务等功能。

  • 社交媒体应用:社交媒体应用可用于人们之间的沟通和分享,例如朋友圈、消息传递、照片共享等功能,以及可能的广告推送和数据分析。

  • 在线教育平台:为了满足远程学习和技能培训的需求,开发一个在线教育平台可以提供各种课程内容、在线学习资源、学生管理系统等功能。

  • 医疗健康管理系统:开发医疗健康管理系统可以帮助医院或医生更好地管理患者信息、诊断结果、药物处方等,并提供预约挂号、在线问诊等功能。

  • 物流与仓储管理系统:物流与仓储管理系统可用于跟踪货物运输情况、库存管理、订单处理以及物流轨迹追踪等,提高物流效率和管理水平。

这些项目都是为了满足特定领域的需求,提供更便捷、高效、可靠的服务。通过开发这些项目,可以改善人们的生活和工作方式,提升效率,促进信息共享和交流。

7、为什么裸写socket编程而不是使用一些成熟的协议

8、项目中遇到的印象深刻的问题

9、TCP如何保证可靠传输

  • 应答和超时重传:发送方将数据划分为多个数据包,并逐个发送。接收方在接收到数据包后,会向发送方发送一个应答确认,表示已成功接收。如果发送方在一定时间内未收到应答确认,则会认为该数据包丢失,触发超时重传机制。

  • 序列号和确认:每个TCP数据包都有一个序列号,用于标识数据的顺序。接收方在接收到数据包后,会返回一个确认号,表示已成功接收到的最后一个连续字节的序列号。如果发送方在一定时间内未收到确认号,则会重传相应的数据包。

  • 滑动窗口:TCP使用滑动窗口机制来控制流量和处理拥塞。发送方和接收方各自维护一个滑动窗口大小,表示可以同时发送或接收的字节数量。通过调整滑动窗口大小,可以实现流量控制和拥塞控制。

  • 连接管理:TCP建立连接时进行三次握手,确保双方能够正常通信。在通信过程中,也会进行周期性的心跳检测以保持连接状态。当通信结束或出现异常情况时,会进行四次挥手来关闭连接。

  • 数据校验:TCP使用校验和机制对数据进行校验,发送方在发送数据时会计算校验和,并将其附加到数据包中。接收方在接收到数据后会重新计算校验和,并与接收到的校验和进行比较,以检测是否出现传输错误。

10、使用TCP编程时,如果服务端程序崩溃了,那么客户端会出现什么情况(分两种情况,服务器关机挂或者服务器上进程挂)

  • 连接断开:当服务端程序崩溃时,它无法继续与客户端保持连接。因此,客户端将检测到连接的异常中断,并收到一个错误或异常的消息。

  • 异常处理:客户端可以通过捕获相应的异常来处理连接断开的情况。这可能是由于网络故障、服务器崩溃或其他问题导致的。根据编程语言和框架的不同,可以使用适当的异常处理机制来进行处理,例如捕获并处理SocketException或ConnectionResetException等异常。

  • 重连尝试:一旦发现与服务端的连接断开,客户端可以尝试重新建立连接。这通常涉及在代码中实现重连逻辑,并尝试重新连接到服务端地址和端口。在进行重连之前,可以设置一定的延迟时间以避免频繁地重连。

  • 用户提示:根据具体需求,客户端可以向用户提供适当的提示或反馈信息,说明服务端不可用或已经崩溃。这样用户就能够了解到问题所在,并可能采取相应措施。

11、服务器关机时,一定要等到客户端触发TCP的keepalive后客户端才会关闭吗,有什么优化方法吗?

当服务器关机时,客户端在没有其他特殊处理的情况下可能会等待TCP的keepalive超时才关闭连接。这是因为TCP协议本身不会立即检测到服务端的异常关闭。

为了优化这种情况,可以考虑以下方法:

  • 设置适当的keepalive参数:通过在客户端设置合理的TCP keepalive参数,可以减少等待时间并更早地检测到服务端的异常关闭。具体的参数包括keepalive间隔、重试次数和超时时间。请注意,不同操作系统和编程语言可能对这些参数有所差异。

  • 使用心跳机制:在应用层实现心跳机制,即定期向服务器发送自定义心跳消息以确保连接的活跃性。如果服务器长时间没有收到心跳消息,则可以判断服务器已经关闭,并及时关闭客户端连接。

  • 引入超时机制:在客户端代码中引入合适的超时机制来处理与服务器之间的通信。例如,使用定时器或异步任务,在一定时间内未收到来自服务端响应,则认为服务端异常关闭,并主动关闭客户端连接。

  • 监控服务端状态:通过监控服务端进程或网络状态,及时发现异常关闭情况,并主动通知客户端进行连接断开处理。这可以通过其他系统或工具实现,如监控软件、集群管理工具等。

12、线程池的参数

  • 核心线程数(core pool size):表示线程池中保持活动状态的最小线程数。即使没有任务需要执行,核心线程也会一直存在。默认情况下,核心线程数为0。

  • 最大线程数(maximum pool size):表示允许的最大线程数量。当工作队列已满且核心线程都在忙碌时,新任务将创建额外的非核心线程来处理任务。达到最大线程数后,如果再有新任务到达,则根据拒绝策略来处理。

  • 空闲线程存活时间(keep-alive time):表示非核心线程在空闲状态下被终止之前等待新任务的最长时间。默认情况下,空闲非核心线程会立即被回收。

  • 工作队列(work queue):用于存放等待执行的任务。当提交一个新任务时,如果当前活动线程数小于核心线程数,则会创建新的核心线程来执行;如果当前活动线程已达到核心线程数但工作队列未满,则将任务加入到工作队列;如果工作队列已满且当前活动和排队的线程数达到最大线程数,则会创建额外的非核心线程来执行任务。

  • 线程工厂(thread factory):用于创建新线程。可以自定义线程工厂,以便对新建立的线程进行特定的设置,如命名、优先级等。

  • 拒绝策略(rejected execution handler):当任务无法被接受执行时采取的策略。常见的拒绝策略包括抛出异常、直接丢弃任务、丢弃队列中最老的任务或在调用者所在的线程中执行该任务。

13、线程和进程的区别(除了常规八股文以外,回答过程中牵扯到了Linux内核源码,给自己挖了坑)

  • 资源占用:进程是程序的一次执行过程,拥有独立的内存空间、文件描述符等资源。而线程是在进程内部运行的轻量级执行单元,共享同一进程的资源。

  • 执行开销:创建、切换和销毁进程所需的开销通常较大,涉及到上下文切换、资源分配等复杂操作。相比之下,线程的创建、切换和销毁开销较小。

  • 并发性:由于多个线程共享同一进程的资源,可以实现更高效地并发执行。而不同进程之间通信需要使用特定的机制(如管道、消息队列等),并发性相对较低。

  • 数据共享与保护:线程之间共享同一进程的数据空间,访问和修改数据更为方便。但也需要进行适当的同步与互斥控制,避免多个线程同时修改导致数据不一致或竞态条件问题。而不同进程之间无法直接共享数据,需要通过额外的IPC(Inter-Process Communication)机制来实现数据交换。

  • 容错性:一个线程出错可能会导致整个进程崩溃,因为它们共享同一进程的资源。而不同进程之间互相独立,一个进程的崩溃不会影响其他进程的正常运行。

14、你看过Linux内核源码吗?(因为怕被问源码所以赶紧回答看过一些源码解析)

未来想从事Linux后台开发,需要学习linux内核吗?

15、线程之间共享全局变量如何协调

  • 互斥锁(Mutex):使用互斥锁来保护共享变量的访问,只允许一个线程在同一时间内访问该变量。其他线程需要等待锁被释放才能进行访问。

  • 信号量(Semaphore):通过信号量实现对临界区资源的控制,限制同时访问的线程数量。可以设置为二进制信号量或计数信号量。

  • 条件变量(Condition Variable):用于线程间的通信和同步。当某个条件不满足时,线程可以进入等待状态,并在满足条件时被唤醒。

  • 原子操作(Atomic Operation):使用原子操作来确保对共享变量的原子性操作,这样多个线程就可以无冲突地进行读写。

  • 锁-free数据结构:采用特殊的数据结构设计来避免锁竞争,如无锁队列、无锁哈希表等。

16、为什么使用条件变量时总会使用互斥锁

  • 使用条件变量时,通常会结合互斥锁来确保线程安全。这是因为条件变量与互斥锁配合使用,能够实现更精细的线程同步和等待/唤醒机制。

  • 当一个线程需要等待某个条件满足时,它会调用条件变量的等待函数将自己阻塞。在另一个线程满足条件后,它会通过条件变量的信号或广播函数来唤醒正在等待的线程。

  • 而为了保证等待和唤醒的正确性,在使用条件变量时必须搭配互斥锁来提供临界区保护。当一个线程要访问共享资源时,先获取互斥锁,并在临界区内判断条件是否满足。如果不满足,则释放锁并通过条件变量进入等待状态;如果满足,则完成操作后释放锁,并通过条件变量唤醒其他等待的线程。

  • 这样做可以避免竞态条件(race condition)和死锁情况的发生,确保多个线程之间按照预期的顺序和状态进行协作。因此,在使用条件变量时总会搭配互斥锁使用

17、自己有没有碰到过C++的内存泄漏

  • 忘记释放堆上分配的内存:使用 new 或 malloc 进行内存分配时,必须在不再需要时使用 delete 或 free 来释放它们。确保每个 new 和 malloc 都有对应的 delete 和 free

  • 异常处理中未释放资源:如果在异常发生时没有正确地清理和释放资源,则可能导致内存泄漏。为了避免这种情况,可以使用 RAII(Resource Acquisition Is Initialization)技术,在对象的构造函数中申请资源,在析构函数中释放资源。

  • 指针误用:悬挂指针或野指针引用无效的内存可能导致内存泄漏。始终确保指针指向有效的对象,并在不需要时将其置为空或删除。

  • 容器未清空:如果使用 STL 容器(如 vector、map、set 等),并且容器中包含指针或动态分配的对象,请记得在不再需要容器时逐个删除其中的元素,以避免内存泄漏。

  • 循环引用:当对象之间存在循环引用时,可能导致无法正确释放内存。可以通过使用智能指针或手动打破循环引用来解决这个问题。

  • 不合理的资源管理:例如文件、数据库连接等资源未及时关闭或释放,也会造成资源泄漏。确保在使用完资源后及时关闭或释放它们。

18、对于大一点的项目如何快速找出C++内存泄漏的代码

  • 使用内存检测工具:借助专门的内存检测工具(如Valgrind、Dr. Memory、AddressSanitizer等),可以对程序进行静态或动态分析,找出潜在的内存泄漏点。这些工具会报告未释放的内存、悬挂指针、重复释放等问题,并提供相关调用栈信息,有助于定位和解决内存泄漏。

  • 执行全面的代码审查:仔细审查代码,寻找可能导致内存泄漏的地方。特别关注涉及动态分配内存(如newmalloc)以及资源申请和释放(如文件打开关闭)等地方。确保每个动态分配都有对应的释放,并注意避免资源管理错误。

  • 使用智能指针:C++11引入了智能指针(如std::shared_ptrstd::unique_ptr),它们能够自动管理资源的生命周期,并在不再需要时自动释放。使用智能指针可以减少手动释放资源所产生的错误。

  • 跟踪对象创建和销毁:通过添加日志或调试输出来跟踪对象的创建和销毁过程,观察是否有未被正确销毁的对象。注意特殊情况,如循环引用或异步操作可能导致内存泄漏。

  • 分析性能和资源占用:观察程序运行时的内存占用情况、内存使用趋势以及不断增长的资源消耗等。如果发现内存或资源持续增加而没有明显的释放,那可能存在潜在的内存泄漏问题。

  • 使用工具进行代码分析:一些集成开发环境(IDE)或代码编辑器提供了代码静态分析功能,可以帮助检测潜在的问题,如未释放的内存或资源管理错误。

19、C++中的虚函数

C++中的虚函数是一种特殊类型的成员函数,用于实现运行时多态性。当一个类将其成员函数声明为虚函数时,它可以在派生类中被重新定义,并且在运行时通过指向基类对象的指针或引用来调用适当的派生类函数。

以下是使用虚函数的一些重要点和语法规则:

  • 声明虚函数:在基类中,通过在成员函数声明前加上关键字virtual来声明虚函数。例如:virtual void functionName();

  • 虚函数的重写:在派生类中,可以使用相同的原型重新定义基类中的虚函数。重写时需要保持相同的返回类型、参数列表和const属性(如果有)。

  • 动态绑定:通过使用指向基类对象的指针或引用来调用虚函数,会根据实际对象类型选择正确的派生类版本,而不是仅仅根据指针或引用类型确定调用哪个函数。这种行为称为动态绑定或后期绑定。

  • 纯虚函数和抽象类:纯虚函数是没有实现体的虚函数,在基类中通过在成员函数声明后加上= 0来声明纯虚函数。拥有纯虚函数的类称为抽象类,不能直接实例化抽象类对象。

  • 虚析构函数:如果一个类中定义了虚析构函数,当通过指向派生类对象的基类指针删除该对象时,会调用适当的派生类析构函数来正确释放内存。

20、构造函数能不能是虚函数

构造函数不能是虚函数。在C++中,虚函数的特性是通过对象的指针或引用来实现动态绑定,在运行时根据对象的类型调用适当的函数。而构造函数用于创建对象,它在对象创建阶段被调用,并且在调用构造函数之前就需要确定要创建的对象类型,因此不能使用虚函数机制。

如果将构造函数声明为虚函数,编译器会产生错误。此外,派生类的构造函数会自动隐式调用基类的相应构造函数以初始化基类部分,而不需要通过虚函数进行动态绑定。

需要注意的是,在某些情况下,可能存在希望实现类似虚构造函数的需求。可以考虑使用工厂模式或其他设计模式来达到相应的目标。

21、能不能在构造函数中调用虚函数,会不会编译报错,能不能实现多态

在构造函数中调用虚函数是可以的,不会导致编译错误。然而,需要注意的是,在基类的构造函数中调用虚函数时,动态绑定并不会按预期工作。

当一个对象正在创建时,它处于基类部分的阶段,并且派生类的成员还没有被初始化。因此,在构造函数中调用虚函数时,只会执行当前对象类型(即基类)定义的版本,而不是派生类中重写的版本。这样做可能会导致意料之外的行为或错误结果。

实现多态应该避免在构造函数和析构函数中调用虚函数。如果需要在创建对象后进行多态操作,可以使用工厂模式或将其延迟到适当的方法中来实现。

22、析构函数能不能是虚函数,为什么析构函数要是虚函数

析构函数可以是虚函数,而且在某些情况下,将析构函数声明为虚函数是必要的。

当基类指针指向一个派生类对象,并通过该指针删除对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数而不会调用派生类的析构函数。这可能导致资源无法正确释放,造成内存泄漏等问题。

通过将基类的析构函数声明为虚函数,可以实现动态绑定,在销毁对象时确保正确调用派生类的析构函数。这样就能够适当地释放派生类所占用的资源,确保程序行为符合预期。

23、C++和python有什么区别

C++和Python是两种不同的编程语言,它们有以下一些主要区别:

  • 语法:C++使用类似于传统的编程语言的语法,强调静态类型和显式内存管理。Python则具有简洁、易读且可表达性强的语法。

  • 类型系统:C++是静态类型语言,需要在编译时进行类型检查,并需要显式声明变量的类型。而Python是动态类型语言,变量的类型在运行时确定,无需显式声明。

  • 内存管理:C++需要手动分配和释放内存,使用new和delete关键字来管理对象的生命周期。而Python通过垃圾回收机制自动管理内存,开发者不需要手动释放内存。

  • 性能:由于C++是编译型语言,直接翻译成机器码执行,通常比Python执行速度更快。但Python具有许多高级库和优化工具,在某些情况下也可以实现较好的性能。

  • 应用领域:C++通常用于系统级开发、游戏开发、嵌入式设备等需要高性能和底层控制的领域。Python适用于快速原型开发、Web应用程序、数据科学等领域。

24、手撕代码:链表重排序,将1 2 3 4 5重排成1 5 2 4 3

(1)定义链表节点结构体,包含一个值和指向下一个节点的指针。

struct ListNode {
int val;
ListNode* next;
};

(2)创建一个函数来实现链表重排序。

void reorderList(ListNode* head) {
if (head == nullptr || head->next == nullptr || head->next->next == nullptr) {
return; // 如果链表为空或只有一个节点,则不需要重排
}

// 使用快慢指针找到链表中点
ListNode* slow = head;
ListNode* fast = head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}

// 将后半部分链表反转
ListNode* prev = nullptr;
ListNode* curr = slow->next;

while (curr) {
ListNode* nextNode = curr->next;
curr->next = prev;
prev = curr;
curr = nextNode;
}

slow->next = nullptr;

// 依次合并前半部分和反转后的后半部分链表
ListNode* p1 = head;
ListNode* p2 = prev;

while (p1 && p2) {
ListNode *temp1, *temp2;

temp1= p1 -> next;
temp2= p2 -> next;

p1 -> next= p2;
p2 -> next= temp1;




/*将当前位置之后的位置记录*/
p1= temp1;
p2= temp2;

}
}

(3)创建一个函数来打印链表。

void printList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
cout << current->val << " ";
current = current->next;
}
cout << endl;
}

(4)在主函数中创建链表,并调用重排序和打印链表的函数进行验证。

int main() {
// 创建原始链表 1->2->3->4->5
ListNode* head = new ListNode{1};
head->next = new ListNode{2};
head->next->next = new ListNode{3};
head->next->next->next = new ListNode{4};
head->next->next->next->next = new ListNode{5};

cout << "原始链表: ";
printList(head);

reorderList(head);

cout << "重排后的链表: ";
printList(head);

return 0;
}

运行以上代码,输出结果为:

原始链表: 1 2 3 4 5 
重排后的链表: 1 5 2 4 3

这样就实现了将给定的链表按照指定规则进行重排序。

25、调用new之后底层会做什么

  • 首先,它会检查所请求的内存大小,并尝试从系统中获取足够大的连续内存块。

  • 如果成功获取到内存,则将返回一个指向新分配内存的指针。

  • 接下来,如果需要,在分配的内存块中会进行初始化。对于基本类型(如整数、浮点数等),不会有任何初始化操作;对于类对象或自定义类型,可能会调用默认构造函数进行初始化。

  • 分配后,您可以使用返回的指针来访问和操作这段内存。可以使用->运算符来访问动态分配对象的成员变量和方法。

  • 当您完成对动态分配的内存的使用后,应该使用delete关键字释放这段内存,以防止资源泄漏。释放内存时,底层将删除之前分配的对象,并标记该部分内存在可供重新分配使用。

26、操作系统如何分配内存,从哪里分配内存

操作系统通过内存管理单元(Memory Management Unit,MMU)来进行内存分配。内存管理单元负责将逻辑地址(由程序使用的地址)转换为物理地址(实际在硬件上的地址)。具体而言,操作系统采用以下两种方式来分配内存:

  • 静态分配:在程序加载和执行之前,操作系统会为每个进程分配一块固定大小的连续内存空间。这个过程通常发生在进程创建时或系统启动时。静态分配的主要优点是效率高、速度快,但也限制了每个进程可用内存的大小。

  • 动态分配:当进程运行时,可能需要更多的内存空间以满足其动态需求。为此,操作系统提供了动态内存分配机制。最常见的方式是使用堆(Heap)来动态分配内存。堆是一个动态增长的区域,在堆中可以通过函数调用(如malloc()new)来申请和释放变长的、非连续的内存块。

从哪里进行内存分配取决于操作系统的具体实现。通常情况下,操作系统将保留一部分物理内存作为自身使用,并将其余部分划分给用户进程使用。物理内存在计算机启动时被初始化,并且在运行过程中,操作系统通过内存管理来跟踪和管理可用的物理内存块。操作系统使用数据结构(如页表、空闲列表等)来记录已分配和未分配的内存块,并根据需要将适当的物理内存映射到进程的地址空间中。

27、归还内存时操作系统会做什么

  • 标记释放:操作系统将收到的释放请求标记为可用的内存块。这样,该内存块就可以重新被其他程序或进程申请使用。

  • 内存合并:如果相邻的空闲内存块存在,操作系统可能会将它们合并成一个更大的连续空闲区域。这样可以提供更大的可用内存块,并减少内存碎片化问题。

  • 更新管理数据结构:操作系统会更新相应的数据结构(如空闲列表)来反映已经释放的内存块。这些数据结构记录了哪些内存块是可用的和哪些是已分配的。

  • 回收物理页面:如果归还的内存页不再被其他进程或程序使用,则操作系统可能会将其回收到物理页面池中以备将来再次分配给需要的进程。

28、内存碎片怎么处理

内存碎片是指内存中存在一些零散的空闲区域,这可能导致可用内存不足以满足大块连续内存的分配请求。为了处理内存碎片问题,操作系统可以采取以下几种方式:

  • 内存合并:当程序释放内存时,操作系统可以将相邻的空闲区域合并成一个更大的连续空闲区域。这样可以提供更大的可用内存块,并减少内存碎片化问题。

  • 紧缩内存:在某些情况下,操作系统可以进行紧缩操作,将已分配的内存块整理到一端,然后释放另一端的空闲区域。这样可以产生一个较大的连续空闲区域。

  • 分配策略优化:优化内存分配算法和策略,尽量避免或减少产生碎片的情况。例如使用动态分区分配算法(如首次适应、最佳适应、最差适应等)来选择最合适的可用内存块进行分配。

  • 虚拟内存技术:通过虚拟内存技术,操作系统可以将部分页面写入硬盘上的交换文件(Swap),从而释放物理内存。当需要时再将页面从交换文件中还原回物理内存。这样可以减少对连续的物理内存的需求,降低内存碎片化问题。

29、解释命名空间(Namespace)在C++中的作用和优势。

命名空间(Namespace)是C++中一种用于组织代码的机制,可以将全局作用域划分为不同的区域,以避免命名冲突,并提供更好的代码结构和可读性。

以下是命名空间在C++中的作用和优势:

  • 避免命名冲突:当我们在编写大型程序或使用多个库时,可能会出现相同名称的函数、变量或类等。使用命名空间可以将这些实体包装到特定的命名空间中,在不同的命名空间中定义相同名称的实体不会产生冲突。

  • 提供更好的代码结构:通过将相关功能或模块放置在相应的命名空间下,可以提供更清晰、组织良好的代码结构。这使得代码易于理解、维护和扩展。

  • 支持重载和扩展:使用命名空间可以支持函数、类等实体的重载。当我们需要为相似但功能稍有差异的对象创建多个版本时,可以利用命名空间来区分它们,并根据需要进行选择调用。

  • 具备嵌套性:C++中的命名空间可以嵌套定义,即在一个命名空间内部可以再定义其他子命名空间。这样可以进一步划分和组织相关联的代码。

  • 可避免全局污染:使用命名空间可以减少全局命名的使用,从而减少全局作用域的变量和函数的数量。这有助于避免不必要的全局变量和函数污染。

  • 提高可读性和可维护性:通过明确指定实体所属的命名空间,代码的可读性得到提高。开发人员可以更清楚地知道特定实体是在哪个命名空间下定义和使用的,从而增强了代码的可维护性。

30、什么是模板(Template)?如何定义一个模板类或模板函数?

模板(Template)是C++中的一种泛型编程机制,允许定义通用的类或函数,可以在多个不同类型上进行实例化。通过使用模板,可以实现代码重用和提供灵活性。

下面是如何定义一个模板类或模板函数的示例:

定义一个模板类:

template <typename T>
class MyClass {
public:
T data;

void display() {
std::cout << "Data: " << data << std::endl;
}
};

在上述示例中,我们使用template <typename T>来声明一个模板类,并通过typename T指定了一个类型参数。这样,MyClass就可以在不同的类型上进行实例化。

定义一个模板函数:

template <typename T>
T getMax(T a, T b) {
return (a > b) ? a : b;
}

在上述示例中,我们使用template <typename T>来声明一个模板函数,并通过typename T指定了一个类型参数。这样,getMax()就可以接收不同类型的参数并返回较大的值。

使用时,可以按照以下方式对模板进行实例化和调用:

MyClass<int> obj; // 实例化为int类型的MyClass对象
obj.data = 10;
obj.display();

int result = getMax<int>(5, 8); // 调用getMax函数并传入int型参数
std::cout << "Max value: " << result << std::endl;

通过模板,我们可以在编写代码时不需要为每个特定类型都重复编写类或函数的定义,而是使用通用的模板进行实例化。这提供了更高的代码重用性和灵活性。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多