理想汽车C++面经
来源:https://www.nowcoder.com/feed/main/detail/1a71f716bcae4e14b0ae6bf2e4ca0724
1、分别介绍一下OSI七层网络模型每层的作用?
- 物理层:这是网络模型的底层,它负责定义物理介质(如电缆、光纤、无线信号等)上的数据传输方式。物理层的主要任务包括数据的传输、电压、频率和光强度的调制等。它确保数据的物理传输,并不负责数据的解释和处理。代表性事物:电缆、光纤、网线等物理传输媒介。
- 数据链路层:数据链路层负责将物理层传输的数据分割成帧(Frames),并负责帧的传输和错误检测。它还管理设备之间的数据链路建立、维护和拆除,通常使用MAC(媒体访问控制)地址来唯一标识设备。以太网帧、MAC(媒体访问控制)地址。
- 网络层:网络层负责定义数据的路由和转发,以确定数据从源到目的地的路径。它使用IP地址来标识设备和子网,并执行路由决策,确保数据在不同网络间正确传输。代表性事物:IP地址、路由器。
- 传输层:传输层负责端到端的通信,它提供端到端的可靠数据传输,包括错误检测和纠正、流量控制以及数据的分段和重组。代表性事物:TCP(传输控制协议)、UDP(用户数据报协议)。
- 会话层:会话层负责建立、管理和终止会话,以确保不同设备之间的通信能够有效进行。它还负责处理同步问题、会话恢复和会话管理。代表性事物:RPC(远程过程调用)、NetBIOS会话。
- 表示层:表示层负责数据的编码、解码和加密解密,以确保不同设备之间的数据可以正确解释和处理。它处理数据格式转换、数据加密和数据压缩等任务。代表性事物:数据压缩、数据格式转换。
- 应用层:应用层是网络模型的最顶层,它负责提供各种网络应用和服务,例如文件传输、电子邮件、网页浏览等。代表性事物:HTTP(超文本传输协议)、SMTP(简单邮件传输协议)、FTP(文件传输协议)。
2、TCP和UDP的区别?
- 连接性
- TCP是一种连接导向的协议,它要求在通信之前建立一个连接。连接建立后,数据传输是可靠的,有序的,并且可以进行双向通信。
- UDP是一种无连接协议,不需要建立连接。每个UDP数据包都是独立的,没有与之前或之后的数据包的关联。这使得UDP更加轻量级,但不可靠。
- 可靠性
- TCP提供可靠的数据传输,确保数据的完整性和顺序。如果数据包丢失或损坏,TCP会自动重传丢失的数据,直到接收方确认收到。
- UDP不提供可靠性保证。数据包可能会丢失、重复或乱序,而且UDP不执行自动重传。
- 流量控制
- TCP使用滑动窗口和拥塞控制算法来进行流量控制,以避免过载网络和接收方。
- UDP不执行流量控制,发送方可以以任何速率发送数据,而不受接收方的限制。
- 头部开销
- TCP头部相对较大,通常包含20字节的头部信息。这增加了数据传输的开销。
- UDP头部相对较小,通常包含8字节的头部信息。这使得UDP在某些情况下更高效。
- 应用场景
- TCP适用于需要可靠数据传输的应用,如网页浏览、文件传输、电子邮件等。它也适用于对数据的顺序性有要求的应用。
- UDP适用于实时性要求高、数据丢失不会引起问题的应用,如音频/视频流传输、在线游戏、DNS查询等。
- 连接状态
- TCP维护连接状态,包括三次握手建立连接和四次握手终止连接。
- UDP没有连接状态,每个数据包都是独立的。
- 性能开销
- 由于提供可靠性和流控,TCP的性能开销较高,适用于低延迟和高可靠性的场景。
- UDP的性能开销较低,适用于高吞吐量和实时性要求高的场景。
3、TCP是否可以广播?
TCP协议通常不用于广播,而是用于点对点通信或点对多点通信,其中一个发送方(服务器)将数据传输给一个或多个接收方(客户端)。
TCP不适合广播的主要原因:
- TCP是一种连接导向的协议,通信的前提是需要在通信双方之间建立连接。在广播情景中,可能有大量的接收方,而TCP不适合建立和维护大规模的连接。
- TCP提供可靠的数据传输,确保数据的完整性和顺序。这意味着在TCP连接中,发送方会等待接收方对每个数据包的确认。在广播中,由于可能有大量的接收方,这种等待会导致严重的性能问题。
- 在局域网中,通常会使用UDP的多播(multicast)或单播(unicast)来替代广播。多播允许将数据传输到订阅组中的所有成员,而单播则将数据直接发送给特定接收方,这两种方式更加适合控制和管理广播数据。
- 现代网络通常对广播数据进行限制,以防止网络拥塞和滥用。广播通常不适用于大规模的公共互联网。
4、TCP建立连接和断开连接的过程?
TCP连接的建立过程(三次握手):
- 第一次握手:
- 客户端向服务器发送一个TCP数据包,其中包含了SYN(同步序列编号)标志位,并选择一个初始序列号(ISN)。此时,客户端处于SYN_SENT状态。
- 这个数据包的目的是告诉服务器,客户端希望建立一个TCP连接。
- 第二次握手:
- 服务器接收到客户端的SYN数据包后,会向客户端发送一个响应,其中包含了SYN和ACK标志位,以及确认号(ACK)字段。
- 服务器也会选择一个初始序列号,这个序列号通常是随机的。
- 此时,服务器处于SYN_RECEIVED状态。
- 第三次握手:
- 客户端接收到服务器的响应后,会向服务器发送一个ACK(确认)数据包,其中包含了服务器发送的初始序列号加1作为确认号。
- 服务器接收到客户端的ACK数据包后,确认连接建立,并将初始序列号加1作为确认号。
- 此时,客户端和服务器都处于已建立连接的状态。
TCP连接的断开过程(四次挥手):
第一次挥手:
- 当客户端希望断开连接时,它向服务器发送一个带有FIN标志位的TCP数据包。这表示客户端不再发送数据。
- 服务器仍可以向客户端发送数据,因此连接仍然保持。
- 此时,客户端处于FIN_WAIT_1状态,服务器处于CLOSE_WAIT状态。
第二次挥手:
服务器接收到客户端的FIN数据包后,发送一个ACK数据包作为确认。服务器此时进入CLOSE_WAIT状态,表示服务器已经接受到了关闭请求。
第三次挥手:
- 服务器当它自己也准备断开连接时,向客户端发送一个带有FIN标志位的TCP数据包。这表示服务器不再发送数据。
- 客户端仍然可以向服务器发送数据。
- 此时,服务器处于LAST_ACK状态,客户端处于FIN_WAIT_2状态。
第四次挥手:
客户端接收到服务器的FIN数据包后,发送一个ACK数据包作为确认。此时,客户端和服务器都进入了CLOSED状态,连接已经完全终止。
5、单播、广播、组播的定义和使用场景?
- 单播:
- 定义: 单播是一种点对点通信方式,其中数据从一个发送方传输到一个特定的接收方。在单播通信中,数据包只有一个目标地址,通常是接收方的唯一标识,如IP地址或MAC地址。
- 使用场景: 单播适用于一对一通信,其中一个发送方与一个接收方之间建立直接连接。例如,Web浏览器从Web服务器请求网页内容,或者电子邮件客户端从邮件服务器接收电子邮件。单播通常用于需要点对点交流的应用。
- 广播:
- 定义: 广播是一种点对多点通信方式,其中数据从一个发送方传输到网络中的所有接收方。广播数据包在网络中被传播到每个设备,以确保每个设备都可以接收到数据。
- 使用场景: 广播适用于需要向网络中的所有设备传递相同信息的场景。例如,DHCP服务器使用广播来提供IP地址分配,ARP协议使用广播来解析IP地址与MAC地址的映射。广播通常用于网络管理和配置的情况,但在现代网络中,广播通常受到限制,以防止滥用。
- 组播:
- 定义: 组播是一种点对多点通信方式,其中数据从一个发送方传输到一个特定组的接收方。接收方设备加入特定的组,以接收组播数据。组播数据包仅传送到加入组的设备。
- 使用场景: 组播适用于需要将数据传输到特定组的接收方的场景,而不是广泛传播到所有设备。例如,实时多媒体流(如视频和音频流)可以使用组播,以减少网络流量,因为只有对特定流感兴趣的接收方才会加入相应的组。组播还在一些多点传输协议中广泛使用,例如路由协议和IPv6。
6、DHCP的下层传输协议?
DHCP是一种用于动态分配IP地址和其他网络配置信息的协议,它是应用层协议,运行在下层的传输协议通常是UDP。
DHCP(应用层):
- DHCP是一个应用层协议,用于自动分配IP地址、子网掩码、默认网关、DNS服务器等网络配置信息给计算机或其他网络设备。
- DHCP的主要任务包括IP地址的分配、地址租赁管理、配置参数的传输和更新。
- DHCP服务器通常在网络中提供IP地址和配置信息,而DHCP客户端设备请求并接收这些信息
DHCP使用UDP协议之所以合适,是因为UDP更加轻量级,不需要建立连接,也不需要维护连接状态,这使得它更适合于快速的IP地址分配和配置信息传输。DHCP服务器通常在网络中监听UDP端口67,而DHCP客户端则监听UDP端口68。DHCP服务器接收客户端的请求,分配IP地址和其他配置信息,并将响应作为UDP数据包发送回客户端。
UDP在DHCP中的使用是为了简化配置和提高效率,但它不提供TCP那样的可靠性。虽然DHCP可以快速分配IP地址和配置信息,但在某些情况下,数据包可能会丢失或重复,因此需要适当的超时和重试机制来处理潜在的问题。
7、Socket建立连接的过程?
客户端:
- 创建一个socket,用函数socket();
- 设置socket属性,用函数setsockopt();* 可选
- 绑定IP地址、端口等信息到socket上,用函数bind();* 可选
- 设置要连接的对方的IP地址和端口等属性;
- 连接服务器,用函数connect();
- 收发数据,用函数send()和recv(),或者read()和write();
- 关闭网络连接;
服务端:
- 创建一个socket,用函数socket();
- 设置socket属性,用函数setsockopt(); * 可选
- 绑定IP地址、端口等信息到socket上,用函数bind();
- 开启监听,用函数listen();
- 接收客户端上来的连接,用函数accept();
- 收发数据,用函数send()和recv(),或者read()和write();
- 关闭网络连接;
- 关闭监听;
8、epoll的LT和ET模式?
- LT 模式:
- 在 LT 模式下,当文件描述符上有数据可读(或可写)时,
epoll_wait将立即返回,即使数据没有被完全读取(或写入)。 - 如果你没有处理所有可用数据(例如,只读取了部分数据),下次调用
epoll_wait时仍然会返回就绪状态,直到所有数据被读取或写入完毕。 - LT 模式适用于典型的阻塞式 I/O 操作,可以简化编程,但需要小心防止进入忙等待状态,因为在没有真正处理完数据的情况下反复触发事件会导致 CPU 使用率升高。
- 在 LT 模式下,当文件描述符上有数据可读(或可写)时,
- ET 模式:
- 在 ET 模式下,
epoll_wait仅在文件描述符上的数据状态发生变化时返回就绪状态。一旦epoll_wait返回,你需要立即处理数据,直到没有更多数据可读(或写)。 - ET 模式需要你主动追踪处理的数据状态,确保数据被完全处理,否则不会再次触发事件。这种方式更高效,因为它减少了触发事件的频率,但编程要求更高。
- 在 ET 模式下,
区别:
- LT 模式相对简单,但可能导致频繁的事件触发,需要额外的处理逻辑来保证数据完整性。通常用于传统阻塞式 I/O 的应用。
- ET 模式需要更多的编程复杂度,但更高效,因为只有状态变化时才触发事件。需要确保在处理就绪状态后,一直读取或写入数据,直到没有更多数据可处理。适用于高性能非阻塞式 I/O 应用。
在选择 LT 或 ET 模式时,要根据具体应用的需求和复杂性来决定。对于典型的阻塞式 I/O 操作,LT 模式可能更容易使用,而对于高性能的非阻塞式 I/O,ET 模式可能更适合。
9、有限状态机的设计与实现?
有限状态机是一种数学模型和计算机科学中的概念,用于描述具有有限数量的状态和状态之间的转移关系的系统。
下边我给出一个有限状态机的设计与实现的参考步骤:
- 首先,明确定义有限状态机的状态。状态是系统所处的不同情况或条件。每个状态应该具有明确定义的含义和特性。例如,在一个自动售货机中,可能有状态 "待命"、"选择饮料"、"支付"、"出货" 等。
- 定义状态之间的转移关系。状态转移描述了系统在不同状态下如何响应输入,以切换到下一个状态。这通常通过绘制状态转移图或表格来表示。状态转移可以是条件触发的,也可以是简单的时间触发。
- 指定有限状态机的初始状态,即在系统启动时所处的状态。
- 定义系统可以接受的输入,这些输入将触发状态转移。输入可以是来自外部环境的信号、事件或数据。
- 如果有限状态机需要在某种条件下终止,例如完成某个任务,那么需要定义一个或多个结束状态,以指示任务的完成。
- 在代码中实现有限状态机的逻辑。这通常包括使用条件语句或表格来确定在每个状态下如何处理输入以进行状态转移。
- 根据输入触发状态转移,从一个状态切换到另一个状态。这通常包括根据输入的条件执行相应的状态转移。
- 在每个状态中,可能需要执行某些操作或生成输出,以响应输入或满足任务的需求。
- 对实现的有限状态机进行测试和验证,以确保其按照设计规范正确运行。测试可以包括单元测试、集成测试和系统测试。
- 根据需要对有限状态机进行优化,以提高性能或增加新功能。还要确保维护状态机,以应对可能的变更或问题。
10、设计模式了解多少?
11、数据库连接池与RAII介绍?
数据库连接池:
数据库连接池是一种用于管理数据库连接的技术,旨在提高数据库访问的性能和资源利用率。它的主要原理是在应用程序启动时创建一组数据库连接,然后在需要时重复使用这些连接,而不是每次都创建新连接。
数据库连接池的主要特点:
- 连接的重用:连接池维护一组可重复使用的数据库连接,避免了在每次数据库操作时创建和销毁连接的开销。
- 资源限制:连接池通常限制了可同时使用的连接数,以防止资源耗尽和系统崩溃。这有助于保护数据库和应用程序免受过多连接的影响。
- 连接的生命周期管理:连接池负责管理连接的生命周期,包括连接的创建、分配、释放和回收。这确保了连接的正确使用和释放。
- 性能提升:通过减少连接的创建和销毁次数,连接池可以显著提高数据库访问性能,降低延迟。
数据库连接池通常与数据库访问框架一起使用,如Java中的DataSource接口,用于管理和提供数据库连接。连接池的设置需要根据应用程序的需求进行调整,以平衡资源利用率和性能。
RAII:
RAII是一种编程范式,主要用于C++中,它将资源的分配和释放与对象的生命周期绑定在一起。这意味着当对象被创建时,资源被分配,而当对象超出作用域时,资源被自动释放。
RAII的主要特点:
- 资源自动分配: 在对象构造期间,资源(如内存、文件句柄、数据库连接等)被分配。
- 资源自动释放: 在对象超出作用域时,析构函数被调用,从而释放相关资源。
- 异常安全性: RAII确保在面临异常时也可以安全释放资源,因为析构函数会在异常抛出时自动调用。
RAII在C++中常见于各种场景,如使用std::shared_ptr来管理动态内存、使用std::ofstream来管理文件句柄、使用数据库连接类来管理数据库连接等。这种方式可以帮助程序员有效地管理资源,避免资源泄漏和内存泄漏。
12、为什么要用多态?多态实现方式?静态多态讲一下,用到了什么编译器技巧?
为什么要使用多态?
- 多态允许在不同的类之间建立通用的接口,从而提高代码的可复用性。
- 多态支持代码的扩展,允许添加新的子类而不影响现有的代码。
- 多态提高了代码的可读性,因为它允许使用通用的抽象方法,而不需要关心具体的实现。
多态的实现方式:
多态可以通过继承和接口实现。在面向对象编程语言中,多态通常通过以下方式实现:
- 方法重载:同一个类中可以定义多个同名的方法,但参数列表不同,这被称为方法重载。编译器会根据传入的参数来确定调用哪个方法。
- 方法重写:子类可以重写(覆盖)父类的方法,以提供自己的实现。父类的引用可以引用子类的对象,从而实现多态。
- 接口:接口定义了一组方法的规范,不提供具体实现。多个类可以实现同一个接口,并提供各自的实现,从而实现多态。
静态多态:
静态多态通常是指编译期多态,即在编译时根据参数和方法签名来确定调用哪个方法。这种多态是在编译时由编译器决定的,而不是在运行时。它通常基于函数重载实现,编译器根据函数的参数类型和数量来选择正确的函数。
静态多态使用了函数重载和函数模板,这是编译器技巧的一部分。编译器根据函数调用的参数类型和数量来匹配合适的函数,这样可以在编译时进行类型检查,提高代码的安全性和性能。
在C++中,通过函数重载和模板,可以实现静态多态。给个示例:
#include <iostream>
void print(int value) {
std::cout << "Printing integer: " << value << std::endl;
}
void print(double value) {
std::cout << "Printing double: " << value << std::endl;
}
int main() {
int x = 5;
double y = 3.14;
print(x); // 调用第一个print函数
print(y); // 调用第二个print函数
return 0;
}13、C++11三种智能指针?
它们用于管理动态分配的内存和自动处理内存资源的释放,从而减少内存泄漏和提高代码的安全性。
std::shared_ptr:std::shared_ptr允许多个智能指针共享同一个对象的所有权。它使用引用计数来跟踪多少个std::shared_ptr共享同一个对象,当最后一个std::shared_ptr离开作用域时,它会自动释放对象的内存。这种智能指针适用于多个部分需要访问同一个对象的情况,但要小心避免循环引用。
#include <memory>
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::shared_ptr<int> anotherSharedPtr = sharedPtr; // 共享同一个对象
return 0; // 在main函数结束时,两个shared_ptr都离开作用域,对象被销毁
}std::unique_ptr:std::unique_ptr表示独占的所有权,即同一时间只能有一个std::unique_ptr拥有一个对象。它是非拷贝able,因此不能共享所有权。这种智能指针适用于只有一个部分需要访问对象的情况,它保证了对象在
std::unique_ptr离开作用域时被释放。
#include <memory>
int main() {
std::unique_ptr<int> uniquePtr = std::make_unique<int>(42);
// std::unique_ptr<int> anotherUniquePtr = uniquePtr; // 错误,std::unique_ptr不可拷贝
return 0; // 在main函数结束时,uniquePtr离开作用域,对象被销毁
}std::weak_ptr:std::weak_ptr用于解决std::shared_ptr可能导致的循环引用问题。它允许观察一个对象,但不增加引用计数,当所有std::shared_ptr指向对象都销毁时,std::weak_ptr不再有效。std::weak_ptr可以通过lock()方法创建一个有效的std::shared_ptr来访问对象,如果对象已经被销毁,则lock()返回一个空的std::shared_ptr。
#include <memory>
int main() {
std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr;
if (auto lockedPtr = weakPtr.lock()) {
// 使用lockedPtr访问对象
}
// 当sharedPtr离开作用域后,对象仍然存活
return 0;
}14、手撕翻转链表?
思路:
翻转链表的基本思路是通过遍历链表,逐个翻转每个节点的指针方向,将其指向前一个节点,从而逐渐完成链表的翻转。需要使用三个指针来辅助操作:当前节点(cur)、前一个节点(prev)和下一个节点(next)。翻转操作如下:
- 将当前节点的下一个节点保存在临时变量next中。
- 将当前节点的下一个节点指向前一个节点prev。
- 更新prev为当前节点。
- 更新当前节点为next。
- 重复上述步骤,直到当前节点为空。
参考代码:
#include <iostream>
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
ListNode* reverseList(ListNode* head) {
ListNode* prev = nullptr;
ListNode* cur = head;
while (cur != nullptr) {
ListNode* next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev; // 返回新的头节点
}
void printList(ListNode* head) {
ListNode* current = head;
while (current != nullptr) {
std::cout << current->val << " -> ";
current = current->next;
}
std::cout << "nullptr" << std::endl;
}
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);
std::cout << "Original Linked List: ";
printList(head);
// 翻转链表
head = reverseList(head);
std::cout << "Reversed Linked List: ";
printList(head);
return 0;
}