依图科技C++后端开发面试题及参考答案

请介绍你所了解的分布式系统

分布式系统是由多个独立的计算节点通过网络连接组成的系统,这些节点共同协作以完成特定的任务。分布式系统的设计目标在于提升系统的性能、可扩展性、可靠性和容错性。

从性能方面来看,分布式系统能够把任务分配到多个节点上并行处理,从而显著缩短任务的执行时间。比如在大数据处理场景中,Hadoop 分布式文件系统(HDFS)和 MapReduce 框架可将大规模数据的处理任务分摊到集群中的多个节点,实现高效的数据处理。

可扩展性是分布式系统的关键特性之一。当系统的负载增加时,可以通过添加新的节点来增强系统的处理能力,实现水平扩展。以电商网站为例,在促销活动期间,可增加服务器节点以应对大量的用户访问请求。

可靠性也是分布式系统的重要特性。通过数据冗余和备份机制,即便部分节点出现故障,系统仍能正常运行。例如,分布式数据库 Cassandra 会将数据复制到多个节点,确保数据的可用性和持久性。

容错性是指系统在面对部分节点故障或网络故障时,仍能维持正常运行的能力。分布式系统通常采用故障检测、自动恢复和负载均衡等技术来实现容错。比如,在分布式系统中使用 ZooKeeper 来管理集群的元数据和协调节点之间的状态,当某个节点出现故障时,ZooKeeper 能够及时检测到并通知其他节点进行相应的处理。

分布式系统的应用场景广泛,涵盖了互联网、金融、医疗等多个领域。在互联网领域,分布式系统被用于构建大规模的网站、搜索引擎和云计算平台;在金融领域,分布式系统可用于处理高频交易和风险评估;在医疗领域,分布式系统可用于存储和分析海量的医疗数据。

解释 CAP 理论

CAP 理论由 Eric Brewer 提出,该理论指出在一个分布式系统中,一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三个特性最多只能同时满足两个。

一致性是指在分布式系统中,所有节点在同一时间看到的数据是一致的。当一个写操作完成后,后续的读操作应该能够读取到最新写入的数据。例如,在一个分布式数据库中,如果某个节点更新了数据,那么其他节点在读取该数据时也应该获取到最新的值。

可用性是指系统在面对各种故障时,仍然能够及时响应客户端的请求。也就是说,无论何时,客户端向系统发送请求,系统都能在合理的时间内给出响应。以电商网站为例,即便部分服务器出现故障,用户仍然能够正常浏览商品和下单。

分区容错性是指系统在网络分区(即节点之间的网络连接出现故障)的情况下,仍然能够继续运行。由于分布式系统通常由多个节点通过网络连接而成,网络故障是不可避免的,因此分区容错性是分布式系统必须具备的特性。

在实际应用中,由于网络故障是不可避免的,所以分区容错性是必须要满足的。因此,在设计分布式系统时,通常需要在一致性和可用性之间进行权衡。

如果选择一致性和分区容错性,那么在网络分区发生时,为了保证数据的一致性,系统可能会牺牲部分可用性。例如,在分布式数据库中,如果发生网络分区,系统可能会暂停部分节点的写操作,直到网络分区问题解决,以确保数据的一致性。

如果选择可用性和分区容错性,那么在网络分区发生时,系统会优先保证节点的可用性,但可能会导致数据的不一致。例如,在一些分布式缓存系统中,为了保证系统的高可用性,当发生网络分区时,各个节点可能会独立处理请求,从而导致数据的不一致。

说明最终一致性策略

最终一致性是一种弱一致性模型,它允许系统在一定时间内存在数据不一致的情况,但最终所有节点的数据会达到一致。最终一致性策略在分布式系统中被广泛应用,因为它在保证系统高可用性和可扩展性的同时,能够在一定程度上满足数据一致性的需求。

最终一致性策略的实现方式有多种,以下是一些常见的方法:

读修复:当客户端读取数据时,如果发现数据不一致,系统会自动触发修复机制,将最新的数据更新到其他节点。例如,在分布式数据库中,当客户端从某个节点读取到旧数据时,系统会从其他节点获取最新的数据,并将其更新到该节点。

写修复:在写操作完成后,系统会将更新后的数据异步地传播到其他节点。在传播过程中,可能会存在一定的延迟,但最终所有节点的数据会达到一致。例如,在分布式文件系统中,当客户端向某个节点写入数据时,系统会将该数据异步地复制到其他节点。

版本控制:通过为数据添加版本号,系统可以判断数据的新旧程度。当发生数据冲突时,系统可以根据版本号来决定采用哪个版本的数据。例如,在分布式版本控制系统中,每个文件都有一个版本号,当多个用户对同一个文件进行修改时,系统可以根据版本号来合并这些修改。

时间戳:使用时间戳来标记数据的更新时间,系统可以根据时间戳来判断数据的先后顺序。当发生数据冲突时,系统可以选择最新的数据。例如,在分布式日志系统中,每个日志记录都有一个时间戳,系统可以根据时间戳来排序和处理日志记录。

最终一致性策略的优点在于它能够提高系统的可用性和可扩展性,因为它允许系统在一定时间内存在数据不一致的情况,从而减少了对系统性能的影响。然而,最终一致性策略也存在一些缺点,例如在数据不一致的时间段内,可能会导致一些业务逻辑出现问题。因此,在使用最终一致性策略时,需要根据具体的业务需求来选择合适的实现方式。

在分布式系统中,各节点分别记录访问次数,若有一台节点失效,怎样保证访问次数数据的可靠性

在分布式系统中,各节点分别记录访问次数,当有一台节点失效时,可采用以下几种方法来保证访问次数数据的可靠性。

数据备份与复制:可以将每个节点的访问次数数据复制到多个其他节点上。例如,使用主从复制的方式,每个节点作为主节点时,将访问次数数据同步到多个从节点。当主节点失效时,可从从节点中选择一个作为新的主节点,并继续记录访问次数。另外,还可以采用多副本复制的策略,将数据复制到多个不同地理位置的节点,以提高数据的可靠性和可用性。

日志记录与恢复:每个节点在记录访问次数的同时,将操作记录到日志文件中。当节点失效后,可通过日志文件来恢复数据。例如,在分布式文件系统中,每个节点会将文件的操作记录到日志中,当节点重启时,可以根据日志文件来恢复文件的状态。此外,还可以采用增量日志的方式,只记录自上次备份以来的操作,减少日志文件的大小和恢复时间。

分布式共识算法:使用分布式共识算法(如 Paxos、Raft 等)来确保多个节点之间的数据一致性。在记录访问次数时,节点需要通过共识算法达成一致,只有当多数节点同意后,才认为操作成功。例如,在 Raft 算法中,有一个领导者节点负责协调所有的写操作,当领导者节点收到客户端的写请求时,会将该请求复制到其他节点,并等待多数节点的确认。如果某个节点失效,系统会通过选举机制选出新的领导者节点,继续保证系统的正常运行。

定期数据同步:定期将各个节点的访问次数数据同步到一个中心节点或其他备份节点。例如,每天凌晨系统负载较低时,将所有节点的访问次数数据同步到一个备份服务器上。在同步过程中,可以采用增量同步的方式,只同步自上次同步以来发生变化的数据,减少同步的数据量和时间。

监控与自动恢复:建立完善的监控系统,实时监测各个节点的状态。当发现某个节点失效时,系统能够自动进行恢复操作。例如,当节点的 CPU 使用率、内存使用率或网络连接出现异常时,监控系统会及时发出警报,并自动尝试重启节点或切换到备用节点。

谈谈你对消息队列的了解

消息队列是一种在分布式系统中用于异步通信的中间件,它允许不同的组件或服务之间通过发送和接收消息来进行通信。消息队列的主要作用是解耦、异步处理和流量削峰。

解耦是指通过消息队列,发送方和接收方不需要直接进行交互,而是通过消息队列进行间接通信。这样可以降低组件之间的耦合度,提高系统的可维护性和可扩展性。例如,在一个电商系统中,订单服务在创建订单后,可以将订单信息发送到消息队列中,而库存服务和物流服务则可以从消息队列中获取订单信息进行相应的处理,订单服务不需要关心库存服务和物流服务的具体实现。

异步处理是消息队列的另一个重要特性。当发送方将消息发送到消息队列后,不需要等待接收方处理完消息就可以继续执行其他任务,从而提高系统的处理效率。例如,在一个用户注册系统中,当用户提交注册信息后,系统可以将注册信息发送到消息队列中,然后立即返回注册成功的消息给用户,而注册信息的验证和存储等操作则可以由其他服务从消息队列中获取消息并进行处理。

流量削峰是指在高并发场景下,消息队列可以作为一个缓冲区,将大量的请求暂时存储起来,然后按照系统的处理能力逐步处理这些请求,从而避免系统因瞬间的高流量而崩溃。例如,在电商系统的促销活动期间,会有大量的用户同时下单,消息队列可以将这些订单请求存储起来,然后按照系统的处理能力依次处理,保证系统的稳定性。

常见的消息队列有 RabbitMQ、Kafka、ActiveMQ 等。RabbitMQ 是一个功能强大的消息队列,它支持多种消息协议,具有高可靠性和灵活性,适用于对消息可靠性要求较高的场景。Kafka 是一个高性能的分布式消息队列,它具有高吞吐量、低延迟的特点,适用于大数据处理和实时流处理等场景。ActiveMQ 是一个开源的消息队列,它支持多种消息传输协议,具有较好的兼容性和易用性。

消息队列的实现通常涉及到生产者、消费者和消息队列服务器三个角色。生产者负责将消息发送到消息队列中,消费者则从消息队列中获取消息并进行处理,消息队列服务器负责存储和管理消息。在使用消息队列时,需要考虑消息的顺序性、可靠性、幂等性等问题。例如,在某些业务场景中,需要保证消息的顺序性,即消息的处理顺序与发送顺序一致;在某些场景中,需要保证消息的可靠性,即消息不会丢失或重复处理;在某些场景中,需要保证消息的幂等性,即多次处理同一个消息不会产生额外的影响。

客户端到服务端的底层流程是怎样的

客户端到服务端的通信在底层涉及多个步骤,以基于 TCP/IP 协议的网络通信为例,下面详细阐述其流程。

客户端侧流程

客户端首先要创建一个套接字(socket),这是网络通信的基础,它为后续的通信提供了一个接口。接着,客户端需要指定服务端的地址和端口号,这是连接服务端的关键信息。随后,客户端调用 connect 函数尝试与服务端建立连接。在这个过程中,客户端会向服务端发送一个 SYN(同步)包,用于同步初始序列号。

当服务端收到 SYN 包后,会返回一个 SYN - ACK(同步确认)包,这表示服务端同意建立连接并确认了客户端的序列号。客户端接收到 SYN - ACK 包后,会再发送一个 ACK(确认)包给服务端,至此,TCP 三次握手完成,客户端与服务端之间的连接成功建立。

连接建立后,客户端就可以通过 send 函数向服务端发送数据。数据在发送前会被封装成 TCP 报文段,添加 TCP 头部信息,如源端口、目的端口、序列号等。然后,TCP 报文段会被进一步封装成 IP 数据报,添加 IP 头部信息,如源 IP 地址、目的 IP 地址等。最后,IP 数据报通过网络接口发送到网络中。

当客户端完成数据发送后,可以调用 close 函数关闭连接。在关闭连接时,客户端会发送一个 FIN(结束)包给服务端,表示请求关闭连接。服务端收到 FIN 包后,会返回一个 ACK 包进行确认,然后服务端也会发送一个 FIN 包给客户端,表示自己也准备关闭连接。客户端收到服务端的 FIN 包后,会再发送一个 ACK 包进行确认,至此,TCP 四次挥手完成,连接正式关闭。

服务端侧流程

服务端同样需要先创建一个套接字,然后将该套接字绑定到指定的地址和端口上,这样客户端才能找到服务端。接着,服务端调用 listen 函数开始监听指定的端口,等待客户端的连接请求。

当服务端收到客户端的 SYN 包后,会进行一系列的处理,然后返回 SYN - ACK 包,同意建立连接。在收到客户端的 ACK 包后,连接正式建立。服务端可以通过 accept 函数接受客户端的连接,并返回一个新的套接字,用于与该客户端进行通信。

服务端通过 recv 函数接收客户端发送的数据。在接收数据时,服务端会从网络中接收 IP 数据报,然后解封装出 TCP 报文段,再从 TCP 报文段中提取出应用层数据。

当服务端完成数据处理后,也可以调用 send 函数向客户端发送响应数据。最后,当服务端需要关闭连接时,会参与 TCP 四次挥手过程,关闭与客户端的连接。

客户端与服务端之间的消息如何处理

客户端与服务端之间的消息处理是一个复杂的过程,涉及多个环节,下面从消息的发送、传输、接收和处理几个方面进行详细说明。

消息发送

客户端在需要向服务端发送消息时,首先要将消息进行编码。编码的目的是将应用层的数据转换为适合在网络中传输的格式,常见的编码方式有 JSON、XML 等。例如,在一个 Web 应用中,客户端可能会将用户的请求信息封装成 JSON 格式的字符串。

编码完成后,客户端会调用套接字的 send 函数将消息发送出去。在发送过程中,消息会被封装成 TCP 报文段,添加 TCP 头部信息,如源端口、目的端口、序列号等。然后,TCP 报文段会被进一步封装成 IP 数据报,添加 IP 头部信息,如源 IP 地址、目的 IP 地址等。

消息传输

消息在网络中传输时,会经过多个网络节点,如路由器、交换机等。这些网络节点会根据 IP 头部信息中的目的 IP 地址,将 IP 数据报转发到合适的下一跳节点,直到数据报到达服务端所在的网络。

在传输过程中,可能会遇到各种问题,如网络拥塞、丢包等。TCP 协议会通过一系列的机制来保证消息的可靠传输,如重传机制、滑动窗口机制等。如果某个 TCP 报文段在传输过程中丢失,接收方会通过发送 ACK 包来告知发送方需要重传该报文段。

消息接收

服务端通过套接字的 recv 函数接收客户端发送的消息。在接收过程中,服务端会从网络中接收 IP 数据报,然后解封装出 TCP 报文段,再从 TCP 报文段中提取出应用层数据。

由于 TCP 是面向字节流的协议,服务端接收到的消息可能是不完整的,或者包含多个消息。因此,服务端需要进行消息的拆分和解析。常见的消息拆分方式有固定长度、分隔符等。例如,如果消息采用固定长度的方式进行传输,服务端可以根据固定长度来拆分消息;如果消息采用分隔符的方式进行传输,服务端可以根据分隔符来拆分消息。

消息处理

服务端接收到完整的消息后,会对消息进行解码。解码的过程是将网络传输的数据转换为应用层可以理解的格式,例如将 JSON 格式的字符串解析为对象。

解码完成后,服务端会根据消息的内容进行相应的处理。例如,如果客户端发送的是一个查询请求,服务端会根据请求的内容查询数据库,并将查询结果封装成消息返回给客户端。

服务端在处理完消息后,会将响应消息进行编码,然后调用套接字的 send 函数将响应消息发送给客户端。客户端接收到服务端的响应消息后,会进行类似的解码和处理过程。

简述 TCP 粘包问题,以及在程序里如何处理

TCP 粘包问题概述

TCP 是面向字节流的传输协议,这意味着在数据传输过程中,TCP 并不关心应用层的数据边界,它只是将应用层的数据依次发送到网络中。因此,在接收端,可能会出现多个应用层消息被粘在一起接收的情况,这就是 TCP 粘包问题。

TCP 粘包问题主要由以下几个原因导致:

  • Nagle 算法:Nagle 算法是为了减少网络中的小数据包而设计的。它会将多个小的数据包合并成一个大的数据包进行发送,从而减少网络开销。但这可能会导致多个应用层消息被合并在一起发送,从而产生粘包问题。
  • TCP 滑动窗口:TCP 滑动窗口机制允许发送方在没有收到接收方确认的情况下,连续发送多个数据包。当发送方发送多个应用层消息时,这些消息可能会被封装在同一个 TCP 数据包中发送,从而导致粘包问题。
  • 接收方处理不及时:如果接收方处理数据的速度较慢,可能会导致多个 TCP 数据包在接收缓冲区中积累,当接收方读取数据时,就会一次性读取多个应用层消息,从而产生粘包问题。
程序中处理 TCP 粘包问题的方法
  • 固定长度法:这种方法要求每个应用层消息的长度是固定的。发送方在发送消息时,如果消息长度不足固定长度,会进行填充;接收方在接收消息时,会按照固定长度来拆分消息。例如,规定每个消息的长度为 100 字节,如果实际消息长度为 50 字节,发送方会在消息后面填充 50 个字节的空字符。接收方每次读取 100 字节的数据,然后进行处理。
// 发送方示例
const int MESSAGE_LENGTH = 100;
char message[MESSAGE_LENGTH];
// 填充消息内容
strcpy(message, "Hello, Server!");
// 不足部分填充
memset(message + strlen(message), '\0', MESSAGE_LENGTH - strlen(message));
send(socket, message, MESSAGE_LENGTH, 0);// 接收方示例
char buffer[MESSAGE_LENGTH];
recv(socket, buffer, MESSAGE_LENGTH, 0);
// 处理消息
  • 分隔符法:这种方法在每个应用层消息的末尾添加一个特定的分隔符,接收方在接收数据时,根据分隔符来拆分消息。例如,使用换行符 \n 作为分隔符,发送方在每个消息的末尾添加 \n,接收方在接收数据时,根据 \n 来拆分消息。
// 发送方示例
std::string message = "Hello, Server!";
message += '\n';
send(socket, message.c_str(), message.length(), 0);// 接收方示例
char buffer[1024];
int recvSize = recv(socket, buffer, sizeof(buffer), 0);
std::string receivedData(buffer, recvSize);
size_t pos = 0;
while ((pos = receivedData.find('\n')) != std::string::npos) {std::string singleMessage = receivedData.substr(0, pos);// 处理单个消息receivedData.erase(0, pos + 1);
}
  • 消息头 + 消息体法:这种方法在每个应用层消息的前面添加一个消息头,消息头中包含消息体的长度信息。接收方在接收数据时,首先读取消息头,获取消息体的长度,然后根据长度读取消息体。
// 定义消息结构体
struct Message {int length;char data[1024];
};// 发送方示例
Message msg;
std::string message = "Hello, Server!";
msg.length = message.length();
strcpy(msg.data, message.c_str());
send(socket, &msg, sizeof(int) + msg.length, 0);// 接收方示例
Message recvMsg;
recv(socket, &recvMsg.length, sizeof(int), 0);
recv(socket, recvMsg.data, recvMsg.length, 0);
// 处理消息

说明 socket 和 TCP 的联系,socket 工作在哪一层

socket 和 TCP 的联系

Socket 是一种网络编程接口,它为应用程序提供了一种方便的方式来进行网络通信。而 TCP(传输控制协议)是一种面向连接的、可靠的传输层协议。Socket 和 TCP 之间存在着密切的联系。

在基于 TCP 的网络通信中,Socket 是 TCP 协议的上层抽象。应用程序通过调用 Socket 提供的接口来使用 TCP 协议进行数据传输。例如,应用程序可以使用 Socket 的 socket 函数创建一个套接字,然后使用 connect 函数与服务端建立 TCP 连接,使用 send 函数发送数据,使用 recv 函数接收数据,最后使用 close 函数关闭连接。

Socket 可以基于不同的传输层协议,如 TCP 或 UDP(用户数据报协议)。当使用 TCP 作为传输层协议时,Socket 提供了面向连接、可靠的数据传输服务。而当使用 UDP 作为传输层协议时,Socket 提供了无连接、不可靠的数据传输服务。

socket 工作的层次

Socket 并不是严格意义上工作在某一层,它是一个跨层的概念。从应用程序的角度来看,Socket 是应用层和传输层之间的接口,应用程序通过调用 Socket 接口来使用传输层协议进行数据传输。

从网络协议栈的角度来看,Socket 提供了一种机制,使得应用程序可以方便地与传输层进行交互。虽然 Socket 本身并没有明确的层次划分,但它主要与传输层协议进行交互,因此可以说 Socket 与传输层密切相关。

在实际的网络通信中,当应用程序使用 Socket 进行数据传输时,数据会从应用层通过 Socket 接口传递到传输层,在传输层进行封装(如添加 TCP 头部信息),然后再传递到网络层进行进一步的封装(如添加 IP 头部信息),最后通过物理层发送到网络中。

一方发送数据,另一方不调用 recv 接收,中间会发生什么报文

当一方发送数据,另一方不调用 recv 接收时,中间会涉及到一系列的 TCP 报文交互,下面分不同阶段进行详细说明。

数据发送阶段

发送方调用 send 函数发送数据后,数据会被封装成 TCP 报文段发送到网络中。TCP 报文段包含了序列号、确认号、窗口大小等信息。发送方会根据 TCP 协议的规定,将数据分割成合适大小的报文段进行发送。

接收方缓冲区处理阶段

当接收方的网络接口接收到 TCP 报文段后,会将其放入接收缓冲区中。由于接收方没有调用 recv 函数来读取缓冲区中的数据,随着发送方不断发送数据,接收缓冲区会逐渐被填满。

当接收缓冲区快满时,接收方会通过 TCP 报文段中的窗口大小字段来通知发送方减小发送窗口的大小,从而限制发送方的发送速率。例如,接收方可能会发送一个 ACK 报文,其中窗口大小字段的值较小,告诉发送方只能发送少量的数据。

发送方重传阶段

如果发送方在一定时间内没有收到接收方对某个报文段的确认(ACK),发送方会认为该报文段丢失,会进行重传。这是 TCP 协议的重传机制,用于保证数据的可靠传输。

接收方缓冲区溢出阶段

如果接收方长时间不调用 recv 函数,接收缓冲区最终会溢出。当接收缓冲区溢出时,接收方会丢弃新到达的 TCP 报文段,并通过发送一个 RST(复位)报文来通知发送方关闭连接。

发送方收到 RST 报文后,会立即关闭连接,并释放相关的资源。整个过程中,TCP 协议通过一系列的机制来保证数据的可靠传输和连接的稳定性,但如果接收方长时间不处理数据,最终会导致连接的关闭。

例如,以下是一个简单的伪代码示例,模拟发送方发送数据和接收方不接收数据的情况:

// 发送方代码
#include <iostream>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock == -1) {std::cerr << "Failed to create socket" << std::endl;return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);if (connect(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {std::cerr << "Failed to connect to server" << std::endl;close(sock);return 1;}const char* message = "Hello, Server!";for (int i = 0; i < 1000; ++i) {send(sock, message, strlen(message), 0);}close(sock);return 0;
}// 接收方代码(不调用 recv)
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>int main() {int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock == -1) {std::cerr << "Failed to create socket" << std::endl;return 1;}sockaddr_in serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(8080);serverAddr.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {std::cerr << "Failed to bind socket" << std::endl;close(sock);return 1;}if (listen(sock, 5) == -1) {std::cerr << "Failed to listen on socket" << std::endl;close(sock);return 1;}int clientSock = accept(sock, nullptr, nullptr);if (clientSock == -1) {std::cerr << "Failed to accept client connection" << std::endl;close(sock);return 1;}// 不调用 recvsleep(60);close(clientSock);close(sock);return 0;
}

在这个示例中,发送方会不断发送数据,而接收方不调用 recv 函数,随着时间的推移,接收方的缓冲区会溢出,最终可能会发送 RST 报文关闭连接。

对于社交软件(如微博、知乎)的消息及针对消息的回复,应使用什么数据结构来存储,以及如何存储这些大量信息

对于社交软件的消息及回复,需要选择合适的数据结构和存储方式来高效处理和存储大量信息。

数据结构选择
  • 树形结构:对于消息及其回复,树形结构是一个很好的选择。可以将每条消息作为树的根节点,针对该消息的回复作为子节点,以此类推。这样的结构能够清晰地展示消息之间的层次关系,方便用户查看和导航。例如,在知乎的问题 - 回答 - 评论体系中,问题就是根节点,回答是一级子节点,评论则是二级或多级子节点。
  • 哈希表:使用哈希表可以快速定位到特定的消息。以消息的唯一标识(如消息 ID)作为键,消息的详细信息(包括消息内容、发布时间、作者等)作为值。这样可以在 \(O(1)\) 的时间复杂度内查找消息。
存储方式
  • 数据库存储
    • 关系型数据库(如 MySQL):适合存储结构化的数据。可以创建多个表,如 messages 表存储消息的基本信息,replies 表存储回复信息,通过外键关联消息和回复。例如,messages 表包含字段 message_iduser_idcontenttimestamp 等,replies 表包含字段 reply_idmessage_iduser_idcontenttimestamp 等。
    • 非关系型数据库(如 MongoDB):对于大量的非结构化或半结构化数据,MongoDB 是一个不错的选择。可以将每条消息及其回复存储为一个文档,文档中可以嵌套存储回复信息。这样可以方便地处理不同层次的回复,并且具有较好的扩展性。
  • 缓存存储:使用缓存(如 Redis)可以提高数据的访问速度。将热门消息及其回复存储在缓存中,当用户请求这些消息时,直接从缓存中获取,减少数据库的访问压力。当缓存中没有所需数据时,再从数据库中获取并更新缓存。
优化策略
  • 数据分区:对于海量数据,可以按照时间、用户等维度进行分区存储。例如,按照月份将消息数据存储在不同的表或集合中,这样可以减少每次查询时需要扫描的数据量。
  • 异步处理:对于消息的发布和回复操作,可以采用异步处理的方式。将用户的操作请求放入消息队列中,由后台任务进行实际的存储操作,提高系统的响应速度。

阐述 C++ 智能指针 shared_ptr 和 unique_ptr 的应用场景

在 C++ 中,智能指针是一种用于管理动态分配内存的工具,能够自动释放不再使用的内存,避免内存泄漏。shared_ptr 和 unique_ptr 是两种常用的智能指针,它们具有不同的应用场景。

shared_ptr 的应用场景
  • 多个对象共享同一资源:当多个对象需要共享同一资源时,shared_ptr 是一个很好的选择。shared_ptr 使用引用计数来管理资源的生命周期,当有新的 shared_ptr 指向该资源时,引用计数加 1;当 shared_ptr 被销毁或重置时,引用计数减 1。当引用计数为 0 时,资源被自动释放。例如,在一个图形处理系统中,多个图形对象可能需要共享同一纹理资源,此时可以使用 shared_ptr 来管理纹理资源。
#include <iostream>
#include <memory>class Texture {
public:Texture() { std::cout << "Texture created" << std::endl; }~Texture() { std::cout << "Texture destroyed" << std::endl; }
};void func(std::shared_ptr<Texture> tex) {// 使用纹理资源
}int main() {std::shared_ptr<Texture> texture = std::make_shared<Texture>();func(texture);// 其他对象也可以使用 texturereturn 0;
}
  • 容器中存储动态分配的对象:当需要在容器(如 std::vector)中存储动态分配的对象时,使用 shared_ptr 可以避免手动管理内存。例如:
#include <iostream>
#include <memory>
#include <vector>class Object {
public:Object() { std::cout << "Object created" << std::endl; }~Object() { std::cout << "Object destroyed" << std::endl; }
};int main() {std::vector<std::shared_ptr<Object>> objects;objects.push_back(std::make_shared<Object>());objects.push_back(std::make_shared<Object>());return 0;
}
unique_ptr 的应用场景
  • 独占资源所有权:当一个对象需要独占某个资源的所有权时,使用 unique_ptrunique_ptr 不允许复制,只能通过移动语义来转移资源的所有权。例如,在一个文件处理系统中,每个文件对象应该独占文件资源的所有权,此时可以使用 unique_ptr 来管理文件资源。
#include <iostream>
#include <memory>
#include <fstream>void processFile(std::unique_ptr<std::ifstream> file) {// 处理文件
}int main() {std::unique_ptr<std::ifstream> file = std::make_unique<std::ifstream>("test.txt");processFile(std::move(file));return 0;
}
  • 避免资源共享带来的问题:在某些情况下,资源的共享可能会导致复杂的同步问题或意外的资源释放。使用 unique_ptr 可以避免这些问题,确保资源的所有权清晰。

实现一个支持 +、-、* 运算的算术表达式

以下是一个简单的 C++ 程序,用于实现一个支持 +、-、* 运算的算术表达式。

#include <iostream>
#include <stack>
#include <string>
#include <cctype>// 定义运算符优先级
int precedence(char op) {if (op == '+' || op == '-')return 1;if (op == '*')return 2;return 0;
}// 执行运算
int applyOp(int a, int b, char op) {switch (op) {case '+': return a + b;case '-': return a - b;case '*': return a * b;}return 0;
}// 计算表达式的值
int evaluate(const std::string& expression) {std::stack<int> values;std::stack<char> ops;for (size_t i = 0; i < expression.length(); i++) {// 如果是空格,跳过if (expression[i] == ' ')continue;// 如果是数字,提取数字if (isdigit(expression[i])) {int val = 0;while (i < expression.length() && isdigit(expression[i])) {val = (val * 10) + (expression[i] - '0');i++;}i--;values.push(val);}// 如果是左括号,压入运算符栈else if (expression[i] == '(') {ops.push(expression[i]);}// 如果是右括号,计算括号内的表达式else if (expression[i] == ')') {while (!ops.empty() && ops.top() != '(') {int val2 = values.top();values.pop();int val1 = values.top();values.pop();char op = ops.top();ops.pop();values.push(applyOp(val1, val2, op));}if (!ops.empty())ops.pop();}// 如果是运算符else {while (!ops.empty() && precedence(ops.top()) >= precedence(expression[i])) {int val2 = values.top();values.pop();int val1 = values.top();values.pop();char op = ops.top();ops.pop();values.push(applyOp(val1, val2, op));}ops.push(expression[i]);}}// 处理剩余的运算符while (!ops.empty()) {int val2 = values.top();values.pop();int val1 = values.top();values.pop();char op = ops.top();ops.pop();values.push(applyOp(val1, val2, op));}return values.top();
}int main() {std::string expression = "3 + 5 * 2 - 4";int result = evaluate(expression);std::cout << "Result: " << result << std::endl;return 0;
}

这个程序使用两个栈,一个用于存储操作数,另一个用于存储运算符。通过遍历表达式,根据运算符的优先级进行计算,最终得到表达式的值。

实现一个能记录最大值的栈

以下是一个实现能记录最大值的栈的 C++ 代码:

#include <iostream>
#include <stack>class MaxStack {
private:std::stack<int> dataStack;std::stack<int> maxStack;public:// 入栈操作void push(int value) {dataStack.push(value);if (maxStack.empty() || value >= maxStack.top()) {maxStack.push(value);}}// 出栈操作void pop() {if (!dataStack.empty()) {if (dataStack.top() == maxStack.top()) {maxStack.pop();}dataStack.pop();}}// 获取栈顶元素int top() {if (!dataStack.empty()) {return dataStack.top();}return -1; // 表示栈为空}// 获取栈中的最大值int getMax() {if (!maxStack.empty()) {return maxStack.top();}return -1; // 表示栈为空}// 判断栈是否为空bool empty() {return dataStack.empty();}
};int main() {MaxStack stack;stack.push(3);stack.push(5);stack.push(2);std::cout << "Max: " << stack.getMax() << std::endl;stack.pop();std::cout << "Max: " << stack.getMax() << std::endl;return 0;
}

这个 MaxStack 类使用两个栈来实现。dataStack 用于存储实际的元素,maxStack 用于记录每个时刻栈中的最大值。在入栈操作时,如果新元素大于等于 maxStack 的栈顶元素,则将新元素也压入 maxStack;在出栈操作时,如果 dataStack 的栈顶元素等于 maxStack 的栈顶元素,则将 maxStack 的栈顶元素也弹出。这样,maxStack 的栈顶元素始终是当前栈中的最大值。

给出一个内存分配函数,找出其中的错误

假设给出以下内存分配函数:

#include <iostream>void* allocateMemory(size_t size) {void* ptr = malloc(size);if (ptr == nullptr) {std::cerr << "Memory allocation failed!" << std::endl;// 这里没有返回值,会导致未定义行为}return ptr;
}int main() {int* arr = static_cast<int*>(allocateMemory(10 * sizeof(int)));if (arr != nullptr) {for (int i = 0; i < 10; i++) {arr[i] = i;}for (int i = 0; i < 10; i++) {std::cout << arr[i] << " ";}std::cout << std::endl;free(arr);}return 0;
}

这个内存分配函数存在以下错误:

  • 缺少返回值:当 malloc 分配内存失败时,函数会输出错误信息,但没有返回值。在 C++ 中,函数必须有明确的返回值,否则会导致未定义行为。可以在错误处理部分添加 return nullptr; 来解决这个问题。
void* allocateMemory(size_t size) {void* ptr = malloc(size);if (ptr == nullptr) {std::cerr << "Memory allocation failed!" << std::endl;return nullptr;}return ptr;
}
  • 异常安全性问题:使用 malloc 进行内存分配时,不会调用对象的构造函数,也不会抛出异常。如果需要分配对象的内存,建议使用 new 运算符,它会自动调用对象的构造函数,并且在内存分配失败时会抛出 std::bad_alloc 异常。
#include <iostream>
#include <new>template <typename T>
T* allocateMemory(size_t count) {try {return new T[count];} catch (const std::bad_alloc& e) {std::cerr << "Memory allocation failed: " << e.what() << std::endl;return nullptr;}
}int main() {int* arr = allocateMemory<int>(10);if (arr != nullptr) {for (int i = 0; i < 10; i++) {arr[i] = i;}for (int i = 0; i < 10; i++) {std::cout << arr[i] << " ";}std::cout << std::endl;delete[] arr;}return 0;
}

这样可以提高代码的异常安全性。

说明 delete [] 和 delete 的区别

在 C++ 里,delete [] 和 delete 均用于释放动态分配的内存,不过它们的使用场景存在显著差异。

delete 主要用于释放通过 new 操作符分配的单个对象的内存。当使用 new 创建单个对象时,系统会为该对象分配一块合适大小的内存,并调用其构造函数。在对象使用完毕后,使用 delete 操作符来释放这块内存,同时会调用对象的析构函数。例如:

class MyClass {
public:MyClass() { std::cout << "Constructor called" << std::endl; }~MyClass() { std::cout << "Destructor called" << std::endl; }
};int main() {MyClass* obj = new MyClass();delete obj;return 0;
}

在上述代码中,new MyClass() 分配了一个 MyClass 对象的内存并调用其构造函数,delete obj 释放该对象的内存并调用其析构函数。

delete [] 则用于释放通过 new [] 操作符分配的数组的内存。当使用 new [] 创建对象数组时,系统会为整个数组分配连续的内存空间,并依次调用每个对象的构造函数。在数组使用完毕后,必须使用 delete [] 来释放这块内存,它会依次调用数组中每个对象的析构函数,然后释放整个数组的内存。例如:

class MyClass {
public:MyClass() { std::cout << "Constructor called" << std::endl; }~MyClass() { std::cout << "Destructor called" << std::endl; }
};int main() {MyClass* arr = new MyClass[3];delete [] arr;return 0;
}

在这个例子中,new MyClass[3] 分配了一个包含 3 个 MyClass 对象的数组的内存,并依次调用每个对象的构造函数,delete [] arr 依次调用每个对象的析构函数,然后释放整个数组的内存。

若使用 delete 来释放通过 new [] 分配的数组内存,只会调用数组中第一个对象的析构函数,而其他对象的析构函数不会被调用,这会造成内存泄漏和未定义行为。同样,若使用 delete [] 来释放通过 new 分配的单个对象的内存,也会引发未定义行为。因此,在释放动态分配的内存时,务必确保使用正确的 delete 形式。

解释 unique_ptr 的底层实现原理

unique_ptr 是 C++ 标准库提供的一种智能指针,其主要作用是独占所指向对象的所有权,保证在其生命周期结束时自动释放所管理的对象,从而避免内存泄漏。

unique_ptr 的底层实现基于 RAII(资源获取即初始化)原则。当创建一个 unique_ptr 对象时,它会在构造函数中获取所管理对象的所有权,而在析构函数中释放该对象的内存。

unique_ptr 采用了移动语义来实现独占所有权。它不允许进行拷贝构造和拷贝赋值操作,因为一旦允许拷贝,就会出现多个 unique_ptr 指向同一个对象的情况,这与独占所有权的原则相违背。例如:

#include <memory>int main() {std::unique_ptr<int> ptr1(new int(10));// 下面这行代码会编译错误,因为 unique_ptr 不允许拷贝// std::unique_ptr<int> ptr2 = ptr1; return 0;
}

不过,unique_ptr 支持移动构造和移动赋值操作。通过移动语义,可以将一个 unique_ptr 的所有权转移到另一个 unique_ptr 中。例如:

#include <memory>int main() {std::unique_ptr<int> ptr1(new int(10));std::unique_ptr<int> ptr2 = std::move(ptr1);// 此时 ptr1 为空,ptr2 拥有对象的所有权return 0;
}

在移动操作中,原 unique_ptr 会放弃对对象的所有权,将其转移给新的 unique_ptr,同时原 unique_ptr 会被置为空。

unique_ptr 还提供了一些成员函数来管理所指向的对象,如 get() 用于获取所指向对象的原始指针,release() 用于释放对对象的所有权并返回原始指针,reset() 用于重置 unique_ptr 所指向的对象。

介绍三种智能指针及其各自的功能

C++ 标准库提供了三种主要的智能指针:unique_ptrshared_ptr 和 weak_ptr,它们各自具有不同的功能和应用场景。

unique_ptr 具有独占所有权的特性,它确保同一时间只有一个 unique_ptr 可以指向某个对象。当 unique_ptr 被销毁或者重置时,它所管理的对象也会被自动释放。unique_ptr 不允许拷贝构造和拷贝赋值,但支持移动构造和移动赋值。这种特性使得 unique_ptr 非常适合用于管理那些独占资源的对象,比如文件句柄、网络连接等。例如:

#include <memory>
#include <fstream>int main() {std::unique_ptr<std::ifstream> file = std::make_unique<std::ifstream>("test.txt");// 其他操作return 0;
}

在这个例子中,file 独占对文件的所有权,当 file 离开作用域时,文件会被自动关闭。

shared_ptr 采用引用计数的方式来管理对象的生命周期。多个 shared_ptr 可以共享同一个对象的所有权,每当有新的 shared_ptr 指向该对象时,引用计数加 1;当 shared_ptr 被销毁或者重置时,引用计数减 1。当引用计数变为 0 时,对象会被自动释放。shared_ptr 适用于多个对象需要共享同一资源的场景,例如在一个图形处理系统中,多个图形对象可能需要共享同一纹理资源。例如:

#include <memory>class Texture {
public:Texture() { std::cout << "Texture created" << std::endl; }~Texture() { std::cout << "Texture destroyed" << std::endl; }
};int main() {std::shared_ptr<Texture> tex1 = std::make_shared<Texture>();std::shared_ptr<Texture> tex2 = tex1;// 此时 tex1 和 tex2 共享同一个 Texture 对象return 0;
}

weak_ptr 是一种弱引用智能指针,它主要用于解决 shared_ptr 可能出现的循环引用问题。weak_ptr 可以指向 shared_ptr 所管理的对象,但不会增加对象的引用计数。weak_ptr 通常需要通过 lock() 函数来获取一个 shared_ptr,从而访问所指向的对象。例如:

#include <memory>
#include <iostream>class B;class A {
public:std::weak_ptr<B> b_ptr;~A() { std::cout << "A destroyed" << std::endl; }
};class B {
public:std::weak_ptr<A> a_ptr;~B() { std::cout << "B destroyed" << std::endl; }
};int main() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;return 0;
}

在这个例子中,A 和 B 类之间使用 weak_ptr 相互引用,避免了循环引用导致的内存泄漏问题。

编写 strcmp 函数

strcmp 函数用于比较两个字符串的大小,它会按照字典序比较两个字符串,若两个字符串相等则返回 0,若第一个字符串小于第二个字符串则返回一个负数,若第一个字符串大于第二个字符串则返回一个正数。以下是 strcmp 函数的实现:

#include <iostream>int strcmp(const char* str1, const char* str2) {while (*str1 && *str2 && *str1 == *str2) {str1++;str2++;}return *str1 - *str2;
}int main() {const char* str1 = "abc";const char* str2 = "abd";int result = strcmp(str1, str2);if (result == 0) {std::cout << "Strings are equal" << std::endl;} else if (result < 0) {std::cout << "str1 is less than str2" << std::endl;} else {std::cout << "str1 is greater than str2" << std::endl;}return 0;
}

在这个实现中,通过一个 while 循环逐个比较两个字符串的字符,直到遇到不同的字符或者字符串结束符 '\0'。最后返回两个不同字符的 ASCII 码差值。

实现单链表的逆序操作

单链表的逆序操作可以通过迭代或者递归的方式实现。以下是迭代方式的实现:

#include <iostream>// 定义单链表节点结构
struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};// 迭代实现单链表逆序
ListNode* reverseList(ListNode* head) {ListNode* prev = nullptr;ListNode* curr = head;while (curr != nullptr) {ListNode* nextTemp = curr->next;curr->next = prev;prev = curr;curr = nextTemp;}return prev;
}// 打印链表
void printList(ListNode* head) {ListNode* curr = head;while (curr != nullptr) {std::cout << curr->val << " ";curr = curr->next;}std::cout << std::endl;
}int main() {// 创建链表 1->2->3->4->5ListNode* 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 list: ";printList(head);ListNode* reversedHead = reverseList(head);std::cout << "Reversed list: ";printList(reversedHead);// 释放链表内存while (reversedHead != nullptr) {ListNode* temp = reversedHead;reversedHead = reversedHead->next;delete temp;}return 0;
}

在迭代实现中,使用三个指针 prevcurr 和 nextTemp 来遍历链表。prev 指针用于记录当前节点的前一个节点,curr 指针用于遍历链表,nextTemp 指针用于保存当前节点的下一个节点。在遍历过程中,将当前节点的 next 指针指向前一个节点,然后更新 prev 和 curr 指针。最后返回新的头节点 prev

递归实现的思路是先递归地反转当前节点的后续节点,然后将当前节点的后续节点的 next 指针指向当前节点,同时将当前节点的 next 指针置为空。以下是递归实现的代码:

#include <iostream>// 定义单链表节点结构
struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};// 递归实现单链表逆序
ListNode* reverseList(ListNode* head) {if (head == nullptr || head->next == nullptr) {return head;}ListNode* newHead = reverseList(head->next);head->next->next = head;head->next = nullptr;return newHead;
}// 打印链表
void printList(ListNode* head) {ListNode* curr = head;while (curr != nullptr) {std::cout << curr->val << " ";curr = curr->next;}std::cout << std::endl;
}int main() {// 创建链表 1->2->3->4->5ListNode* 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 list: ";printList(head);ListNode* reversedHead = reverseList(head);std::cout << "Reversed list: ";printList(reversedHead);// 释放链表内存while (reversedHead != nullptr) {ListNode* temp = reversedHead;reversedHead = reversedHead->next;delete temp;}return 0;
}

递归实现的代码相对简洁,但在处理大规模链表时可能会导致栈溢出问题。

分别用 C 和 C++ 实现数组删除某一个元素的功能

C 语言实现

在 C 语言里,数组的长度是固定的,要删除数组中的某个元素,需要把该元素之后的所有元素往前移动一位,再减少数组的有效长度。以下是一个示例代码:

#include <stdio.h>// 删除数组中指定位置的元素
void deleteElement(int arr[], int *size, int index) {if (index < 0 || index >= *size) {return;}for (int i = index; i < *size - 1; i++) {arr[i] = arr[i + 1];}(*size)--;
}int main() {int arr[] = {1, 2, 3, 4, 5};int size = sizeof(arr) / sizeof(arr[0]);int index = 2;printf("Before deletion: ");for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");deleteElement(arr, &size, index);printf("After deletion: ");for (int i = 0; i < size; i++) {printf("%d ", arr[i]);}printf("\n");return 0;
}

在这个代码中,deleteElement 函数接收数组、数组大小的指针以及要删除元素的索引作为参数。如果索引有效,就把该索引之后的元素依次往前移动一位,最后减小数组的大小。

C++ 实现

在 C++ 中,可以使用标准库的容器(如 std::vector)来更方便地实现数组元素的删除。std::vector 是动态数组,它提供了 erase 方法来删除指定位置的元素。示例代码如下:

#include <iostream>
#include <vector>int main() {std::vector<int> vec = {1, 2, 3, 4, 5};int index = 2;std::cout << "Before deletion: ";for (int num : vec) {std::cout << num << " ";}std::cout << std::endl;if (index >= 0 && index < vec.size()) {vec.erase(vec.begin() + index);}std::cout << "After deletion: ";for (int num : vec) {std::cout << num << " ";}std::cout << std::endl;return 0;
}

在这段代码中,std::vector 的 erase 方法接收一个迭代器作为参数,这里通过 vec.begin() + index 得到要删除元素的迭代器,然后调用 erase 方法删除该元素。

说明头文件中声明 static 变量和非 static 变量的区别

在头文件中声明 static 变量和非 static 变量存在显著区别,这些区别主要体现在作用域和链接属性上。

非 static 变量

在头文件中声明的非 static 变量具有外部链接属性。这意味着该变量可以在不同的源文件中被访问和使用。当多个源文件包含同一个声明了非 static 变量的头文件时,这些源文件实际上共享同一个变量。不过,为了避免重复定义的错误,通常需要在头文件中使用 extern 关键字进行声明,然后在某个源文件中进行定义。例如:

// header.h
extern int globalVar;// source1.c
#include "header.h"
int globalVar = 10;// source2.c
#include "header.h"
#include <stdio.h>int main() {printf("%d\n", globalVar);return 0;
}

在这个例子中,globalVar 在 header.h 中使用 extern 声明,在 source1.c 中进行定义,source2.c 可以访问并使用这个全局变量。

static 变量

在头文件中声明的 static 变量具有内部链接属性。每个包含该头文件的源文件都会拥有这个变量的一个独立副本。也就是说,不同源文件中的 static 变量是相互独立的,它们不会相互影响。例如:

// header.h
static int staticVar = 20;// source1.c
#include "header.h"
#include <stdio.h>void func1() {staticVar++;printf("source1: %d\n", staticVar);
}// source2.c
#include "header.h"
#include <stdio.h>void func2() {staticVar++;printf("source2: %d\n", staticVar);
}// main.c
#include <stdio.h>
#include "header.h"extern void func1();
extern void func2();int main() {func1();func2();return 0;
}

在这个例子中,source1.c 和 source2.c 都包含了 header.h,它们各自拥有 staticVar 的一个副本,func1 和 func2 对 staticVar 的修改不会相互影响。

阐述多态的实现方式

多态是面向对象编程的重要特性之一,它允许不同的对象对同一消息做出不同的响应。在 C++ 中,多态主要通过以下两种方式实现:

静态多态(编译时多态)

静态多态在编译阶段就确定了要调用的函数。它主要通过函数重载和模板来实现。

函数重载:在同一个作用域内,可以定义多个同名函数,但这些函数的参数列表必须不同(参数个数、参数类型或参数顺序不同)。编译器会根据调用函数时提供的实参来选择合适的函数进行调用。例如:

#include <iostream>int add(int a, int b) {return a + b;
}double add(double a, double b) {return a + b;
}int main() {int result1 = add(1, 2);double result2 = add(1.5, 2.5);std::cout << "Integer result: " << result1 << std::endl;std::cout << "Double result: " << result2 << std::endl;return 0;
}

在这个例子中,add 函数被重载,根据实参的类型,编译器会在编译时选择合适的 add 函数进行调用。

模板:模板是一种泛型编程的工具,它允许编写通用的代码,而不需要指定具体的数据类型。通过模板,可以实现函数模板和类模板。例如:

#include <iostream>template <typename T>
T add(T a, T b) {return a + b;
}int main() {int result1 = add(1, 2);double result2 = add(1.5, 2.5);std::cout << "Integer result: " << result1 << std::endl;std::cout << "Double result: " << result2 << std::endl;return 0;
}

在这个例子中,add 是一个函数模板,编译器会根据调用时的实参类型自动实例化出相应的函数。

动态多态(运行时多态)

动态多态在运行阶段才确定要调用的函数。它主要通过虚函数和继承来实现。

虚函数:在基类中使用 virtual 关键字声明的函数称为虚函数。当通过基类指针或引用调用虚函数时,会根据指针或引用所指向的对象的实际类型来决定调用哪个类的虚函数。例如:

#include <iostream>class Shape {
public:virtual void draw() {std::cout << "Drawing a shape." << std::endl;}
};class Circle : public Shape {
public:void draw() override {std::cout << "Drawing a circle." << std::endl;}
};class Rectangle : public Shape {
public:void draw() override {std::cout << "Drawing a rectangle." << std::endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw();shape2->draw();delete shape1;delete shape2;return 0;
}

在这个例子中,Shape 类的 draw 函数是虚函数,Circle 和 Rectangle 类重写了该虚函数。通过基类指针 shape1 和 shape2 调用 draw 函数时,会根据指针所指向的对象的实际类型来调用相应的 draw 函数。

介绍 stl 空间配置器

STL(标准模板库)空间配置器是 STL 中一个重要的组件,它负责管理内存的分配和释放。STL 容器(如 std::vectorstd::list 等)在需要内存时会通过空间配置器来获取,在不需要内存时会通过空间配置器来释放。

空间配置器的作用
  • 内存管理:空间配置器负责从系统中分配内存,并在不需要时将内存归还给系统。它可以根据容器的需求,动态地分配和释放内存,提高内存的使用效率。
  • 封装内存操作:空间配置器将内存的分配和释放操作封装起来,使得 STL 容器的实现与具体的内存管理细节分离。这样,容器的实现者可以专注于容器的功能,而不需要关心内存的分配和释放。
标准空间配置器

STL 提供了一个标准的空间配置器 std::allocator,它是一个模板类,可以用于分配任意类型的内存。std::allocator 的使用非常简单,大多数 STL 容器默认使用 std::allocator 作为空间配置器。例如:

#include <iostream>
#include <vector>
#include <memory>int main() {std::vector<int, std::allocator<int>> vec;vec.push_back(1);vec.push_back(2);vec.push_back(3);for (int num : vec) {std::cout << num << " ";}std::cout << std::endl;return 0;
}

在这个例子中,std::vector 使用 std::allocator<int> 作为空间配置器来分配和管理 int 类型的内存。

自定义空间配置器

除了标准空间配置器,用户还可以自定义空间配置器。自定义空间配置器需要满足一定的接口要求,例如需要提供 allocate 方法用于分配内存,deallocate 方法用于释放内存等。自定义空间配置器可以根据具体的需求进行优化,例如实现内存池、减少内存碎片等。例如:

#include <iostream>
#include <vector>
#include <memory>// 自定义空间配置器
template <typename T>
class MyAllocator {
public:using value_type = T;MyAllocator() = default;template <typename U>MyAllocator(const MyAllocator<U>&) {}T* allocate(std::size_t n) {return static_cast<T*>(::operator new(n * sizeof(T)));}void deallocate(T* p, std::size_t) {::operator delete(p);}
};int main() {std::vector<int, MyAllocator<int>> vec;vec.push_back(1);vec.push_back(2);vec.push_back(3);for (int num : vec) {std::cout << num << " ";}std::cout << std::endl;return 0;
}

在这个例子中,MyAllocator 是一个自定义的空间配置器,它实现了 allocate 和 deallocate 方法,用于分配和释放内存。

为什么要使用多进程

在软件开发中,使用多进程有诸多好处,下面从几个方面详细阐述:

提高系统资源利用率

现代计算机通常具有多个 CPU 核心,单进程程序只能利用一个 CPU 核心,而多进程可以将不同的任务分配到不同的 CPU 核心上并行执行,从而充分利用系统的多核资源,提高系统的整体性能。例如,在一个图像处理软件中,可以使用一个进程负责图像的读取和预处理,另一个进程负责图像的特效处理,这样可以大大缩短图像处理的时间。

增强系统的稳定性和可靠性

当一个进程出现崩溃或异常时,不会影响其他进程的正常运行。例如,在一个浏览器中,每个标签页可以作为一个独立的进程运行。如果某个标签页的进程崩溃,只会关闭该标签页,而不会导致整个浏览器崩溃,从而提高了系统的稳定性和可靠性。

方便进行任务管理和调度

多进程可以将不同的任务分离成独立的进程,每个进程可以有自己的优先级和调度策略。操作系统可以根据系统的负载情况和进程的优先级,合理地分配 CPU 时间片,从而实现更高效的任务管理和调度。例如,在一个服务器程序中,可以将一些耗时的任务(如数据备份、日志处理等)放在独立的进程中运行,避免影响其他重要任务的执行。

实现分布式计算

多进程可以在不同的计算机上运行,通过网络进行通信和协作,实现分布式计算。分布式计算可以将一个大型任务分解成多个小任务,分配到不同的计算机上并行执行,从而大大提高计算能力和处理速度。例如,在大数据处理、科学计算等领域,经常会使用分布式计算来处理海量数据。

隔离性和安全性

不同的进程之间具有独立的内存空间和系统资源,一个进程无法直接访问另一个进程的内存和资源,从而提供了一定的隔离性和安全性。例如,在一个操作系统中,不同用户的进程是相互隔离的,一个用户的进程无法访问其他用户的进程的资源,从而保护了用户的隐私和数据安全。

列举进程间通信的方式

进程间通信(Inter - Process Communication,IPC)是指在不同进程之间传播或交换信息的技术。以下是常见的进程间通信方式:

管道(Pipe)

管道是一种半双工的通信方式,数据只能在一个方向上流动,分为无名管道和有名管道。无名管道只能用于具有亲缘关系的进程之间通信,如父子进程。有名管道可以在任意两个进程之间通信,它以文件的形式存在于文件系统中。

#include <stdio.h>
#include <unistd.h>
#include <string.h>int main() {int fd[2];char buf[100];pipe(fd);pid_t pid = fork();if (pid == 0) {close(fd[0]);const char* msg = "Hello from child";write(fd[1], msg, strlen(msg));close(fd[1]);} else {close(fd[1]);read(fd[0], buf, sizeof(buf));printf("Received: %s\n", buf);close(fd[0]);}return 0;
}
消息队列(Message Queue)

消息队列是消息的链表,存放在内核中并由消息队列标识符标识。进程可以向消息队列中发送消息,也可以从消息队列中读取消息。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

共享内存(Shared Memory)

共享内存是最快的一种 IPC 方式,它允许两个或多个进程共享同一块物理内存区域。多个进程可以直接对共享内存进行读写操作,避免了数据的复制。但需要注意的是,多个进程对共享内存的访问需要进行同步和互斥控制。

信号量(Semaphore)

信号量主要用于实现进程间的同步和互斥。它是一个计数器,用于控制多个进程对共享资源的访问。当信号量的值大于 0 时,表示可以访问共享资源;当信号量的值小于等于 0 时,表示需要等待。

套接字(Socket)

套接字可用于不同主机之间的进程通信,也可用于同一主机上不同进程之间的通信。它支持多种协议,如 TCP 和 UDP。通过套接字,进程可以建立连接、发送和接收数据。

信号(Signal)

信号是一种异步通信方式,用于通知进程发生了某种事件。例如,当用户按下 Ctrl + C 时,会向当前进程发送一个 SIGINT 信号,进程可以捕获这个信号并进行相应的处理。

如何排查网络丢包问题

网络丢包会导致网络性能下降,影响用户体验。以下是排查网络丢包问题的一些方法:

基本网络工具检测

使用 ping 命令可以检测目标主机是否可达以及是否存在丢包现象。例如,ping -c 10 192.168.1.1 表示向 192.168.1.1 发送 10 个 ICMP 数据包,通过观察丢包率来判断网络状况。如果丢包率较高,可能存在网络问题。

traceroute 命令可以显示数据包从源主机到目标主机所经过的路由节点。通过分析路由节点,可以找出可能出现问题的网络设备。

检查网络设备

检查路由器、交换机等网络设备的状态,查看是否有设备过热、端口故障等问题。可以登录到网络设备的管理界面,查看设备的日志信息,了解是否有异常情况发生。

检查网络线缆

检查网络线缆是否连接正常,是否有破损、松动等情况。可以尝试更换网络线缆,看是否能够解决丢包问题。

检查网络带宽

使用网络带宽监测工具,如 iftopnethogs 等,查看网络带宽的使用情况。如果网络带宽被占满,可能会导致丢包现象。可以考虑升级网络带宽或者优化网络应用程序。

检查防火墙和安全软件

防火墙和安全软件可能会阻止某些网络数据包的传输,导致丢包。可以暂时关闭防火墙和安全软件,查看丢包问题是否解决。如果问题解决,则需要调整防火墙和安全软件的规则。

若 CPU 占用过高,如何进行排查

当 CPU 占用过高时,会影响系统的性能和稳定性。以下是排查 CPU 占用过高问题的方法:

系统层面监控

使用系统自带的监控工具,如 Linux 下的 tophtop 命令,Windows 下的任务管理器。这些工具可以实时显示系统中各个进程的 CPU 占用情况,通过查看 CPU 占用率较高的进程,找出可能存在问题的进程。

进程分析

对于 CPU 占用率较高的进程,可以进一步分析其代码逻辑。查看进程是否存在死循环、大量的计算任务等情况。可以使用调试工具,如 Linux 下的 gdb,对进程进行调试,找出问题所在。

资源竞争分析

检查进程是否存在资源竞争问题,如多个线程同时访问共享资源,导致 CPU 不断进行上下文切换。可以使用工具,如 Linux 下的 pstack 命令,查看进程的线程栈信息,分析线程的执行情况。

硬件层面检查

检查 CPU 硬件是否存在问题,如 CPU 过热、风扇故障等。可以使用硬件监控工具,如鲁大师等,查看 CPU 的温度、转速等信息。如果 CPU 温度过高,可能会导致 CPU 降频,从而影响性能。

怎样判断是程序 bug 还是 CPU 瓶颈导致的性能问题

判断是程序 bug 还是 CPU 瓶颈导致的性能问题,可以从以下几个方面入手:

观察性能表现

如果性能问题是突然出现的,并且之前程序运行正常,那么很可能是程序 bug 导致的。例如,程序中出现了死循环、内存泄漏等问题,会导致 CPU 占用过高。如果性能问题随着业务量的增加而逐渐出现,那么可能是 CPU 瓶颈导致的。

分析程序代码

对程序代码进行审查,查看是否存在逻辑错误、不合理的算法等问题。可以使用代码分析工具,如静态代码分析工具,检查代码中是否存在潜在的问题。

监控系统资源

使用系统监控工具,如 tophtop 等,查看 CPU、内存、磁盘等系统资源的使用情况。如果 CPU 占用率一直很高,并且其他系统资源使用正常,那么可能是 CPU 瓶颈导致的。如果除了 CPU 占用率高之外,还存在内存泄漏、磁盘 I/O 过高 等问题,那么可能是程序 bug 导致的。

压力测试

对程序进行压力测试,逐渐增加业务量,观察程序的性能表现。如果在压力测试过程中,程序的性能随着业务量的增加而线性下降,那么可能是 CPU 瓶颈导致的。如果在压力测试过程中,程序出现了崩溃、错误等异常情况,那么可能是程序 bug 导致的。

编写一个 while (true) 死循环,分析其 CPU 使用情况

以下是一个简单的 while (true) 死循环的 C++ 代码示例:

#include <iostream>int main() {while (true) {// 空循环}return 0;
}

在这个代码中,while (true) 会不断地执行循环体,由于循环体为空,程序会不断地进行循环判断。

CPU 使用情况分析

当运行这个程序时,CPU 的使用率会接近 100%。这是因为 while (true) 循环没有任何阻塞操作,CPU 会一直处于忙碌状态,不断地执行循环判断语句。在单核 CPU 系统中,这会导致整个系统的性能下降,因为其他进程无法获得足够的 CPU 时间。在多核 CPU 系统中,该程序会占用一个 CPU 核心的全部资源。

为了降低 CPU 的使用率,可以在循环中添加适当的阻塞操作,如 sleep 函数。以下是改进后的代码:

#include <iostream>
#include <unistd.h>int main() {while (true) {sleep(1); // 睡眠 1 秒}return 0;
}

在这个改进后的代码中,sleep(1) 会让程序暂停 1 秒,这样 CPU 就有时间去处理其他任务,从而降低了 CPU 的使用率。

列举 k8s 常用命令

Kubernetes(k8s)作为一款强大的容器编排系统,有诸多常用命令可帮助用户管理集群。

集群信息查看类

kubectl cluster - info:能快速显示集群的基本信息,像 Kubernetes 主节点和服务的地址等,便于用户了解集群的整体状况。 kubectl get nodes:可查看集群中所有节点的信息,包含节点名称、状态、角色等,让用户对集群的节点资源有清晰认知。

Pod 管理类

kubectl get pods:用于列出当前命名空间下的所有 Pod,能查看 Pod 的名称、状态、重启次数、运行时间等信息,方便掌握 Pod 的运行情况。 kubectl describe pod <pod - name>:会详细展示指定 Pod 的信息,涵盖 Pod 的配置、容器信息、事件等,有助于排查 Pod 出现的问题。 kubectl delete pod <pod - name>:可删除指定的 Pod,在需要清理不再使用的 Pod 时非常有用。

部署管理类

kubectl get deployments:列出当前命名空间下的所有部署,显示部署的名称、副本数、可用副本数等信息,方便用户了解部署的状态。 kubectl create deployment <deployment - name> --image = <image - name>:用于创建一个新的部署,指定部署名称和使用的容器镜像。 kubectl scale deployment <deployment - name> --replicas = <number>:可以调整指定部署的副本数量,以应对不同的业务需求。

服务管理类

kubectl get services:列出当前命名空间下的所有服务,展示服务的名称、类型、集群 IP、端口等信息,帮助用户管理服务的访问。 kubectl expose deployment <deployment - name> --type = <service - type> --port = <port>:将一个部署暴露为服务,可指定服务类型(如 ClusterIP、NodePort 等)和端口。

为什么选择 fastdfs,其原理是什么

FastDFS 是一个开源的轻量级分布式文件系统,因其诸多优势被广泛选择。

选择 FastDFS 的原因
  • 高性能:采用了分块存储和负载均衡技术,能快速处理大量的文件上传和下载请求。例如,在处理海量小文件时,其读写性能表现出色,可满足高并发场景的需求。
  • 高可扩展性:支持水平扩展,可通过添加存储节点来增加系统的存储容量和处理能力,适应不断增长的业务需求。
  • 容错性强:具备数据冗余和备份机制,当某个存储节点出现故障时,数据不会丢失,确保系统的可靠性。
  • 简单易用:提供了简洁的 API 接口,方便开发者集成到自己的应用程序中,降低了开发成本。
FastDFS 的原理

FastDFS 由跟踪服务器(Tracker Server)、存储服务器(Storage Server)和客户端(Client)三部分组成。

  • 跟踪服务器:负责管理存储服务器的状态信息,接收客户端的请求,为客户端分配合适的存储服务器。它维护着存储服务器的元数据,如存储服务器的 IP 地址、端口、磁盘使用情况等。
  • 存储服务器:负责实际的文件存储和管理。它将文件按照一定的规则存储在本地磁盘上,并为每个文件生成唯一的文件名。存储服务器之间会进行数据同步,保证数据的一致性。
  • 客户端:是使用 FastDFS 服务的应用程序。客户端向跟踪服务器请求存储服务器的信息,然后直接与存储服务器进行文件的上传和下载操作。

是否曾将 fastdfs 存储节点部署到 k8s 中

在实际项目中,是可以将 FastDFS 存储节点部署到 Kubernetes(k8s)中的。将 FastDFS 存储节点部署到 k8s 有诸多好处,如利用 k8s 的自动化部署、伸缩和管理功能,提高系统的可靠性和可维护性。

部署步骤
  • 创建配置文件:为 FastDFS 的跟踪服务器和存储服务器创建配置文件,如 tracker.conf 和 storage.conf,并将其作为 ConfigMap 部署到 k8s 中。
  • 创建存储卷:为存储服务器创建持久化存储卷,以保证数据的持久化。可以使用 k8s 的 PersistentVolume 和 PersistentVolumeClaim 来实现。
  • 创建 Deployment 和 Service:分别为跟踪服务器和存储服务器创建 Deployment,用于管理 Pod 的生命周期。同时,为跟踪服务器和存储服务器创建 Service,以便客户端可以访问。
注意事项
  • 网络配置:确保 FastDFS 各组件之间的网络通信正常,需要合理配置 k8s 的网络策略。
  • 数据同步:在存储节点扩展时,要保证数据的同步,避免数据不一致的问题。
  • 资源管理:合理分配 k8s 资源,确保 FastDFS 服务的性能和稳定性。

说明 http 和 https 的区别

HTTP(超文本传输协议)和 HTTPS(超文本传输安全协议)在网络通信中扮演着重要角色,二者存在显著区别。

安全性
  • HTTP:是明文传输协议,数据在传输过程中以明文形式存在,容易被窃听和篡改。例如,在公共无线网络中,攻击者可以通过抓包工具获取用户的敏感信息,如用户名、密码等。
  • HTTPS:是在 HTTP 的基础上加入了 SSL/TLS 协议,对数据进行加密传输。它使用对称加密和非对称加密相结合的方式,保证数据的机密性和完整性。即使数据被截获,攻击者也无法解密其中的内容。
端口号
  • HTTP:默认使用 80 端口进行通信。
  • HTTPS:默认使用 443 端口进行通信。
证书
  • HTTP:不需要使用 SSL/TLS 证书。
  • HTTPS:需要使用 SSL/TLS 证书来验证服务器的身份。证书由受信任的第三方机构颁发,包含服务器的公钥和相关信息。客户端通过验证证书的有效性来确保与合法的服务器进行通信。
性能
  • HTTP:由于不需要进行加密和解密操作,传输速度相对较快。
  • HTTPS:加密和解密操作会消耗一定的系统资源,导致传输速度相对较慢。但随着硬件性能的提升和加密算法的优化,这种性能差异在逐渐减小。

描述 TLS 握手过程,若公钥被截获并收到一个假的公钥,该如何处理

TLS(传输层安全协议)握手过程是客户端和服务器建立安全连接的重要步骤。

TLS 握手过程
  1. 客户端问候:客户端向服务器发送一个 ClientHello 消息,包含客户端支持的 TLS 版本、加密算法列表、随机数等信息。
  2. 服务器问候:服务器收到 ClientHello 消息后,发送 ServerHello 消息,选择一个 TLS 版本和加密算法,并发送自己的证书和随机数。
  3. 客户端验证证书:客户端验证服务器证书的有效性,包括证书的颁发机构、有效期、域名等信息。如果证书有效,客户端从证书中提取服务器的公钥。
  4. 生成会话密钥:客户端使用服务器的公钥加密一个预主密钥,并发送给服务器。客户端和服务器使用预主密钥和之前的随机数生成会话密钥。
  5. 完成握手:客户端和服务器分别发送 ChangeCipherSpec 消息,表示后续的数据将使用会话密钥进行加密。然后,客户端和服务器分别发送 Finished 消息,完成握手过程。
公钥被截获并收到假公钥的处理方法
  • 证书验证:客户端在收到服务器的证书后,会通过验证证书的签名来确保证书的真实性。证书由受信任的第三方机构(CA)颁发,CA 使用自己的私钥对服务器的公钥和相关信息进行签名。客户端可以通过 CA 的公钥验证签名的有效性,如果签名无效,则说明证书可能是伪造的。
  • 证书链验证:客户端会验证证书的证书链,从服务器的证书开始,逐级验证到根证书。如果证书链中存在无效的证书,则说明证书可能是伪造的。
  • 吊销列表检查:客户端可以检查证书的吊销列表(CRL)或在线证书状态协议(OCSP),查看证书是否被吊销。如果证书被吊销,则说明证书可能是伪造的或存在安全问题。

通过以上方法,客户端可以有效地防止使用假的公钥,保证通信的安全性。

详述三次握手和四次挥手过程,以及每个阶段对应的状态,说明 timewait 状态存在的意义

三次握手过程及状态

三次握手是 TCP 协议建立连接的过程,旨在确保客户端和服务器双方都具备发送和接收数据的能力。

  • 客户端发送 SYN 包:客户端向服务器发送一个 SYN 包,其中包含客户端的初始序列号 ISN(c),此时客户端进入 SYN_SENT 状态。这个 SYN 包是在请求建立连接,告知服务器自己想要进行通信。
  • 服务器响应 SYN + ACK 包:服务器接收到客户端的 SYN 包后,会发送一个 SYN + ACK 包给客户端。这个包中包含服务器的初始序列号 ISN(s) 以及对客户端 SYN 包的确认号 ACK = ISN(c) + 1,服务器进入 SYN_RCVD 状态。这一步表明服务器同意建立连接,并给出了自己的初始序列号。
  • 客户端发送 ACK 包:客户端收到服务器的 SYN + ACK 包后,会发送一个 ACK 包给服务器,确认号为 ACK = ISN(s) + 1,客户端进入 ESTABLISHED 状态。服务器收到该 ACK 包后也进入 ESTABLISHED 状态。至此,双方的连接建立成功,可以开始进行数据传输。
四次挥手过程及状态

四次挥手是 TCP 协议关闭连接的过程,用于有序地终止双方的通信。

  • 客户端发送 FIN 包:客户端完成数据发送后,向服务器发送一个 FIN 包,请求关闭连接,此时客户端进入 FIN_WAIT_1 状态。
  • 服务器响应 ACK 包:服务器收到客户端的 FIN 包后,发送一个 ACK 包给客户端,确认号为 ACK = FIN 序列号 + 1,服务器进入 CLOSE_WAIT 状态。客户端收到该 ACK 包后进入 FIN_WAIT_2 状态。
  • 服务器发送 FIN 包:服务器完成数据发送后,向客户端发送一个 FIN 包,请求关闭连接,服务器进入 LAST_ACK 状态。
  • 客户端响应 ACK 包:客户端收到服务器的 FIN 包后,发送一个 ACK 包给服务器,确认号为 ACK = FIN 序列号 + 1,客户端进入 TIME_WAIT 状态。服务器收到该 ACK 包后进入 CLOSED 状态。客户端在 TIME_WAIT 状态停留一段时间后也进入 CLOSED 状态。
TIME_WAIT 状态的意义
  • 确保最后一个 ACK 包能到达服务器:如果客户端直接关闭连接,而最后一个 ACK 包在传输过程中丢失,服务器会重发 FIN 包。由于客户端已经关闭,无法响应,会导致服务器无法正常关闭。而客户端处于 TIME_WAIT 状态时,可以重新发送 ACK 包,保证服务器能正常关闭。
  • 防止旧的连接数据干扰新的连接:在网络中,可能存在一些延迟的数据包。如果客户端直接关闭连接,这些延迟的数据包可能会干扰新的连接。TIME_WAIT 状态的存在可以让这些延迟的数据包在这段时间内自然消失,避免对新连接造成影响。

用位图法找出大数据中重复的数

位图法是一种高效的数据处理方法,特别适用于处理大数据集中的重复数据。其基本思想是使用一个二进制位来表示一个数是否存在。

#include <iostream>
#include <vector>// 位图类
class BitMap {
private:std::vector<unsigned int> bits;int size;public:BitMap(int num) {size = (num >> 5) + 1;bits.resize(size, 0);}// 设置某一位为 1void set(int num) {int index = num >> 5;int offset = num & 31;bits[index] |= (1 << offset);}// 检查某一位是否为 1bool get(int num) {int index = num >> 5;int offset = num & 31;return (bits[index] & (1 << offset)) != 0;}
};// 找出重复的数
void findDuplicates(int arr[], int n) {BitMap bitMap(1000000); // 假设数据范围在 0 - 1000000for (int i = 0; i < n; i++) {if (bitMap.get(arr[i])) {std::cout << "重复的数: " << arr[i] << std::endl;} else {bitMap.set(arr[i]);}}
}int main() {int arr[] = {1, 2, 3, 2, 4, 5, 4};int n = sizeof(arr) / sizeof(arr[0]);findDuplicates(arr, n);return 0;
}

在上述代码中,BitMap 类用于管理位图。set 方法用于将某一位设置为 1,get 方法用于检查某一位是否为 1。findDuplicates 函数遍历数组,对于每个数,先检查其在位图中是否已经存在,如果存在则说明是重复的数,否则将其在位图中标记为已存在。

找出两个链表的交点

要找出两个链表的交点,可以采用以下步骤:

  • 首先,计算两个链表的长度。
  • 然后,让较长的链表的指针先走差值步,使得两个指针到交点的距离相等。
  • 最后,同时移动两个指针,直到它们相遇,相遇的节点即为交点。
#include <iostream>// 链表节点定义
struct ListNode {int val;ListNode* next;ListNode(int x) : val(x), next(nullptr) {}
};// 计算链表长度
int getLength(ListNode* head) {int length = 0;ListNode* curr = head;while (curr != nullptr) {length++;curr = curr->next;}return length;
}// 找出两个链表的交点
ListNode* getIntersectionNode(ListNode* headA, ListNode* headB) {int lenA = getLength(headA);int lenB = getLength(headB);ListNode* longer = lenA > lenB ? headA : headB;ListNode* shorter = lenA > lenB ? headB : headA;int diff = std::abs(lenA - lenB);// 让较长的链表指针先走差值步while (diff > 0) {longer = longer->next;diff--;}// 同时移动两个指针,直到相遇while (longer != nullptr && shorter != nullptr) {if (longer == shorter) {return longer;}longer = longer->next;shorter = shorter->next;}return nullptr;
}int main() {// 创建链表 A: 1 -> 2 -> 3 -> 6 -> 7ListNode* headA = new ListNode(1);headA->next = new ListNode(2);headA->next->next = new ListNode(3);ListNode* intersection = new ListNode(6);headA->next->next->next = intersection;intersection->next = new ListNode(7);// 创建链表 B: 4 -> 5 -> 6 -> 7ListNode* headB = new ListNode(4);headB->next = new ListNode(5);headB->next->next = intersection;ListNode* result = getIntersectionNode(headA, headB);if (result != nullptr) {std::cout << "交点的值为: " << result->val << std::endl;} else {std::cout << "没有交点" << std::endl;}return 0;
}

在上述代码中,getLength 函数用于计算链表的长度,getIntersectionNode 函数用于找出两个链表的交点。通过先计算长度差,让较长链表的指针先走差值步,然后同时移动两个指针,最终找到交点。

解释 top 命令中虚拟内存和物理内存对应的字段

在 Linux 系统中,top 命令是一个常用的系统监控工具,用于实时显示系统中各个进程的资源使用情况。其中,与虚拟内存和物理内存相关的字段有以下几个:

虚拟内存相关字段
  • VIRT(Virtual Memory Size):表示进程使用的虚拟内存总量,包括进程代码段、数据段、共享库、堆、栈等所占用的内存空间。虚拟内存是操作系统为每个进程分配的一个连续的地址空间,它并不一定对应实际的物理内存。进程可以使用的虚拟内存空间通常很大,但实际使用的物理内存可能只是其中的一部分。
  • SWAP:表示进程使用的交换空间大小。当物理内存不足时,操作系统会将一些不常用的内存页面交换到磁盘上的交换空间中,以释放物理内存。SWAP 字段显示了进程当前使用的交换空间的大小。
物理内存相关字段
  • RES(Resident Set Size):表示进程当前实际占用的物理内存大小,也就是进程在物理内存中驻留的页面数量。RES 不包括已经被交换到磁盘上的页面。
  • SHR(Shared Memory Size):表示进程使用的共享内存大小。共享内存是多个进程可以共同访问的内存区域,例如共享库、共享数据结构等。SHR 字段显示了进程当前使用的共享内存的大小。

通过这些字段,用户可以了解进程对虚拟内存和物理内存的使用情况,从而判断系统的内存使用状况,找出内存占用过高的进程,进行相应的优化和调整。

说明共享内存的实现方式

共享内存是一种高效的进程间通信方式,它允许不同的进程直接访问同一块物理内存区域,从而避免了数据的复制,提高了数据传输的效率。以下是几种常见的共享内存实现方式:

POSIX 共享内存

POSIX 共享内存是一种跨平台的共享内存实现方式,它基于 POSIX 标准。在 Linux 系统中,可以使用以下步骤实现 POSIX 共享内存:

  • 创建共享内存对象:使用 shm_open 函数创建一个共享内存对象,并指定其名称和访问权限。
  • 调整共享内存对象的大小:使用 ftruncate 函数调整共享内存对象的大小。
  • 将共享内存对象映射到进程的地址空间:使用 mmap 函数将共享内存对象映射到进程的地址空间,使得进程可以直接访问该内存区域。
  • 使用共享内存:进程可以像访问普通内存一样访问共享内存区域,进行数据的读写操作。
  • 解除映射和删除共享内存对象:使用 munmap 函数解除共享内存对象的映射,使用 shm_unlink 函数删除共享内存对象。
#include <iostream>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>#define SHARED_MEMORY_NAME "/my_shared_memory"
#define SHARED_MEMORY_SIZE 1024int main() {// 创建共享内存对象int fd = shm_open(SHARED_MEMORY_NAME, O_CREAT | O_RDWR, 0666);if (fd == -1) {perror("shm_open");return 1;}// 调整共享内存对象的大小if (ftruncate(fd, SHARED_MEMORY_SIZE) == -1) {perror("ftruncate");close(fd);return 1;}// 将共享内存对象映射到进程的地址空间char* shared_memory = static_cast<char*>(mmap(nullptr, SHARED_MEMORY_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0));if (shared_memory == MAP_FAILED) {perror("mmap");close(fd);return 1;}// 使用共享内存const char* message = "Hello, shared memory!";snprintf(shared_memory, SHARED_MEMORY_SIZE, "%s", message);// 解除映射和删除共享内存对象if (munmap(shared_memory, SHARED_MEMORY_SIZE) == -1) {perror("munmap");}if (shm_unlink(SHARED_MEMORY_NAME) == -1) {perror("shm_unlink");}close(fd);return 0;
}
System V 共享内存

System V 共享内存是一种传统的共享内存实现方式,主要用于 Unix 和 Linux 系统。其实现步骤如下:

  • 创建共享内存段:使用 shmget 函数创建一个共享内存段,并指定其键值、大小和访问权限。
  • 将共享内存段附加到进程的地址空间:使用 shmat 函数将共享内存段附加到进程的地址空间,使得进程可以直接访问该内存区域。
  • 使用共享内存:进程可以像访问普通内存一样访问共享内存区域,进行数据的读写操作。
  • 将共享内存段从进程的地址空间分离:使用 shmdt 函数将共享内存段从进程的地址空间分离。
  • 删除共享内存段:使用 shmctl 函数删除共享内存段。
#include <iostream>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <string.h>#define SHM_SIZE 1024
#define SHM_KEY 1234int main() {// 创建共享内存段int shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);if (shmid == -1) {perror("shmget");return 1;}// 将共享内存段附加到进程的地址空间char* shared_memory = static_cast<char*>(shmat(shmid, nullptr, 0));if (shared_memory == reinterpret_cast<char*>(-1)) {perror("shmat");return 1;}// 使用共享内存const char* message = "Hello, shared memory!";strcpy(shared_memory, message);// 将共享内存段从进程的地址空间分离if (shmdt(shared_memory) == -1) {perror("shmdt");}// 删除共享内存段if (shmctl(shmid, IPC_RMID, nullptr) == -1) {perror("shmctl");}return 0;
}

无论是 POSIX 共享内存还是 System V 共享内存,都需要注意进程间的同步和互斥问题,以避免数据竞争和不一致的情况发生。通常可以使用信号量、互斥锁等机制来实现进程间的同步和互斥。

说明 send 和 recv 函数的缺点

在网络编程中,send 和 recv 函数是用于在套接字上发送和接收数据的常用函数,不过它们存在一些缺点,下面从不同方面进行分析。

数据传输可靠性方面

send 和 recv 函数本身不具备自动重传机制。当在网络状况不佳时,比如出现丢包、数据包损坏等情况,这些函数无法自动处理这些问题。若发送方使用 send 函数发送数据,数据在传输途中丢失,接收方就无法收到完整的数据,而 send 函数在发送数据时,只要数据被复制到套接字的发送缓冲区,就会返回成功,它不会关心数据是否真正被对方接收。同理,recv 函数在接收数据时,若遇到数据包丢失,也不会自动要求发送方重传数据。例如,在一个实时视频流传输的场景中,如果出现丢包,send 和 recv 函数无法保证视频的流畅性,可能会导致视频出现卡顿、花屏等现象。

数据完整性方面

这两个函数不能保证数据的完整性。send 函数只是将数据从用户空间复制到套接字的发送缓冲区,而 recv 函数从套接字的接收缓冲区读取数据。在数据传输过程中,可能会出现数据被截断、乱序等问题。例如,当发送方发送一个较大的数据包时,由于网络的 MTU(最大传输单元)限制,数据包可能会被分割成多个小的数据包进行传输。如果在传输过程中这些小数据包的顺序发生变化或者部分数据包丢失,recv 函数接收到的数据可能就不完整或者顺序错误。而且,send 和 recv 函数本身不会对数据进行校验,无法检测出数据是否被篡改。

阻塞特性方面

send 和 recv 函数默认是阻塞的。当调用 send 函数时,如果发送缓冲区已满,函数会阻塞,直到有足够的空间可以发送数据;当调用 recv 函数时,如果接收缓冲区为空,函数会阻塞,直到有数据到达。在某些场景下,这种阻塞特性会影响程序的性能和响应能力。例如,在一个多用户的网络服务器程序中,如果某个客户端的连接出现问题,导致 send 或 recv 函数一直阻塞,那么服务器可能会无法及时处理其他客户端的请求,从而影响整个系统的性能。虽然可以通过设置套接字为非阻塞模式来避免阻塞,但这会增加编程的复杂度。

错误处理方面

send 和 recv 函数的错误处理机制相对有限。当函数调用失败时,它们通常只是返回一个错误码,程序员需要根据错误码来判断具体的错误原因。而且,这些错误码可能不够详细,对于一些复杂的网络问题,很难通过错误码准确地定位问题所在。例如,当 send 函数返回 EAGAIN 或 EWOULDBLOCK 错误码时,只表示当前操作会阻塞,但无法确定是因为网络延迟、缓冲区满还是其他原因导致的。

可扩展性方面

在处理大规模并发连接时,使用 send 和 recv 函数会面临一些挑战。由于每个连接都需要调用这两个函数进行数据的发送和接收,当连接数量增加时,会导致系统的开销增大,性能下降。而且,对于一些复杂的网络协议和应用场景,send 和 recv 函数的功能可能不够强大,需要程序员自己实现更多的逻辑来满足需求。例如,在实现一个分布式系统时,可能需要实现数据的分片、重组、加密等功能,仅使用 send 和 recv 函数是不够的,需要额外的代码来完成这些任务。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/78663.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python cv2滤波与模糊处理:从原理到实战

在图像处理领域&#xff0c;滤波与模糊是预处理阶段的两大核心操作&#xff0c;既能消除噪声干扰&#xff0c;又能实现艺术化效果。本文将结合OpenCV的cv2库&#xff0c;系统讲解滤波与模糊的原理及Python实现&#xff0c;带你从理论到实战全面掌握这项技术。 一、滤波与模糊的…

在 Laravel 12 中实现 WebSocket 通信时进行身份验证

在 Laravel 12 中实现 WebSocket 通信时&#xff0c;若需在身份验证失败后主动断开客户端连接&#xff0c;需结合 频道认证机制 和 服务端主动断连操作。以下是具体实现步骤&#xff1a; 一、身份验证流程设计 WebSocket 连接的身份验证通常通过 私有频道&#xff08;Private …

FPGA----基于ZYNQ 7020实现petalinux并运行一个程序

引言&#xff1a;上一节我们讲到了使用Alinx 7020b自带的sd卡中的petalinux进行epics的编译&#xff0c;但此种方案个性化程度不足。如&#xff1a;我们项目需要FPGA侧的配合&#xff0c;那么我们需要重新编译petalinx。 注意&#xff1a;本文的知识点来自下面两篇文章&#x…

Spring Web MVC————入门(1)

今天开始正式带大家学习Spring部分的内容了&#xff0c;大家尝试去弄个专业版嗷&#xff0c;学习起来爽一点 在idea中下载这个插件就行了 我们之后开始创建Spring项目&#xff0c; 蓝色 部分自己起名&#xff0c;type选Maven&#xff0c;其他的默认就好了&#xff0c;之后nex…

Vue3 中用 canvas 封装抽奖转盘组件:设定中奖概率及奖项图标和名称

在 Web 应用开发中&#xff0c;抽奖功能是提升用户参与度的常用手段。使用 Vue3 结合 canvas 技术&#xff0c;我们可以轻松实现一个高度自定义的抽奖转盘组件&#xff0c;不仅能设定中奖概率&#xff0c;还能灵活配置奖项图标和名称。本文将详细介绍该组件的实现原理、步骤&am…

Linux 硬盘和光驱系统管理

一、硬盘与目录的容量 [rootwww ~]# df [-ahikHTm] [目录或档名] 选项与参数&#xff1a; -a &#xff1a;列出所有的档案系统&#xff0c;包括系统特有的 /proc 等档案系统&#xff1b; -k &#xff1a;以 KBytes 的容量显示各档案系统&#xff1b; -m &#xff1a;以 MByt…

2.Spring Boot中集成Guava Cache或者Caffeine

一、在Spring Boot(1.x版本)中集成Guava Cache 注意&#xff1a; Spring Boot 2.x用户&#xff1a;优先使用Caffeine&#xff0c;性能更优且维护活跃。 1. 添加依赖 在pom.xml中添加Guava依赖&#xff1a; <dependency><groupId>com.google.guava</groupId&…

黑马点评day02(缓存)

2、商户查询缓存 2.1 什么是缓存? 前言:什么是缓存? 就像自行车,越野车的避震器 举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样; 同样,实际开发中,系统也需要"避震…

头歌禁止复制怎么解除(简单版)

被头歌数据库作业禁止复制整神之后&#xff0c;主啵尝试网上各种解除方法&#xff0c;最后发现一个最简单且最快速的解除方法。 在浏览器中搜索万能复制插件 下载完成之后就可以随便复制粘贴啦 超简单 下载只需几秒

【无基础】小白解决Docker pull时报错:https://registry-1.docker.io/v2/

Docker Compose 启动失败问题解决方案 错误描述 执行 docker compose up -d 时出现以下错误&#xff1a; [] Running 9/9✘ api Error context canceled …

【数据结构】二叉树、堆

文章目录 二叉树的概念及结构定义特殊的二叉树核心性质存储方式 二叉树的链式存储前序遍历中序遍历后序遍历层序遍历 二叉树的顺序存储父子关系的推导堆&#xff08;heap&#xff09;堆的概念向上调整算法和向下调整算法向上调整算法向下调整算法 堆的创建堆的插入堆的删除 堆的…

Vue3响应式原理那些事

文章目录 1 响应式基础:Proxy 与 Reflect1.1 Proxy 代理拦截1.2 Reflect 确保 `this` 指向正确1.2.1 修正 `this` 指向问题1.2.2 统一的操作返回值1.3 与 Vue2 的对比2 依赖收集与触发机制2.1 全局依赖存储结构:WeakMap → Map → Set2.2 依赖收集触发时机2.3 依赖收集核心实…

精选10个好用的WordPress免费主题

10个好用的WordPress免费主题 1. Astra Astra 是全球最受欢迎的WordPress免费主题。它功能丰富&#xff0c;易于使用&#xff0c;SEO友好&#xff0c;是第一个安装量突破100万的非默认主题&#xff0c;并获得了5000多个五星好评。 它完美集成了Elementor、Beaver&#xff0c;…

【SaaS多租架构】数据隔离与性能平衡

SaaS多租户架构:数据隔离与性能平衡 一、技术背景及发展二、技术特点:数据隔离与性能优化的双核心三、技术细节:实现路径与关键技术四、实际案例分析五、未来发展趋势结语一、技术背景及发展 多租户架构是云计算与SaaS(软件即服务)模式的核心技术,其核心目标是通过共享基…

部署GM DC Monitor 一体化监控预警平台

1&#xff09;首先在官网下载镜像文件 广目&#xff08;北京&#xff09;软件有限公司广目&#xff08;北京&#xff09;软件有限公司https://www.gm-monitor.com/col.jsp?id1142&#xff09;其次进行部署安装&#xff0c;教程如下&#xff1a; 1. 基础环境要求 1) 系统&…

Webug4.0靶场通关笔记15- 第19关文件上传(畸形文件)

目录 第19关 文件上传(畸形文件) 1.打开靶场 2.源码分析 &#xff08;1&#xff09;客户端源码 &#xff08;2&#xff09;服务器源码 3.渗透实战 &#xff08;1&#xff09;构造脚本 &#xff08;2&#xff09;双写绕过 &#xff08;3&#xff09;访问脚本 本文通过《…

架构思维:构建高并发读服务_热点数据查询的架构设计与性能调优

文章目录 一、引言二、热点查询定义与场景三、主从复制——垂直扩容四、应用内前置缓存4.1 容量上限与淘汰策略4.2 延迟刷新&#xff1a;定期 vs. 实时4.3 逃逸流量控制4.4 热点发现&#xff1a;被动 vs. 主动 五、降级与限流兜底六、前端&#xff0f;接入层其他应对七、模拟压…

宝塔面板运行docker的jenkins

1.在宝塔面板装docker&#xff0c;以及jenkins 2.ip:端口访问jenkins 3.获取密钥&#xff08;点击日志&#xff09; 4.配置容器内的jdk和maven环境&#xff08;直接把jdk和maven文件夹放到jenkins容器映射的data文件下&#xff09; 点击容器-->管理-->数据存储卷--.把相…

C语言 ——— 函数

目录 函数是什么 库函数 学习使用 strcpy 库函数 自定义函数 写一个函数能找出两个整数中的最大值 写一个函数交换两个整型变量的内容 牛刀小试 写一个函数判断一个整数是否是素数 写一个函数判断某一年是否是闰年 写一个函数&#xff0c;实现一个整型有序数组的二分…

笔记本电脑升级计划(2017———2025)

ThinkPad T470 (2017) vs ThinkBook 16 (2025) 完整性能对比报告 一、核心硬件性能对比 1. CPU性能对比&#xff08;i5-7200U vs Ultra9-285H&#xff09; 参数i5-7200U (2017)Ultra9-285H (2025)提升百分比核心架构2核4线程 (Skylake)16核16线程 (6P8E2LPE)700%核心数制程工…