百度提前批C++一面
公众号:阿Q技术站
来源:https://www.nowcoder.com/feed/main/detail/7ef8161d101149b6b0c5b18c7b6b6fb0?sourceSSR=users
1、水平触发和边缘触发的区别
- 水平触发(Level-Triggered):
- 当文件描述符上的事件发生时,如果该文件描述符上还有数据可读或可写,
epoll
将会不断通知应用程序,直到相应的事件被处理。 - 即使应用程序没有处理完所有数据,下一次
epoll_wait
仍然会返回文件描述符上的事件。
- 当文件描述符上的事件发生时,如果该文件描述符上还有数据可读或可写,
- 边缘触发(Edge-Triggered):
- 只在文件描述符状态发生变化时通知应用程序一次,而不是在状态保持的情况下重复通知。
- 如果一个文件描述符上有可读或可写事件发生,
epoll
将通知应用程序,但如果应用程序没有读写完所有的数据,下一次epoll_wait
调用将不会再次返回相同的文件描述符。
2、epoll selcet/poll的区别
- 可扩展性:
epoll
: 在文件描述符较多时,性能上的扩展性更好。epoll
使用了红黑树的数据结构,因此在大量文件描述符的情况下,性能不会随着文件描述符数量的增加而线性下降。select
和poll
: 在文件描述符数量增加时,性能可能会线性下降,因为它们使用线性扫描的方式来检查就绪状态。
- 触发方式:
epoll
: 可以支持水平触发和边缘触发两种触发方式,可以更灵活地处理事件。select
和poll
: 通常采用水平触发,需要在每次检查就绪状态后重新设置文件描述符,而不能像epoll
一样在事件发生后保持触发状态。
- 接口的效率:
epoll
: 通过epoll_ctl
来注册或删除事件,通过epoll_wait
来等待就绪事件,这两个系统调用的参数较少,不需要每次都传递全部的文件描述符集合,因此在调用时更为高效。select
和poll
: 需要传递全部的文件描述符集合给内核,因此在文件描述符较多时,效率可能降低。
- 可移植性:
select
: 是 POSIX 标准的一部分,相对来说更为通用,但有一些平台可能对其实现有所不同。poll
: 也是 POSIX 标准的一部分,但在某些系统上可能不提供。epoll
: 是 Linux 特有的,不同的操作系统可能没有相应的实现。
3、用户态和内核的使用场景的区别 和如何切换内核态(什么场景切换内核态)
- 用户态:
- 用户态是程序执行的一种模式,其中程序运行在受限的、用户级的环境中。
- 在用户态执行的程序无法直接访问底层硬件资源或执行特权指令。它们必须通过系统调用(system call)请求内核执行某些操作。
- 内核态:
- 内核态是操作系统内核执行的一种特权模式,具有对底层硬件资源和系统全局状态的直接访问权限。
- 在内核态执行的代码具有更高的权限,可以执行特权指令,直接操作硬件资源。
切换内核态的场景:
在计算机系统中,用户态和内核态的切换通常发生在以下场景:
-
系统调用:
- 当用户程序需要执行特权操作(如文件 I/O、网络通信、进程管理等)时,它会通过系统调用请求内核执行相应的服务。
- 这时会发生用户态到内核态的切换,将控制权交给操作系统内核。
-
中断:
- 当硬件设备或外部事件需要引起处理时,它会触发中断。
- 当发生中断时,CPU会暂停当前执行的程序,切换到内核态执行中断处理程序。
-
异常:
- 异常是由当前执行的指令引起的错误或异常情况,例如除零错误。
- 当异常发生时,CPU会切换到内核态执行相应的异常处理程序。
-
信号处理:
当一个进程接收到信号(例如
SIGSEGV
表示段错误),会触发信号处理程序的执行,这通常在内核态进行。
切换内核态的过程:
-
保存上下文:
在发生用户态到内核态的切换时,当前进程的上下文(包括寄存器状态、程序计数器等)需要保存起来,以便稍后返回用户态时能够继续执行。
-
切换堆栈:
内核通常使用自己的堆栈,因此在切换到内核态时,需要切换到内核的堆栈上。
-
执行内核代码:
切换到内核态后,开始执行相应的内核代码,完成请求的操作。
-
恢复上下文:
在执行完内核态的操作后,需要恢复之前保存的用户态的上下文,以便返回用户态时能够继续执行用户程序。
4、项目中的指标测试
5、项目有没有考虑多进程?
6、网络库后有没有做起来,落地项目?
7、tcp/udp的区别
- 连接性:
- TCP: 面向连接的协议,通信前需要建立连接,然后进行数据传输,最后释放连接。提供可靠的、面向连接的通信,确保数据的完整性和顺序性。
- UDP: 无连接的协议,通信时不需要建立连接,直接进行数据传输。提供不可靠的、无连接的通信,不保证数据的完整性和顺序性。
- 可靠性:
- TCP: 提供可靠的通信,通过序号、应答和重传等机制,确保数据的可靠性,适用于对数据可靠性要求较高的场景。
- UDP: 不提供可靠性保证,数据可能会丢失或乱序,适用于对实时性要求较高的场景。
- 数据流:
- TCP: 提供面向流的通信,数据传输是连续的字节流,保证数据的有序性。
- UDP: 提供面向报文的通信,数据以数据报的形式传输,每个数据报相对独立,不保证有序性。
- 通信开销:
- TCP: 通信开销较大,因为需要建立连接、维护状态信息、进行可靠性保证。
- UDP: 通信开销较小,因为无连接、不维护状态信息,适用于对通信延迟要求较低的场景。
- 适用场景:
- TCP: 适用于需要可靠性、有序性的应用,如文件传输、网页访问、邮件传输等。
- UDP: 适用于实时性要求高、对可靠性要求不高的应用,如音频、视频流传输、在线游戏等。
- 头部开销:
- TCP: 头部开销相对较大,包含序号、确认号、窗口大小等字段,用于维护连接状态。
- UDP: 头部开销较小,只包含源端口、目标端口、长度和校验和等基本字段。
8、http和tcp有什么关系?
HTTP(超文本传输协议)和TCP(传输控制协议)是两个不同层次的协议,但它们之间存在紧密的关系。HTTP是应用层协议,而TCP是传输层协议,HTTP协议是基于TCP协议之上构建的。
-
传输层关系:
HTTP协议是应用层协议,而TCP是传输层协议。HTTP使用TCP作为其传输层协议,这是因为TCP提供了可靠的、面向连接的通信,能够保证数据的可靠性和有序性。
-
连接性:
HTTP协议是无状态的,每个请求和响应之间是独立的。然而,为了在这种无状态的协议上建立持久的通信,通常会使用TCP的持久连接(Keep-Alive)特性,以减少连接建立和断开的开销,提高性能。
-
协议栈:
在网络协议栈中,HTTP位于更高的层次,而TCP位于更底层。HTTP通过TCP进行数据的可靠传输,而TCP则使用IP(Internet Protocol)作为网络层协议。
-
连接建立:
在使用HTTP进行通信时,通常需要先建立TCP连接,然后通过这个TCP连接进行HTTP请求和响应的传输。HTTP在TCP连接上发送请求,接收响应,然后关闭连接(对于非持久连接的情况)。
-
端口:
HTTP默认使用TCP端口80进行通信。在TCP连接的基础上,通过指定端口来区分不同的应用程序,使得计算机上可以同时运行多个不同的服务。
9、https和http的区别是那些?
- 加密机制:
- HTTP: 传输过程中的数据是明文的,不进行加密处理。因此,HTTP通信容易被窃听,存在安全隐患。
- HTTPS: 通过使用 SSL/TLS 协议进行加密,能够保护数据的隐私性,防止数据被中间人窃取或篡改。
- 端口:
- HTTP: 默认使用端口80进行通信。
- HTTPS: 默认使用端口443进行通信。
- 安全性:
- HTTP: 通信是明文的,安全性较差,容易受到中间人攻击。
- HTTPS: 通过SSL/TLS加密通信,提供了更高的安全性,防止数据在传输过程中被窃取或篡改。
- 证书:
- HTTP: 不需要证书,是无状态的协议。
- HTTPS: 需要使用SSL/TLS证书,用于验证服务器的身份,并确保通信的安全性。
- 连接方式:
- HTTP: 是无状态的,每次请求都是独立的,不保留前后状态。
- HTTPS: 由于使用了SSL/TLS协议,可以通过SSL会话保留状态,使得连接更可靠。
- 性能:
- HTTP: 通信速度较快,适用于一些不涉及隐私信息的场景。
- HTTPS: 由于加密和解密的过程会增加一定的计算负担,相比HTTP略显复杂,通信速度可能会稍慢一些。
10、get和post的区别有那些(安全且幂等,除了这些还有没?)
- 数据位置:
- GET: 将数据附加在URL后面,以查询字符串的形式传递。数据可见,长度有限,一般用于传递少量非敏感数据。
- POST: 将数据包含在请求体中,对数据长度没有限制。适用于传递大量数据或包含敏感信息的数据。
- 数据安全性:
- GET: 由于数据在URL中可见,不适用于传递敏感信息,例如密码。数据被保存在浏览器的历史记录、服务器日志等地方。
- POST: 数据在请求体中,相对于GET更安全,适用于传递敏感信息。
- 数据大小:
- GET: 由于数据附加在URL中,长度有限制,一般不适合传递大文件或大量数据。
- POST: 数据包含在请求体中,可以传递大文件或大量数据。
- 缓存:
- GET: 请求可以被缓存,适用于幂等操作(多次执行不会产生不同结果)。
- POST: 请求不能被缓存,适用于非幂等操作。
- 幂等性:
- GET: 幂等,多次执行不会产生不同的结果。
- POST: 非幂等,多次执行可能会产生不同的结果。
- 适用场景:
- GET: 适用于无副作用、幂等的操作,例如获取资源。
- POST: 适用于有副作用、非幂等的操作,例如提交表单、上传文件。
- 可见性:
- GET: 数据可见,适用于通过URL传递参数,可书签化。
- POST: 数据不可见,适用于传递敏感信息。
11、Session和cookie的区别是什么?
- 存储位置:
- Cookie: 存储在客户端(浏览器)中,以文本文件的形式保存在用户的计算机上。
- Session: 存储在服务器端,通常以一种服务器维护的数据结构的形式保存在服务器的内存或数据库中。
- 安全性:
- Cookie: 相对不够安全,因为存储在客户端,用户可以查看和修改Cookie的内容。可以设置Cookie的属性来增强安全性,如设置为HttpOnly以防止被JavaScript访问。
- Session: 相对更安全,因为数据存储在服务器端,用户无法直接访问和修改Session数据。
- 存储容量:
- Cookie: 存储容量较小,通常限制在几KB。
- Session: 存储容量较大,受服务器内存或数据库存储的限制,通常能存储更多的数据。
- 生命周期:
- Cookie: 可以设置过期时间,可以是会话级别的(浏览器关闭即失效)或持久性的(在一定时间内有效)。
- Session: 生命周期通常与用户的会话一致,当用户关闭浏览器或一定时间不活动时,Session可能会失效。
- 使用场景:
- Cookie: 适用于在客户端存储少量的不敏感信息,如用户偏好设置、跟踪用户行为等。
- Session: 适用于存储对安全性要求较高的、敏感的用户数据,如用户身份认证信息、购物车内容等。
- 跨域问题:
- Cookie: 可以设置跨域访问,但受同源策略的限制。
- Session: 在不同域名之间难以实现,通常使用其他机制,如跨域认证。
12、为什么用cookie?
Cookies 是一种在客户端存储小段数据的机制,通常由 Web 服务器通过 HTTP 协议发送给客户端的浏览器,并保存在用户本地计算机上。
- 会话管理: Cookies 可以用于管理用户的会话。通过在客户端存储一个会话标识符,服务器可以识别和跟踪用户的会话状态。这对于在用户浏览不同页面时保持用户登录状态非常有用。
- 用户跟踪: Cookies 可以用于跟踪用户的行为和偏好。通过存储一些用户偏好设置或跟踪用户的浏览历史,网站可以提供更个性化的体验。
- 购物车管理: 在电子商务网站中,Cookies 常被用于管理用户的购物车。它可以存储用户选择的商品和相关信息,使得用户在不同页面之间保持购物车的一致性。
- 广告定向: Cookies 可以用于收集用户的浏览行为,从而进行广告定向。广告商可以根据用户的兴趣和行为向其提供更有针对性的广告。
- 性能优化: Cookies 可以用于性能优化,例如存储一些临时的用户状态信息,避免重复的服务器请求。
- 用户身份验证: Cookies 可以包含用户的身份验证信息,使得用户在访问受保护的页面时不需要重新输入用户名和密码。
13、cookie具体原理
- 服务器端创建 Cookie:
当用户访问一个网站时,服务器在 HTTP 响应头中通过 Set-Cookie 标头创建一个 Cookie。该标头包含了一些关键信息,如 Cookie 的名称、值、过期时间、域名、路径等。
Set-Cookie: name=value; expires=Sat, 23 Oct 2023 00:00:00 GMT; path=/; domain=.example.com; secure; HttpOnly
- 发送 Cookie 到客户端:
浏览器接收到包含 Set-Cookie 的响应后,将该 Cookie 存储在本地。在接下来的每次请求中,浏览器都会将存储在本地的相关 Cookie 信息通过 Cookie 请求头发送给服务器。
Cookie: name=value
- 服务器端读取 Cookie:
服务器在收到包含 Cookie 请求头的请求时,能够解析其中的 Cookie 信息。通过读取这些信息,服务器可以识别用户、维护用户的会话状态、提供个性化体验等。
- Cookie 存储和管理:
浏览器会将 Cookies 存储在本地的 Cookie 存储区域中。这个存储区域的实现可以是文本文件、内存等。Cookies 通常是基于域名和路径来进行存储的,不同的域名和路径可能拥有不同的 Cookies。
- 过期和更新:
Cookies 可以设置过期时间,一旦过期,浏览器将不再发送该 Cookie。如果设置了持久性 Cookie,它将一直保存在本地,直到用户清除浏览器的 Cookie 缓存或 Cookie 过期。
- 安全性:
可以通过设置安全标志(Secure Flag)使得 Cookies 只在加密的 HTTPS 连接中传输,提高安全性。同样,通过设置 HttpOnly 标志,可以防止通过 JavaScript 访问 Cookies,从而减少跨站脚本攻击(XSS)的风险。
14、长连接和短连接的区别?
- 连接时长:
- 长连接: 指的是客户端和服务器建立连接后,在一定时间内保持连接处于打开状态,多次数据传输可以共享同一个连接。
- 短连接: 指的是每次通信完成后,客户端和服务器断开连接,下一次通信需要重新建立连接。
- 连接开销:
- 长连接: 由于连接在一定时间内保持打开,减少了连接和断开连接的开销。但可能会因为长时间保持连接而占用服务器资源。
- 短连接: 每次通信需要建立新的连接,连接的建立和断开会带来一定的开销,但不会长时间占用服务器资源。
- 适用场景:
- 长连接: 适用于需要频繁通信或需要保持实时性的场景,如即时通讯、实时数据推送等。
- 短连接: 适用于每次通信之间有较长时间间隔,且不需要保持实时性的场景,如传统的网页浏览。
- 资源占用:
- 长连接: 由于连接保持打开,可能会占用服务器上的资源,尤其是在连接数较多时。
- 短连接: 每次通信结束后关闭连接,释放了服务器资源,适用于服务器资源受限的情况。
- 失败恢复:
- 长连接: 在网络中断或其他故障时,需要处理连接断开和重连的逻辑,以保持长连接的稳定性。
- 短连接: 每次通信都是独立的,连接问题往往容易通过重新建立连接来解决。
15、http怎么实现长连接?
HTTP/1.1 引入了持久连接(Keep-Alive)机制,使得在一个 TCP 连接上可以发送和接收多个 HTTP 请求和响应,从而实现了长连接。在没有持久连接的情况下,每个 HTTP 请求都需要建立一个新的 TCP 连接,而持久连接则允许多个请求通过同一个连接进行传输。
- Connection 头部:
在 HTTP/1.1 中,引入了 Connection 头部,用于指示是否要保持连接打开。如果 Connection 头部的值为 “keep-alive”,则表示要使用持久连接。
Connection: keep-alive
- Keep-Alive 头部:
使用 Keep-Alive 头部可以进一步设置连接的参数,如超时时间和最大请求数。这样可以控制连接在空闲时的保持时间,以及在达到最大请求数时是否重新建立连接。
Keep-Alive: timeout=5, max=1000
- 持久连接的实际效果:
当客户端和服务器都支持持久连接时,TCP 连接将保持打开状态,可以在该连接上发送多个 HTTP 请求。服务器在响应中会明确指示连接是否关闭。
Connection: keep-alive
在一个持久连接上,客户端可以发送多个请求,并且服务器可以在一个连接上返回多个响应,而无需每次都重新建立连接。
16、排序算法中常用的 稳定性和时间复杂度。
- 冒泡排序(Bubble Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n)(已经有序)
- 平均情况:O(n^2)
- 最坏情况:O(n^2)
- 插入排序(Insertion Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n)(已经有序)
- 平均情况:O(n^2)
- 最坏情况:O(n^2)
- 选择排序(Selection Sort):
- 稳定性:不稳定
- 时间复杂度:
- 最优情况:O(n^2)
- 平均情况:O(n^2)
- 最坏情况:O(n^2)
- 归并排序(Merge Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n log n)
- 平均情况:O(n log n)
- 最坏情况:O(n log n)
- 快速排序(Quick Sort):
- 稳定性:不稳定
- 时间复杂度:
- 最优情况:O(n log n)
- 平均情况:O(n log n)
- 最坏情况:O(n^2)
- 堆排序(Heap Sort):
- 稳定性:不稳定
- 时间复杂度:
- 最优情况:O(n log n)
- 平均情况:O(n log n)
- 最坏情况:O(n log n)
- 计数排序(Counting Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n + k)(k 是整数的范围)
- 平均情况:O(n + k)
- 最坏情况:O(n + k)
- 桶排序(Bucket Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n + k)(k 是桶的个数)
- 平均情况:O(n + n^2/k + k)
- 最坏情况:O(n^2)
- 基数排序(Radix Sort):
- 稳定性:稳定
- 时间复杂度:
- 最优情况:O(n * k)(k 是关键字的位数)
- 平均情况:O(n * k)
- 最坏情况:O(n * k)
17、快排的时间复杂度和空间复杂度(logn)要算递归
快速排序的基本思想是通过选择一个基准元素,将数组分成左右两部分,并对左右两部分分别进行排序。
时间复杂度:
在每一次划分中,都会确定一个基准元素的位置,使得基准元素左边的元素都小于基准,右边的元素都大于基准。这样的划分使得每个元素都参与了 logn 次划分(因为每次划分都将数据规模减半)。因此,快速排序的平均时间复杂度是 O(n log n)。
空间复杂度:
空间复杂度主要包括递归调用时的栈空间和一些辅助变量的空间。快速排序是一个原地排序算法,它的递归实现并不需要额外的存储空间,所以空间复杂度是 O(log n)。
18、快排手撕
基于分治的思想,通过选取一个基准元素将数组划分为两个子数组,然后对子数组进行递归排序。
参考代码:
递归:
#include <iostream>
#include <vector>
void quickSort(std::vector<int>& arr, int low, int high) {
if (low < high) {
// 划分操作,返回基准元素的位置
int pivotIndex = partition(arr, low, high);
// 对基准元素左右两侧进行递归排序
quickSort(arr, low, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, high);
}
}
int partition(std::vector<int>& arr, int low, int high) {
// 选择最右边的元素作为基准
int pivot = arr[high];
// i 指向小于基准的区域的最后一个元素
int i = low - 1;
// 遍历数组,将小于基准的元素放到左侧,大于基准的元素放到右侧
for (int j = low; j < high; ++j) {
if (arr[j] <= pivot) {
// 将小于基准的元素交换到左侧区域
i++;
std::swap(arr[i], arr[j]);
}
}
// 将基准元素交换到正确的位置
std::swap(arr[i + 1], arr[high]);
// 返回基准元素的位置
return i + 1;
}
int main() {
std::vector<int> arr = {12, 4, 5, 6, 7, 3, 1, 15};
int n = arr.size();
std::cout << "Original array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
quickSort(arr, 0, n - 1);
std::cout << "Sorted array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
quickSort
函数进行递归排序,而 partition
函数负责对数组进行划分。选择最右边的元素作为基准,遍历数组,将小于基准的元素交换到左侧,大于基准的元素交换到右侧。最后,将基准元素放到正确的位置。通过递归调用 quickSort
函数,可以完成整个数组的排序。
非递归:
#include <iostream>
#include <vector>
#include <stack>
void quickSort(std::vector<int>& arr, int low, int high) {
std::stack<std::pair<int, int>> stack; // 用于模拟递归调用的栈
stack.push(std::make_pair(low, high));
while (!stack.empty()) {
std::pair<int, int> range = stack.top();
stack.pop();
int pivotIndex = partition(arr, range.first, range.second);
// 对基准元素左右两侧进行非递归排序
if (pivotIndex - 1 > range.first) {
stack.push(std::make_pair(range.first, pivotIndex - 1));
}
if (pivotIndex + 1 < range.second) {
stack.push(std::make_pair(pivotIndex + 1, range.second));
}
}
}
int partition(std::vector<int>& arr, int low, int high) {
int pivot = arr[high];
int i = low - 1;
for (int j = low; j < high; ++j) {
if (arr[j] <= pivot) {
i++;
std::swap(arr[i], arr[j]);
}
}
std::swap(arr[i + 1], arr[high]);
return i + 1;
}
int main() {
std::vector<int> arr = {12, 4, 5, 6, 7, 3, 1, 15};
int n = arr.size();
std::cout << "Original array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
quickSort(arr, 0, n - 1);
std::cout << "Sorted array: ";
for (int num : arr) {
std::cout << num << " ";
}
std::cout << std::endl;
return 0;
}
使用一个栈 std::stack<std::pair<int, int>> stack
来存储待处理的数组范围。初始时将整个数组的范围入栈,然后通过迭代模拟递归的过程。在每一次迭代中,取出栈顶的数组范围,进行划分操作,并将划分后的两个子数组的范围入栈。这样,通过栈的操作,完成了非递归的快速排序。