腾讯面经

算法题

无序数组第k大数
最大子数组和
一个经典2*N多米诺骨牌
力扣240
lc151. 反转字符串中的单词
LRU手撕 重要
K翻转链表
滑动窗口最大值
K轮转链表
合并两个有序链表
旋转数组找最小值
查找二叉搜索树第K个节点的思路
升序链表合并降序链表
构造函数能不能虚函数
最长递增子序列
叉树搜索树返回第k大的值
括号匹配
子数组最大和
找数组中出现次数大于长度一半的数
leetcode41(缺失的第一个正数)
剪绳子

个人介绍

“面试官您好,我叫许涵飞,目前就读于郑州轻工业大学智能科学与技术专业,现为大三学生。过去一年,我有幸在郑州轻软科技有限公司担任 Qt 客户端开发工程师,参与了跟进了项目的开发,积累了一定的上位机开发经验。

在项目中,我熟悉使用 C++ 和 Qt 进行桌面端开发。例如,在电子音乐教室系统中,我通过 Qt 信号槽机制实现实时音频波形展示,在九泰管道智能检测机器人项目中,我利用 QQuick3D 实现点云数据的 3D 可视化,并通过 UDP 实现数据传输和解析。这些经历让我对客户端开发的技术挑战有了深刻认识,并培养了一定的技术攻关能力。

同时,我具备一定的学习能力和抗压能力,在工作中能够快速适应新的技术和环境。我对贵公司的岗位描述和需求进行了研究,发现我的技能与经验较为契合。我希望能将我的技术专长与团队的力量结合,提升贵公司产品的用户体验和市场竞争力。”

项目

根据你的项目描述,面试官可能会从以下几个方面进行深入提问,以考察你的技术深度、解决问题的能力以及项目的真实性。以下是可能的问题方向及应对建议:


1. 项目背景与需求分析

可能的问题

  • 你提到的“电子音乐教室系统”是为了解决什么问题?它的核心功能是什么?
  • 这个系统的用户群体是谁?你如何保证功能满足用户需求?
  • 在“管道智能检测机器人”项目中,为什么需要实时点云数据的 3D 渲染?

建议回答

面试官关注的是你对项目整体的理解和需求背景的把握。可以从项目的目标、解决的核心问题、用户场景入手。

示例
电子音乐教室系统旨在解决传统音乐教学中教师和学生互动性不足的问题,通过实时音频处理、波形可视化和多通道录制功能,提升教学效率。用户主要是音乐教师和学生,因此我们特别注重界面的直观性和交互性,例如通过波形展示帮助教师实时评估学生的练习效果。

追问可能

  • 如何设计满足用户需求的交互?
  • 是否做过用户反馈?如果有,如何改进?

2. 技术选型与实现细节

可能的问题

  • 为什么选择 Qt 和 FFmpeg?这些技术在项目中如何分工协作?
  • Qt 的信号槽机制在波形更新中是怎么实现的?是否有性能瓶颈?
  • QQuick3D 如何实现点云渲染?与其他渲染技术(如 OpenGL)相比有何优劣?

建议回答

展示你对技术选型的清晰思考,并提供具体实现细节。

示例
我们选择 Qt 因为它具备跨平台能力,能够减少不同操作系统间的重复开发工作。FFmpeg 则是用于音视频处理的优秀开源库,在音频滤波、重采样方面有丰富的 API 支持。在实时音频波形的更新中,我使用 Qt 的信号槽机制,将后台采集的音频数据通过信号发送到前端 QML 组件,每隔 50ms 更新一次波形。为了提升性能,我们对更新频率进行了限制,并通过多线程保证实时性。

在管道检测项目中,QQuick3D 提供了一个较高层次的 3D 渲染接口,减少了我们直接使用 OpenGL 编程的复杂度,特别是在点云数据动态更新和交互设计上更方便。

追问可能

  • 信号槽更新频率如何确定?
  • 如果波形更新卡顿,如何定位问题?
  • 点云数据量较大时,如何优化渲染性能?

3. 性能优化

可能的问题

  • 多线程任务调度如何实现?哪些任务被分配到线程中?线程间如何通信?
  • 在实时音频处理或点云渲染中,是否遇到性能瓶颈?如何优化?
  • 在网络传输中,如何保证实时性和数据完整性?

建议回答

从实际问题入手,解释遇到的挑战、分析原因以及解决办法。

示例
在音频波形更新中,我们使用 QThread 将数据采集和处理分配到后台线程,主线程专注于 UI 渲染。线程之间通过信号槽进行数据同步。初期我们确实遇到波形卡顿的问题,原因是音频数据处理和波形绘制都在主线程进行,优化后通过 QThread 实现了任务分离,大幅提升了系统流畅性。

对于点云渲染,我们采用了分块加载的方式,对大规模点云数据进行分段解析和渲染,避免一次性加载导致卡顿。此外,通过降低点的显示密度和异步加载提升了帧率。

追问可能

  • QThread 使用过程中,如何避免线程安全问题?
  • 分块加载点云数据如何设计?有哪些权衡?
  • 网络传输中的丢包问题如何解决?

4. 网络通信

可能的问题

  • 使用 TCP/UDP 的具体场景是什么?为什么选择这种协议?
  • 数据传输过程中是否需要序列化?如何实现?
  • 如何确保实时音频或点云数据的传输质量?

建议回答

重点解释协议选型和传输质量保证的措施。

示例
我们在音频数据传输中使用了 TCP 协议,因为其可靠性可以保证音频数据的完整性。在点云数据传输中则使用了 UDP 协议,UDP 的低延迟更适合实时传输。为了确保传输质量,我们在 UDP 中引入了简单的重传机制,通过序列号检查数据包的完整性。此外,音频数据在传输前使用轻量级的压缩算法减小带宽占用,点云数据则通过稀疏化和分块传输减轻了网络负担。

追问可能

  • 选择 TCP/UDP 时的性能对比测试数据?
  • 重传机制的具体实现?是否会增加延迟?
  • 数据压缩的算法选择和效果?

5. 项目难点与问题解决

可能的问题

  • 项目中你遇到的最大难点是什么?你是如何解决的?
  • 音频滤波或重采样的具体实现流程是怎样的?
  • 实现点云 3D 渲染时,如何处理数据更新与显示的冲突?

建议回答

展示你的问题分析能力和解决问题的思路,具体步骤要清晰。

示例
在音频处理模块中,初期我们遇到的问题是音频滤波后的延迟较高,影响实时性。我们通过分析发现是因线程切换导致的延迟。优化方案是将滤波和重采样任务合并到同一线程中,并使用 FFmpeg 的高效 API 实现滤波器预设,最终将延迟从 300ms 降低到了 50ms。

在点云渲染中,挑战是动态更新与渲染之间的冲突。为此,我们在接收线程完成数据解析后,通过信号槽通知渲染线程更新显示,同时采用双缓冲机制,避免显示过程中读取数据导致的闪烁问题。

追问可能

  • 线程切换延迟的具体优化指标?
  • 双缓冲机制如何设计?是否有线程安全问题?

总结建议

  • 提前梳理技术细节:准备好每个项目涉及的核心技术和算法,做到心中有数。
  • 逻辑清晰,层层递进:回答时从整体到细节,避免泛泛而谈,最好结合代码实现的核心逻辑讲解。
  • 坦诚面对不懂的问题:如果遇到不会的问题,可以表达自己思考的方向或如何查找答案,展现学习能力。

以下是可能的追问以及针对每个问题的详细答案:


1. 设计满足用户需求的交互,是否做过用户反馈?如果有,如何改进?

答案
在电子音乐教室系统中,为了设计满足用户需求的交互,我们在开发初期与多位音乐教师沟通,了解他们的教学痛点,比如学生音频练习的实时反馈和多通道录制的需求。
用户反馈中提到最初版本的波形展示不够直观,我们根据建议调整了颜色编码来区分学生的音频轨道,并增加了缩放和滚动功能,让教师可以更方便地检查特定时间段的音频波形。通过持续的用户反馈和迭代,我们的产品逐渐贴合用户的实际使用场景。


2. 信号槽更新频率如何确定?

答案
音频波形更新频率的选择是一个平衡 UI 流畅性和性能消耗的问题。我们通过实验发现,每秒更新 20 次(即 50ms 一次)可以确保波形展示流畅且对主线程性能影响较小。
如果更新频率过高,主线程可能会因频繁绘制而出现卡顿;如果频率过低,用户会感到波形不够实时。在性能测试中,我们使用 Qt 自带的工具(如 QML Profiler)分析了主线程的负载,最终验证了这一更新频率的合理性。


3. 如果波形更新卡顿,如何定位问题?

答案
定位波形更新卡顿问题时,我们使用以下方法:

  1. 使用 QML Profiler:分析是否存在频繁的主线程阻塞。
  2. 检查信号槽机制:确保信号槽调用过程中没有长时间运行的阻塞代码。
  3. 优化数据处理逻辑:将音频数据采集、处理和绘制拆分到不同的线程,避免处理耗时的任务在主线程中执行。
  4. 渲染优化:减少波形绘制中的细节,比如降低采样点数量或使用 GPU 加速渲染。

最终通过调整线程分工和减少绘制负载解决了卡顿问题。


4. QThread 使用过程中,如何避免线程安全问题?

答案
我们采取了以下措施避免线程安全问题:

  1. 数据同步:通过 QMutexQReadWriteLock 保护共享资源,避免多个线程同时修改数据。
  2. 避免直接访问共享资源:线程之间通过信号槽通信,不直接共享数据,信号槽机制本身是线程安全的。
  3. 任务分工明确:主线程专注于 UI 渲染,后台线程处理数据,避免资源争用。

例如,在音频波形更新中,我们通过信号将处理后的数据传递给主线程,主线程只负责渲染,不直接操作音频数据,确保线程安全。


5. 分块加载点云数据如何设计?有哪些权衡?

答案
设计思路

  • 将点云数据按空间位置分块,每次只加载当前视图范围内的点云数据。
  • 使用四叉树或八叉树对点云数据进行分区管理,通过视口裁剪机制动态加载和卸载数据。

权衡点

  1. 加载性能 vs 内存占用
    分块加载减少了一次性加载的内存占用,但增加了视口切换时的加载延迟。我们通过预加载相邻块来缓解切换延迟的问题。
  2. 分块大小的选择
    块过大可能导致加载时间长,过小则增加管理复杂性。我们通过测试选择了 1MB 左右的分块大小,性能和复杂性达到了平衡。

6. 选择 TCP/UDP 时的性能对比测试数据?

答案
在实际项目中,我们对 TCP 和 UDP 的性能进行了测试:

  1. TCP(音频传输)
    • 平均延迟:50ms
    • 丢包率:<0.1%
    • 适用场景:需要数据完整性(如音频文件传输)。
  2. UDP(点云数据传输)
    • 平均延迟:20ms
    • 丢包率:约 2%(无重传机制时)。
    • 适用场景:实时性要求较高的数据(如实时点云渲染)。

根据测试结果,音频传输对完整性要求高,我们选择 TCP;点云数据实时性优先,选择 UDP。


7. 重传机制的具体实现?是否会增加延迟?

答案
实现方法

  • 在每个 UDP 数据包中加入序列号,接收端检查序列号连续性,发现丢包时通过反馈机制请求发送端重传。
  • 设置重传时间窗口(如 50ms),超过窗口的数据丢弃,确保整体实时性。

延迟影响

  • 单次重传增加的延迟大约为网络往返时间(如 20-50ms)。
  • 我们对非关键数据(如部分点云片段)采用丢弃策略,对关键数据才请求重传,减少了延迟影响。

8. 数据压缩的算法选择和效果?

答案
算法选择

  • 音频数据使用简单的 ADPCM(自适应差分脉冲编码调制)算法,压缩比约 2:1。
  • 点云数据使用 Octree 压缩,压缩比约 4:1,同时保留了空间结构信息。

效果
通过数据压缩,我们将音频传输的带宽需求从 256kbps 降低到 128kbps;点云数据从每秒 20MB 降低到 5MB,有效降低了网络负载。


9. 双缓冲机制如何设计?是否有线程安全问题?

答案
设计方法

  • 在内存中维护两个缓冲区(前台和后台缓冲区),后台线程负责数据更新,前台线程负责渲染。
  • 数据更新完成后,后台缓冲区和前台缓冲区交换角色,确保渲染过程中数据一致。

线程安全保障

  • 通过 QMutex 保护缓冲区切换操作,避免读写冲突。
  • 使用信号槽机制通知前台线程更新渲染,后台线程完成更新后才触发缓冲区切换。

总结
准备这些追问答案时,尽量结合项目中的实际经验,提供清晰、具体、可量化的例子,展现你的技术深度和解决问题的能力。如果遇到无法回答的追问,可以冷静分析,并提出自己的思考方向。

25八股文

TCP怎么避免拥塞new

TCP 通过 拥塞窗口(cwnd) 动态调整发送速率来避免拥塞。当连接开始时,采用 慢启动(指数增长)快速探测网络容量;当达到一定阈值后进入 拥塞避免 阶段(线性增长)。如果检测到丢包(通过超时或重复 ACK),TCP 会将窗口减小,重新调整发送速率。快速重传和快速恢复机制用于尽快修复轻微丢包,同时避免大幅降低发送效率。通过这些机制,TCP 实现了对网络状态的动态适应,减少拥塞的发生。

http都是用的TCP吗(http3不是,用的UDP)new

HTTP/3 使用的是 UDP 而不是 TCP,通过基于 UDP 的 QUIC 协议 提供可靠传输功能。QUIC 集成了类似 TCP 的可靠性和拥塞控制机制,同时减少了连接建立的延迟。

那http3用UDP怎么保证可靠传输(QUIC协议包装过一次,聊quic原理)new

简短:HTTP/3 使用基于 UDP 的 QUIC 协议,通过分片重传、流级别的可靠性、多路复用、拥塞控制和 ACK 确认机制,来实现类似 TCP 的可靠传输,同时减少延迟和队头阻塞问题。

复杂:HTTP/3 使用基于 UDP 的 QUIC 协议 来保证可靠传输,具体通过以下机制实现:

  1. 数据分片与重传

    • QUIC 将数据分片并标记序号,如果某个分片丢失,会针对该分片单独重传,而无需重新传输整个数据流。
  2. 流级别的可靠性

    • QUIC 支持多路复用,将数据分为独立的流,每个流都有自己的序号管理。一个流的丢包不会影响其他流的传输。
  3. 拥塞控制和流量控制

    • QUIC 实现了与 TCP 类似的拥塞控制机制(如基于带宽、RTT 计算)以及流量控制机制,防止网络过载。
  4. 确认机制

    • QUIC 使用 ACK 报文确认接收的数据分片,并允许接收端汇报丢失的分片,帮助发送端快速恢复丢包。
  5. 传输加密

    • QUIC 内置了 TLS 1.3 协议,确保数据传输的完整性和安全性,防止丢包导致的数据篡改。

通过这些设计,QUIC 在 UDP 的基础上实现了类似 TCP 的可靠性,同时解决了 TCP 的部分缺陷(如连接延迟和队头阻塞问题)。

进程和线程聊一下new

进程线程是操作系统中的两种基本运行单位:

  • 进程:是程序的一个独立运行实例,拥有独立的内存空间和系统资源。进程之间相互独立,通信需要通过进程间通信(IPC)。

    • 特点:资源隔离、启动和切换开销较大。
  • 线程:是进程中的一个执行单元,多个线程共享进程的内存和资源。线程之间通信简单,但需要注意同步和互斥问题。

    • 特点:切换开销小,但线程间共享资源可能引发竞争。

简单对比

  • 隔离性:进程更独立,线程共享内存。
  • 性能:线程比进程轻量,创建和切换更快。
  • 适用场景:进程适合独立任务,线程适合并发计算。

操作系统给进程分配的空间结构什么样(32系统1g内核,堆栈,.bss .data那些)new

在 32 位操作系统中,典型的进程内存布局如下(以 1GB 内核空间为例):

  1. 内核空间(1GB)

    • 高地址部分(通常从 0xC00000000xFFFFFFFF)是为操作系统内核保留的,用户态进程无法直接访问。
  2. 用户空间(3GB)

    • 从低地址到高地址,划分为以下主要区域:
      • 栈(Stack):用于函数调用、局部变量等,地址向下增长(从高地址向低地址)。
      • 堆(Heap):用于动态分配的内存,地址向上增长(从低地址向高地址)。
      • BSS 段:存放未初始化的全局变量,初始值为 0,占用实际内存。
      • 数据段(.data):存放已初始化的全局变量和静态变量。
      • 代码段(.text):存放可执行程序的指令,通常是只读的。

简单表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
0xFFFFFFFF | 内核空间(1GB)
| ---------------------
| 栈(动态局部变量、函数调用)
| ---------------------
| 堆(动态分配内存)
| ---------------------
| BSS 段(未初始化全局/静态变量)
| ---------------------
| 数据段(已初始化全局/静态变量)
| ---------------------
0x00000000 | 代码段(程序指令)
```

**注意**:堆和栈之间有一段未使用空间,当两者增长时,可能发生冲突(内存不足)。
## 2g的物理空间,但是给进程分配了4g空间发生什么(从虚拟内存原理那块说明)new
简单:当物理内存只有 2GB,但为进程分配了 4GB 虚拟内存,操作系统通过 **虚拟内存** 技术实现。虚拟内存将虚拟地址映射到物理地址,活跃的页面保存在物理内存中,不活跃的页面则存储在磁盘的交换区(swap)。当需要访问不在内存的页面时,触发缺页中断,从磁盘加载页面。这种机制使进程认为自己有完整的 4GB 空间,但频繁的磁盘操作可能导致性能下降。

复杂:当物理内存为 2GB,但给进程分配了 4GB 的虚拟内存时,操作系统利用了 **虚拟内存(Virtual Memory)** 的机制,通过地址映射和分页技术,使每个进程可以认为自己有一片完整的独立内存空间。以下是工作原理的简要说明:

---

### **1. 虚拟内存的概念**
- **虚拟地址空间**:进程使用的是虚拟地址,通常为 4GB(在 32 位系统中),这并不与实际的物理内存直接对应。
- **物理内存**:操作系统通过一部分物理内存(如 2GB)和 **磁盘(如交换分区/文件)** 提供虚拟内存的支持。

---

### **2. 地址映射(分页机制)**
- **页(Page)**:虚拟内存被划分成固定大小的页(如 4KB)。
- **页表(Page Table)**:操作系统为每个进程维护一个页表,记录虚拟地址和物理地址的映射关系。
- 当进程访问某个虚拟地址时:
1. 通过 **页表** 找到该虚拟地址对应的物理地址。
2. 如果数据在物理内存中,直接访问。
3. 如果数据不在物理内存中(即发生 **缺页中断**),则操作系统从磁盘加载对应页到内存,并更新页表。

---

### **3. 物理内存不足时的策略**
- 当物理内存(2GB)无法同时容纳所有活跃进程的页面时,操作系统采用 **页面置换算法**,将暂时不用的页写入磁盘交换区(swap),腾出空间存储新的页面。
- 常见的页面置换算法:
- **LRU(最近最少使用)**:替换最近最少被访问的页面。
- **FIFO(先进先出)**:替换最早加载的页面。

---

### **4. 用户视角**
- 每个进程仍然认为自己有完整的 4GB 虚拟地址空间,因为地址转换和缺页处理对用户透明。
- 但实际受限于物理内存和磁盘速度:
- 如果进程需要大量的内存,频繁的缺页中断会导致性能下降(**抖动**)。

---

### **总结**
通过虚拟内存机制,操作系统在 2GB 物理内存的情况下,仍然能为每个进程分配 4GB 虚拟地址空间。地址映射、页表和磁盘交换的结合,保证了进程的内存访问需求,但过度依赖磁盘会导致性能问题。
## C++ volatile关键字有什么用new
在 C++ 中,**`volatile`** 关键字用于告诉编译器一个变量的值可能会被程序外部(如硬件或其他线程)修改,因此编译器在优化代码时不能对该变量的访问进行假设或优化。

### **主要作用:**
1. **防止编译器优化**
- 对 `volatile` 变量的每次访问,编译器都会直接从内存中读取,而不是使用寄存器中的缓存值。
- 避免因编译器优化导致的访问问题。

2. **适用于特殊场景**
- **硬件寄存器**:与硬件设备通信时,某些寄存器的值可能随时变化。
- **多线程**:一个线程中修改的变量,另一个线程需要及时读取(但仅用 `volatile` 不能完全解决多线程问题,需要结合同步机制如 `std::atomic`)。

---

### **示例代码:**
```cpp
volatile int flag = 0;

// 示例:等待某硬件状态改变
while (flag == 0) {
// 因为 `flag` 是 volatile,每次循环都会重新读取它的值
// 而不会使用寄存器中的缓存值
}

注意事项:

  1. 不是线程安全工具

    • volatile 仅防止优化,不解决数据竞争问题。多线程访问需配合 std::atomic 或锁。
  2. 主要用于特殊硬件场景

    • 在现代 C++ 中,volatile 的使用较少,更多依赖其他同步和内存模型工具。

总结
volatile 关键字用于告诉编译器防止优化变量访问,适合硬件编程或处理随时可能被外部修改的变量,但在多线程中需配合更高级的同步工具来保障线程安全。

C++函数调用约定了解过吗 new

C++ 的函数调用约定决定了函数参数的传递方式、栈的清理责任以及返回值的处理规则。主要有以下几种:

  1. cdecl:默认调用约定,参数从右到左入栈,调用方清理栈,可支持可变参数。
  2. stdcall:参数从右到左入栈,被调用方清理栈,常用于 Windows API。
  3. fastcall:优先通过寄存器传递参数,其余参数按 cdecl 入栈,被调用方清理栈,用于性能优化。
  4. thiscall:用于类的非静态成员函数调用,通过寄存器传递 this 指针,其余参数从右到左入栈。
  5. vectorcall:优化用于传递浮点和向量类型参数,通过寄存器处理更多参数。

调用约定的选择可以影响性能或确保与不同平台、语言间的兼容性。

C++ 中 std::string 是如何管理字符串的?它和 C 风格字符串(char*)的区别是什么?

std::string 的管理方式

  • 动态分配内存std::string 使用动态分配的方式来存储字符串,可以根据需要自动调整大小。
  • 内部封装:它封装了内存管理、长度管理等细节,并且提供了一系列操作字符串的接口,如拼接、查找、切片等。
  • 自动清理std::string 在作用域结束时会自动释放内存,避免内存泄漏。

与 C 风格字符串(char*)的区别

  1. 内存管理

    • **std::string**:自动管理内存,无需手动分配或释放。
    • **char***:需要手动分配和释放内存,易出错。
  2. 长度管理

    • **std::string**:内部保存字符串长度,访问时间复杂度为 O(1)。
    • **char***:长度需要通过遍历字符串计算(如 strlen),时间复杂度为 O(n)。
  3. 功能性

    • **std::string**:提供丰富的成员函数,如拼接、比较、查找、替换等。
    • **char***:只能使用标准库函数(如 strcpystrcmp),操作不方便。
  4. 安全性

    • **std::string**:处理越界、内存分配失败等问题更安全。
    • **char***:容易出现越界访问或未释放内存的问题。

简单总结
std::string 更安全、更方便、功能更强大,适合现代 C++ 编程;char* 较底层,效率高但容易出错,适合对性能有极高要求的特定场景。

C++ 中如何高效地拼接字符串?

总结:
小规模拼接:直接用 += 或 append。
大规模拼接:使用 std::ostringstream 或预分配内存提高效率。
性能极限场景:考虑使用 std::vector 或其他自定义优化方法。

在多线程环境下,如何安全地操作 std::string?是否需要额外的同步机制?

总结
多线程只读:无需额外同步。
多线程读写:必须使用同步机制,如互斥锁(std::mutex)。
独立副本:使用 thread_local 避免共享数据。

std::vector 和 std::list 的底层实现和使用场景有什么区别?请具体说明插入、删除和随机访问的性能差异。

底层实现

  • **std::vector**:动态数组,元素连续存储。
  • **std::list**:双向链表,元素通过指针连接,非连续存储。

性能差异

  1. 插入/删除

    • **std::vector**:
      • 末尾插入/删除:时间复杂度 O(1)。
      • 中间插入/删除:时间复杂度 O(n),需要移动后续元素。
    • **std::list**:
      • 任意位置插入/删除:时间复杂度 O(1),只需调整指针。
  2. 随机访问

    • **std::vector**:支持随机访问,时间复杂度 O(1)。
    • **std::list**:不支持随机访问,需从头或尾遍历,时间复杂度 O(n)。

使用场景

  • **std::vector**:

    • 需要高效的随机访问或末尾插入/删除。
    • 元素数量较少,插入/删除操作较少。
  • **std::list**:

    • 需要频繁的中间插入/删除操作。
    • 不需要随机访问。

简单总结

  • **选择 std::vector**:需要随机访问,插入/删除主要在末尾。
  • **选择 std::list**:需要频繁的中间插入/删除操作,不关心随机访问。

如何在 C++ 中选择合适的容器(如 std::vector、std::list)来存储和操作数据?举一个具体的应用场景。

选择容器的原则

  1. 数据访问需求

    • 需要随机访问:选择 std::vector
    • 只需顺序遍历:std::liststd::deque 均可。
  2. 插入/删除频率

    • 频繁在中间插入/删除:选择 std::list
    • 插入/删除主要在末尾:选择 std::vectorstd::deque
  3. 存储结构要求

    • 元素数量动态变化且连续存储:选择 std::vector
    • 需要高效管理头部或尾部:选择 std::deque

具体应用场景

场景:文本编辑器的字符存储

  • 需求

    • 插入和删除字符频繁。
    • 不需要随机访问。
  • 选择容器

    • 使用 std::list,因为链表的插入/删除效率高,避免因频繁操作引发的大量内存移动。

总结:根据操作模式选择合适的容器能显著提高效率。

std::vector 的容量增长是如何管理的?扩容机制的时间复杂度是什么?

std::vector 的容量增长管理

  • 容量增长策略
    • std::vector 的大小超过当前容量时,会自动扩容,通常容量会按 倍数增长(一般是 1.5 倍或 2 倍,具体依赖于实现)
    • 扩容时,会分配一块更大的连续内存,并将原有元素拷贝到新内存中。

扩容机制的时间复杂度

  • 单次扩容:需要拷贝所有元素到新内存,时间复杂度为 **O(n)**,其中 n 是当前元素数量。
  • 均摊复杂度:由于扩容频率较低,插入操作的 **均摊时间复杂度为 O(1)**。

总结

  • std::vector 通过倍数扩容减少内存分配次数。
  • 单次扩容复杂度为 O(n),但大多数情况下,插入操作的 **均摊复杂度为 O(1)**,因此性能较优。

std::unordered_map 的底层实现原理是什么?如何处理哈希冲突?

std::unordered_map 的底层实现原理

  • 基于 哈希表 实现,通过哈希函数将键映射到对应的桶(bucket)。
  • 每个桶存储一组键值对(key-value),用于快速查找。

哈希冲突的处理方式

  • 使用 链地址法(chaining):
    • 每个桶存储一个链表(或其他容器,如 std::forward_list),哈希冲突时,将多个键值对存储在同一个桶中链表的不同节点上。

简单总结

  • 快速查找:依赖哈希函数,平均时间复杂度为 **O(1)**。
  • 冲突处理:通过链地址法解决冲突,冲突严重时性能可能退化为 **O(n)**。
  • 适合用于需要快速查找或插入的场景。

std::unordered_map 的时间复杂度是什么?在什么情况下可能退化到O(n)?

std::unordered_map 的时间复杂度

  • 查找、插入、删除:平均时间复杂度是 **O(1)**,因为哈希表通过哈希函数直接定位桶的位置。

退化为 O(n) 的情况

当哈希冲突严重时,std::unordered_map 的性能可能退化到 **O(n)**。
具体情况包括:

  1. 哈希函数质量差

    • 哈希函数分布不均匀,大量键被映射到同一个桶中,链表长度增大。
  2. 大量数据插入

    • 如果桶的数量不足且未及时扩容,大量元素集中在少数桶中,导致链表变长。
  3. 恶意输入

    • 攻击者设计输入,使所有键的哈希值相同(例如,使用简单或弱的哈希函数),所有键都落入同一个桶。

解决办法

  1. 使用高质量的哈希函数(默认的 std::hash 一般足够)。
  2. 定期调整负载因子(Load Factor),避免桶过于拥挤。
  3. 如果性能要求严格,可使用 **std::map**(红黑树实现,时间复杂度稳定在 O(log n))。

总结

std::unordered_map 在正常情况下具有 O(1) 的性能,但在哈希冲突严重时可能退化为 O(n)。选择合适的哈希函数和控制负载因子可以减少退化风险。

C++ 中 std::unordered_map 是线程不安全的,如果要在多线程环境中使用哈希表,你会如何设计一个线程安全的版本?

在多线程环境下,设计一个线程安全的 std::unordered_map 需要考虑并发访问的场景。以下是常见的解决方案:


1. 全局锁保护

  • 方法:使用一个 std::mutex 对整个哈希表加锁,所有操作(插入、删除、查找)都串行化。
  • 优点:实现简单,适合小规模并发场景。
  • 缺点:高并发时性能较低。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <unordered_map>
#include <mutex>

template <typename K, typename V>
class ThreadSafeUnorderedMap {
private:
std::unordered_map<K, V> map;
std::mutex mtx;

public:
void insert(const K& key, const V& value) {
std::lock_guard<std::mutex> lock(mtx);
map[key] = value;
}

bool find(const K& key, V& value) {
std::lock_guard<std::mutex> lock(mtx);
auto it = map.find(key);
if (it != map.end()) {
value = it->second;
return true;
}
return false;
}

void erase(const K& key) {
std::lock_guard<std::mutex> lock(mtx);
map.erase(key);
}
};

2. 分段锁(分桶锁)

  • 方法:对哈希表的不同桶(bucket)分别加锁,每个桶使用独立的锁,只加锁访问的桶。
  • 优点:减少锁的粒度,提高并发性能。
  • 缺点:实现复杂,需手动管理桶锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <vector>
#include <unordered_map>
#include <mutex>
#include <shared_mutex>

template <typename K, typename V>
class ThreadSafeUnorderedMap {
private:
static const size_t BUCKET_COUNT = 16;
std::vector<std::unordered_map<K, V>> buckets{BUCKET_COUNT};
std::vector<std::shared_mutex> locks{BUCKET_COUNT};

size_t getBucketIndex(const K& key) const {
return std::hash<K>{}(key) % BUCKET_COUNT;
}

public:
void insert(const K& key, const V& value) {
size_t index = getBucketIndex(key);
std::unique_lock<std::shared_mutex> lock(locks[index]);
buckets[index][key] = value;
}

bool find(const K& key, V& value) {
size_t index = getBucketIndex(key);
std::shared_lock<std::shared_mutex> lock(locks[index]);
auto it = buckets[index].find(key);
if (it != buckets[index].end()) {
value = it->second;
return true;
}
return false;
}

void erase(const K& key) {
size_t index = getBucketIndex(key);
std::unique_lock<std::shared_mutex> lock(locks[index]);
buckets[index].erase(key);
}
};

3. 使用现成的线程安全容器

  • 方法:使用第三方库,如 tbb::concurrent_hash_map 或 **boost::unordered_map**,它们提供高性能的线程安全哈希表。
  • 优点:性能优化较好,无需自己实现。
  • 缺点:需依赖外部库。

总结

  • 小规模并发:使用全局锁保护。
  • 高并发场景:分段锁(分桶锁)方案更高效。
  • 复杂项目:优先选择成熟的第三方线程安全容器。

线程安全如何解决排查应对,sync和reen区别(换成cpp考察),乐观悲观锁(cpp考察)

线程安全的解决和排查

  1. 解决线程安全问题

    • 锁机制:使用 std::mutexstd::shared_mutex 控制对共享资源的访问。
    • 原子操作:使用 std::atomic 提供无锁操作支持。
    • 线程安全容器:如 concurrent_unordered_map(TBB 库)或自实现的分段锁机制。
  2. 排查线程安全问题

    • 工具检测:使用工具如 Valgrind(Helgrind 模块)、ThreadSanitizer 检测数据竞争。
    • 代码审查:明确共享资源的访问范围和生命周期,检查潜在的竞争条件。

synchronizedReentrantLock 的区别(C++ 对应考察)

Java 中的 synchronizedReentrantLock 对应 C++ 中的以下机制:

  1. **std::mutex**(类似 synchronized):

    • 特性
      • 非递归锁,简单易用。
      • 线程需要显式加锁和解锁(可以用 std::lock_guard 简化)。
    • 缺点:不支持重入(递归锁)。
  2. **std::recursive_mutex**(类似 ReentrantLock):

    • 特性
      • 支持同一线程多次获取同一锁(递归锁)。
      • 可解决递归调用时的死锁问题,但需谨慎使用,避免滥用递归锁导致复杂性增加。

乐观锁和悲观锁(C++ 对应考察)

  1. 悲观锁

    • 含义:假定操作会发生冲突,先加锁再操作,保证安全性。
    • 实现:使用 std::mutexstd::shared_mutex 实现。
    • 适用场景:高冲突的多线程场景,需要强一致性。
    • 示例
      1
      2
      3
      4
      5
      std::mutex mtx;
      void critical_section() {
      std::lock_guard<std::mutex> lock(mtx);
      // 操作共享资源
      }
  2. 乐观锁

    • 含义:假定操作不会发生冲突,直接操作资源,若检测到冲突则重试。
    • 实现:使用 std::atomic 或 CAS(Compare-And-Swap)。
    • 适用场景:低冲突、高性能需求的场景。
    • 示例
      1
      2
      3
      4
      5
      6
      std::atomic<int> value{0};
      void optimistic_update(int expected, int new_value) {
      while (!value.compare_exchange_weak(expected, new_value)) {
      expected = value.load();
      }
      }

总结考察点

  1. 线程安全的基本手段:锁、原子操作、线程安全容器。
  2. 悲观锁:使用 std::mutex,确保强一致性。
  3. 乐观锁:使用 std::atomic 和 CAS,追求高性能但需处理冲突。
  4. 递归锁:考察 std::recursive_mutex 的应用场景和限制。

Redis为什么快 Redis数据类型和结构 MySQL存储引擎 和 隔离级别

Redis 为什么快

  1. 全内存操作
    • Redis 将所有数据存储在内存中,读写操作不涉及磁盘 I/O,极大提升了速度。
  2. 单线程模型
    • 使用事件驱动的单线程架构,避免了多线程上下文切换和锁争用问题。
  3. 高效的数据结构
    • 采用优化的数据结构(如跳表、压缩列表),能快速处理大量操作。
  4. 异步非阻塞 I/O
    • 使用 I/O 多路复用(如 epoll),同时处理多个客户端连接,响应迅速。
  5. 持久化优化
    • 写时日志(AOF)和定期快照(RDB)让持久化对性能影响最小化。

Redis 数据类型和结构

  1. 字符串(String)

    • 最基本的数据类型,支持普通键值存储、计数器等场景。
    • 底层实现:简单动态字符串(SDS)。
  2. 哈希(Hash)

    • 存储键值对集合,适合对象表示,如用户信息。
    • 底层实现:压缩列表或哈希表(取决于数据规模)。
  3. 列表(List)

    • 有序字符串列表,支持队列、栈等操作。
    • 底层实现:压缩列表或双向链表。
  4. 集合(Set)

    • 无序集合,支持快速去重、交集、并集操作。
    • 底层实现:哈希表或整数数组。
  5. 有序集合(Sorted Set)

    • 带分数的有序集合,适合排行榜等场景。
    • 底层实现:跳表和哈希表组合。
  6. 位图(BitMap)HyperLogLogGEO 等扩展类型。


MySQL 存储引擎

  1. InnoDB(默认引擎):

    • 支持事务、外键、MVCC,适合高并发场景。
    • 索引:聚簇索引(Clustered Index)。
  2. MyISAM

    • 轻量级引擎,不支持事务和外键,适合只读或低并发场景。
    • 索引:非聚簇索引(Non-Clustered Index)。
  3. Memory

    • 数据存储在内存中,速度快但不持久化,适合临时数据。
  4. 其他引擎

    • CSV:存储为 CSV 文件。
    • NDB:适合分布式集群场景(MySQL Cluster)。

MySQL 隔离级别

  1. 读未提交(Read Uncommitted)
    • 事务可以读取未提交的数据,可能导致脏读。
  2. 读已提交(Read Committed)
    • 只能读取已提交的数据,避免脏读,但可能出现不可重复读。
  3. 可重复读(Repeatable Read)(InnoDB 默认):
    • 保证事务中多次读取的结果一致,避免脏读和不可重复读,但可能出现幻读。
  4. 可串行化(Serializable)
    • 最高级别,事务按顺序执行,完全避免脏读、不可重复读和幻读,但性能最低。

总结

  1. Redis 快的原因:全内存操作、单线程、I/O 多路复用等优化。
  2. Redis 数据类型:支持多种高效结构(String、List、Set 等),灵活满足业务需求。
  3. MySQL 存储引擎:InnoDB 支持事务和高并发,MyISAM 更轻量。
  4. MySQL 隔离级别:从低到高,依次是 读未提交、读已提交、可重复读、可串行化,需根据业务需求选择。

HTTP 1.1->2.0->3.0(详细讲解从1.1到3.0) HTTPS 状态码

HTTP 1.1 → 2.0 → 3.0 演进详细讲解


HTTP/1.1

  1. 核心特性

    • 持久连接:默认使用 Connection: keep-alive,避免每次请求都重新建立 TCP 连接。
    • 管道化(Pipeline):支持在同一个连接中同时发送多个请求,但需要按顺序接收响应。
    • 缓存控制:引入了 Cache-ControlETag 等头部,提升缓存管理能力。
    • 虚拟主机支持:通过 Host 头部允许同一 IP 地址支持多个域名。
  2. 主要问题

    • 队头阻塞(Head-of-Line Blocking):一个请求未完成会阻塞后续请求,即便在管道化模式下依然存在。
    • 单请求-响应模型:每次请求需带完整头部信息,导致冗余。
    • 高延迟:受限于 TCP 握手和慢启动机制,延迟较高。

HTTP/2.0

  1. 核心改进

    • 二进制分帧:将数据分为帧传输,帧内包含标识信息,减少了头部解析的复杂性。
    • 多路复用:在单个 TCP 连接中并行处理多个请求和响应,解决了队头阻塞问题(应用层队头阻塞)。
    • 头部压缩:使用 HPACK 算法压缩 HTTP 头部,减少带宽消耗。
    • 服务器推送(Server Push):服务器可主动向客户端推送资源,减少请求延迟。
  2. 局限性

    • TCP 队头阻塞仍存在:虽然多路复用解决了应用层问题,但 TCP 连接本身依然受队头阻塞影响。
    • 复杂性增加:二进制分帧和流的管理增加了实现难度。

HTTP/3.0

  1. 核心改进

    • 基于 QUIC 协议:使用 UDP 代替 TCP,QUIC 提供可靠性和加密支持,解决了 TCP 的队头阻塞问题。
    • 无队头阻塞:QUIC 每个流独立处理,避免了因一个流阻塞而影响其他流。
    • 更快的连接建立
      • 使用 0-RTT 和 1-RTT 的握手机制,首次连接延迟低。
      • TLS 1.3 集成在 QUIC 中,无需额外握手。
    • 内置加密:QUIC 协议默认加密,保证数据传输安全。
  2. 优点

    • 低延迟:减少握手次数和队头阻塞。
    • 高效传输:流量控制粒度更细,适合高并发场景。
  3. 不足

    • 兼容性问题:依赖底层 UDP,部分防火墙或 NAT 设备可能限制。
    • 实现复杂:QUIC 的逻辑比 TCP 和 HTTP/2.0 更复杂。

HTTPS

  1. 定义

    • HTTPS 是 HTTP + TLS(或 SSL),通过加密层保护数据传输的安全性。
  2. 核心功能

    • 加密:防止数据被窃听。
    • 认证:验证服务器身份(通过证书)。
    • 完整性:确保数据未被篡改。
  3. 握手流程(TLS 1.3 简化):

    • 客户端发送 ClientHello 包,附带加密算法列表。
    • 服务端返回 ServerHello,确认加密算法并附上证书。
    • 客户端验证证书,生成会话密钥。
    • 双方使用密钥加密通信。

HTTP 状态码

1xx 信息响应

  • 100 Continue:客户端应继续请求。
  • 101 Switching Protocols:协议切换(如 WebSocket)。

2xx 成功响应

  • 200 OK:请求成功。
  • 201 Created:资源已创建。
  • 204 No Content:请求成功,无内容返回。

3xx 重定向响应

  • 301 Moved Permanently:永久重定向。
  • 302 Found:临时重定向。
  • 304 Not Modified:资源未修改,使用缓存。

4xx 客户端错误

  • 400 Bad Request:请求有误。
  • 401 Unauthorized:未认证。
  • 403 Forbidden:拒绝访问。
  • 404 Not Found:资源未找到。
  • 429 Too Many Requests:请求过多。

5xx 服务端错误

  • 500 Internal Server Error:服务器内部错误。
  • 502 Bad Gateway:网关错误。
  • 503 Service Unavailable:服务不可用。
  • 504 Gateway Timeout:网关超时。

总结

  • HTTP/1.1 → HTTP/2.0 → HTTP/3.0 演进解决了效率、队头阻塞、延迟问题,但实现复杂性逐渐增加。
  • HTTPS 提供加密、认证和完整性保障。
  • 状态码 表示请求响应的结果和错误类别,需熟练掌握常见状态码的含义。

TCP 拥塞控制流量控制

TCP 拥塞控制与流量控制

流量控制(Flow Control)

流量控制是为了避免接收端被发送端数据压垮,确保发送端不会发送超过接收端处理能力的数据量。

机制

  1. 滑动窗口(Sliding Window)

    • 每个 TCP 连接都有一个接收窗口 (Receive Window),表示接收端当前能接收的数据量。
    • 发送端根据接收端的窗口大小决定发送数据的数量。
  2. 动态调整

    • 接收端通过 ACK 包中的窗口大小字段通知发送端最新的接收能力。
    • 如果接收窗口大小为 0,发送端暂停发送,周期性发送探测包询问接收端是否恢复。

目标
让发送速度与接收速度匹配,避免接收端被压垮。


拥塞控制(Congestion Control)

拥塞控制是为了避免网络因过载而性能下降,通过控制发送端流量来防止网络拥塞。

机制

  1. 慢启动(Slow Start)

    • 初始发送速度较低,拥塞窗口 (Congestion Window) 从小值开始。
    • 每收到一个 ACK,窗口加倍(指数增长)。
    • 当达到阈值 (ssthresh) 后进入拥塞避免。
  2. 拥塞避免(Congestion Avoidance)

    • 窗口增长变为线性增长,每次收到 ACK 增加一个固定值。
    • 通过逐步提高发送速率观察网络状态。
  3. 快速重传(Fast Retransmit)

    • 如果收到多个重复的 ACK,认为某个数据包丢失,立即重传丢失的数据包,而无需等待超时。
  4. 快速恢复(Fast Recovery)

    • 当检测到丢包时,将 ssthresh 设置为当前窗口的一半,重传丢失包,并进入线性增长阶段。

关键点

  • 拥塞控制通过动态调整 拥塞窗口 (cwnd) 来适应网络状况。
  • 丢包或延迟增加被认为是拥塞的信号,触发窗口收缩。

流量控制与拥塞控制的对比

方面 流量控制 拥塞控制
目标 防止接收端被压垮 防止网络过载
依据 接收端的处理能力 网络的当前状况(丢包、延迟)
控制单位 接收窗口 (Receive Window) 拥塞窗口 (Congestion Window)
触发信号 接收端通告窗口 丢包、超时或延迟增加

总结

  • 流量控制 侧重于发送端和接收端的配合,确保数据不会过载接收端。
  • 拥塞控制 关注网络层,动态调整发送速率以避免网络拥塞。
  • 两者结合使用,保证数据传输的效率和稳定性。

前端到后端会经历什么过程,过程涉及什么协议,协议有什么字段

前端到后端的通信过程

  1. 用户发起请求:用户在浏览器中访问网站,前端向后端发送 HTTP 或 HTTPS 请求。

详细过程

1. DNS 解析

  • 浏览器根据用户输入的 URL,通过 DNS 协议解析出目标服务器的 IP 地址。
  • 协议:DNS(基于 UDP,默认端口 53)。
  • 重要字段
    • 请求字段
      • 查询类型(A、AAAA、CNAME 等)。
      • 查询名(如 www.example.com)。
    • 响应字段
      • 查询结果(目标 IP 地址)。
      • TTL(缓存时间)。

2. TCP 连接建立

  • 浏览器通过 TCP 与后端服务器建立连接。
  • 协议:TCP(三次握手)。
  • 重要字段
    • SYN、ACK 标志:三次握手标志。
    • 序列号(Sequence Number):数据包的编号。
    • 窗口大小(Window Size):流量控制相关。

3. TLS 握手(如果使用 HTTPS)

  • 如果是 HTTPS,建立加密连接。
  • 协议:TLS(默认端口 443)。
  • 重要字段
    • ClientHello:客户端支持的加密算法、随机数等。
    • ServerHello:服务器选择的加密算法、证书、公钥等。
    • Finished:双方确认加密通道建立完成。

4. HTTP 请求

  • 浏览器向服务器发送 HTTP 请求,后端接收并处理。
  • 协议:HTTP(或 HTTPS)。
  • 重要字段
    • 请求字段
      • 方法:GETPOSTPUTDELETE 等。
      • URL:请求的资源路径。
      • Headers(头部):附加信息(如 User-AgentAuthorization 等)。
      • Body:请求体,通常包含数据(如 JSON)。
    • 响应字段
      • 状态码:200、404、500 等。
      • Headers:附加信息(如 Content-TypeSet-Cookie)。
      • Body:返回的内容(如 HTML、JSON)。

5. 数据传输

  • 数据在传输层中分片,通过 IP 协议在网络中路由。
  • 协议:IP(默认 IPv4 或 IPv6)。
  • 重要字段
    • 源 IP 地址、目标 IP 地址。
    • 数据包大小、校验和。

总结

阶段 协议 重要字段
DNS 解析 DNS 查询类型、查询名、TTL、目标 IP 地址
TCP 建立连接 TCP SYN、ACK、序列号、窗口大小
TLS 握手(HTTPS) TLS ClientHello、ServerHello、公钥、证书、加密算法
HTTP 请求与响应 HTTP 或 HTTPS 方法、URL、状态码、Headers、Body
数据传输 IP、TCP 或 UDP 源/目标 IP 地址、端口号、数据包大小、校验和

这些协议协同工作,确保前端请求能成功到达后端,后端响应能准确返回到前端。

抽象类和接口的区别 单元测试怎么写,是如何执行的 冒泡排序空间复杂度会不会随数据量变大而变大 冒泡排序和快速排序对比

C++ 抽象类和接口的区别

  1. 抽象类

    • 含有至少一个纯虚函数的类称为抽象类。
    • 可以包含普通函数和成员变量。
    • 允许部分实现,但不能直接实例化。
  2. 接口

    • 在 C++ 中没有专门的接口定义,但可以通过纯抽象类模拟接口。
    • 纯抽象类:所有成员函数都是纯虚函数,且不包含任何成员变量。
    • 只能定义方法,不提供任何实现。

区别总结

  • 抽象类可以包含成员变量和部分实现;接口(纯抽象类)完全没有实现,只定义方法。
  • 抽象类更适合实现通用功能的基类,接口更适合定义标准化行为。

单元测试怎么写,如何执行

  1. 编写单元测试

    • 使用 C++ 单元测试框架(如 Google Test、Catch2)。
    • 为每个函数或模块编写独立的测试用例。
    • 测试用例包括输入、期望输出和实际结果的比较。
  2. 单元测试的结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <gtest/gtest.h>

    // 被测试函数
    int add(int a, int b) {
    return a + b;
    }

    // 测试用例
    TEST(AdditionTest, HandlesPositiveInput) {
    EXPECT_EQ(add(1, 2), 3);
    }

    int main(int argc, char **argv) {
    ::testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
    }
  3. 执行单元测试

    • 编译测试代码生成可执行文件。
    • 运行测试,框架会输出每个测试用例的通过或失败信息。

单元测试流程

  • 编写测试用例。
  • 使用框架运行测试。
  • 分析失败用例并修复代码。
  • 重复执行,确保所有测试通过。

冒泡排序空间复杂度是否随数据量变大而增加

  • 冒泡排序的空间复杂度为 ( O(1) )

    • 只需要常数级的额外空间(用于临时变量交换数据)。
    • 数据存储在原数组中,不需要额外分配内存。
  • 数据量变大不会增加额外空间需求

    • 冒泡排序的空间需求不依赖于输入数据量,只依赖于排序算法本身。

冒泡排序和快速排序对比

对比点 冒泡排序 快速排序
算法思想 相邻元素比较并交换位置,逐步冒泡 分治法,选一个基准元素分区
时间复杂度 最坏情况 ( O(n^2) ),平均 ( O(n^2) ) 最坏情况 ( O(n^2) ),平均 ( O(n \log n) )
空间复杂度 ( O(1) ),原地排序 ( O(\log n) )(递归栈)
适用场景 数据量小且近乎有序 数据量大且随机分布
稳定性 稳定(相等元素不改变相对位置) 不稳定
实现复杂度 简单,适合入门学习 较复杂,需要掌握分治思想

总结

  • 冒泡排序适合小规模数据、教学场景。
  • 快速排序是高效的通用排序算法,在大数据量时表现更好。

智力题:140g的盐 各有2g和7g的砝码 只能用三次天平,如何称出50g 和 90 g

7*7g砝码得到49g盐,2g砝码得到2g盐,2g盐自己对半秤到平衡得到两份1g的盐。
于是有了49+1=50g

线程池、参数配置、默认线程池、Atomic类、synchronized、volatile、JVM结构、GC、可达性分析、GC Root、finalize,进程线程、三次握手四次挥手、TCPUDP,okhttp

C++版回答:

线程池

  • 作用:线程池用于高效地管理线程,避免频繁创建和销毁线程,提高并发性能。
  • 实现
    • C++ 标准库没有直接提供线程池,可以使用开源库(如 Boost::ThreadPool)或自行实现。
    • 自己实现时通常包括一个任务队列和固定数量的工作线程,工作线程不断从任务队列中取任务执行。
  • 参数配置
    • 核心线程数:线程池初始化时的线程数量。
    • 最大线程数:线程池能够扩展的最大线程数量。
    • 任务队列:存放待执行任务的队列。
    • 超时时间:线程空闲超过设定时间后销毁。
  • 默认线程池:C++20 中引入 std::async 时可以使用默认线程池,但无法直接配置线程池参数。

Atomic 类

  • 作用:提供线程安全的原子操作,避免竞争条件。
  • 实现
    • C++ 提供 std::atomic,支持基本数据类型(如整数、指针等)的原子操作。
    • 使用 CAS(Compare-And-Swap)保证无锁操作。
  • 常用方法
    • std::atomic<int>:原子整型变量。
    • load()store():安全读取和写入。
    • fetch_add()fetch_sub():原子加减操作。

synchronized(C++的替代方案)

  • C++ 没有 synchronized 关键字,常用以下方式实现线程同步:
    • 互斥锁std::mutexstd::recursive_mutex
    • 条件变量std::condition_variable
    • 读写锁std::shared_mutex

volatile

  • 作用volatile 告诉编译器不要优化对变量的访问,确保每次读取或写入都直接操作内存。
  • 局限性:在多线程环境中,volatile 无法保证线程安全,只能确保变量的可见性。

JVM结构(替换为C++内存模型)

  • C++ 内存模型由以下区域组成:
    • 代码段:存放可执行指令。
    • 数据段
      • .bss:未初始化的全局变量和静态变量。
      • .data:已初始化的全局变量和静态变量。
    • :动态分配内存区域(如 new 操作分配的内存)。
    • :每个线程独有,用于函数调用和局部变量存储。

GC(垃圾回收)

  • C++ 没有自动垃圾回收,内存管理由开发者负责。
  • RAII(Resource Acquisition Is Initialization)是常见的内存管理方式,依赖对象的生命周期和智能指针(如 std::unique_ptrstd::shared_ptr)来管理资源。

可达性分析与 GC Root

  • C++ 没有 Java 的可达性分析机制,但智能指针的引用计数类似于 GC。
    • std::shared_ptr:使用引用计数判断对象是否需要销毁。
    • 循环引用问题可以用 std::weak_ptr 解决。

finalize

  • C++ 使用析构函数代替 finalize
    • 析构函数在对象销毁时调用,用于资源释放。
    • 避免显式调用析构函数,以确保资源正确释放。

进程与线程

  • 进程:操作系统分配资源的最小单位。
  • 线程:CPU 调度的最小单位。
  • 对比
    • C++ 使用 std::thread 创建线程。
    • 线程共享进程的内存空间,适合多任务协作。
    • 进程之间通过 IPC(如管道、共享内存)通信。

三次握手与四次挥手

  • 三次握手:TCP 建立连接。
    1. 客户端发送 SYN。
    2. 服务端返回 SYN+ACK。
    3. 客户端发送 ACK,连接建立。
  • 四次挥手:TCP 断开连接。
    1. 客户端发送 FIN。
    2. 服务端返回 ACK。
    3. 服务端发送 FIN。
    4. 客户端返回 ACK。

TCP 和 UDP

  • TCP:面向连接,可靠传输,支持流量控制、拥塞控制和数据重传。
  • UDP:无连接,低延迟,适合实时通信(如视频直播)。

okhttp 的 C++ 替代

  • C++ 中没有直接等价的 OkHttp,但可以使用以下库:
    • libcurl:功能强大的 HTTP 客户端库,支持多种协议。
    • Boost.Beast:基于 Boost.Asio 的 HTTP 和 WebSocket 实现。
    • cpp-httplib:轻量级的单头文件 HTTP 库。

需要进一步详细解释某一部分,欢迎指出!

在使用 Qt 时,很多功能可以通过其内置的模块和工具来实现。以下是结合 Qt 框架对这些概念的适配和实现:


线程池(Qt 的实现方式)

  • Qt 提供了 QThreadPoolQRunnable 类,可以方便地创建和管理线程池。
    • 使用方法
      1. 创建一个 QRunnable 对象(表示一个任务)。
      2. 将其加入 QThreadPool 进行管理和调度。
        1
        2
        3
        QThreadPool *pool = QThreadPool::globalInstance();
        QRunnable *task = new MyTask(); // MyTask 继承 QRunnable
        pool->start(task);
    • 参数配置
      • QThreadPool::setMaxThreadCount(int count):设置线程池的最大线程数。
      • 默认的全局线程池使用硬件线程数(std::thread::hardware_concurrency())作为线程数。

Atomic 类(Qt 的实现方式)

  • Qt 提供了 QAtomicInt 和 **QAtomicPointer**,类似于 std::atomic,用于线程安全的原子操作。
    • 用法
      1
      2
      3
      QAtomicInt counter = 0;
      counter.fetchAndAddRelaxed(1); // 原子递增
      counter.fetchAndSubRelaxed(1); // 原子递减

synchronized 替代(Qt 中的锁机制)

  • Qt 提供了多种同步机制,适配多线程编程:
    • 互斥锁QMutex
      1
      2
      3
      4
      QMutex mutex;
      mutex.lock();
      // Critical section
      mutex.unlock();
    • 读写锁QReadWriteLock
      • 用于读多写少场景,类似 std::shared_mutex
    • 信号量QSemaphore
      • 控制对共享资源的访问数量。

volatile(Qt 的实现)

  • Qt 中没有直接的 volatile 替代,但大部分线程安全操作可通过 QMutexQAtomic* 实现。
  • volatile 的语义在 Qt 中通常使用信号槽机制确保内存的可见性。

JVM 结构(Qt 中内存管理)

  • Qt 的内存管理依赖于 父子对象机制
    • 一个对象销毁时会自动销毁其子对象,减少内存泄漏风险。
    • 示例
      1
      2
      3
      QWidget *parent = new QWidget;
      QPushButton *button = new QPushButton(parent); // 父子关系
      delete parent; // button 会自动销毁

GC(垃圾回收)

  • Qt 没有自动垃圾回收,但通过以下方式管理内存:
    • 父子对象机制:自动销毁子对象。
    • QSharedPointerQScopedPointer:智能指针管理对象生命周期。
      1
      QSharedPointer<MyObject> obj = QSharedPointer<MyObject>::create();

finalize(Qt 的替代)

  • Qt 中没有类似 Java finalize 的功能,资源释放依赖于:
    • 析构函数(如手动释放资源)。
    • 智能指针(自动回收动态分配的内存)。

三次握手与四次挥手(Qt 网络模块)

  • Qt 提供 QTcpSocket 和 **QTcpServer**,隐藏了 TCP 的握手和挥手细节:
    • 三次握手QTcpSocketconnectToHost 方法实现连接。
    • 四次挥手disconnectFromHost 方法实现断开连接。

TCP 和 UDP(Qt 网络模块)

  • TCP:使用 QTcpSocketQTcpServer 实现可靠的点对点通信。
  • UDP:使用 QUdpSocket 进行无连接的数据报通信。
    • 示例
      1
      2
      3
      QUdpSocket udpSocket;
      QByteArray data = "Hello, UDP!";
      udpSocket.writeDatagram(data, QHostAddress::LocalHost, 12345);

okhttp 替代(Qt 网络请求模块)

  • Qt 提供了 **QNetworkAccessManager**,用于发送 HTTP 请求:
    • 示例
      1
      2
      3
      4
      5
      6
      7
      8
      9
      QNetworkAccessManager manager;
      QNetworkRequest request(QUrl("http://example.com"));
      QNetworkReply *reply = manager.get(request);

      QObject::connect(reply, &QNetworkReply::finished, [&]() {
      QByteArray data = reply->readAll();
      qDebug() << "Response:" << data;
      reply->deleteLater();
      });

Qt 的模块化设计和跨平台支持,使其可以高效地处理网络通信和线程池任务。对于更复杂的需求,可以结合 Qt 提供的机制自行实现!

计网有哪些层

计算机网络分为 五层模型七层模型,具体如下:


五层模型

  1. 物理层

    • 负责比特流的传输,即将数据转化为电信号、光信号或无线信号在传输介质上传输。
    • 设备:网线、光纤、网卡、集线器等。
  2. 数据链路层

    • 负责将比特流组装成帧,提供可靠的数据传输。包括差错检测、流量控制等。
    • 协议:以太网、Wi-Fi (802.11)。
    • 设备:交换机。
  3. 网络层

    • 负责路由和寻址,将数据从源主机传输到目标主机。
    • 协议:IP (IPv4/IPv6)、ICMP、ARP。
    • 设备:路由器。
  4. 传输层

    • 提供端到端的数据传输,分为面向连接的 TCP 和无连接的 UDP
    • 协议:TCP、UDP。
    • 主要功能:流量控制、可靠传输、错误检测。
  5. 应用层

    • 面向用户,提供网络服务,例如文件传输、邮件、浏览网页等。
    • 协议:HTTP/HTTPS、FTP、DNS、SMTP。

七层模型(OSI 模型)

将五层模型的 应用层 细化为三层,更精确描述网络功能:

  1. 物理层
  2. 数据链路层
  3. 网络层
  4. 传输层
  5. 会话层
    • 管理会话的建立、维护和终止。
  6. 表示层
    • 负责数据的编码、解码、压缩、加密、解密。
  7. 应用层
    • 直接为用户提供服务。

总结

  • 五层模型是简化版,常用于实际网络描述。
  • 七层模型(OSI)更详细,常用于教学或理论分析。

一个网址从回车到解析出网页的全过程

当你在浏览器地址栏输入一个网址并按下回车,解析出网页的全过程可以分为以下几个步骤:


1. DNS 解析(域名解析)

  • 作用:将人类易读的域名(如 www.example.com)转换为计算机可识别的 IP 地址(如 93.184.216.34)。
  • 过程
    1. 浏览器检查本地 DNS 缓存是否有对应的 IP 地址。
    2. 如果本地没有,查询操作系统的 DNS 缓存。
    3. 如果操作系统没有,向配置的 DNS 服务器(如本地网络的 DNS 或 ISP 提供的 DNS)发起查询。
    4. DNS 服务器递归查询,逐级从根域名服务器、顶级域名服务器(如 .com)、权威域名服务器获取 IP 地址。

2. TCP 连接建立

  • 作用:通过 TCP 协议在客户端(浏览器)与服务器之间建立连接。
  • 过程
    1. 三次握手
      • 客户端向服务器发送一个 SYN 数据包,表示发起连接请求。
      • 服务器回复一个 SYN-ACK 数据包,表示同意连接。
      • 客户端再回复一个 ACK 数据包,确认连接已建立。

3. HTTP/HTTPS 请求

  • 作用:浏览器向服务器发送具体的资源请求。
  • 过程
    1. 浏览器构造 HTTP 请求头,包含以下内容:
      • 请求方法(如 GET、POST)。
      • 请求的资源路径(如 /index.html)。
      • Host(主机名,如 www.example.com)。
      • 用户代理(User-Agent,标明浏览器类型)。
    2. 如果是 HTTPS,先进行 TLS 握手
      • 确认加密算法,交换密钥,建立安全连接。
    3. 请求发送到服务器。

4. 服务器处理请求

  • 作用:服务器接收请求并返回相应的资源内容。
  • 过程
    1. 服务器根据请求的资源路径(如 /index.html)查找对应文件或动态生成内容。
    2. 返回 HTTP 响应,包含:
      • 响应状态码(如 200、404)。
      • 响应头(如 Content-Type, Cache-Control)。
      • 响应体(网页内容或数据)。

5. 浏览器接收响应并渲染页面

  • 作用:将服务器返回的 HTML 内容解析并呈现为网页。
  • 过程
    1. 解析 HTML
      • 浏览器解析 HTML 文档,生成 DOM 树。
    2. 解析 CSS
      • 下载 CSS 文件,解析样式规则,生成 CSSOM 树。
    3. 解析 JavaScript
      • 下载并执行 JavaScript 脚本,可能动态修改 DOM。
    4. 加载资源
      • 加载图片、字体、视频等资源。
    5. 渲染
      • 将 DOM 树和 CSSOM 树合并成渲染树,绘制到屏幕上。

6. 后续优化和用户交互

  • 作用:优化加载速度和用户体验。
  • 过程
    • 浏览器可能使用缓存机制减少重复请求。
    • 与服务器保持连接,处理后续的 AJAX 请求或动态更新。

总结

  1. DNS 解析:将域名转换为 IP 地址。
  2. TCP 连接:三次握手建立连接。
  3. HTTP/HTTPS 请求:发送资源请求。
  4. 服务器处理:生成响应并返回数据。
  5. 浏览器渲染:解析 HTML、CSS、JS,显示网页内容。
  6. 用户交互:通过 AJAX 或 WebSocket 实现动态更新和交互。

整个过程依赖多个协议(DNS、TCP、HTTP/HTTPS)协同完成。

网络五元组是什么

网络五元组 是用于唯一标识网络通信中的一个数据流(如 TCP 连接)的五个关键属性,通常包括以下内容:

  1. 源IP地址(Source IP Address)

    • 数据流的发送方 IP 地址。
  2. 目标IP地址(Destination IP Address)

    • 数据流的接收方 IP 地址。
  3. 源端口号(Source Port)

    • 发送方的端口号,用于标识应用进程。
  4. 目标端口号(Destination Port)

    • 接收方的端口号,用于标识接收方的应用服务(如 80 表示 HTTP)。
  5. 协议类型(Protocol)

    • 使用的传输层协议类型,如 TCPUDP

示例

假设有一个 TCP 数据包:

  • 源IP地址:192.168.1.1
  • 目标IP地址:10.0.0.1
  • 源端口号:12345
  • 目标端口号:80
  • 协议类型:TCP

那么它的五元组表示为:
<192.168.1.1, 10.0.0.1, 12345, 80, TCP>


用途

  1. 标识唯一的网络连接
    五元组是区分多个数据流的核心依据。例如,多个客户端同时访问同一台服务器,五元组可用于区分它们各自的连接。

  2. 防火墙规则
    防火墙可以根据五元组规则来过滤特定的流量。

  3. 负载均衡
    负载均衡设备可基于五元组分配流量到不同的服务器。

  4. 网络监控与分析
    用于记录和分析网络通信行为,帮助定位问题或检测攻击行为。


注意

  • 五元组中的协议类型是传输层协议(如 TCP、UDP),而不涉及应用层协议(如 HTTP)。

https和http区别

HTTPHTTPS 的主要区别在于数据传输的安全性。以下是两者的对比:


1. HTTP(HyperText Transfer Protocol)

  • 简介:超文本传输协议,用于在客户端(浏览器)和服务器之间传输数据。
  • 特性
    • 数据明文传输,缺乏加密,容易被窃听或篡改。
    • 默认使用 80 端口。
  • 缺点
    • 数据在网络中以明文形式传递,存在安全隐患(如敏感信息泄露)。

2. HTTPS(HyperText Transfer Protocol Secure)

  • 简介:基于 HTTP 加入了 SSL/TLS 协议,用于加密通信,增强安全性。
  • 特性
    • 数据加密:通过 SSL/TLS 加密数据,防止被窃听。
    • 数据完整性:确保传输数据未被篡改。
    • 身份验证:使用数字证书验证服务器身份,防止伪装攻击。
    • 默认使用 443 端口。
  • 安全机制
    1. 加密:使用对称加密算法(如 AES)保护传输数据的机密性。
    2. 认证:使用非对称加密(如 RSA)和数字证书确保通信双方身份。
    3. 握手过程:通过 SSL/TLS 协商会话密钥。

3. HTTP 和 HTTPS 的主要区别

特性 HTTP HTTPS
安全性 无加密,明文传输,易被攻击 加密传输,安全性高
使用端口 80 443
协议层 应用层协议 应用层协议 + 加密层(SSL/TLS)
速度 较快(无加密开销) 较慢(加密和握手耗时)
成本 无需额外成本 需要购买 SSL/TLS 证书(部分免费)
使用场景 普通数据传输,不敏感信息 敏感信息传输(支付、登录等)

4. HTTPS 的优点

  • 防止敏感信息泄露(如登录密码、银行卡号)。
  • 提高网站可信度,增强用户信任。
  • 对搜索引擎优化(SEO)更友好,部分搜索引擎优先 HTTPS 网站。

5. HTTPS 的不足

  • 需要证书,可能增加部署成本。
  • 加密和握手过程会增加计算开销,略微影响速度。

总结

HTTPS 是 HTTP 的安全版本,采用加密通信、防止窃听和篡改,特别适合需要保护敏感数据的网站。

mysql索引底层用的什么数据结构

MySQL 的索引底层主要使用了 B+ 树 和 哈希表 数据结构,根据索引的类型和存储引擎不同,使用的结构也有所不同。


1. B+ 树索引(主要结构)

  • 使用场景:MySQL 中最常用的索引结构,尤其是 InnoDB 存储引擎的主索引和辅助索引。
  • 特性
    1. 有序性:B+ 树中的节点按照顺序存储,支持范围查询。
    2. 叶子节点链表:B+ 树的所有数据存储在叶子节点,并通过双向链表相连,便于顺序访问。
    3. 平衡性:B+ 树高度平衡,查询效率稳定。
  • 适用索引类型
    • 主键索引(聚簇索引,InnoDB 特有)。
    • 辅助索引(二级索引)。

B+ 树的优点

  1. 快速定位:B+ 树的时间复杂度为 **O(log N)**。
  2. 磁盘访问优化:每个节点存储多个键值,减少磁盘 I/O 次数。
  3. 范围查询高效:有序存储,支持范围和排序操作。

2. 哈希表索引

  • 使用场景:MyISAM 引擎的哈希索引或 Memory 引擎的唯一哈希索引。
  • 特性
    1. 直接定位:通过哈希函数计算键值的存储位置,查找速度非常快,时间复杂度接近 **O(1)**。
    2. 无序性:哈希表索引不支持范围查询或排序。
    3. 冲突处理:通过链地址法或开放地址法解决冲突。
  • 适用索引类型
    • 精确查找,尤其是等值查询。

哈希表的优点

  1. 等值查询速度快。
  2. 内存占用相对较小。

哈希表的缺点

  1. 不支持范围查询和排序。
  2. 哈希冲突会降低查询性能。
  3. 索引值变化频繁时,重建成本高。

3. 空间索引(R-Tree)

  • 使用场景:适用于地理空间数据(如二维坐标点、矩形范围)。
  • 底层结构:R-Tree(多维数据索引结构)。
  • 适用场景:GIS 数据库中的空间查询(如范围查询、最近邻查询)。

4. 索引类型和存储引擎的关系

  1. InnoDB 存储引擎(默认引擎)
    • 主索引(聚簇索引):B+ 树。
    • 辅助索引:B+ 树。
  2. MyISAM 存储引擎
    • 主索引:B+ 树。
    • 不支持聚簇索引。
  3. Memory 存储引擎
    • 默认使用哈希表索引,支持 B+ 树索引。
  4. 其他引擎
    • NDB:哈希索引。
    • CSV:不支持索引。

总结

  • B+ 树 是 MySQL 索引的主要数据结构,适合范围查询和排序。
  • 哈希表 用于快速等值查询,但不支持范围操作。
  • 根据业务需求和存储引擎选择合适的索引类型,可以显著提高查询性能。

讲一下b树,b+树,为什么用b+树

B 树和 B+ 树概述

B 树B+ 树 是两种平衡树数据结构,广泛用于数据库和文件系统的索引结构中。


1. B 树

  • 定义
    B 树是一种多叉平衡树,所有节点按照一定规则存储键值,支持快速查找、插入和删除。
  • 特性
    1. 每个节点可以存储多个键值(关键字),每个键值有指向子节点的指针。
    2. 所有键值在节点内按升序排列。
    3. 非叶子节点也存储实际数据,且可以终止查找。
    4. 树高度平衡。

2. B+ 树

  • 定义
    B+ 树是 B 树的一种改进版本,所有数据都存储在叶子节点,非叶子节点仅存储索引信息。
  • 特性
    1. 所有键值按升序排列,数据只存储在叶子节点。
    2. 叶子节点通过双向链表相连,支持顺序遍历。
    3. 非叶子节点不存储数据,仅存储索引,用于导航。
    4. 树高度平衡。

3. B 树与 B+ 树的区别

特性 B 树 B+ 树
数据存储 数据存储在非叶子节点和叶子节点 数据存储仅在叶子节点
查询范围 范围查询效率较低 通过链表顺序遍历叶子节点,效率高
节点大小 节点存储数据,节点占用更多空间 非叶子节点仅存储索引,占用空间更少
查找性能 查找目标可能终止于任何节点 查找过程更一致,必然到达叶子节点
顺序遍历 不支持快速顺序遍历 叶子节点链表支持快速顺序遍历

4. 为什么选择 B+ 树?

4.1 顺序访问更高效

  • B+ 树的叶子节点通过链表相连,支持高效的范围查询和顺序遍历,非常适合数据库的区间查询、排序等操作。

4.2 磁盘 I/O 友好

  • 非叶子节点只存储索引信息,节点更小,一次可以加载更多节点到内存中,减少磁盘 I/O 次数,提升性能。

4.3 查询性能稳定

  • 所有查找都需要到达叶子节点,查询路径一致,性能更稳定。

4.4 更高的存储利用率

  • 非叶子节点只存索引,占用空间小,同样高度下,B+ 树能存储更多数据,提高磁盘利用率。

4.5 更适合范围查询

  • B+ 树通过链表顺序遍历叶子节点即可实现范围查询,而 B 树需要中序遍历整棵树,效率较低。

5. 应用场景

  • 数据库索引:MySQL 的 InnoDB 存储引擎索引就是基于 B+ 树。
  • 文件系统:如 NTFS 文件系统中使用 B+ 树存储目录项。
  • 键值存储:如 LevelDB 的索引部分使用 B+ 树。

总结

  • B+ 树是对 B 树的优化版本,叶子节点链表、索引与数据分离等特性,使其更加适合数据库的高效查询、范围查询和磁盘存储场景。

线程和进程区别和其通信方式

线程和进程的区别

1. 基本概念

  • 进程:操作系统分配资源的基本单位,每个进程有独立的地址空间、堆、栈等资源。
  • 线程:CPU 调度的基本单位,是进程中的一个执行流,线程共享进程的资源。

2. 区别对比

特性 进程 线程
地址空间 独立,进程之间互不干扰 共享同一进程的地址空间
资源开销 创建和切换开销较大,需分配独立资源 创建和切换开销较小,依赖于进程资源
通信方式 通信复杂,需使用 IPC(进程间通信) 直接共享内存,通信简单
运行效率 独立执行,切换成本高 轻量级,切换成本低
崩溃影响 一个进程崩溃不会影响其他进程 一个线程崩溃可能导致整个进程崩溃

进程间通信方式

  1. 管道(Pipe)

    • 特点:单向通信,父子进程间使用。
    • 适用场景:简单的数据传输。
    • 缺点:只能在有亲缘关系的进程间使用。
  2. 命名管道(FIFO)

    • 特点:支持无亲缘关系的进程间通信。
    • 适用场景:跨进程通信需要唯一标识。
  3. 共享内存(Shared Memory)

    • 特点:在多个进程间共享一块内存区域。
    • 优点:通信速度最快。
    • 缺点:需加锁同步,复杂性较高。
  4. 消息队列(Message Queue)

    • 特点:消息形式传递数据,支持无亲缘关系的进程通信。
    • 优点:顺序保证。
    • 缺点:性能较低。
  5. 信号(Signal)

    • 特点:用来通知进程的异步事件。
    • 适用场景:事件触发的简单通信。
  6. 套接字(Socket)

    • 特点:跨主机通信,基于网络协议(TCP/UDP)。
    • 适用场景:分布式通信。
  7. 信号量(Semaphore)

    • 特点:用于多进程对共享资源的同步访问。
    • 适用场景:进程间的资源竞争控制。

线程间通信方式

  1. 共享内存

    • 特点:线程共享进程的全局变量、堆等资源。
    • 优点:效率高。
    • 缺点:需要加锁(如 std::mutexstd::atomic)来保护数据一致性。
  2. 条件变量(Condition Variable)

    • 特点:用于线程间的等待和通知机制。
    • 适用场景:线程同步(例如生产者-消费者模型)。
  3. 信号量(Semaphore)

    • 特点:线程之间的同步工具。
    • 适用场景:资源数量有限的情况下(如线程池管理)。
  4. 事件(Event)

    • 特点:用于通知线程事件发生。
  5. 消息队列

    • 特点:通过队列传递消息(如 std::queue + 加锁)。
    • 适用场景:线程间数据解耦。

总结

  • 线程是轻量级的执行单位,通信方式主要依赖共享内存和同步工具,性能高但需注意同步问题。
  • 进程是独立的执行单位,通信方式较多,但大多开销较高,适用于隔离性更高的任务场景。

linux常用命令有哪些 要查内存占用是哪个命令 要查端口占用情况要用哪个命令

Linux 常用命令分类


1. 文件和目录操作

  • ls:列出目录内容。
  • cd:切换目录。
  • pwd:显示当前目录路径。
  • mkdir:创建目录。
  • rm:删除文件或目录。
  • cp:复制文件或目录。
  • mv:移动文件或重命名文件。
  • touch:创建空文件。
  • find:查找文件。
  • cat:查看文件内容。
  • more / less:分页查看文件内容。

2. 系统信息查看

  • uname -a:查看操作系统信息。
  • who:查看当前登录用户。
  • uptime:查看系统运行时间和负载。
  • hostname:查看主机名。
  • df -h:查看磁盘空间使用情况。
  • du -sh:查看指定文件或目录大小。
  • top:实时查看系统性能、进程。
  • htop:增强版的 top(需手动安装)。

3. 内存占用查看

  • free -h:查看内存使用情况(-h 以人类可读的格式显示)。
  • top:实时查看内存占用。
  • htop:交互式查看内存、CPU占用。
  • cat /proc/meminfo:显示详细内存信息。

4. 网络操作

  • ifconfig:查看网络接口信息(现代系统使用 ip addr 替代)。
  • ping:测试网络连接。
  • curl:发送 HTTP 请求。
  • wget:下载文件。
  • netstat -tunlp:查看网络连接和端口占用情况。
  • ss -tunlpnetstat 的现代替代,查看端口占用更高效。

5. 端口占用查看

  • netstat -tunlp:查看占用端口的进程及其 PID。
  • ss -tunlp:类似于 netstat,性能更高。
  • lsof -i :<port>:查看指定端口的占用情况。
    • 示例:lsof -i :8080

6. 用户管理

  • whoami:显示当前用户。
  • id:显示用户和组信息。
  • adduser / useradd:添加用户。
  • passwd:修改密码。
  • su:切换用户。
  • chmod:修改文件权限。
  • chown:修改文件所有者。

7. 进程管理

  • ps:查看当前进程。
  • top / htop:实时监控进程。
  • kill:终止进程。
    • 示例:kill -9 <PID>
  • pkill:通过进程名称终止。
    • 示例:pkill nginx

重点命令

  1. 查看内存占用
    • free -htop / htop
  2. 查看端口占用
    • netstat -tunlpss -tunlp
    • lsof -i :<port> 查看特定端口。

cpp的常用数据结构哪些是引用类型,map的底层数据结构是什么

C++ 常用数据结构中哪些是引用类型?

在 C++ 中,标准库(STL)的常用数据结构主要以值语义为主(拷贝、移动语义),没有内置的引用类型数据结构。但你可以通过指针或 std::reference_wrapper 来实现引用类型的效果。

常用 STL 容器

容器 是否为引用类型
std::vector 否,值语义(存储元素本身或指针)
std::deque 否,值语义
std::list 否,值语义
std::set 否,值语义
std::map 否,值语义
std::unordered_map 否,值语义
std::reference_wrapper 是,提供引用语义

实现引用类型的方式

  1. 使用 指针:容器存储对象的指针(如 std::vector<T*>),间接实现引用语义。
  2. 使用 **std::reference_wrapper**:包装引用类型,例如 std::vector<std::reference_wrapper<T>>

std::map 的底层数据结构

1. 数据结构

  • std::map 的底层实现基于 红黑树(Red-Black Tree)
    • 红黑树是一种自平衡二叉搜索树,能在最坏情况下提供对数时间复杂度的操作。

2. 性能特点

  • 插入、删除、查找
    • 时间复杂度为 ( O(\log n) ),因为红黑树的高度是对数级别。
  • 有序性
    • std::map 保证键值是有序的,按键值的升序存储。

3. 红黑树的特点

  • 每个节点为红或黑。
  • 根节点始终为黑色。
  • 红节点的子节点必须为黑色(红色节点不能连续出现)。
  • 从任意节点到叶子节点的路径,黑色节点数量必须相同。

4. 使用场景

  • 需要对键值进行 排序范围查询 的场景,适合使用 std::map

总结

  1. C++ STL 容器本身以值语义为主,但可以通过存储指针或 std::reference_wrapper 实现引用语义。
  2. std::map 的底层是红黑树,提供 ( O(\log n) ) 的插入、删除、查找性能,且保持键值有序。

项目为什么用多线程不用多进程

在项目中选择多线程还是多进程,取决于性能需求、资源分配、开发复杂度等因素。以下是 为什么更倾向于使用多线程而不是多进程 的几点核心原因:


1. 内存共享效率更高

  • 多线程:线程共享同一个进程的内存空间,包括全局变量、堆、代码段等,线程之间可以直接读写共享数据而无需额外的进程间通信(IPC)。
  • 多进程:每个进程都有独立的内存空间,进程间通信需要通过管道、消息队列、共享内存或套接字等方式,增加了开发复杂度和性能开销。

示例

  • 适合需要频繁共享数据的场景(如日志记录、共享缓存)时,多线程更高效。

2. 上下文切换开销更小

  • 多线程:线程的上下文切换比进程轻量级,因为线程共享相同的地址空间,不需要切换虚拟内存页表和内核资源。
  • 多进程:进程切换时,操作系统需要切换整个虚拟地址空间,开销更大。

示例

  • 适合需要快速响应、实时性较高的场景(如网络服务器或 GUI 应用)。

3. 资源占用更少

  • 多线程:线程使用的资源更少,因为它们共用进程的资源(如打开的文件描述符)。
  • 多进程:进程需要独立分配系统资源(如文件句柄、内存、CPU 时间),资源占用更高。

示例

  • 高并发场景下(如 Web 服务器或游戏引擎),多线程比多进程更能降低系统资源消耗。

4. 开发和调试复杂度

  • 多线程:由于线程共享内存,数据同步需要加锁处理(如 mutexcondition_variable 等),这增加了一定的调试难度。
  • 多进程:进程间通信(IPC)复杂,调试时需要跟踪多个独立的进程,难以管理和维护。

示例

  • 对复杂任务分解、资源共享需求高的项目,多线程开发更简单。

5. 使用场景

多线程适合:

  • 高并发处理,如 Web 服务器、网络爬虫。
  • CPU 密集型任务的并行计算。
  • GUI 应用程序(主线程负责 UI,子线程负责后台任务)。

多进程适合:

  • 高隔离性需求,如运行不可信的插件或沙箱环境。
  • 大型分布式系统,每个模块独立运行。
  • 内存泄漏或崩溃隔离需求高的场景。

总结

使用多线程而不是多进程的主要原因是:多线程共享内存空间,通信效率高,上下文切换开销小,资源占用少,更适合需要频繁数据共享、高并发、性能要求较高的场景。但在需要高隔离性或安全性的场景下,仍然会选择多进程。

共享内存的底层原理

共享内存的底层原理

共享内存(Shared Memory)是进程间通信(IPC)的一种高效方式,它允许多个进程直接访问同一块内存区域。这种机制避免了进程间通过管道、消息队列等进行数据复制的开销。

以下是共享内存的底层原理和关键概念:


1. 共享内存的基础

  • 虚拟地址映射:每个进程都有独立的虚拟地址空间。共享内存的核心是将同一块物理内存区域映射到多个进程的虚拟地址空间中,使它们可以访问相同的物理内存。
  • 操作系统支持:共享内存通常由操作系统内核管理,通过提供系统调用(如 POSIX 标准的 shmgetshmat)或内存映射(如 mmap)接口实现。

2. 共享内存的工作机制

2.1 创建共享内存

  • 一个进程使用系统调用(如 shmgetmmap)创建共享内存段。
  • 操作系统分配一块物理内存并将其标识符(shmid)返回给调用进程。

2.2 进程附加共享内存

  • 使用 shmatmmap 将共享内存段映射到自己的虚拟地址空间。
  • 此时,多个进程的虚拟地址空间中有相同的物理内存映射。

2.3 读写共享内存

  • 共享内存直接通过指针操作读写,访问速度非常快,无需经过内核的中转。
  • 写入的数据对所有附加该共享内存段的进程立即可见。

2.4 分离与删除

  • 当一个进程不再需要访问共享内存时,可以调用 shmdt(POSIX)或取消映射。
  • 所有进程分离后,创建共享内存的进程可以调用 shmctl 删除共享内存段。

3. 操作系统如何实现共享内存

3.1 内存映射

  • 操作系统为共享内存分配一块 物理页框(Page Frame),并将其虚拟地址映射到多个进程的页表中。
  • 虚拟内存到物理内存的映射表(Page Table)是共享内存的核心机制。

3.2 标志和权限

  • 操作系统通过共享内存的标识符(如 POSIX 的 shmid)来管理内存段,并设置读写权限。
  • 权限控制可以防止未授权的进程访问共享内存。

3.3 同步机制

  • 操作系统本身不提供数据同步功能。多个进程同时访问共享内存时,可能会发生数据竞争。
  • 用户需要使用互斥锁(mutex)、信号量(semaphore)或条件变量(condition variable)来同步访问。

4. 常见的共享内存实现

4.1 POSIX 共享内存

  • 函数:shm_openmmap
  • 共享内存段在内核中维护,命名空间由文件系统挂载点 /dev/shm 管理。

4.2 System V 共享内存

  • 函数:shmgetshmatshmdtshmctl
  • 通过 IPC 机制分配和管理,支持较大的共享内存。

4.3 内存映射文件(Memory-Mapped Files)

  • 使用 mmap 将文件内容直接映射到虚拟内存空间。
  • 进程间可以通过映射同一文件实现共享内存。

5. 优点与缺点

优点

  • 高性能:无需内核中转,直接读写物理内存。
  • 低延迟:相比管道、消息队列,数据不需要复制。
  • 灵活性:适合大规模数据共享。

缺点

  • 需要手动同步:多个进程同时访问可能导致竞争,需要用户实现同步机制。
  • 资源管理复杂:共享内存需要显式分配、附加、分离和删除。
  • 内存分配受限:共享内存大小受系统配置限制。

6. 应用场景

  • 高性能服务器:如 Redis 使用共享内存技术优化性能。
  • 视频或图像处理:多个进程需要快速共享大规模数据。
  • 实时通信:进程间需要共享实时状态或数据流。

共享内存是高性能 IPC 的关键技术,但它需要额外的同步机制来确保线程安全和数据一致性。

数组越界一定会崩溃吗

在C++中,数组越界不一定会导致程序崩溃,但它是一种未定义行为。具体表现取决于以下因素:


1. 内存布局的影响

  • 访问越界的内存未被使用:如果越界的内存地址尚未被分配给其他变量或程序,可能会读取到意外的数据或写入成功,但不会立刻崩溃。
  • 访问越界的内存已被占用:如果越界的内存地址属于其他变量、堆栈或其他程序,可能会导致数据破坏,程序逻辑出错,甚至崩溃。

2. 运行时环境的影响

  • Debug模式:在某些编译器的调试模式下,可能会对数组的边界进行检查。一旦越界,程序可能会抛出错误或崩溃。
  • Release模式:通常没有边界检查,越界行为可能直接表现为读取垃圾数据或覆盖其他内存。

3. 操作系统的保护机制

  • 如果越界访问的地址超出了当前程序的地址空间(如访问非法内存页),操作系统会触发段错误(Segmentation Fault),导致程序崩溃。
  • 如果越界的地址仍在程序的合法地址范围内(但属于其他变量或堆栈),操作系统不会干预,程序行为不可预测。

4. 编译器优化的影响

在高优化级别下,编译器可能假设数组不会越界,因此可能忽略一些潜在的越界错误,导致更难发现问题。


5. 实际案例

1. 越界未崩溃:

1
2
3
4
5
6
#include <iostream>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
std::cout << arr[6] << std::endl; // 越界访问,但可能不会崩溃
return 0;
}

结果:程序可能输出垃圾值,但不会崩溃。

2. 越界导致崩溃:

1
2
3
4
5
6
#include <iostream>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
arr[10000] = 42; // 写入超出数组范围的地址
return 0;
}

结果:可能导致程序崩溃,触发段错误。


总结

数组越界是一种未定义行为,可能的后果包括:

  • 读取垃圾数据或覆盖其他变量。
  • 程序逻辑错误,表现为难以预测的行为。
  • 触发段错误,程序崩溃。

因此,尽量避免数组越界,可以使用**std::array** 或 **std::vector**,它们在调试模式下提供越界检查,有助于提高程序的安全性。

c++的new和malloc有什么区别

C++ 中的 new 和 C 语言中的 malloc 都是用于动态内存分配的,但它们在功能、用法和底层实现上有显著区别:


1. 功能差异

new

  • 调用构造函数new 不仅分配内存,还会调用对象的构造函数(如果是类类型)。
  • 返回类型安全:返回的指针是分配类型的正确指针,不需要强制类型转换。

malloc

  • 仅分配内存malloc 只负责分配内存,不会调用构造函数。
  • **返回 void***:需要手动强制转换为目标类型指针。

2. 用法差异

new

1
2
int* ptr = new int(5);        // 分配一个整型并初始化为 5
MyClass* obj = new MyClass(); // 分配一个 MyClass 对象并调用其构造函数

malloc

1
2
int* ptr = (int*)malloc(sizeof(int));       // 分配一个整型,未初始化
MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); // 分配 MyClass 对象,但不会调用构造函数

3. 释放内存

  • new 分配的内存必须用 delete 释放,同时调用对象的析构函数。
  • malloc 分配的内存必须用 free 释放,不调用析构函数。

示例:

1
2
3
4
5
int* ptr = new int(5);
delete ptr; // 调用析构函数并释放内存

int* ptr2 = (int*)malloc(sizeof(int));
free(ptr2); // 仅释放内存

4. 内存分配方式

  • **new**:由 C++ 的运行时库实现,底层通常依赖 malloc 或类似机制,同时添加了构造函数的调用逻辑。
  • **malloc**:直接调用 C 的标准库函数,分配内存时只考虑字节数。

5. 错误处理

  • **new**:分配失败会抛出 std::bad_alloc 异常(也可以用 nothrow 版本避免异常)。
    1
    int* ptr = new(std::nothrow) int; // 返回 nullptr 而不是抛出异常
  • **malloc**:分配失败返回 nullptr,需要显式检查。
    1
    2
    3
    4
    int* ptr = (int*)malloc(sizeof(int));
    if (ptr == nullptr) {
    // 处理分配失败
    }

6. 类型安全

  • new 是类型安全的,不需要类型转换。
  • malloc 返回 void*,需要显式强制类型转换。

总结

特性 new malloc
内存分配 分配内存并调用构造函数 仅分配内存
内存释放 delete,调用析构函数 free,不调用析构函数
返回类型 类型安全,返回正确类型指针 返回 void*,需强制转换
错误处理 抛出异常 返回 nullptr
用途 C++ 动态内存管理 C 风格动态内存管理

推荐:在 C++ 中优先使用 newdelete,因为它们与对象的构造和析构配合得更好,而 malloc 更适用于纯 C 风格的程序。

free 释放 new 会怎么样,free 两次呢

1. 用 free 释放 new 分配的内存

如果用 free 释放由 new 分配的内存,会导致未定义行为。可能的后果包括:

  • 程序崩溃。
  • 内存泄漏。
  • 内存损坏,导致后续操作不稳定。

原因

  • new 分配的内存由 C++ 运行时管理,它不仅分配内存,还调用构造函数。
  • delete 不仅释放内存,还调用析构函数,用于正确清理资源。
  • free 是 C 的内存管理函数,不知道如何处理 C++ 的构造函数和析构函数。

示例:

1
2
int* ptr = new int(10); 
free(ptr); // 未定义行为,可能崩溃

正确做法:

1
2
int* ptr = new int(10);
delete ptr; // 调用析构函数并释放内存

2. 用 free 两次

free 两次释放同一块内存是严重错误,也会导致未定义行为

可能的后果:

  • 程序崩溃:访问已释放的内存可能触发段错误(Segmentation Fault)。
  • 内存损坏:双重释放可能破坏内存分配器的状态,影响后续内存分配和释放。
  • 难以调试:这种错误通常不容易被立即发现,可能在后续代码中表现为随机错误。

示例:

1
2
3
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // 未定义行为,可能崩溃

防御措施:

  1. 将指针置空:释放内存后,将指针设为 nullptr,避免重复释放。

    1
    2
    3
    int* ptr = (int*)malloc(sizeof(int));
    free(ptr);
    ptr = nullptr; // 避免双重释放
  2. 使用智能指针(C++):如 std::unique_ptrstd::shared_ptr,自动管理内存,避免手动释放。

    1
    auto ptr = std::make_unique<int>(10); // 内存会自动释放

总结

  1. free 释放 new 的内存,或者用 delete 释放 malloc 的内存,都会导致未定义行为
  2. 重复调用 freedelete 释放内存是错误的,可能导致程序崩溃或内存损坏。
  3. 在现代 C++ 中,推荐使用智能指针来管理内存,减少手动释放的风险。

tcp和udp的区别

TCP 和 UDP 的区别

特性 TCP(Transmission Control Protocol) UDP(User Datagram Protocol)
是否连接 面向连接,需建立连接(三次握手) 无连接,不需要建立连接
传输可靠性 可靠传输,提供数据确认、重传机制 不可靠传输,无确认、无重传
有序性 保证数据按顺序到达,使用序号控制 不保证数据按顺序到达
数据校验 提供校验,确保数据完整 提供简单的校验,但不保证完整
流量控制 支持流量控制和拥塞控制 无流量控制和拥塞控制
速度 相对较慢,因有连接建立和数据确认机制 相对较快,传输时无连接建立、无确认机制
适用场景 适用于可靠性要求高的场景(如文件传输、邮件、网页加载等) 适用于实时性要求高、容忍丢包的场景(如视频流、语音等)
首部开销 较大,20-60 字节 较小,8 字节
协议编号 IP 协议中的协议号为 6 IP 协议中的协议号为 17

具体区别详解

  1. 可靠性

    • TCP 提供可靠的传输:通过校验和、序列号、确认应答和超时重传机制,确保数据准确到达。
    • UDP 不提供可靠传输:数据包可能丢失、重复或乱序。
  2. 连接机制

    • TCP 是面向连接的,需要通过三次握手建立连接,四次挥手断开连接。
    • UDP 是无连接的,直接发送数据,无需连接建立和断开。
  3. 性能与速度

    • TCP 为了保证可靠性,引入了确认、重传、流量控制等机制,增加了延迟。
    • UDP 简单直接,开销小,适合对实时性要求高的场景。
  4. 使用场景

    • TCP:文件传输(FTP)、电子邮件(SMTP)、网页浏览(HTTP/HTTPS)等。
    • UDP:在线游戏、视频直播、语音通话、DNS 查询等。

总结

  • 选择 TCP:需要可靠性和数据完整性,比如文件传输或网页访问。
  • 选择 UDP:追求低延迟和实时性,可以容忍丢包,比如语音、视频流或实时游戏。

tcp怎么实现可靠传输

TCP 实现可靠传输的机制

TCP 通过以下机制实现可靠传输,确保数据从发送端准确无误地传递到接收端:


1. 数据分段和序列号

  • 数据分段:将应用层的大数据分割为多个小段(称为 TCP 段)。
  • 序列号:每个段都有唯一的序列号,用于标记数据的顺序,确保接收端能够按正确顺序组装数据。

2. 确认应答(ACK)

  • 每接收到一个 TCP 段,接收端都会发送一个确认应答(ACK),告诉发送端该数据已正确接收。
  • ACK 包中包含下一个期望接收的数据的序列号。

3. 超时重传

  • 发送端在发送每个数据段后会启动一个计时器,等待 ACK。
  • 如果在超时时间内没有收到 ACK,则认为数据丢失或损坏,会重新发送该段。

4. 校验和

  • 每个 TCP 段包含一个校验和字段,用于验证数据的完整性。
  • 接收端通过校验和检测数据在传输过程中是否发生损坏,若发现错误会丢弃该段。

5. 流量控制

  • TCP 使用滑动窗口机制控制发送端的数据量,避免发送过多数据导致接收端缓存溢出。
  • 接收端通过 ACK 包中的窗口大小字段(Window Size)告诉发送端当前可接收的缓冲区大小。

6. 拥塞控制

  • TCP 通过算法(如慢启动、拥塞避免、快速重传和快速恢复)动态调整数据发送速率,避免网络拥塞。
  • 拥塞控制通过检测网络状况(如丢包或延迟)来调整发送窗口大小。

7. 数据有序性

  • TCP 接收端会按序列号重组数据,即使数据包乱序到达,也能按照正确顺序交付给应用层。

8. 重复数据处理

  • 如果接收端收到重复的数据段,会丢弃这些重复段,并重新确认(ACK)已接收的数据,确保数据无冗余。

总结

TCP 通过 序列号、确认应答、超时重传、校验和、流量控制、拥塞控制等机制,实现了数据的可靠传输,确保数据完整、按顺序到达,并能应对数据丢失、乱序或网络拥塞等问题。

三次握手和四次挥手可以少一次吗

三次握手和四次挥手是否可以少一次?

  1. 三次握手

    • 不可以少一次
      因为三次握手的主要目的是确保通信双方的发送和接收能力均正常。如果减少一次,无法完全确认通信链路的可靠性,可能导致问题。

    详细原因

    • 第一次握手:客户端发送 SYN,告诉服务端“我要建立连接”。
      服务端此时只知道客户端想建立连接,但无法确定自己的回应是否能被客户端收到。
    • 第二次握手:服务端发送 SYN+ACK,告诉客户端“我收到了你的请求,并且我也准备好了”。
      客户端此时可以确认服务端已准备好,但服务端仍无法确认客户端是否准备好。
    • 第三次握手:客户端发送 ACK,告诉服务端“我收到了你的回应,现在可以开始通信了”。
      这一步后,服务端才能确认客户端准备好了。

    如果省略第三次握手,服务端可能错误地认为客户端准备好了,而客户端可能未收到服务端的响应,导致通信失败。


  1. 四次挥手

    • 也不可以少一次
      因为四次挥手的主要目的是确保双方都能正确关闭连接,并且双方的数据传输都已完成。如果减少一次,会导致资源无法正确释放,或者一方未完全关闭连接。

    详细原因

    • 第一次挥手:客户端发送 FIN,表示“我已经没有数据要发送了”。
      服务端收到后,知道客户端数据已发送完,但自己还有数据要发。
    • 第二次挥手:服务端发送 ACK,表示“我知道了,但我还没发完数据”。
    • 第三次挥手:服务端发送 FIN,表示“我也没有数据要发了”。
      客户端收到后,知道服务端也完成了数据发送。
    • 第四次挥手:客户端发送 ACK,表示“我知道了,连接可以关闭了”。

    如果省略最后一次 ACK,服务端可能不知道客户端是否已收到关闭信号,从而保持连接,浪费资源。


总结

  • 三次握手和四次挥手都不能少一次,因为它们设计的目的是为了确保连接的建立与关闭都能够双向确认,减少次数会破坏协议的可靠性和一致性。

四次挥手过程

TCP 四次挥手的详细过程

四次挥手是 TCP 协议中断开连接的过程,用于确保双方的数据传输已完成,连接可以安全关闭。


阶段和步骤

  1. 第一次挥手:客户端发送 FIN

    • 客户端主动发起断开连接,向服务端发送一个 FIN 标志的数据包。
    • 表示客户端已经没有数据要发送了,但仍可以接收来自服务端的数据。
    • 客户端状态变为:FIN-WAIT-1
  2. 第二次挥手:服务端发送 ACK

    • 服务端接收到 FIN 后,发送一个 ACK 确认包,表示“已收到你的请求”。
    • 此时,服务端可能还有数据需要发送。
    • 服务端状态变为:CLOSE-WAIT
    • 客户端状态变为:FIN-WAIT-2
  3. 第三次挥手:服务端发送 FIN

    • 当服务端完成数据发送后,主动向客户端发送 FIN 标志的数据包,表示“我也没有数据要发送了”。
    • 服务端状态变为:LAST-ACK
  4. 第四次挥手:客户端发送 ACK

    • 客户端接收到服务端的 FIN 后,发送一个 ACK 确认包,表示“我知道了,连接可以关闭了”。
    • 客户端进入TIME-WAIT状态,等待一段时间(通常是 2 个最大报文段寿命,2MSL),以确保服务端收到 ACK。
    • 客户端超时后,连接正式关闭。
    • 服务端接收到 ACK 后,直接进入关闭状态(CLOSED)。

状态变化总结

阶段 客户端状态 服务端状态
客户端发送 FIN FIN-WAIT-1 ESTABLISHED
服务端发送 ACK FIN-WAIT-2 CLOSE-WAIT
服务端发送 FIN FIN-WAIT-2 LAST-ACK
客户端发送 ACK TIME-WAIT CLOSED
等待 2MSL 后关闭连接 CLOSED CLOSED

重要细节

  1. TIME-WAIT 的作用

    • 确保最后一个 ACK 被服务端成功接收。
    • 防止旧连接的重复数据包干扰新连接。
  2. 为何需要四次?

    • TCP 是全双工通信,双方需要分别关闭自己的发送方向。
    • 第一次和第二次挥手完成客户端到服务端的关闭,第三次和第四次挥手完成服务端到客户端的关闭

tcp数据到达是有序的还是乱序的,交给上层是有序的还是乱序的

TCP 数据到达的顺序性和交付给上层的顺序性

  1. 数据到达的顺序性
    • 可能是乱序的
      在实际的网络传输中,TCP 数据包(段)可能会因为路由不同、网络拥塞或其他原因乱序到达接收端。
      比如,发送的序列号为 1、2、3 的三个段,可能接收端先收到序列号为 3 的段,再收到 1 和 2。

  1. 交付给上层的顺序性
    • 一定是有序的
      TCP 在接收端通过 序列号重组缓冲区 来确保将数据按正确顺序交付给上层应用。
      如果接收端发现数据乱序,会将数据暂存在重组缓冲区中,直到缺失的数据段被重传并接收到,再将完整且有序的数据交付给上层。

关键机制

  1. 序列号
    每个 TCP 数据段都有唯一的序列号,用于标记数据的顺序。

  2. 确认应答(ACK)
    接收端通过 ACK 告知发送端已成功接收的数据段,并通知下一个期望的序列号。

  3. 重传机制
    如果某个数据段丢失或超时,发送端会根据接收端的重复 ACK 或超时机制重新发送数据段。

  4. 重组缓冲区
    接收端会将乱序到达的数据存入重组缓冲区,等待缺失数据到达后再按照正确顺序组装。


总结

  • 到达顺序:TCP 数据包可能乱序到达接收端。
  • 交付顺序:TCP 使用序列号、重组缓冲区等机制,确保交付给上层的数据是有序的。

浏览器输入网址过程(吟唱)

浏览器输入网址到显示网页的全过程

  1. 用户输入 URL

    • 用户在浏览器地址栏中输入网址(如 https://example.com)。
  2. DNS 解析

    • 浏览器先检查缓存(浏览器缓存、本地系统缓存、路由器缓存、ISP 缓存)中是否有目标域名的 IP 地址。
    • 如果缓存中没有,向 DNS 服务器发起查询,将域名解析为目标服务器的 IP 地址。
  3. 建立 TCP 连接

    • 使用目标服务器的 IP 地址,浏览器通过 TCP 协议与服务器建立连接(三次握手)。
  4. 发起 HTTPS 请求(若使用 HTTPS)

    • 浏览器与服务器协商 SSL/TLS 加密协议,完成安全连接的建立。
    • 双方通过握手交换密钥后,开始加密通信。
  5. 发送 HTTP 请求

    • 浏览器通过 TCP 连接向服务器发送 HTTP 请求:
      • 请求行:指定请求方法(如 GET)、目标资源路径等。
      • 请求头:包含浏览器信息、支持的压缩格式、缓存等。
      • 请求体:用于 POST 请求时发送数据。
  6. 服务器处理请求

    • 服务器接收请求后,根据请求的路径和参数,处理业务逻辑(如查询数据库、加载资源等)。
    • 将生成的响应(HTML、CSS、JS、图片等)打包成 HTTP 响应并返回。
  7. 浏览器接收响应

    • 浏览器接收服务器的 HTTP 响应,解析响应头和响应体。
    • 如果是重定向响应(如 301302 状态码),浏览器根据 Location 头字段访问新的 URL。
  8. 解析 HTML

    • 浏览器开始解析 HTML 文档,构建 DOM 树。
    • 解析过程中发现 CSS 文件、JS 脚本、图片等资源时,启动并发请求加载这些资源。
  9. 加载和执行 CSS、JS

    • CSS:加载后解析生成 CSSOM 树,与 DOM 树合并为渲染树。
    • JS:加载后执行。JS 可能修改 DOM 和 CSSOM。
  10. 布局和绘制

    • 布局(Layout):根据渲染树计算元素的大小和位置。
    • 绘制(Painting):将渲染树的内容绘制到屏幕上。
  11. 显示页面

    • 浏览器将绘制的页面显示在用户屏幕上。
    • 如果有异步资源(如图片或动态加载的数据),页面会更新并重新绘制。

涉及的协议和技术

  1. DNS(域名解析协议):将域名解析为 IP 地址。
  2. TCP/IP:建立通信连接,传输数据。
  3. HTTPS(HTTP over TLS/SSL):提供加密通信和数据完整性。
  4. HTTP/1.1、HTTP/2、HTTP/3:定义请求和响应的格式与行为。
  5. 浏览器渲染:HTML 解析、CSS 样式应用、JS 执行、页面绘制。

总结

整个过程是从用户输入网址开始,经过域名解析、建立连接、发送请求、接收响应、页面渲染,最终将网页显示在浏览器中。

数据链路层工作原理

数据链路层的工作原理

数据链路层是 OSI 模型中的第二层,负责在同一网络中的直接节点之间传输数据,并确保数据的正确传输。以下是其主要工作原理:


1. 数据链路层的职责

  • 封装成帧:将网络层传递下来的数据(IP 数据报)封装为帧(Frame),并附加帧头和帧尾。
  • 流量控制:通过控制数据发送速率,防止发送端过快而接收端处理不过来。
  • 差错检测:通过帧尾的校验和(如 CRC),检测数据在传输过程中是否出错。
  • 物理寻址:通过 MAC 地址(硬件地址)标识网络设备。
  • 数据传输:负责节点之间的直接数据传输。

2. 工作原理

2.1 封装与解封装

  • 发送端:

    1. 接收来自网络层的数据包。
    2. 在数据包前面加上帧头(包含源 MAC 地址、目的 MAC 地址等信息)。
    3. 在数据包后面加上帧尾(通常包含校验和 CRC,用于错误检测)。
    4. 将完整的帧传递到物理层。
  • 接收端:

    1. 从物理层接收帧。
    2. 验证帧尾的校验和,检查数据是否完整。
    3. 去掉帧头和帧尾,将数据包传递给网络层。

2.2 差错检测

  • 数据链路层使用校验和(如 循环冗余校验 CRC)检测传输中的错误。
  • 如果接收到的帧有错误,可能会直接丢弃或请求重新发送(在可靠链路中,如 PPP)。

2.3 流量控制

  • 数据链路层通过机制(如停止-等待协议)来控制发送端的发送速度。
  • 防止发送端的数据传输速率超过接收端的处理能力。

2.4 物理寻址

  • 每个网络设备都有唯一的 MAC 地址。
  • 数据链路层在帧头中使用源 MAC 和目的 MAC 地址,确保帧能够正确传递到目标设备。

2.5 数据传输方式

  • 数据链路层支持三种传输方式:
    1. 单播(Unicast):一对一传输。
    2. 广播(Broadcast):将数据发送到同一网络中的所有设备。
    3. 多播(Multicast):将数据发送到一个特定的设备组。

3. 数据链路层协议

常见的协议包括:

  • 以太网(Ethernet):最广泛使用的局域网协议。
  • 点对点协议(PPP):主要用于点对点通信,如拨号网络。
  • 令牌环(Token Ring):一种通过令牌控制传输权的局域网技术。

4. 数据链路层的分层

在部分协议中(如以太网),数据链路层还可进一步划分为两个子层:

  1. 逻辑链路控制(LLC)子层:负责帧的多路复用和差错检测。
  2. 介质访问控制(MAC)子层:负责物理地址控制和介质访问。

总结

数据链路层通过封装帧、差错检测、流量控制和物理寻址等机制,为网络层提供可靠的直接通信服务。它在局域网和广域网环境中均起到关键作用。

单例模式和缺点

单例模式简介

单例模式(Singleton Pattern) 是一种设计模式,用于确保一个类在整个程序运行期间只有一个实例,并提供全局访问点。


实现方式

  1. 懒汉模式(延迟初始化)
    实例在第一次被访问时创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Singleton {
    public:
    static Singleton& getInstance() {
    static Singleton instance; // C++11线程安全
    return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    private:
    Singleton() {}
    };
  2. 饿汉模式(提前初始化)
    实例在程序启动时创建。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class Singleton {
    public:
    static Singleton& getInstance() {
    return instance;
    }
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;
    private:
    Singleton() {}
    static Singleton instance;
    };
    Singleton Singleton::instance;

单例模式的优点

  1. 唯一性:保证一个类只有一个实例,节省系统资源。
  2. 全局访问点:提供了全局访问该实例的方式,便于管理共享资源。
  3. 延迟加载(针对懒汉模式):实例只在需要时创建,节省内存。

单例模式的缺点

  1. 不易扩展

    • 单例类的代码高度耦合,难以继承和修改。
    • 测试难度大,因为单例的全局状态会对测试产生干扰。
  2. 多线程问题(在非线程安全环境下):

    • 如果没有适当的锁机制或 C++11 的 static 特性,懒汉模式的实例化可能出现竞争问题。
  3. 隐藏依赖性

    • 使用单例模式会隐藏类之间的依赖关系,使代码的可读性和可维护性下降。
  4. 可能导致资源泄漏

    • 单例类的生命周期与程序一致,不会被垃圾回收,可能会占用不必要的资源。
  5. 违背单一职责原则

    • 单例类不仅负责管理自身实例,还承担全局访问的功能,增加了类的职责。

总结

单例模式适合用在全局唯一的资源管理场景(如配置管理器、日志系统等)。但需注意其局限性,尤其是对代码的可扩展性和测试的影响。

虚拟内存

虚拟内存简介

虚拟内存是计算机系统中用来扩展物理内存能力的一种机制,它通过将物理内存磁盘存储结合,向每个进程提供一个连续、独立的地址空间


虚拟内存的主要概念

  1. 地址空间
    每个进程有独立的虚拟地址空间,通常被划分为用户空间内核空间。例如,32 位系统中,用户空间是 0x00000000 到 0xBFFFFFFF(3GB),内核空间是 0xC0000000 到 0xFFFFFFFF(1GB)。

  2. 页(Page)
    虚拟内存被划分为固定大小的块,称为,典型大小是 4KB。

  3. 页表(Page Table)
    维护虚拟地址到物理地址的映射关系。


虚拟内存的工作原理

  1. 地址转换

    • CPU 访问数据时产生一个虚拟地址(逻辑地址)。
    • 使用页表将虚拟地址映射到物理地址。
    • 页表缓冲区(TLB,Translation Lookaside Buffer)缓存常用的页表项,提升转换效率。
  2. 分页机制

    • 如果访问的虚拟地址所对应的物理内存不存在(页面未加载到物理内存),会触发缺页中断
    • 操作系统会将对应页面从磁盘加载到内存。
  3. 交换(Swapping)

    • 当内存不足时,操作系统会将一些不常用的页面保存到磁盘(称为交换区或页面文件),腾出内存空间。

虚拟内存的优点

  1. 进程隔离
    每个进程拥有独立的地址空间,互相不会干扰,提升了安全性。

  2. 内存扩展
    虚拟内存提供的地址空间可以大于物理内存(例如 2GB 物理内存可提供 4GB 虚拟地址空间)。

  3. 高效内存利用
    仅在需要时才将页面加载到内存中,减少了内存浪费。

  4. 简化编程
    程序员可以假设有一段连续的内存,而不需要关心实际的物理内存布局。


虚拟内存的缺点

  1. 性能开销
    页表查找和缺页中断处理会导致性能下降,尤其是频繁的页面交换(Thrashing)会显著降低性能。

  2. 内存占用
    页表本身需要占用一定的内存(多级页表减少占用)。

  3. 复杂性
    实现虚拟内存需要硬件支持(MMU)和操作系统的复杂管理。


总结

虚拟内存通过地址映射、分页和页面交换机制,让每个进程感知到一个连续的、独立的内存空间,极大地提升了系统的灵活性和稳定性。但它也带来了性能和管理上的开销。

LRU、LFU

LRU(Least Recently Used)与 LFU(Least Frequently Used)简介

LRU 和 LFU 是常见的缓存替换算法,用于管理有限容量缓存中的数据,当缓存满时决定哪些数据需要被淘汰。


1. LRU(最近最少使用)

LRU 原理

  • 基于“最近使用的数据更有可能再次使用”的原则。
  • 淘汰最近最少被访问的数据。

实现方法

  1. 数据结构
    通常使用一个 双向链表 + 哈希表

    • 双向链表维护数据访问的顺序(最早访问的数据在链表头,最近访问的数据在链表尾)。
    • 哈希表用于快速查找缓存中的数据是否存在。
  2. 操作时间复杂度

    • 查找:O(1)(通过哈希表)。
    • 插入/更新:O(1)(调整链表)。

适用场景

  • 适用于访问具有时间局部性的数据场景,例如网页缓存。

2. LFU(最少使用)

LFU 原理

  • 基于“使用频率越高的数据越有可能再次使用”的原则。
  • 淘汰使用频率最少的数据。

实现方法

  1. 数据结构
    使用一个 哈希表 + 频率链表

    • 哈希表存储键值对及其访问频率。
    • 频率链表维护所有数据按使用频率排序。
  2. 操作时间复杂度

    • 查找:O(1)(通过哈希表)。
    • 插入/更新:O(log n) 或 O(1)(维护频率链表)。

适用场景

  • 适用于访问具有频率局部性的数据场景,例如推荐系统的缓存。

LRU 与 LFU 的对比

特性 LRU LFU
淘汰策略 最近最少使用的数据 最少使用频率的数据
时间复杂度 O(1) O(log n) 或 O(1)
适用场景 时间局部性较强的数据 频率局部性较强的数据
实现复杂度 较低 较高

总结

  • LRU:适合短期访问热点数据。
  • LFU:适合长期访问高频数据。
  • 结合策略:可以结合两者,例如 LRU-K(基于 K 次最近使用)和 LFU-LRU 混合策略,根据具体场景优化缓存性能。

说一下二分查找

二分查找(Binary Search)

二分查找是一种在有序数组中快速查找目标值的算法。通过每次将搜索范围缩小一半,可以显著降低查找的时间复杂度。


原理

  • 前提条件:数组或数据结构必须是有序的(升序或降序)。
  • 步骤
    1. 定义左边界 left 和右边界 right
    2. 计算中间位置 mid = (left + right) / 2
    3. 比较 nums[mid] 和目标值 target
      • 如果 nums[mid] == target,返回索引 mid
      • 如果 nums[mid] < target,目标值在右半部分,将左边界更新为 mid + 1
      • 如果 nums[mid] > target,目标值在左半部分,将右边界更新为 mid - 1
    4. 重复以上步骤,直到找到目标值或 left > right(未找到目标值)。

代码实现(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <vector>
using namespace std;

int binarySearch(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 防止溢出
if (nums[mid] == target) {
return mid; // 找到目标值
} else if (nums[mid] < target) {
left = mid + 1; // 缩小到右半部分
} else {
right = mid - 1; // 缩小到左半部分
}
}
return -1; // 未找到
}

时间复杂度

  • 最佳情况:O(1)(目标值恰好是中间值)。
  • 最坏情况:O(log n)(每次范围缩小一半,最多需对数次比较)。
  • 平均情况:O(log n)。

空间复杂度

  • 迭代版:O(1)。
  • 递归版:O(log n)(递归调用栈深度)。

优缺点

  • 优点

    1. 时间复杂度低,适合在大规模有序数据中查找。
    2. 实现简单,逻辑清晰。
  • 缺点

    1. 只能用于有序数据。
    2. 不适合频繁插入/删除操作的动态数据。

扩展应用

  1. 查找第一个等于目标值的元素
  2. 查找最后一个等于目标值的元素
  3. 查找第一个大于等于目标值的元素
  4. 查找最后一个小于等于目标值的元素

示例(查找第一个大于等于目标值的元素):

1
2
3
4
5
6
7
8
9
10
11
12
int lowerBound(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) {
left = mid + 1; // target 在右半部分
} else {
right = mid; // mid 可能是答案,继续向左缩小范围
}
}
return left;
}

总结

二分查找是搜索有序数据的高效方法,在算法设计中经常作为子模块使用,如解决旋转数组问题、查找插入位置等。理解并掌握其变种(如上下界查找)是算法学习的重要内容。

protobuf的优势 如何自定义数据格式解决粘包问题

Protocol Buffers(Protobuf)的优势

  1. 高效性

    • Protobuf 使用紧凑的二进制格式,占用更少的带宽和存储空间。
    • 序列化和反序列化速度快。
  2. 跨语言支持

    • Protobuf 支持多种编程语言(如 C++、Java、Python 等),便于多平台开发和数据交互。
  3. 易扩展性

    • 通过字段编号(field number)实现版本兼容性,新增字段不会影响旧版本的解析。
  4. 结构化数据支持

    • 支持嵌套消息、枚举、数组等复杂数据结构,适用于复杂应用场景。
  5. 工具链完善

    • 提供强大的工具(如 protoc 编译器)生成高效的代码。

如何自定义数据格式解决粘包问题(C++)

粘包问题
在 TCP 通信中,由于 TCP 是流式协议,多个数据包可能会在接收端合并成一个,或者一个数据包会被拆分成多个小段,这就是粘包问题。

解决方案
通过设计自定义数据格式,明确消息边界,以便正确地解析消息。


自定义数据格式设计

  1. 固定长度消息头

    • 包含消息的长度(比如 4 字节)和消息类型(比如 2 字节)。
    • 消息头固定长度,便于接收端解析消息大小。
  2. 消息体

    • 消息体包含实际的数据内容,可以是序列化后的 Protobuf 数据或其他格式。

示例数据格式:

1
| Length (4 bytes) | Type (2 bytes) | Data (variable length) |

实现示例

1. 发送端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <cstring>
#include <vector>
#include <cstdint>
#include <arpa/inet.h> // 用于 htonl/htons

void sendMessage(int socket, uint16_t type, const std::vector<uint8_t>& data) {
uint32_t length = data.size();
uint32_t networkLength = htonl(length); // 转为网络字节序
uint16_t networkType = htons(type); // 转为网络字节序

// 构建消息
std::vector<uint8_t> buffer(4 + 2 + data.size());
memcpy(buffer.data(), &networkLength, 4); // 写入长度
memcpy(buffer.data() + 4, &networkType, 2); // 写入类型
memcpy(buffer.data() + 6, data.data(), data.size()); // 写入数据

// 发送消息
send(socket, buffer.data(), buffer.size(), 0);
}

2. 接收端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
#include <vector>
#include <cstdint>
#include <unistd.h>
#include <arpa/inet.h>

void receiveMessage(int socket) {
uint32_t length = 0;
uint16_t type = 0;

// 读取消息头
if (recv(socket, &length, 4, MSG_WAITALL) != 4) {
std::cerr << "Failed to read message length.\n";
return;
}
if (recv(socket, &type, 2, MSG_WAITALL) != 2) {
std::cerr << "Failed to read message type.\n";
return;
}

// 转为主机字节序
length = ntohl(length);
type = ntohs(type);

// 读取消息体
std::vector<uint8_t> data(length);
if (recv(socket, data.data(), length, MSG_WAITALL) != static_cast<ssize_t>(length)) {
std::cerr << "Failed to read message body.\n";
return;
}

// 处理消息
std::cout << "Received message of type " << type << " and length " << length << ".\n";
}

如何结合 Protobuf 使用

  • 消息体部分使用 Protobuf 序列化后的数据。
  • 在解析消息时,使用 Protobuf 的反序列化工具解析消息体数据。

总结

  1. Protobuf 提供了高效、结构化的数据序列化方式,非常适合网络通信。
  2. 自定义数据格式解决粘包问题,通过消息头明确边界。
  3. 结合 Protobuf 和自定义数据格式,可以构建高效、可靠的网络通信系统。

模版元编程实现编译期计算斐波那契数列

模板元编程是 C++ 的一种强大特性,可以在编译期进行计算。下面是用模板元编程实现斐波那契数列的示例代码。

编译期计算斐波那契数列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

// 递归模板实现斐波那契数列
template<int N>
struct Fibonacci {
static constexpr int value = Fibonacci<N - 1>::value + Fibonacci<N - 2>::value;
};

// 模板特化用于递归终止
template<>
struct Fibonacci<0> {
static constexpr int value = 0;
};

template<>
struct Fibonacci<1> {
static constexpr int value = 1;
};

int main() {
constexpr int n = 10;
std::cout << "Fibonacci<" << n << ">::value = " << Fibonacci<n>::value << '\n';
return 0;
}

代码解析

  1. 递归模板

    • Fibonacci<N>value 定义为 Fibonacci<N - 1>::value + Fibonacci<N - 2>::value
    • 这类似于递归函数调用,但发生在编译期。
  2. 模板特化

    • 特化 Fibonacci<0>Fibonacci<1>,分别定义为 01,用于终止递归。
  3. 编译期计算

    • Fibonacci<10>::value 的结果将在编译期计算完成,运行时直接输出值,性能极高。

结果

运行上述代码,输出结果:

1
Fibonacci<10>::value = 55

优势

  • 编译期完成计算,无运行时开销。
  • 可用于生成编译期常量,优化性能。

注意事项

  • 递归深度受模板递归层次限制(通常为 1024),超出时会引发编译错误。
  • 对于大规模计算,可以结合 C++17 的 constexpr 函数实现更灵活的编译期计算。

智能指针 完美转发

智能指针

智能指针是 C++ 提供的一种内存管理工具,负责自动管理动态分配的内存,避免手动调用 delete 造成的内存泄漏或重复释放问题。主要的智能指针包括:


1. std::unique_ptr

  • 特点:独占所有权,不能共享。
  • 用途:用于一个对象的所有权明确属于单个指针的场景。
  • 优点:轻量且安全,生命周期明确。
  • 示例
    1
    2
    3
    4
    5
    #include <memory>
    #include <iostream>

    std::unique_ptr<int> ptr = std::make_unique<int>(10);
    std::cout << *ptr << std::endl;

2. std::shared_ptr

  • 特点:共享所有权,多方共享同一个对象。
  • 用途:适用于需要多个指针共享同一资源的场景。
  • 优点:通过引用计数实现共享,最后一个持有者销毁时释放内存。
  • 示例
    1
    2
    3
    4
    5
    6
    #include <memory>
    #include <iostream>

    std::shared_ptr<int> sp1 = std::make_shared<int>(20);
    std::shared_ptr<int> sp2 = sp1; // 共享所有权
    std::cout << *sp2 << std::endl;

3. std::weak_ptr

  • 特点:弱引用,不影响引用计数。
  • 用途:用于打破循环引用,常与 std::shared_ptr 配合使用。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    #include <memory>
    #include <iostream>

    std::shared_ptr<int> sp = std::make_shared<int>(30);
    std::weak_ptr<int> wp = sp; // 弱引用
    if (auto sp_from_wp = wp.lock()) { // 检查是否有效
    std::cout << *sp_from_wp << std::endl;
    }

完美转发

完美转发是 C++ 中使用 右值引用模板 将函数参数精确地转发给目标函数的一种技术。

核心要点

  • 利用 模板类型推导 和 **std::forward**,保留参数的值类别(左值或右值)。
  • 适用于需要构造对象或调用其他函数时避免不必要拷贝的场景。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <utility> // std::forward

void print(int& x) {
std::cout << "Lvalue: " << x << std::endl;
}

void print(int&& x) {
std::cout << "Rvalue: " << x << std::endl;
}

template <typename T>
void forward_example(T&& arg) {
print(std::forward<T>(arg)); // 完美转发,保持值类别
}

int main() {
int a = 10;

forward_example(a); // 转发左值
forward_example(20); // 转发右值
return 0;
}

运行结果

1
2
Lvalue: 10
Rvalue: 20

原理

  1. T&& 是万能引用

    • 当传入左值时,T 推导为 int&T&& 实际上是 int& &,按规则折叠为 int&
    • 当传入右值时,T 推导为 intT&& 保持为 int&&
  2. **std::forward**:

    • 左值会被保持为左值,右值保持为右值。
    • 防止传参过程中发生拷贝或移动。

智能指针与完美转发结合

在构造函数中,利用完美转发构造智能指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <memory>
#include <iostream>

class MyClass {
public:
template <typename T>
MyClass(T&& value) : ptr(std::make_unique<T>(std::forward<T>(value))) {}

void display() {
std::cout << *ptr << std::endl;
}

private:
std::unique_ptr<int> ptr;
};

int main() {
MyClass obj(42);
obj.display();
return 0;
}

排序算法 (如果将数据结构变成链表 哪几种排序算法时间复杂度提高)

排序算法及链表场景下时间复杂度变化

在数组和链表两种数据结构中,排序算法的效率会受到结构特性的影响。链表的随机访问性能较差,但插入和删除操作效率较高。这种特性会导致某些排序算法在链表上表现不佳,时间复杂度发生变化。


排序算法的分析

1. 冒泡排序(Bubble Sort)

  • 数组时间复杂度:( O(n^2) )
  • 链表时间复杂度:( O(n^2) )
    原因:冒泡排序依赖于相邻元素的交换,链表无法快速访问相邻节点,每次比较需要遍历链表到指定位置,性能没有提升。

2. 选择排序(Selection Sort)

  • 数组时间复杂度:( O(n^2) )
  • 链表时间复杂度:( O(n^2) )
    原因:选择排序需要找到未排序部分的最小值,链表需要遍历未排序区域,因此时间复杂度不变。

3. 插入排序(Insertion Sort)

  • 数组时间复杂度:( O(n^2) )
  • 链表时间复杂度:( O(n^2) )
    原因:虽然链表插入的性能较高,但查找插入位置仍需要遍历链表,时间复杂度保持为 ( O(n^2) )。

4. 快速排序(Quick Sort)

  • 数组时间复杂度:平均 ( O(n \log n) ),最差 ( O(n^2) )
  • 链表时间复杂度:平均 ( O(n^2) ),最差 ( O(n^2) )
    原因:快速排序对数组通过随机访问进行划分,效率较高;但链表中随机访问需要遍历,划分效率降低。

5. 归并排序(Merge Sort)

  • 数组时间复杂度:( O(n \log n) )
  • 链表时间复杂度:( O(n \log n) )
    原因:归并排序可以天然适配链表的结构,通过拆分链表为子链表再合并,无需随机访问,时间复杂度不变。

6. 堆排序(Heap Sort)

  • 数组时间复杂度:( O(n \log n) )
  • 链表时间复杂度:( O(n^2) )
    原因:堆排序依赖数组的随机访问来维护堆的性质,链表不支持高效随机访问,导致操作复杂度显著增加。

结论

在将数据结构从数组替换为链表后:

  1. 时间复杂度显著提高的排序算法

    • 快速排序:随机访问的高效性被链表的顺序访问特性限制。
    • 堆排序:链表无法有效维护堆结构。
  2. 时间复杂度保持不变或适配链表的排序算法

    • 归并排序:无需随机访问,天然适配链表。
    • 插入排序、冒泡排序、选择排序:时间复杂度本身较高,链表仅会影响常数时间开销。

实际建议

对于链表排序,推荐使用 归并排序,其分治思想与链表特性完美结合,效率最高且实现简单。

二叉树节点多出一个指向父节点的指针 问N个结点的时候有多少个指向空的指针

在一个二叉树中,每个节点可能有以下三个指针:

  • 左子节点指针
  • 右子节点指针
  • 父节点指针

我们需要计算这些指针中有多少个是空的。


分析

  1. 二叉树节点数:共有 ( N ) 个节点。

  2. 左子指针和右子指针

    • 每个节点可能有一个左子节点指针或右子节点指针。
    • 整棵树中,非空的左右子指针的总数等于 ( N-1 )(因为二叉树中有 ( N-1 ) 条边连接父节点和子节点)。
    • 因此,空的左右子指针数为:
      [
      空的左右子指针数 = 2N - (N-1) = N+1
      ]
  3. 父节点指针

    • 每个节点有一个指向父节点的指针,除了根节点的父节点指针为空,其余 ( N-1 ) 个节点的父节点指针非空。
    • 因此,空的父节点指针数为 ( 1 )。

总计空指针数

总空指针数为:
[
空指针总数 = 空的左右子指针数 + 空的父节点指针数 = (N+1) + 1 = N+2
]


结论

对于一个有 ( N ) 个节点的二叉树,如果每个节点多了一个指向父节点的指针,则总共有 ( N+2 ) 个空指针。

两个字符串 分别有添加 删除 更新操作 用伪代码实现一个变成另一个

以下是一个基于 动态规划 的伪代码,用于将一个字符串转换为另一个字符串,支持 添加、删除、更新 三种操作。这个问题对应经典的 编辑距离(Levenshtein Distance)


伪代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function minEditDistance(s1, s2):
m = length of s1
n = length of s2

// 创建一个 (m+1) x (n+1) 的二维数组 dp,用于存储子问题的最优解
dp = 2D array of size (m+1, n+1)

// 初始化边界条件
for i from 0 to m:
dp[i][0] = i // 从 s1 的前 i 个字符变成空字符串,需删除 i 次
for j from 0 to n:
dp[0][j] = j // 从空字符串变成 s2 的前 j 个字符,需添加 j 次

// 填充 dp 表
for i from 1 to m:
for j from 1 to n:
if s1[i-1] == s2[j-1]: // 当前字符相同,无需操作
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(
dp[i-1][j] + 1, // 删除操作
dp[i][j-1] + 1, // 添加操作
dp[i-1][j-1] + 1 // 更新操作
)

// 返回将 s1 变为 s2 的最小编辑距离
return dp[m][n]

示例执行过程

假设输入:

  • ( s1 = “kitten” )
  • ( s2 = “sitting” )

初始化 dp 表:

  • dp[0][0] = 0
  • dp[i][0] = i
  • dp[0][j] = j
“” s i t t i n g
“” 0 1 2 3 4 5 6 7
k 1
i 2
t 3
t 4
e 5
n 6

填表过程:

根据状态转移方程依次填充表格,最后返回 dp[m][n]。


操作路径追踪

为了获得具体的操作步骤,可以在填表的过程中记录路径,例如:

  1. 如果当前选择了 删除操作,记录 delete s1[i-1]
  2. 如果选择了 添加操作,记录 insert s2[j-1]
  3. 如果选择了 更新操作,记录 update s1[i-1] to s2[j-1]

通过从 dp[m][n] 回溯,可以生成一个完整的操作序列。


时间复杂度

  • 时间复杂度:( O(m \times n) ),双层循环计算 dp 表。
  • 空间复杂度:( O(m \times n) ),存储 dp 表(可优化为 ( O(\min(m, n)) ))。

lambda表达式和bind的区别,lambda表达式引用捕获有什么风险(没答上来,面试官引导往多线程的角度考虑)

Lambda 表达式和 std::bind 的区别

  1. 语法和可读性

    • Lambda 表达式
      • 是一种匿名函数,语法简洁,直接内联定义,易于阅读。
      • 支持捕获外部变量(值捕获或引用捕获)。
    • **std::bind**:
      • 用于创建一个函数对象,将一个已有的函数与某些参数绑定,形成一个新的可调用对象。
      • 相对较冗长,可读性较差。
  2. 功能

    • Lambda 表达式更灵活,可以直接访问外部变量。
    • **std::bind**不支持捕获变量,仅能绑定已有函数的参数。
  3. 性能

    • Lambda 表达式通常性能更好,编译器可以对其内联优化。
    • **std::bind**会生成额外的函数对象,可能有较小的性能开销。
  4. 使用场景

    • Lambda 表达式适合简单、局部的回调或计算。
    • std::bind适合需要生成多次调用、固定绑定参数的场景。

Lambda 表达式引用捕获的风险

在多线程场景中,引用捕获([&]可能带来以下风险:

  1. 捕获的引用在生命周期内失效

    • 如果线程异步运行,捕获的引用可能指向了已经销毁的对象,导致 悬空引用 问题。
    • 例如:引用了一个局部变量,而局部变量在主线程中作用域结束后被释放,异步线程访问时会出错。
  2. 数据竞争

    • 如果多个线程通过引用捕获访问同一个变量,没有同步机制,则会发生数据竞争,导致未定义行为。

示例

生命周期问题

1
2
3
4
5
6
7
8
void example() {
int x = 10;
std::thread t([&]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << x << std::endl; // x 可能已经被销毁
});
t.detach();
} // x 离开作用域,线程可能访问已销毁的 x

解决方法

  • 改用 值捕获[=])或复制变量:
    1
    2
    3
    4
    std::thread t([x]() {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << x << std::endl; // 安全
    });

数据竞争问题

1
2
3
4
5
6
7
8
9
10
11
12
void example() {
int counter = 0;
std::thread t1([&]() {
for (int i = 0; i < 1000; ++i) counter++;
});
std::thread t2([&]() {
for (int i = 0; i < 1000; ++i) counter++;
});
t1.join();
t2.join();
std::cout << counter << std::endl; // 未定义行为
}

解决方法

  • 使用 互斥锁原子操作

    1
    2
    3
    4
    5
    6
    7
    std::mutex mtx;
    std::thread t1([&]() {
    for (int i = 0; i < 1000; ++i) {
    std::lock_guard<std::mutex> lock(mtx);
    counter++;
    }
    });
  • 或者使用 std::atomic<int>

    1
    2
    3
    4
    std::atomic<int> counter = 0;
    std::thread t1([&]() {
    for (int i = 0; i < 1000; ++i) counter++;
    });

总结

  • **Lambda 表达式 vs. std::bind**:

    • Lambda 表达式更灵活,支持变量捕获。
    • std::bind适合固定绑定参数,但功能有限。
  • 引用捕获风险(多线程场景)

    1. 生命周期失效导致悬空引用。
    2. 多线程访问导致数据竞争。
  • 应对措施

    • 避免引用捕获,改用值捕获或显式复制。
    • 使用同步机制(互斥锁、std::atomic)防止数据竞争。

虚函数表,存在在父类中还是在子类中(答得不是很好)

虚函数表(vtable)本质上是一个由编译器生成的、与类相关的数据结构,用于支持 C++ 的多态机制。它的具体存储位置和工作原理如下:

虚函数表存储在哪里?

  1. 虚函数表的存在

    • 虚函数表本身是一个全局或静态的结构,与类相关,而不是存储在具体的类实例中。
    • 每个具有虚函数的类都会生成一个对应的虚函数表,表中存储指向该类的虚函数的指针。
  2. 虚指针(vptr)

    • 每个包含虚函数的对象实例中,会有一个隐藏的指针,称为虚指针(vptr),它指向该对象对应类的虚函数表。
    • 虚指针(vptr)存储在对象的内存中,通常是对象布局中的第一个成员。
  3. 虚函数表的归属

    • 虚函数表与 相关,不管是父类还是子类,只要一个类声明了虚函数,它就有一个虚函数表。
    • 如果子类继承了父类并重写了父类的虚函数,则子类会拥有自己的虚函数表,表中存储子类重写后的虚函数指针。

虚函数表在父类和子类中的关系

  • 父类

    • 父类有自己的虚函数表,表中存储指向父类虚函数的指针。
    • 如果一个对象是父类类型,虚指针会指向父类的虚函数表。
  • 子类

    • 如果子类重写了父类的虚函数,子类会生成自己的虚函数表,覆盖父类对应的函数指针。
    • 如果子类没有重写某个父类的虚函数,那么子类的虚函数表会保留指向父类该虚函数的指针。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual ~Base() = default;
};

class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func2() override { cout << "Derived::func2" << endl; }
};

int main() {
Base* obj = new Derived();
obj->func1(); // 输出 Derived::func1
obj->func2(); // 输出 Derived::func2
delete obj;
return 0;
}

内存结构分析

  1. Base 类

    • Base 的虚函数表:[&Base::func1, &Base::func2]
    • Base 对象的虚指针(vptr)指向 Base 的虚函数表。
  2. Derived 类

    • Derived 的虚函数表:[&Derived::func1, &Derived::func2]
    • Derived 对象的虚指针(vptr)指向 Derived 的虚函数表。
  3. 动态绑定

    • 当通过父类指针调用虚函数时,虚指针会指向运行时对象的虚函数表,从而调用子类实现的虚函数。

总结

  • 虚函数表存在于类级别,与类相关。
  • 对象实例中存储一个虚指针(vptr),指向对应类的虚函数表。
  • 父类和子类都有自己的虚函数表,子类的虚函数表会覆盖父类中被重写的虚函数指针。

构造函数初始化列表和在函数体进行初始化的区别(没答上来)

在 C++ 中,构造函数初始化列表和在构造函数体内进行初始化有以下区别:


1. 构造顺序

  • 初始化列表

    • 成员变量按照其声明的顺序(而不是初始化列表中的顺序)进行初始化。
    • 初始化列表直接初始化成员变量,跳过默认构造后再赋值的步骤。
  • 构造函数体内初始化

    • 成员变量会先调用默认构造函数(如果有),然后在构造函数体内进行赋值。

2. 性能

  • 初始化列表

    • 更高效:成员变量直接使用提供的值进行初始化,没有多余的默认构造和赋值操作。
    • 特别是对于复杂对象或自定义类类型,这种方式避免了额外的拷贝或默认构造。
  • 构造函数体内初始化

    • 可能低效:成员变量会先进行默认初始化,然后再通过赋值操作修改值。这会导致额外的开销,尤其是当成员变量是类类型时。

3. 特定场景的必要性

  • 必须使用初始化列表的情况

    1. 常量成员变量const 成员只能通过初始化列表初始化,不能在函数体内赋值。
    2. 引用类型reference 成员变量必须在初始化列表中初始化,因为引用一旦绑定,无法改变。
    3. 没有默认构造函数的类类型:成员类型如果没有默认构造函数,必须在初始化列表中显式初始化。
  • 可以在函数体内赋值的情况

    • 对于普通的非 const 成员,赋值操作没有限制,可以在构造函数体内完成。

4. 可读性

  • 初始化列表

    • 更直观,能够一眼看出各个成员变量的初始值。
    • 推荐用于初始化成员变量的方式。
  • 构造函数体内

    • 当初始化需要复杂的逻辑时,可能会更适合放在构造函数体内,但这种情况较少。

示例

初始化列表

1
2
3
4
5
6
7
8
class MyClass {
const int a;
int b;
public:
MyClass(int x, int y) : a(x), b(y) { // 初始化列表
// No further assignments needed
}
};

构造函数体内初始化

1
2
3
4
5
6
7
8
class MyClass {
const int a;
int b;
public:
MyClass(int x, int y) : a(x) { // 必须在初始化列表中初始化 const 变量
b = y; // b 通过赋值初始化
}
};

总结

  1. 性能:初始化列表避免了多余的默认构造和赋值操作,效率更高。
  2. 必要性:常量成员、引用类型和无默认构造函数的成员必须用初始化列表。
  3. 推荐:尽量使用初始化列表来初始化成员变量,除非有复杂逻辑需要在构造函数体内实现。

如何防止内存泄漏

防止内存泄漏是 C++ 开发中的重要任务,以下是常见的方法和策略:


1. 使用智能指针

  • C++ 提供了 std::unique_ptrstd::shared_ptr 等智能指针来自动管理内存。
    • **std::unique_ptr**:独占所有权,确保对象的生命周期受控且无重复释放。
    • **std::shared_ptr**:支持多所有权,并通过引用计数自动释放内存。
    • **std::weak_ptr**:避免循环引用问题。
  • 示例
    1
    std::unique_ptr<int> ptr = std::make_unique<int>(42);  // 自动释放

2. 避免裸指针

  • 避免使用 newdelete 显式分配和释放内存,转而使用 STL 容器或智能指针。

3. 使用 RAII(资源获取即初始化)

  • 利用类的构造函数获取资源,析构函数释放资源。
  • 示例
    1
    2
    3
    4
    5
    class Resource {
    public:
    Resource() { /* Acquire resource */ }
    ~Resource() { /* Release resource */ }
    };

4. 优先使用 STL 容器

  • 使用 std::vectorstd::map 等容器管理内存,而不是手动分配动态数组或动态对象。
  • 容器会自动管理对象的生命周期,防止忘记释放内存。

5. 避免循环引用

  • 当使用 std::shared_ptr 时,如果两个对象互相持有 **std::shared_ptr**,可能导致引用计数无法归零,造成内存泄漏。
    • 解决方案:将一端的 std::shared_ptr 替换为 **std::weak_ptr**。
  • 示例
    1
    2
    3
    4
    struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 避免循环引用
    };

6. 定期检查内存分配

  • 使用工具(如 ValgrindAddressSanitizer)检测内存泄漏和未释放的内存。

7. 避免早退出导致的泄漏

  • 如果函数中存在多种退出路径,确保在所有路径都释放动态分配的资源。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    void func() {
    int* data = new int[100];
    if (/* some condition */) {
    delete[] data; // 确保释放
    return;
    }
    delete[] data;
    }

8. 自定义内存管理

  • 对于复杂场景,可以封装动态内存分配和释放操作。
  • 示例
    1
    2
    3
    4
    class MemoryManager {
    void* allocate(size_t size) { /* Custom allocation logic */ }
    void deallocate(void* ptr) { /* Custom deallocation logic */ }
    };

9. 定义析构函数释放资源

  • 在类中动态分配内存时,确保定义析构函数释放内存。
  • 示例
    1
    2
    3
    4
    5
    6
    class MyClass {
    int* data;
    public:
    MyClass() { data = new int[100]; }
    ~MyClass() { delete[] data; }
    };

10. 使用 deletedelete[] 配对

  • 确保释放内存时使用正确的操作:
    • **delete**:释放单个对象。
    • **delete[]**:释放动态数组。

11. 避免悬空指针

  • 在释放内存后,将指针设置为 nullptr
  • 示例
    1
    2
    3
    int* ptr = new int(42);
    delete ptr;
    ptr = nullptr; // 避免悬空指针

总结

防止内存泄漏的核心思想是:尽量减少手动内存管理,借助智能指针和 RAII 进行自动管理,避免复杂指针操作和循环引用,必要时借助工具检测问题

介绍一下智能指针,将普通指针转换成智能指针有什么风险(没答上来)

智能指针介绍

智能指针是 C++ 标准库中提供的一种工具,用来管理动态分配的内存,避免内存泄漏和指针悬空等问题。智能指针通过 RAII(资源获取即初始化)原理,确保资源在生命周期结束时自动释放。

C++ 提供了以下几种常用的智能指针:

1. std::unique_ptr

  • 特点
    • 独占所有权,同一时间只能有一个 std::unique_ptr 拥有对象。
    • 不可复制,但可以通过 std::move 转移所有权。
  • 适用场景
    • 动态分配对象只需要一个所有者。
  • 示例
    1
    std::unique_ptr<int> ptr = std::make_unique<int>(42); // 自动管理内存

2. std::shared_ptr

  • 特点
    • 支持多所有权,多个 std::shared_ptr 可以共享同一个对象。
    • 使用引用计数管理内存,当引用计数为 0 时释放对象。
  • 适用场景
    • 动态分配对象需要共享所有权。
  • 示例
    1
    2
    std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
    std::shared_ptr<int> ptr2 = ptr1; // 共享所有权

3. std::weak_ptr

  • 特点
    • std::shared_ptr 配合使用,避免循环引用。
    • 不影响引用计数。
  • 适用场景
    • 用于弱引用,观察 std::shared_ptr 管理的对象。
  • 示例
    1
    2
    std::shared_ptr<int> shared = std::make_shared<int>(42);
    std::weak_ptr<int> weak = shared; // 弱引用,不增加引用计数

普通指针转换为智能指针的风险

将普通指针转换为智能指针可能带来以下风险:

1. 多次释放内存

  • 如果普通指针已经被手动释放,或者被多个智能指针管理,会导致 双重释放 问题。
  • 示例
    1
    2
    3
    int* rawPtr = new int(42);
    std::shared_ptr<int> sp1(rawPtr);
    std::shared_ptr<int> sp2(rawPtr); // rawPtr 被多次管理,可能导致多次释放

2. 无法保证原始指针的唯一性

  • 如果普通指针被其他代码持有且操作,将导致不可预测行为。
  • 示例
    1
    2
    3
    int* rawPtr = new int(42);
    std::shared_ptr<int> sp(rawPtr);
    delete rawPtr; // 手动释放与智能指针管理冲突

3. 内存泄漏

  • 如果在转换前已有逻辑分配了对象但未释放,可能引发内存泄漏。
  • 示例
    1
    2
    3
    int* rawPtr = new int(42);
    // 假设未释放就退出,导致泄漏
    std::unique_ptr<int> up(rawPtr); // 自动释放,但此前的内存管理混乱

4. 循环引用问题

  • 使用 std::shared_ptr 时,如果两个对象互相持有 std::shared_ptr,会导致循环引用,内存无法释放。
  • 解决办法:将一方改为 std::weak_ptr
  • 示例
    1
    2
    3
    4
    struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev; // 使用 weak_ptr 避免循环引用
    };

建议

  1. 避免从普通指针直接构造智能指针,推荐使用 std::make_uniquestd::make_shared
  2. 确保普通指针的唯一性,防止同一指针被多个智能指针管理。
  3. 慎用原始指针与智能指针混用,保持代码清晰一致。
  4. 理解智能指针的生命周期和适用场景,避免误用导致性能问题或内存泄漏。

引用和指针的区别

引用和指针的区别

引用(reference)和指针(pointer)是 C++ 中用于操作内存的两种重要工具。尽管它们功能上有些相似,但有明显的区别。


1. 定义和语法

  • 引用

    • 是某个变量的别名,必须在定义时初始化。
    • 语法简单,直接通过 & 定义。
    • 示例
      1
      2
      int x = 10;
      int& ref = x; // ref 是 x 的引用
  • 指针

    • 是一个变量,存储另一个变量的内存地址。
    • 可以在定义后再初始化,并能指向不同的对象。
    • 示例
      1
      2
      int x = 10;
      int* ptr = &x; // ptr 存储 x 的地址

2. 是否可以为空

  • 引用

    • 引用必须绑定到一个有效对象,不能为空。
    • 一旦绑定后,引用始终指向初始化时的对象,无法更改绑定。
    • 示例
      1
      2
      int x = 10;
      int& ref = x; // 必须初始化
  • 指针

    • 指针可以为空(nullptr),用来表示它没有指向任何对象。
    • 示例
      1
      int* ptr = nullptr; // 空指针

3. 访问方式

  • 引用

    • 使用起来就像直接操作原变量,不需要解引用操作。
    • 示例
      1
      2
      3
      int x = 10;
      int& ref = x;
      ref = 20; // 等同于 x = 20;
  • 指针

    • 使用指针访问变量时需要解引用操作符(*)。
    • 示例
      1
      2
      3
      int x = 10;
      int* ptr = &x;
      *ptr = 20; // 修改 x 的值

4. 可否重新绑定

  • 引用

    • 引用一旦绑定到某个变量,就不能更改其绑定。
    • 示例
      1
      2
      3
      int x = 10, y = 20;
      int& ref = x;
      ref = y; // 修改的是 x 的值,而不是重新绑定到 y
  • 指针

    • 指针可以指向不同的对象,重新赋值即可更改指向。
    • 示例
      1
      2
      3
      int x = 10, y = 20;
      int* ptr = &x;
      ptr = &y; // 指针指向 y

5. 内存模型

  • 引用

    • 编译器通过原变量的地址实现引用,引用本身不占用额外的内存。
  • 指针

    • 指针本身是一个变量,存储目标变量的地址,占用额外的内存。

6. 使用场景

  • 引用

    • 更适合用于参数传递和返回值,语法简单且安全。
    • 示例:函数传递
      1
      2
      3
      void updateValue(int& ref) {
      ref += 10; // 修改调用者传入的变量
      }
  • 指针

    • 更灵活,可以动态分配内存、实现复杂数据结构(如链表)。
    • 示例:动态分配
      1
      2
      int* ptr = new int(10); // 动态分配内存
      delete ptr; // 手动释放

总结

特点 引用 指针
定义是否需要初始化 必须初始化 可以不初始化
是否可以为空 不可以为空 可以为空(nullptr
是否可以重新绑定 不可以重新绑定 可以指向其他变量
内存占用 不占用额外内存 占用内存存储地址
使用灵活性 简单、安全 更灵活,可动态分配内存

使用建议

  • 当需要语法简洁、操作简单时,使用引用。
  • 当需要更灵活的功能(如动态分配、指向不同对象)时,使用指针。

右值引用

右值引用(Rvalue Reference)

右值引用是 C++11 引入的新特性,用于更高效地操作临时对象(右值),减少不必要的拷贝,提高性能。右值引用的语法是使用双 &&


1. 什么是右值?

  • 左值(Lvalue):表示在内存中有明确地址的变量,可以取地址。
    例如:
    1
    int x = 10; // x 是左值
  • 右值(Rvalue):表示无法取地址的临时值或匿名对象,通常是表达式的结果。
    例如:
    1
    2
    int x = 10;
    int y = x + 5; // x + 5 是右值

2. 右值引用的定义

右值引用允许我们通过引用捕获右值,从而避免不必要的拷贝操作。

  • 语法

    1
    int&& rref = 10; // rref 是右值引用,绑定到右值 10
  • 注意:右值引用只能绑定到右值,不能绑定到左值。

    1
    2
    int x = 10;
    int&& rref = x; // ❌ 错误,不能将右值引用绑定到左值

3. 右值引用的用途

(1) 支持移动语义(Move Semantics)

  • 在对象的转移过程中,通过右值引用避免拷贝操作,提高性能。

  • 常用于实现 std::movestd::vector 的动态扩容。

    示例:移动构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyClass {
    int* data;
    public:
    MyClass(int size) : data(new int[size]) {}
    ~MyClass() { delete[] data; }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
    other.data = nullptr; // 转移所有权
    }
    };

(2) 完美转发(Perfect Forwarding)

  • 使用右值引用和 std::forward,可以实现函数模板的参数完美转发。

    示例:

    1
    2
    3
    4
    template <typename T>
    void wrapper(T&& arg) {
    process(std::forward<T>(arg)); // 根据传入参数的类型选择左值或右值
    }

(3) 优化临时对象操作

  • 捕获右值临时对象,直接操作其资源,减少拷贝。
    示例:
    1
    2
    std::string str1 = "Hello";
    std::string str2 = std::move(str1); // 使用右值引用转移资源

4. 左值引用和右值引用的区别

特点 左值引用(Lvalue Reference) 右值引用(Rvalue Reference)
能绑定的对象 左值 右值
修改目标是否可行 可以修改 可以修改
常见用途 用于函数参数传递或返回值 用于移动语义、完美转发等

5. 示例代码:右值引用与移动语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class MyClass {
string data;

public:
MyClass(string s) : data(s) {}
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
cout << "Move Constructor Called" << endl;
}
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
data = std::move(other.data);
}
cout << "Move Assignment Called" << endl;
return *this;
}
};

int main() {
MyClass obj1("Hello");
MyClass obj2 = std::move(obj1); // 调用移动构造函数
return 0;
}

输出:

1
Move Constructor Called

6. 注意事项

  • 右值引用的风险:如果操作后的右值引用继续使用,可能引发未定义行为。
  • 右值引用的语义:右值引用用于短期资源转移或优化,不应持久化。
  • 使用右值引用时,请结合 std::movestd::forward

总结

右值引用是 C++ 提供的一个高效工具,主要用于:

  1. 移动语义:避免不必要的拷贝,提高性能。
  2. 完美转发:模板函数中正确转发左值和右值。
  3. 优化临时对象操作:捕获右值进行操作。

通过右值引用,可以显著优化现代 C++ 程序的性能和资源管理能力。

TCP四次挥手为什么还要等2MSL

在 TCP 的四次挥手中,主动关闭连接的一方在发送完最后一个 ACK(确认报文)后会进入 TIME_WAIT 状态,并等待 2MSL(两倍的最大报文生存时间,Maximum Segment Lifetime)。等待 2MSL 的主要原因如下:


1. 确保被动关闭方收到 ACK

  • 在四次挥手的过程中,主动关闭方的最后一个 ACK 可能会丢失。
  • 如果被动关闭方没有收到 ACK,会重新发送 FIN 报文。
  • 主动关闭方在 TIME_WAIT 状态下,可以重传 ACK,确保被动关闭方知道连接已正常关闭。

2. 防止旧连接的数据影响新连接

  • TCP 中的每个连接都由五元组(源 IP、源端口、目的 IP、目的端口、协议)唯一标识。
  • 如果一个连接迅速被复用,旧连接中延迟到达的报文可能被误认为属于新连接,导致数据混淆。
  • 等待 2MSL 时间后,旧连接中延迟到达的报文会被丢弃,不再影响新连接。

3. 确保网络中遗留的报文清除

  • TCP 报文在网络中可能有一定的延迟,等待 2MSL 确保所有与该连接相关的报文都已经被丢弃,不会影响后续通信。

MSL 是什么?

  • MSL(Maximum Segment Lifetime) 是报文在网络中存活的最大时间。
  • 它是一个 TCP 协议规定的固定值,通常为 30 秒到 2 分钟,具体取决于操作系统的实现。

总结

TCP 等待 2MSL 的原因:

  1. 确保对方收到最后的 ACK。
  2. 防止旧连接数据影响新连接。
  3. 确保网络中遗留的报文清除。

虽然 2MSL 会让主动关闭方多占用一段时间的资源,但这是 TCP 提供可靠性和数据安全的重要手段。

TCP的可靠体现在哪方面

TCP(Transmission Control Protocol)的可靠性体现在以下几个方面:


1. 数据传输的确认机制

  • ACK(Acknowledgment)机制:TCP 在数据传输过程中,接收方会通过 ACK 确认已经收到的数据。如果发送方在超时时间内没有收到 ACK,会重传数据。
  • 顺序保证:TCP 会根据序列号(Sequence Number)将乱序到达的数据重新排序后交给应用层,确保数据以正确的顺序交付。

2. 超时重传

  • 如果发送方在一定时间内没有收到接收方的 ACK,TCP 会重新发送丢失的数据段,确保数据到达对方。

3. 流量控制

  • 使用 滑动窗口机制 来控制发送方的发送速度,防止接收方的缓冲区溢出。
  • 滑动窗口动态调整,以适应网络和接收方的处理能力。

4. 拥塞控制

  • TCP 使用算法(如慢启动、拥塞避免、快速重传和快速恢复)来减少网络拥塞的影响。
  • 发送方根据网络的拥塞状态动态调整发送速率,确保网络稳定性和数据传输的可靠性。

5. 数据校验

  • 每个 TCP 数据段都有一个 校验和(Checksum) 字段,用于验证数据在传输过程中是否被篡改或出错。
  • 如果数据校验失败,接收方会丢弃数据段,并让发送方重传。

6. 全双工通信

  • TCP 提供双向通信能力,双方可以同时发送和接收数据,并分别维护各自的序列号和确认号,保证每一方向的数据传输都可靠。

7. 连接管理

  • 三次握手:确保通信双方能够互相确认对方的接收能力和发送能力,建立可靠的连接。
  • 四次挥手:确保双方都能正确关闭连接,避免数据丢失或连接混乱。

8. 数据分片与组装

  • TCP 会根据路径上的最大传输单元(MTU)对大数据分片,接收方在接收到所有片段后将其重新组装成完整数据,确保大数据能够可靠传输。

总结

TCP 的可靠性主要通过 确认机制、超时重传、流量控制、拥塞控制、数据校验 等手段保障,能够确保数据在不可靠的网络环境下实现可靠传输。

TCP三次握手的作用(确认发送的序列号,面试官一直在引导这个方向但还是没有答不上来)

TCP三次握手的作用主要体现在确保可靠通信的基础上建立连接,并通过确认序列号(Sequence Number)来实现以下几点:


1. 确认双方的发送和接收能力

  • 第一次握手:客户端发送一个带有初始序列号 ( ISN_c ) 的 SYN 包给服务器,表明客户端希望建立连接,并告诉服务器自己的序列号起点。
  • 第二次握手:服务器收到 SYN 后,向客户端返回一个 SYN+ACK 包,同时带上自己的初始序列号 ( ISN_s ),确认收到了客户端的 SYN 请求。
  • 第三次握手:客户端收到 SYN+ACK 后,再发送一个 ACK 包确认,表示双方的序列号和连接状态已经同步完成。

通过握手,双方确认彼此的发送能力(序列号的传递)和接收能力(ACK 确认收到的序列号)。


2. 确定初始序列号,防止旧连接干扰

  • 在握手过程中,客户端和服务器各自生成一个随机的初始序列号 ( ISN ),并交换确认。
  • 这个初始序列号是后续数据传输的起点,确保数据传输的可靠性和正确性。
  • 通过确认序列号,防止旧连接的报文对新连接产生干扰。

3. 建立可靠的全双工通信

  • TCP 是全双工通信协议,三次握手确保通信双方都已准备好发送和接收数据。
  • 第一次握手:客户端告诉服务器“我要发送数据了”。
  • 第二次握手:服务器告诉客户端“我也可以发送数据了”。
  • 第三次握手:客户端告诉服务器“我们都准备好了”。

4. 防止未初始化的连接误导接收方

  • 如果没有三次握手,可能会出现服务器因为接收到重复的 SYN 报文(网络延迟或重发)而错误建立连接,导致资源浪费。
  • 三次握手通过明确的 SYN 和 ACK 机制,避免了这种误操作。

总结

三次握手的核心作用是:

  1. 确定双方的发送和接收能力。
  2. 确认并同步双方的初始序列号 ( ISN ),为后续可靠数据传输奠定基础。
  3. 防止历史连接数据干扰新连接。
  4. 建立全双工通信。

序列号的确认是三次握手的重点,它确保了 TCP 的可靠性和正确性。如果握手未能完成,连接就不会被建立,避免不必要的资源浪费。

介绍死锁,如何解除

什么是死锁?

死锁是指多个线程或进程在竞争资源时,因相互等待而导致永远无法继续执行的状态。具体来说,死锁发生的四个必要条件(“死锁四条件”)是:

  1. 互斥条件:资源不能被多个线程同时占用。
  2. 占有且等待:一个线程占有资源的同时,等待其他线程释放资源。
  3. 不可剥夺:资源不能被强行剥夺,只能由持有线程主动释放。
  4. 循环等待:多个线程之间形成资源的循环等待链。

如何解除死锁?

1. 避免死锁的方法

通过破坏“死锁四条件”来预防死锁:

  • 破坏互斥条件:尽量减少对共享资源的独占访问,使用无锁编程或锁替代机制(如读写锁)。
  • 破坏占有且等待
    • 在申请资源时,必须一次性申请所有所需资源。
    • 线程在持有资源时不允许再申请新资源。
  • 破坏不可剥夺
    • 如果一个线程无法获取所需资源,要求它释放已占有的资源。
  • 破坏循环等待
    • 所有资源按固定顺序申请,确保不会形成资源等待环。
    • 使用银行家算法动态检测并避免死锁。

2. 检测和解除死锁的方法

如果死锁已经发生,可以通过以下方式解决:

  • 资源剥夺:强制某些线程释放资源,可能会引起程序状态的不一致。
  • 终止线程:强行终止部分线程,释放它们占用的资源(可能导致数据丢失或不一致)。
  • 检测循环等待
    • 通过检测资源分配图中是否存在环来判断死锁。
    • 找到死锁的线程链,选择中止某些线程或释放部分资源。

3. 使用超时机制

  • 为线程获取锁设置超时时间,如果超时则放弃操作,降低死锁发生的概率。

实际代码中如何避免死锁?

  • **使用 std::lock**:C++ 中的 std::lock 可同时申请多个锁,避免因锁的顺序问题导致死锁。
  • 尽量减少锁的粒度:尽可能使用小范围的锁定区域。
  • 尝试无锁编程:通过 std::atomic 等工具避免使用互斥锁。

示例:破坏循环等待的策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <mutex>
#include <thread>
#include <iostream>

std::mutex m1, m2;

void thread1() {
std::lock(m1, m2); // 使用std::lock避免死锁
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
std::cout << "Thread 1 acquired both locks\n";
}

void thread2() {
std::lock(m1, m2); // 保证锁的顺序一致
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
std::cout << "Thread 2 acquired both locks\n";
}

int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}

总结

  1. 死锁的本质是资源竞争和线程的相互等待。
  2. 预防死锁可以通过破坏死锁条件、资源顺序分配等方式。
  3. 解决死锁需要检测循环等待,并通过释放资源或终止线程解除死锁。
  4. 编程中推荐使用标准库工具(如 std::lock)来避免复杂的死锁问题。

结合死锁,给了一段C++的应用,问有什么后果(没答上来)

对于面试中结合死锁的C++应用问题,面试官通常会给出一段多线程代码,让你分析其中可能的问题和后果。以下是通用的思路和可能的后果:


分析思路:

  1. 识别临界区

    • 找出哪些资源(如锁、共享变量)需要多线程同步。
    • 确定这些资源是否可能被多个线程同时访问。
  2. 检查锁的使用

    • 锁的申请顺序是否一致?是否可能存在循环等待?
    • 是否有线程在持有锁后阻塞或长时间不释放?
  3. 观察潜在问题

    • 是否使用了多个锁?多个锁是否可能导致死锁?
    • 是否有可能出现资源竞争、数据不一致或长时间等待?
  4. 模拟执行流程

    • 根据代码逻辑,推测多个线程运行时的可能路径,找到潜在的死锁点或逻辑漏洞。

后果分析:

  1. 死锁
    如果代码中多个线程互相持有资源并等待对方释放,则会导致死锁。程序将卡住,无法继续执行。

  2. 资源泄漏
    如果死锁发生,某些资源(如内存、文件句柄、锁等)可能永远无法被释放,导致资源泄漏。

  3. 线程饥饿
    一个线程长时间持有锁,导致其他线程无法访问资源,可能引发性能问题或线程饥饿。

  4. 程序崩溃
    如果没有正确处理死锁或资源竞争,可能会导致程序崩溃(如访问空指针、非法操作等)。


例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <thread>
#include <mutex>

std::mutex m1, m2;

void thread1() {
std::lock_guard<std::mutex> lock1(m1); // 持有m1
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(m2); // 尝试获取m2
std::cout << "Thread 1 acquired both locks\n";
}

void thread2() {
std::lock_guard<std::mutex> lock2(m2); // 持有m2
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock1(m1); // 尝试获取m1
std::cout << "Thread 2 acquired both locks\n";
}

int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}

分析:

  • 问题

    1. thread1 在持有 m1 的同时尝试获取 m2
    2. thread2 在持有 m2 的同时尝试获取 m1
    3. 两个线程互相等待对方释放资源,导致死锁。
  • 后果

    • 程序会进入死锁状态,两个线程都无法继续执行,程序卡住。

解决方法:

  1. 统一锁顺序
    确保多个线程获取锁的顺序一致,例如始终先获取 m1,再获取 m2

  2. 使用 std::lock 避免死锁
    使用 std::lock 同时申请多个锁,避免死锁问题。

1
2
3
4
5
6
void thread1() {
std::lock(m1, m2); // 同时获取多个锁,避免死锁
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
std::cout << "Thread 1 acquired both locks\n";
}

总结:

面试中,结合死锁的问题,常见后果是程序卡住(死锁)、性能下降(线程饥饿)或资源泄漏。关键是要分析代码的资源申请顺序和并发逻辑,提出针对性的解决方案(如统一锁顺序、使用 std::lock 或其他同步机制)。

C++中二维数组按行访问和按列访问(往空间局部性的角度答)

在 C++ 中,二维数组的按行访问和按列访问会影响性能,主要是因为空间局部性和内存缓存(Cache)的工作原理。


空间局部性

  1. 按行访问

    • 在内存中,二维数组是按照行优先(Row-major Order)存储的。
    • 按行访问时,内存地址是连续的,访问同一行的元素时,CPU 缓存(Cache)可以预取多个元素。
    • 性能更高,因为利用了空间局部性

    示例:

    1
    2
    3
    4
    5
    6
    7
    int arr[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    for (int i = 0; i < 3; ++i) {
    for (int j = 0; j < 3; ++j) {
    // 按行访问
    int val = arr[i][j];
    }
    }
  2. 按列访问

    • 按列访问时,跳跃访问不同行的元素,内存地址不连续。
    • CPU 缓存预取失效,导致频繁的缓存缺失(Cache Miss)。
    • 性能较低,因为没有充分利用空间局部性

    示例:

    1
    2
    3
    4
    5
    6
    for (int j = 0; j < 3; ++j) {
    for (int i = 0; i < 3; ++i) {
    // 按列访问
    int val = arr[i][j];
    }
    }

内存访问示意图(假设二维数组为 arr[3][3]):

  • 内存布局(行优先存储):

    1
    arr[0][0], arr[0][1], arr[0][2], arr[1][0], arr[1][1], arr[1][2], arr[2][0], arr[2][1], arr[2][2]
  • 按行访问

    • 访问顺序:arr[0][0] -> arr[0][1] -> arr[0][2] -> arr[1][0] -> ...
    • 优点:连续访问,Cache 命中率高。
  • 按列访问

    • 访问顺序:arr[0][0] -> arr[1][0] -> arr[2][0] -> arr[0][1] -> ...
    • 缺点:跨行访问,跳跃内存,Cache Miss 增加。

性能对比

  1. 按行访问的时间复杂度和按列访问相同,均为 (O(n^2)),但按行访问的实际运行时间更短
  2. 对于较大的二维数组(如几百行几百列),按列访问可能导致性能显著下降。

结论

  • 按行访问性能更高,因为符合内存的存储方式,利用了空间局部性和缓存预取。
  • 按列访问性能较差,除非数据存储方式专门调整为列优先(Column-major Order)。

LT与ET的区别

LT(Level Triggered,电平触发)和 ET(Edge Triggered,边缘触发)是 I/O 多路复用机制中对事件通知方式的两种模式。它们主要应用于像 epoll 这样的 I/O 多路复用接口中,用于监听文件描述符(fd)的状态变化。


1. LT(电平触发)

  • 特点

    • 默认模式,只要文件描述符处于就绪状态(如可读/可写),就会不断通知应用程序。
    • 应用程序可以多次处理该事件,直到状态不再就绪(如数据读取完毕)。
    • 对事件处理的要求相对宽松,允许多次尝试。
  • 行为

    • 如果文件描述符上有数据未读取,epoll_wait 会每次调用都通知应用程序,直到数据全部处理完毕。
  • 优点

    • 实现简单,不容易漏掉数据。
    • 适合大多数应用场景,尤其是对实时性要求不高的场景。
  • 缺点

    • 可能导致重复通知和不必要的系统调用,降低性能。

2. ET(边缘触发)

  • 特点

    • 高效模式,仅在文件描述符状态从非就绪变为就绪时通知应用程序(即状态发生变化时)。
    • 一旦通知后,应用程序必须立即处理该事件,否则可能漏掉事件。
  • 行为

    • 数据未处理完但文件描述符不再发生状态变化时,不会再次通知应用程序。
    • 需要结合非阻塞 I/O 使用,保证每次能处理尽可能多的数据。
  • 优点

    • 通知次数减少,系统调用更少,效率更高。
    • 适合高性能网络服务器等实时性要求高的场景。
  • 缺点

    • 实现复杂,稍有不慎可能导致事件丢失或死锁。

3. 举例

假设一个文件描述符有数据到达:

  • LT 模式

    • epoll_wait 会不断通知应用程序,直到数据被完全读取。
    • 适合一次处理不完所有数据的情况。
  • ET 模式

    • epoll_wait 只会在数据到达时通知应用程序一次。
    • 应用程序必须设置非阻塞 I/O,并尽可能地一次性读取所有数据。

4. 适用场景

  • LT(电平触发)

    • 适用于简单应用或不熟悉异步编程的场景。
    • 允许应用程序有更多的容错机会。
  • ET(边缘触发)

    • 适用于高性能服务器或实时性要求较高的场景。
    • 通常用于 Web 服务器(如 Nginx)或高并发网络应用程序。

5. 总结

  • LT 模式简单易用,适合大多数场景,但可能引入额外的系统调用开销。
  • ET 模式高效但复杂,需要配合非阻塞 I/O 和一次性读取所有数据的逻辑,否则可能丢失事件。

select,poll,epoll的区别

selectpollepoll 的区别

它们是 Linux 提供的 I/O 多路复用机制,用于监控多个文件描述符(fd)的状态(如是否可读、可写等),从而实现高效的 I/O 操作。以下是它们的主要区别:


1. select

  • 特点

    • 最早的 I/O 多路复用接口(1983年)。
    • 使用一个固定大小的数组(fd_set)来存储待监控的文件描述符。
    • 每次调用都需要将所有 fd 传递给内核。
  • 优点

    • 跨平台支持较好(如 Windows 也支持 select)。
  • 缺点

    • 文件描述符数量有限:最多只能监控 1024 个文件描述符(FD_SETSIZE 可调整)。
    • 性能低下:每次调用都需要拷贝 fd 集合到内核,并遍历所有文件描述符,复杂度为 (O(n))。
    • 状态不可保留:每次调用需要重新设置 fd 集合。

2. poll

  • 特点

    • 改进于 select,于 1996 年引入。
    • 采用链表结构(pollfd 数组),避免了 select 的固定大小限制。
    • 没有文件描述符数量限制(理论上由系统内存决定)。
    • 每次调用仍需要将 pollfd 数组传递到内核,并遍历所有文件描述符。
  • 优点

    • 支持任意数量的文件描述符。
    • 接口更加灵活,避免了 select 的固定大小数组限制。
  • 缺点

    • 性能低下:和 select 一样,每次调用都需要拷贝整个 pollfd 数组,复杂度为 (O(n))。
    • 状态不可保留:每次调用需要重新初始化。

3. epoll

  • 特点

    • 高效的事件驱动机制,于 Linux 2.5.44 引入。
    • 核心机制是 事件通知,内核提供了一个事件管理器,可以动态注册、修改、删除待监控的文件描述符。
    • 采用了 红黑树双向链表 来管理事件,避免了每次调用时的遍历开销。
  • 优点

    • 性能高:支持 边缘触发(ET) 模式,避免频繁系统调用。
    • 状态可保留:fd 注册后无需每次重新传递。
    • 适合大规模并发:复杂度接近 (O(1)),适合高并发场景(如 10k+ 连接)。
    • 支持任意数量的文件描述符。
  • 缺点

    • 仅支持 Linux,不跨平台。

4. 核心差异

特性 select poll epoll
文件描述符限制 1024(默认) 无限制(理论上) 无限制(理论上)
性能 遍历所有 fd,复杂度 (O(n)) 遍历所有 fd,复杂度 (O(n)) 动态触发,复杂度接近 (O(1))
状态保留 不支持 不支持 支持
触发机制 水平触发(LT) 水平触发(LT) 支持 LT 和 ET
跨平台 跨平台支持(Linux、Windows) 跨平台支持 仅支持 Linux

5. 应用场景

  • **select**:适用于文件描述符数量较少,简单跨平台的场景。
  • **poll**:适用于文件描述符数量较多,但需要一定灵活性的场景。
  • **epoll**:适用于高并发服务器(如 Web 服务器)或大量连接的场景(如 Nginx)。

总结

  • 如果是小规模项目或需要跨平台支持,可以选择 selectpoll
  • 如果是高性能、高并发的网络编程,在 Linux 环境下,优先选择 epoll

inline的优缺点

C++ 中 inline 的优缺点

inline 是一种提示编译器将函数展开为内联代码的方式,用于减少函数调用的开销。以下是 inline 的优缺点:


优点

  1. 减少函数调用开销

    • 函数调用通常会引入栈帧管理、参数传递等开销。使用 inline 后,函数调用被展开为内联代码,避免了这些额外开销。
  2. 提高执行效率

    • 减少了函数调用的时间开销,尤其对于小型函数或频繁调用的函数,可以提高程序运行效率。
  3. 有利于编译器优化

    • 内联展开后,编译器可以进行更强的优化,比如常量传播、寄存器分配等,从而生成更高效的代码。
  4. 简化代码编写

    • 在头文件中定义 inline 函数,可以避免代码重复,提高代码的可维护性。

缺点

  1. 代码体积增大(代码膨胀)

    • 内联展开会使每次调用的地方都插入函数代码,导致代码体积增加。如果函数较大且调用次数多,会导致二进制文件变大。
  2. 不适合递归函数

    • 递归函数通常不能完全展开为内联代码,内联的意义丧失。
  3. 编译时间增加

    • 由于内联函数通常定义在头文件中,且会在每个包含它的编译单元中展开,可能增加编译时间。
  4. 可能导致性能下降

    • 如果函数过大而被内联,会占用大量指令缓存,从而影响运行性能。
  5. 内联仅为编译器的建议

    • inline 并不保证一定会被内联展开,具体是否内联由编译器决定。现代编译器往往能自动判断是否需要内联,因此显式 inline 有时是多余的。

适用场景

  1. 小型、频繁调用的函数(如简单的 getter 和 setter)。
  2. 性能敏感代码中需要消除函数调用开销的场景。
  3. 定义在头文件中的函数,为了避免重复定义问题。

不适用场景

  1. 递归函数或函数体较大的函数。
  2. 对代码大小或指令缓存敏感的场景。
  3. 需要频繁更改的函数(增大编译时间,影响效率)。

总结
inline 是一种优化工具,但需谨慎使用。现代编译器能够根据函数的实际情况决定是否内联,因此显式 inline 的使用应以提高代码可读性和避免重复定义为主,而不是强制展开代码。

Typedef和define的区别

typedef#define 的区别

typedef#define 都可以用于定义别名,但它们的作用和使用方式有明显的区别。


1. 功能和作用

  • typedef
    用于为已有类型定义新的类型别名。它是 C 和 C++ 的一种类型定义机制,能够更好地与类型系统结合。

  • #define
    是一种预处理器指令,用于定义宏,可以用来为常量、表达式或简单的文本替换创建别名。它在编译之前由预处理器完成文本替换。


2. 语法区别

  • typedef

    • 作用于类型,用于定义类型别名。
    • 编译器在类型检查时会考虑其定义。

    示例:

    1
    2
    typedef unsigned int uint;
    uint a = 10; // 等价于 unsigned int a = 10;
  • #define

    • 进行简单的文本替换,无类型检查。
    • 宏替换不具有作用域,会在整个编译单元中生效。

    示例:

    1
    2
    #define uint unsigned int
    uint a = 10; // 等价于 unsigned int a = 10;

3. 适用范围

  • typedef

    • 只能用于类型的定义。
    • 适用于复杂类型(如指针、结构体等)。

    示例:

    1
    2
    typedef int* IntPtr;
    IntPtr p1, p2; // 等价于 int* p1; int* p2;
  • #define

    • 适用于常量、表达式或简单代码片段的文本替换。

    示例:

    1
    2
    #define MAX_SIZE 100
    int arr[MAX_SIZE];

4. 类型安全性

  • typedef

    • 是类型系统的一部分,受编译器检查,具有类型安全性。
  • #define

    • 仅是文本替换,不受编译器的类型检查,可能导致意外错误。

    示例:

    1
    2
    #define IntPtr int*
    IntPtr p1, p2; // 实际上是 int* p1; int p2; // 容易导致混淆

5. 使用场景

  • typedef

    • 用于定义复杂类型的别名(如指针、结构体、函数指针等)。
    • 推荐使用 typedefusing(C++11 引入)代替 #define
  • #define

    • 用于定义简单的常量或宏,但现代 C++ 中更推荐使用 constconstexpr

总结

特性 typedef #define
作用 定义类型别名 文本替换(包括常量、表达式等)
类型安全 是,受编译器检查 否,纯文本替换
使用范围 仅用于类型 类型、常量、表达式等
推荐程度 推荐,代码更安全可读 不推荐,用现代替代方案(如 constexpr

现代 C++ 建议:尽量使用 typedef(或 using)和 constexpr 替代 #define,以提高代码的安全性和可维护性。

读写线程在一个队列会发生什么线程安全问题

当多个线程在同一个队列上进行读写操作时,如果没有进行适当的同步或保护,就会发生线程安全问题。以下是常见的线程安全问题及其原因:


1. 数据竞争(Data Race)

  • 问题描述
    如果多个线程同时访问队列,并至少有一个线程对队列进行写操作,就可能发生数据竞争。
  • 后果
    • 队列中的数据可能被多个线程同时修改,导致数据损坏或丢失。
    • 读取线程可能读取到不完整或无效的数据。

2. 并发修改异常

  • 问题描述
    一个线程正在修改队列(如插入或删除元素),而另一个线程同时访问队列(如遍历或读取)。如果操作没有加锁保护,队列的内部状态可能变得不一致。
  • 后果
    • 可能导致程序崩溃(如访问非法内存或触发断言)。
    • 读取到的数据可能与预期不符。

3. 死锁(Deadlock)

  • 问题描述
    如果队列的操作需要加锁来保证线程安全,而多个线程在获取锁时产生了循环等待,就可能导致死锁。
  • 后果
    • 所有线程都被阻塞,程序无法继续运行。

4. 饥饿(Starvation)

  • 问题描述
    如果锁的使用不当,例如某个线程长时间占用锁或优先级不当,可能导致其他线程无法访问队列。
  • 后果
    • 部分线程的任务得不到执行。

5. 虚假唤醒(Spurious Wakeup)

  • 问题描述
    使用条件变量进行同步时,线程可能会被虚假唤醒(即条件未满足时被唤醒)。
  • 后果
    • 线程可能进入不正确的逻辑分支,导致数据状态异常。

解决方案

  1. 互斥锁(Mutex)保护队列

    • 在对队列进行读写操作时使用互斥锁。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      std::mutex mtx;
      std::queue<int> queue;

      void writer() {
      std::lock_guard<std::mutex> lock(mtx);
      queue.push(42);
      }

      void reader() {
      std::lock_guard<std::mutex> lock(mtx);
      if (!queue.empty()) {
      int value = queue.front();
      queue.pop();
      }
      }
  2. 使用线程安全的队列

    • 使用 C++ 提供的线程安全容器(如 std::queue + std::mutex)或其他库(如 boost::lockfree::queue)。
    • C++20 引入的 std::atomic 提供了更强的支持,但对队列需额外设计。
  3. 条件变量实现生产者-消费者模型

    • 使用条件变量让读取线程等待,直到队列中有数据。
    • 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      std::mutex mtx;
      std::condition_variable cv;
      std::queue<int> queue;

      void writer() {
      std::unique_lock<std::mutex> lock(mtx);
      queue.push(42);
      cv.notify_one(); // 唤醒等待的读取线程
      }

      void reader() {
      std::unique_lock<std::mutex> lock(mtx);
      cv.wait(lock, [] { return !queue.empty(); });
      int value = queue.front();
      queue.pop();
      }
  4. 无锁队列

    • 使用无锁数据结构(如环形缓冲区或 CAS 操作)避免锁开销。
    • 推荐用于高性能场景。

总结:在多线程环境下操作队列时,必须确保同步机制正确,否则容易引发线程安全问题。常见的解决方案包括使用锁、条件变量或无锁数据结构,具体选择取决于性能要求和应用场景。

二叉树遍历方法及介绍

二叉树的遍历方法主要分为以下几类,根据遍历顺序可以进一步细分。以下是每种方法及其特点:


1. 深度优先遍历(DFS,Depth-First Search)

(1) 前序遍历(Preorder Traversal)

  • 访问顺序:根节点 → 左子树 → 右子树。
  • 特点
    • 首先处理当前节点,再递归处理其左、右子树。
    • 适合用于复制二叉树结构或表达式树的前缀表达式。
  • 代码实现
    1
    2
    3
    4
    5
    6
    void preorder(TreeNode* root) {
    if (!root) return;
    cout << root->val << " "; // 访问根节点
    preorder(root->left); // 遍历左子树
    preorder(root->right); // 遍历右子树
    }

(2) 中序遍历(Inorder Traversal)

  • 访问顺序:左子树 → 根节点 → 右子树。
  • 特点
    • 按照升序或降序访问节点时常用此方法。
    • 在 BST(二叉搜索树)中,中序遍历会按顺序输出所有元素。
  • 代码实现
    1
    2
    3
    4
    5
    6
    void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left); // 遍历左子树
    cout << root->val << " "; // 访问根节点
    inorder(root->right); // 遍历右子树
    }

(3) 后序遍历(Postorder Traversal)

  • 访问顺序:左子树 → 右子树 → 根节点。
  • 特点
    • 适合用于释放资源或表达式树的后缀表达式。
    • 常用于计算二叉树节点的累计值(如求和或深度)。
  • 代码实现
    1
    2
    3
    4
    5
    6
    void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root->left); // 遍历左子树
    postorder(root->right); // 遍历右子树
    cout << root->val << " "; // 访问根节点
    }

2. 广度优先遍历(BFS,Breadth-First Search)

  • 访问顺序:按照层次顺序逐层访问节点(又称层序遍历)。
  • 特点
    • 借助队列逐层访问节点,先访问同层的左侧节点再访问右侧节点。
    • 常用于查找最短路径或层次相关的操作。
  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void levelOrder(TreeNode* root) {
    if (!root) return;
    queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
    TreeNode* node = q.front();
    q.pop();
    cout << node->val << " "; // 访问当前节点
    if (node->left) q.push(node->left); // 压入左子节点
    if (node->right) q.push(node->right); // 压入右子节点
    }
    }

3. 非递归遍历(迭代方法)

  • 深度优先遍历的非递归实现需要显式使用栈,而广度优先遍历通常使用队列。

(1) 中序遍历的非递归实现

  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    void inorderIterative(TreeNode* root) {
    stack<TreeNode*> stk;
    TreeNode* curr = root;
    while (curr || !stk.empty()) {
    while (curr) { // 一路向左,压入栈中
    stk.push(curr);
    curr = curr->left;
    }
    curr = stk.top();
    stk.pop();
    cout << curr->val << " "; // 访问节点
    curr = curr->right; // 转向右子树
    }
    }

(2) 前序遍历的非递归实现

  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void preorderIterative(TreeNode* root) {
    if (!root) return;
    stack<TreeNode*> stk;
    stk.push(root);
    while (!stk.empty()) {
    TreeNode* node = stk.top();
    stk.pop();
    cout << node->val << " "; // 访问当前节点
    if (node->right) stk.push(node->right); // 压入右子节点
    if (node->left) stk.push(node->left); // 压入左子节点
    }
    }

(3) 后序遍历的非递归实现

  • 代码实现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    void postorderIterative(TreeNode* root) {
    if (!root) return;
    stack<TreeNode*> stk;
    TreeNode* lastVisited = nullptr;
    while (root || !stk.empty()) {
    if (root) {
    stk.push(root);
    root = root->left;
    } else {
    TreeNode* node = stk.top();
    if (node->right && lastVisited != node->right) {
    root = node->right; // 转向右子树
    } else {
    cout << node->val << " "; // 访问当前节点
    lastVisited = node;
    stk.pop();
    }
    }
    }
    }

4. 特殊遍历

(1) 之字形遍历(Zigzag Traversal)

  • 特点:类似层序遍历,但奇数层从左到右,偶数层从右到左。
  • 实现思路:结合队列和双端队列或两个栈。

(2) 边界遍历(Boundary Traversal)

  • 特点:访问树的边界,包括左边界、叶节点、右边界。

总结

  • DFS 方法:包括前序、中序和后序,适合递归或栈操作。
  • BFS 方法:层序遍历适合队列操作。
  • 选择方法:具体使用哪种遍历取决于应用场景,如打印顺序、搜索路径或计算属性等。

虚函数的实现原理,虚函数表

虚函数的实现原理

在 C++ 中,虚函数的实现是通过 虚函数表(vtable)虚指针(vptr) 实现的。这是 C++ 用来实现运行时多态的机制。


虚函数表 (vtable)

  1. 概念

    • 虚函数表是一个由编译器生成的函数指针数组,用于存储类的虚函数的地址。
    • 每个类如果包含虚函数,编译器会为这个类生成一个对应的虚函数表。
  2. 内容

    • 虚函数表中存储的是该类的所有虚函数的函数地址。
    • 如果子类覆盖了父类的虚函数,虚函数表中的指针会被更新为子类的函数地址。

虚指针 (vptr)

  1. 概念

    • 在每个包含虚函数的类的对象中,编译器会隐式添加一个指针成员,称为虚指针(vptr)。
    • 这个指针指向虚函数表的起始位置。
  2. 初始化与更新

    • 在构造函数中,虚指针会被初始化,指向该对象所属类的虚函数表。
    • 如果存在多层继承关系,虚指针会在子类的构造函数中更新为指向子类的虚函数表。

虚函数调用过程

  1. 编译时

    • 当调用虚函数时,编译器生成代码以通过虚指针(vptr)访问虚函数表(vtable)。
  2. 运行时

    • 通过虚指针找到虚函数表。
    • 从虚函数表中找到对应函数的地址并调用它。

虚函数表结构示例

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;

class Base {
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual ~Base() {}
};

class Derived : public Base {
public:
void func1() override { cout << "Derived::func1" << endl; }
void func2() override { cout << "Derived::func2" << endl; }
};

虚函数表的生成

  1. Base 类的虚函数表

    • vtable_Base
      1
      2
      [0] -> Base::func1
      [1] -> Base::func2
    • 虚指针 vptr:Base 类对象的 vptr 指向 vtable_Base
  2. Derived 类的虚函数表

    • vtable_Derived
      1
      2
      [0] -> Derived::func1
      [1] -> Derived::func2
    • 虚指针 vptr:Derived 类对象的 vptr 指向 vtable_Derived

调用过程

1
2
Base* obj = new Derived();
obj->func1();
  1. 通过 objvptr 找到 vtable_Derived
  2. 查找 vtable_Derived 中对应 func1 的地址。
  3. 调用 Derived::func1

虚函数表的特性

  1. 每个类有一个虚函数表

    • 同一个类的所有对象共享一个虚函数表。
  2. 子类覆盖虚函数

    • 子类会在虚函数表中替换父类对应的虚函数。
  3. 无虚函数的类

    • 如果一个类没有虚函数,则不生成虚函数表,也没有虚指针。

虚函数表的开销

  1. 存储开销

    • 每个包含虚函数的类对象需要一个虚指针。
    • 每个包含虚函数的类需要一个虚函数表。
  2. 时间开销

    • 调用虚函数需要额外的指针查找操作。

总结

  1. **虚指针 (vptr)**:每个对象中隐含的指针,指向该对象所属类的虚函数表。
  2. **虚函数表 (vtable)**:存储虚函数地址的指针数组,用于实现运行时多态。
  3. 调用机制:通过虚指针定位到虚函数表,再通过虚函数表调用具体的函数。
  4. 动态多态:子类可以重写父类的虚函数,通过虚函数表实现动态绑定。

输入一个URL,计算机会发生什么

当你在浏览器中输入一个 URL(例如 https://www.example.com),并按下回车时,计算机会经历以下过程:


1. 解析 URL

  • 提取协议:如 https,决定通信方式。
  • 提取域名:如 www.example.com,需要进行 DNS 解析。
  • 提取路径和参数:如 /index.html?id=123,用于指定资源和请求参数。

2. DNS 解析

  • 目的:将域名(如 www.example.com)解析为 IP 地址。
  • 过程:
    1. 浏览器缓存:检查是否有对应域名的 IP 地址缓存。
    2. 操作系统缓存:如果浏览器没有缓存,检查系统 DNS 缓存。
    3. 本地域名服务器:操作系统向本地 DNS 服务器查询。
    4. 递归解析:如果本地 DNS 没有记录,向根 DNS 服务器、顶级域名服务器(TLD)和权威 DNS 服务器递归查询,直到找到对应 IP 地址。
  • 返回:解析得到目标 IP 地址。

3. 建立 TCP 连接

  • 使用 TCP 协议建立连接(通常是三次握手)。
    1. 客户端发送 SYN 包(同步序列号)。
    2. 服务器回复 SYN-ACK 包。
    3. 客户端发送 ACK 包,连接建立。
  • 如果是 HTTPS,还需进行 TLS/SSL 握手,协商加密通信方式。

4. 发送 HTTP/HTTPS 请求

  • 浏览器构造 HTTP/HTTPS 请求:
    • 请求行:包含方法(如 GET)、路径、HTTP 版本。
    • 请求头:包括 User-Agent、Cookie 等信息。
    • 请求体(可选):POST 请求时携带的数据。
  • 请求被封装到 TCP 数据包,通过 IP 协议传输到目标服务器。

5. 服务器处理请求

  • 接收请求:服务器接收 TCP 数据包并解析 HTTP 请求。
  • 资源定位:根据请求路径(如 /index.html)查找对应资源。
  • 动态处理(如运行后端代码):如果请求需要动态生成内容(如 PHP、Python 等)。
  • 返回响应:服务器将结果封装成 HTTP 响应,包括:
    • 响应行:状态码(如 200、404)。
    • 响应头:如 Content-Type、Cache-Control。
    • 响应体:HTML、JSON 或其他内容。

6. 浏览器接收响应

  • 浏览器通过 TCP 接收服务器返回的 HTTP 响应。
  • 解析响应数据:
    • 根据响应头内容(如 Content-Type)确定资源类型。
    • 渲染 HTML,加载 CSS 和 JavaScript。
    • 异步加载图片或其他资源。

7. 渲染页面

  • HTML 解析
    • 转换成 DOM 树。
  • CSS 解析
    • 转换成样式规则并应用到 DOM 树,生成渲染树。
  • JavaScript 执行
    • 执行脚本,可能修改 DOM。
  • 页面绘制
    • 浏览器将渲染树绘制到屏幕上。

8. 后续请求

  • 如果 HTML 页面中包含其他资源(如图片、CSS、JavaScript),浏览器会重复发送 HTTP 请求获取这些资源。
  • 通过缓存机制减少重复请求。

总结

输入 URL 后,主要涉及:

  1. DNS 解析:将域名解析为 IP。
  2. 建立连接:TCP 三次握手(HTTPS 需额外 TLS 握手)。
  3. 发送请求:构造并发送 HTTP/HTTPS 请求。
  4. 接收响应:服务器处理并返回数据。
  5. 渲染页面:解析 HTML、CSS、JavaScript,并绘制页面。

程序是如何在计算机上跑起来的?

程序在计算机上运行的过程可以分为以下几个阶段,从代码到执行,依赖硬件、操作系统、编译器、链接器等多个组件的协同工作:


1. 编写代码

  • 程序员使用高级编程语言(如 C++、Python)编写程序源代码。
  • 源代码是人类可读的,需要经过编译或解释才能被计算机执行。

2. 编译和链接

编译

  • 编译器(如 GCC、Clang)将源代码转换为目标代码(机器码的中间形式,通常是二进制文件)。
  • 编译过程:
    1. 词法分析:将代码分解为标记(Token)。
    2. 语法分析:验证代码是否符合语法规则。
    3. 语义分析:检查代码逻辑是否正确(如类型检查)。
    4. 中间代码生成:生成抽象中间表示。
    5. 优化:对代码进行优化(如循环展开、常量折叠)。
    6. 目标代码生成:生成与硬件架构对应的汇编代码和机器码。

链接

  • 链接器将多个目标文件(包括程序代码和库文件)合并为一个可执行文件。
  • 动态链接:链接器仅在运行时加载库文件。
  • 静态链接:链接器将所有依赖的库文件直接嵌入到可执行文件中。

3. 加载

  • 操作系统通过加载器将可执行文件加载到内存中,准备执行。
  • 加载过程:
    1. 分配内存:为程序分配虚拟内存地址空间。
    2. 加载代码段:将程序代码段(.text)加载到内存。
    3. 加载数据段:将全局变量(.data)和未初始化变量(.bss)加载到内存。
    4. 堆栈初始化:为程序堆(动态内存)和栈(函数调用记录)分配内存。
    5. 动态链接:如果程序依赖动态库(如 libc.so),此时会加载它们。

4. 程序执行

  • 指令执行
    1. CPU 根据程序入口地址找到第一条指令并开始执行。
    2. 每条指令通过 CPU 的指令周期(取指令、解码、执行)被处理。
  • 寄存器与内存交互
    • CPU 使用寄存器存储临时数据,并与内存中的堆、栈、全局变量等区域交互。
  • 系统调用
    • 程序通过系统调用(如文件读写、网络通信)请求操作系统服务。
  • 多任务调度
    • 操作系统的调度器在多个程序之间切换,分配 CPU 时间片,使多任务并行执行。

5. 程序结束

  • 程序执行完毕后,操作系统回收其占用的资源:
    • 释放内存、关闭文件描述符、清理动态分配的资源等。
  • 如果程序因错误(如分段错误)终止,操作系统也会回收资源。

程序运行的关键支撑点

  1. 硬件

    • CPU 执行机器指令。
    • 内存提供临时存储。
    • I/O 设备(如磁盘、网络)支持数据输入输出。
  2. 操作系统

    • 提供虚拟内存、文件管理、多任务调度等核心服务。
    • 负责程序的加载和运行时支持。
  3. 编译器和链接器

    • 将高级语言代码转换为机器可执行的二进制代码。
  4. 库和框架

    • 提供常用功能的封装,简化开发。

示例:一个简单的 C++ 程序运行过程

1
2
3
4
5
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
  1. 编译g++ main.cpp -o main
    编译器将代码转换为机器码,生成 main 可执行文件。

  2. 加载:执行 ./main 时,操作系统加载器加载 main 文件到内存。

  3. 执行:CPU 从程序入口开始执行,完成打印任务。

  4. 结束:程序返回 0,操作系统回收资源。

设计模式了解吗?

了解一些常用的设计模式,主要分为三大类:创建型、结构型、行为型,下面是对这些模式的简单介绍和使用场景:


一、创建型模式

  1. 单例模式(Singleton)

    • 特点:保证一个类只有一个实例,提供全局访问点。
    • 使用场景:需要全局唯一对象,例如日志管理器、配置管理器。
    • C++实现
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      class Singleton {
      public:
      static Singleton& getInstance() {
      static Singleton instance;
      return instance;
      }
      private:
      Singleton() {}
      Singleton(const Singleton&) = delete;
      Singleton& operator=(const Singleton&) = delete;
      };
  2. 工厂方法模式(Factory Method)

    • 特点:定义一个接口用于创建对象,但让子类决定实例化哪个类。
    • 使用场景:需要灵活创建不同的对象类型。
    • 例子:根据不同类型创建数据库连接对象。
  3. 抽象工厂模式(Abstract Factory)

    • 特点:提供创建一系列相关对象的接口,无需指定具体类。
    • 使用场景:跨平台 GUI 工程中生成不同风格的界面组件。
  4. 建造者模式(Builder)

    • 特点:将复杂对象的构建过程与表示分离。
    • 使用场景:需要构建复杂对象,例如 HTML 生成器、游戏角色生成器。
  5. 原型模式(Prototype)

    • 特点:通过复制已有对象来创建新对象。
    • 使用场景:对象创建成本较高,需要通过克隆快速生成实例。

二、结构型模式

  1. 适配器模式(Adapter)

    • 特点:将一个类的接口转换成客户期望的接口。
    • 使用场景:新旧代码兼容,例如接口不匹配时。
  2. 装饰器模式(Decorator)

    • 特点:动态地给对象增加功能。
    • 使用场景:为某对象添加不同的行为,例如日志输出格式。
  3. 代理模式(Proxy)

    • 特点:为对象提供一个代理以控制对该对象的访问。
    • 使用场景:远程代理、虚拟代理、安全代理。
  4. 桥接模式(Bridge)

    • 特点:将抽象部分与实现部分分离,使它们可以独立变化。
    • 使用场景:跨平台应用的不同实现。
  5. 组合模式(Composite)

    • 特点:将对象组合成树形结构以表示“部分-整体”的层次结构。
    • 使用场景:文件系统(文件和文件夹的结构)。
  6. 外观模式(Facade)

    • 特点:提供一个高层次的接口,使子系统更易使用。
    • 使用场景:隐藏复杂子系统的内部实现,例如封装多个子系统接口。
  7. 享元模式(Flyweight)

    • 特点:共享对象以减少内存使用。
    • 使用场景:重复使用小粒度对象,例如字符渲染、缓存池。

三、行为型模式

  1. 观察者模式(Observer)

    • 特点:定义对象间的一对多依赖关系。
    • 使用场景:事件通知系统,例如 GUI 事件处理。
  2. 策略模式(Strategy)

    • 特点:定义一系列算法,将每个算法封装起来,使它们可以互换。
    • 使用场景:支付系统支持不同支付方式。
  3. 命令模式(Command)

    • 特点:将请求封装为对象,支持撤销和重做。
    • 使用场景:任务调度系统、事务操作。
  4. 状态模式(State)

    • 特点:允许对象在内部状态改变时改变其行为。
    • 使用场景:状态机,例如电梯状态变化。
  5. 责任链模式(Chain of Responsibility)

    • 特点:为请求创建一个链,使多个对象有机会处理请求。
    • 使用场景:日志系统支持不同日志级别处理。
  6. 中介者模式(Mediator)

    • 特点:通过中介者对象封装对象之间的交互。
    • 使用场景:MVC 架构中,控制器作为中介者协调模型和视图。
  7. 迭代器模式(Iterator)

    • 特点:提供一种方法顺序访问集合对象的元素。
    • 使用场景:容器遍历,例如 std::vector 的迭代器。
  8. 模板方法模式(Template Method)

    • 特点:在父类中定义算法框架,允许子类实现具体步骤。
    • 使用场景:定义操作步骤但允许子类定制部分实现。
  9. 备忘录模式(Memento)

    • 特点:在不破坏封装的情况下保存对象的状态。
    • 使用场景:撤销和恢复功能。
  10. 解释器模式(Interpreter)

    • 特点:为特定语言定义语法解释器。
    • 使用场景:编译器中的语法解析、表达式求值。

C++中设计模式的应用

  • 智能指针(单例模式、代理模式):如 std::shared_ptr
  • STL迭代器(迭代器模式):如 std::vector::iterator
  • 流操作(装饰器模式):如 std::cout <<

设计模式的选择需要根据具体问题场景,不要为了使用而使用。

说一下工厂模式?工厂模式的抽象工厂和单例工厂了解吗?

工厂模式简介

工厂模式是一种创建型设计模式,它通过定义一个用于创建对象的接口,让子类决定实例化哪个类。工厂模式将对象的创建过程与使用过程分离,降低了代码的耦合性。


工厂模式分类

工厂模式一般分为以下三种类型:

  1. 简单工厂模式(Simple Factory)

    • 特点:通过一个工厂类根据条件动态地创建不同的实例。
    • 优点:将对象创建的逻辑集中到一个工厂类中,客户端无需了解具体类名。
    • 缺点:不符合开闭原则,新增类型需要修改工厂代码。
  2. 工厂方法模式(Factory Method)

    • 特点:定义一个创建对象的抽象接口,由子类决定实例化具体的对象。
    • 优点:符合开闭原则,新增对象时无需修改现有工厂类。
    • 缺点:每新增一个产品,需要添加一个具体工厂类。
  3. 抽象工厂模式(Abstract Factory)

    • 特点:提供一个接口,创建一系列相关或相互依赖的对象,而无需指定它们的具体类。
    • 优点:可以生产一族对象,保持系统的一致性。
    • 缺点:扩展新产品族较困难,需要修改抽象工厂及其实现类。

简单工厂模式示例(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>
#include <memory>
using namespace std;

// 产品基类
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};

// 具体产品A
class ProductA : public Product {
public:
void use() override {
cout << "Using ProductA" << endl;
}
};

// 具体产品B
class ProductB : public Product {
public:
void use() override {
cout << "Using ProductB" << endl;
}
};

// 工厂类
class SimpleFactory {
public:
static unique_ptr<Product> createProduct(const string& type) {
if (type == "A") {
return make_unique<ProductA>();
} else if (type == "B") {
return make_unique<ProductB>();
}
return nullptr;
}
};

// 客户端代码
int main() {
auto product = SimpleFactory::createProduct("A");
if (product) product->use();
return 0;
}

工厂方法模式示例(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <iostream>
#include <memory>
using namespace std;

// 产品基类
class Product {
public:
virtual void use() = 0;
virtual ~Product() = default;
};

// 具体产品A
class ProductA : public Product {
public:
void use() override {
cout << "Using ProductA" << endl;
}
};

// 工厂基类
class Factory {
public:
virtual unique_ptr<Product> createProduct() = 0;
virtual ~Factory() = default;
};

// 具体工厂A
class FactoryA : public Factory {
public:
unique_ptr<Product> createProduct() override {
return make_unique<ProductA>();
}
};

// 客户端代码
int main() {
unique_ptr<Factory> factory = make_unique<FactoryA>();
auto product = factory->createProduct();
product->use();
return 0;
}

抽象工厂模式示例(C++)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <memory>
using namespace std;

// 产品A基类
class ProductA {
public:
virtual void useA() = 0;
virtual ~ProductA() = default;
};

// 产品B基类
class ProductB {
public:
virtual void useB() = 0;
virtual ~ProductB() = default;
};

// 具体产品A1
class ProductA1 : public ProductA {
public:
void useA() override {
cout << "Using ProductA1" << endl;
}
};

// 具体产品B1
class ProductB1 : public ProductB {
public:
void useB() override {
cout << "Using ProductB1" << endl;
}
};

// 抽象工厂
class AbstractFactory {
public:
virtual unique_ptr<ProductA> createProductA() = 0;
virtual unique_ptr<ProductB> createProductB() = 0;
virtual ~AbstractFactory() = default;
};

// 具体工厂1
class Factory1 : public AbstractFactory {
public:
unique_ptr<ProductA> createProductA() override {
return make_unique<ProductA1>();
}

unique_ptr<ProductB> createProductB() override {
return make_unique<ProductB1>();
}
};

// 客户端代码
int main() {
unique_ptr<AbstractFactory> factory = make_unique<Factory1>();
auto productA = factory->createProductA();
auto productB = factory->createProductB();
productA->useA();
productB->useB();
return 0;
}

单例工厂模式

  • 在简单工厂模式中,可以将工厂类本身设计为单例模式,确保工厂实例全局唯一。
  • 实现方式
    1. 将构造函数设置为 private
    2. 提供一个 static 方法获取唯一实例。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class SingletonFactory {
    public:
    static SingletonFactory& getInstance() {
    static SingletonFactory instance;
    return instance;
    }

    unique_ptr<Product> createProduct(const string& type) {
    if (type == "A") {
    return make_unique<ProductA>();
    } else if (type == "B") {
    return make_unique<ProductB>();
    }
    return nullptr;
    }

    private:
    SingletonFactory() {}
    SingletonFactory(const SingletonFactory&) = delete;
    SingletonFactory& operator=(const SingletonFactory&) = delete;
    };

总结

  • 简单工厂适用于产品种类较少且变化不频繁的场景。
  • 工厂方法适用于需要扩展产品种类的场景,符合开闭原则。
  • 抽象工厂适用于需要创建一族相关产品的场景。
  • 单例工厂将工厂类设计为全局唯一,适用于全局需要统一工厂逻辑的场景。

选择合适的工厂模式取决于项目需求的复杂性和扩展性要求。

Qt 框架中,设计模式被广泛应用于解决常见的软件设计问题。以下是 Qt 中常用的设计模式及其应用场景:


1. 信号与槽机制(观察者模式)

  • 设计模式:观察者模式(Observer Pattern)。
  • 作用
    • 用于事件驱动的编程模型。多个对象可以订阅一个对象的状态变化。
    • Qt 的信号与槽机制本质上实现了观察者模式。
  • 应用场景
    • GUI 事件处理(按钮点击、窗口更新)。
    • 数据模型与视图同步(如 QAbstractItemModelQTableView)。
  • 示例
    1
    connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);

2. 单例模式

  • 设计模式:单例模式(Singleton Pattern)。
  • 作用
    • 确保一个类仅有一个实例,并提供全局访问点。
    • Qt 中可以通过 Q_GLOBAL_STATIC 或自定义方式实现单例。
  • 应用场景
    • 管理全局配置或状态,如 QSettings、日志管理器等。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class LogManager {
    public:
    static LogManager& instance() {
    static LogManager instance;
    return instance;
    }

    private:
    LogManager() {}
    LogManager(const LogManager&) = delete;
    LogManager& operator=(const LogManager&) = delete;
    };

3. 工厂模式

  • 设计模式:工厂模式(Factory Pattern)。
  • 作用
    • 用于动态创建对象,而不依赖具体类。
    • Qt 的 QStyleFactoryQPluginLoader 等内部实现了工厂模式。
  • 应用场景
    • 动态创建界面组件(如不同主题的按钮、窗口)。
    • 加载插件或模块。
  • 示例
    1
    2
    QStyle* style = QStyleFactory::create("Fusion");
    app.setStyle(style);

4. 代理模式

  • 设计模式:代理模式(Proxy Pattern)。
  • 作用
    • 在不直接操作目标对象的情况下,通过代理类控制对目标对象的访问。
    • Qt 提供了 QSortFilterProxyModel,用于对数据模型进行排序和过滤。
  • 应用场景
    • 数据模型的排序与过滤。
    • 延迟加载资源。
  • 示例
    1
    2
    3
    4
    QSortFilterProxyModel* proxyModel = new QSortFilterProxyModel(this);
    proxyModel->setSourceModel(sourceModel);
    proxyModel->setFilterRegExp(QRegExp("pattern", Qt::CaseInsensitive));
    tableView->setModel(proxyModel);

5. 策略模式

  • 设计模式:策略模式(Strategy Pattern)。
  • 作用
    • 将算法的实现封装成独立的类,允许在运行时动态替换算法。
    • Qt 的 QStyle 是策略模式的典型应用,支持不同的绘制风格。
  • 应用场景
    • GUI 绘制风格切换。
    • 不同策略下的操作执行(如绘图策略)。
  • 示例
    1
    QApplication::setStyle(QStyleFactory::create("Fusion"));

6. 装饰器模式

  • 设计模式:装饰器模式(Decorator Pattern)。
  • 作用
    • 动态地给对象添加功能,而不改变其接口。
    • 在 Qt 中,QWidget 的子类可以通过 setStyleSheet 修改外观,类似装饰器模式。
  • 应用场景
    • 动态修改控件外观。
    • 添加行为(如自定义事件过滤器)。
  • 示例
    1
    button->setStyleSheet("background-color: blue; color: white;");

7. 模板方法模式

  • 设计模式:模板方法模式(Template Method Pattern)。
  • 作用
    • 定义一个操作的框架,将具体步骤的实现延迟到子类中。
    • Qt 的事件处理(如 QWidget::paintEvent)是模板方法的应用。
  • 应用场景
    • 自定义控件绘制。
    • 处理事件的统一流程。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    class CustomWidget : public QWidget {
    protected:
    void paintEvent(QPaintEvent* event) override {
    QPainter painter(this);
    painter.setBrush(Qt::blue);
    painter.drawRect(rect());
    }
    };

8. 状态模式

  • 设计模式:状态模式(State Pattern)。
  • 作用
    • 将对象的行为根据状态变化分离到不同的状态类中。
    • Qt 的 QStateMachine 实现了状态模式。
  • 应用场景
    • 控件的多状态管理(如按钮的启用、禁用、悬停状态)。
    • 动画状态切换。
  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    QStateMachine machine;
    QState* state1 = new QState();
    QState* state2 = new QState();
    state1->assignProperty(button, "text", "State 1");
    state2->assignProperty(button, "text", "State 2");
    machine.addState(state1);
    machine.addState(state2);
    machine.setInitialState(state1);
    machine.start();

总结

  • Qt 的设计模式广泛应用于 GUI 和业务逻辑的开发中。
  • 观察者模式工厂模式 是最常见的,用于事件处理和对象创建。
  • 其他模式(如状态模式、策略模式)在动态行为管理和扩展性设计中也有重要作用。
  • 选择模式 时,应根据具体需求和扩展性要求来决定。

24八股文

C++内存管理

C++ 的内存管理是指对内存分配、使用和释放的操作。C++ 提供了多种内存管理机制,涵盖栈、堆、全局和静态区域等。以下是 C++ 内存管理的详细介绍:


1. 内存区域划分

在 C++ 程序中,内存通常分为以下几部分:

  • 栈区(Stack)

    • 由编译器自动分配和释放,用于局部变量、函数参数等。
    • 内存分配效率高,但容量有限,不能灵活管理。
    • 变量生命周期随作用域结束自动销毁。
    • 示例:
      1
      2
      3
      void func() {
      int x = 10; // 栈分配
      }
  • 堆区(Heap)

    • 由程序员手动分配和释放(new/deletemalloc/free)。
    • 用于动态分配内存,生命周期由程序控制,需避免内存泄漏。
    • 示例:
      1
      2
      int* ptr = new int(5);  // 动态分配
      delete ptr; // 释放内存
  • 全局/静态区(Global/Static)

    • 包括全局变量、静态变量、常量等。
    • 程序启动时分配,结束时释放。
    • 示例:
      1
      2
      int globalVar = 0;        // 全局变量
      static int staticVar = 1; // 静态变量
  • 代码区(Text)

    • 存放程序的代码(机器指令),通常只读。
    • 用于防止代码被意外修改。

2. 动态内存管理

动态内存管理是 C++ 的重要特性,允许在运行时分配和释放内存。

newdelete

  • 用于在堆上分配和释放单个对象或数组。
  • 示例:
    1
    2
    3
    4
    5
    int* ptr = new int(42);   // 分配单个对象
    delete ptr; // 释放对象

    int* arr = new int[10]; // 分配数组
    delete[] arr; // 释放数组

mallocfree

  • C 风格的动态内存管理函数,不会调用构造函数和析构函数。
  • 示例:
    1
    2
    int* ptr = (int*)malloc(sizeof(int));  // 动态分配
    free(ptr); // 释放内存

3. 内存管理的常见问题

  • 内存泄漏

    • 动态分配的内存未释放,导致程序长期运行时内存占用持续增加。
    • 解决:确保每次 new 都有对应的 delete
  • 野指针

    • 指针指向无效或已释放的内存区域。
    • 解决:释放指针后将其置为 nullptr
      1
      2
      3
      int* ptr = new int(5);
      delete ptr;
      ptr = nullptr; // 防止野指针
  • 重复释放

    • 对同一块内存多次调用 deletefree
    • 解决:确保每块内存只释放一次。

4. 智能指针

C++11 引入了 智能指针,自动管理内存,避免手动调用 delete,减少内存泄漏的风险。

常用智能指针

  • **std::unique_ptr**:

    • 独占所有权,不能复制。
    • 示例:
      1
      std::unique_ptr<int> ptr = std::make_unique<int>(42);
  • **std::shared_ptr**:

    • 共享所有权,多线程环境中引用计数线程安全。
    • 示例:
      1
      2
      std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
      std::shared_ptr<int> ptr2 = ptr1; // 共享所有权
  • **std::weak_ptr**:

    • 弱引用,用于避免循环引用。
    • 示例:
      1
      2
      std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
      std::weak_ptr<int> weakPtr = ptr1;

5. 内存分配的性能优化

  • 内存池(Memory Pool)

    • 为特定大小的对象分配内存,提高分配效率。
    • 示例:std::vector 可能在底层使用内存池。
  • 对象缓存

    • 对常用的对象类型重复使用内存,减少分配和释放的开销。
  • 栈分配优先

    • 尽量使用栈分配而非堆分配,因为栈分配的速度更快。

6. 内存管理的调试工具

  • Valgrind
    • 检测内存泄漏、未初始化内存访问等问题。
  • AddressSanitizer
    • 编译器工具,用于检测内存问题。
  • Memory Checker
    • 检查动态内存的使用情况。

总结

  • C++ 提供灵活的内存管理机制,既支持自动管理,也允许手动控制。
  • 动态内存管理需要特别注意内存泄漏、野指针等问题。
  • 引入智能指针是现代 C++ 的推荐做法,极大提高了内存管理的安全性和效率。
  • 使用调试工具可以快速发现和解决内存管理中的问题。

多态

多态(Polymorphism)是面向对象编程(OOP)中的一个核心特性,指的是同一个接口或方法在不同的对象上表现出不同的行为。简单来说,多态允许在不同的上下文中调用相同的方法,但每次调用时会有不同的行为。

多态的类型

C++ 中有两种常见的多态类型:

  1. 编译时多态(静态多态)

    • 发生在编译阶段,主要通过函数重载和运算符重载实现。
    • 在编译时决定调用哪个函数或操作符。
    • 例子:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Print {
      public:
      void display(int i) {
      std::cout << "Integer: " << i << std::endl;
      }
      void display(double d) {
      std::cout << "Double: " << d << std::endl;
      }
      };
      在此例中,根据传入的参数类型调用不同的 display 方法,实现了编译时多态。
  2. 运行时多态(动态多态)

    • 发生在程序运行时,主要通过虚函数和继承实现。
    • 在运行时,通过父类指针或引用调用子类的函数。
    • 例子:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      class Animal {
      public:
      virtual void speak() { std::cout << "Animal speaks" << std::endl; }
      };

      class Dog : public Animal {
      public:
      void speak() override { std::cout << "Dog barks" << std::endl; }
      };

      class Cat : public Animal {
      public:
      void speak() override { std::cout << "Cat meows" << std::endl; }
      };

      int main() {
      Animal* animal1 = new Dog();
      Animal* animal2 = new Cat();
      animal1->speak(); // Dog barks
      animal2->speak(); // Cat meows
      }
      在此例中,speak 是一个虚函数,父类 Animal 的指针指向不同的子类对象(DogCat),根据对象的实际类型调用相应的函数,实现了运行时多态。

多态的关键概念

  1. 虚函数(Virtual Functions)

    • 在基类中声明为 virtual 的函数,可以在派生类中重写。
    • 通过基类指针或引用调用虚函数时,调用的是派生类中的版本,而不是基类中的版本,这种机制称为“动态绑定”。
  2. 动态绑定(Dynamic Binding)

    • 动态绑定是指在程序运行时根据对象的实际类型来决定调用哪个函数。通过虚函数表(vtable)来实现动态绑定。
  3. 继承

    • 多态通常需要继承关系。基类指针或引用指向派生类对象时,能够实现不同对象的行为表现。

多态的实现原理

  • 虚函数表(vtable)

    • 每个含有虚函数的类都会有一个虚函数表(vtable),表中存储了该类的虚函数指针。对象实例中保存指向该虚函数表的指针。
    • 当通过基类指针调用虚函数时,程序通过虚函数表找到对应的函数地址,并调用它。
  • 虚函数表的结构

    • 每个类只有一个虚函数表,其中存储了该类所有虚函数的地址。
    • 对象的实例中包含一个指向该虚函数表的指针,指针用于在运行时确定应该调用哪个版本的函数。

多态的优缺点

  • 优点

    • 提高代码的可扩展性:可以通过继承和重写方法实现不同的行为,增加新的功能时不需要修改现有代码。
    • 增强代码的复用性:通过统一的接口,可以灵活操作不同类型的对象。
  • 缺点

    • 性能开销:虚函数的动态绑定会增加一些运行时的开销,尤其是在多层继承和大量虚函数调用时。
    • 难以调试:多态的使用可能导致错误发生在运行时,调试起来相对复杂。

总结

  • 多态使得不同的类可以通过统一的接口表现不同的行为,增强了代码的灵活性和可维护性。
  • 编译时多态通过函数重载实现,运行时多态通过虚函数和继承实现。
  • 虚函数、动态绑定和虚函数表是实现运行时多态的核心机制。

C和C++区别

C 和 C++ 是两种不同的编程语言,尽管它们有很多相似之处,C++ 是在 C 的基础上发展而来的,增加了许多新特性。下面是它们的主要区别:

1. 编程范式

  • C:C 是一种过程化编程语言,强调的是通过函数来组织代码,程序的执行按顺序逐行进行。
  • **C++**:C++ 是多范式语言,既支持过程化编程,也支持面向对象编程(OOP)。C++ 引入了类(class)、对象(object)、继承、封装和多态等面向对象的特性。

2. 面向对象编程(OOP)

  • C:C 语言不支持面向对象的特性,所有的代码都基于函数。
  • **C++**:C++ 是一个面向对象的语言,支持类和对象。它允许开发者使用封装、继承和多态等OOP的特性来设计程序。

3. 内存管理

  • C:C 使用 mallocfree 来进行内存分配和释放。
  • **C++**:C++ 提供了更灵活的内存管理方式,使用 newdelete 来分配和释放内存。C++ 还支持自动内存管理(如智能指针)以减少内存泄漏问题。

4. 标准库

  • C:C 提供了一个较为基础的标准库,包括字符串操作、文件处理、数学运算等基本功能。
  • **C++**:C++ 标准库比 C 更为强大,除了包括 C 标准库中的内容外,还提供了 STL(标准模板库),如 vectorlistmapset 等容器以及算法、迭代器等工具。

5. 函数重载和运算符重载

  • C:C 不支持函数重载和运算符重载。一个函数名在同一作用域中是唯一的。
  • **C++**:C++ 支持函数重载(同名函数根据参数不同来区分)和运算符重载(可以改变运算符的行为,如自定义加法操作符)。

6. 异常处理

  • C:C 语言没有内置的异常处理机制,错误处理通常通过返回值或全局错误标志来实现。
  • **C++**:C++ 支持异常处理机制(try-catch),可以通过 throw 抛出异常,try 块捕获异常,catch 块处理异常。

7. 模板

  • C:C 语言没有模板功能。
  • **C++**:C++ 引入了模板(template)机制,可以编写泛型函数和泛型类,提供了很强的代码复用能力。模板是 C++ 中非常强大的特性之一。

8. 名字空间(Namespace)

  • C:C 没有名字空间的概念,在 C 中,所有标识符(如函数、变量名)都在全局作用域中,可能会导致命名冲突。
  • **C++**:C++ 引入了名字空间(namespace)来解决命名冲突问题,允许开发者将相关代码组织到不同的命名空间中。

9. 引用(Reference)

  • C:C 没有引用的概念,只有指针。
  • **C++**:C++ 引入了引用(reference),它是一个别名,允许通过引用传递参数,而不需要使用指针。

10. 继承和多态

  • C:C 不支持继承或多态。
  • **C++**:C++ 支持类的继承,可以实现子类继承父类的成员,并且通过虚函数(virtual function)实现多态,允许基类指针指向派生类对象并调用派生类的方法。

11. 构造函数和析构函数

  • C:C 没有构造函数和析构函数的概念。
  • **C++**:C++ 支持构造函数和析构函数,它们分别在对象创建和销毁时自动调用,用于初始化和清理对象。

12. 编译期和运行期检查

  • C:C 语言编译时对类型和语法的检查较为严格,运行时检查较少。
  • **C++**:C++ 语言提供了更多的编译期检查,支持模板元编程等机制,让一些错误在编译时就能被发现。

13. 多重继承

  • C:C 语言不支持继承。
  • **C++**:C++ 支持多重继承,即一个类可以继承多个基类。

总结

  • C 语言:更接近硬件、系统级编程,适合需要高效、直接控制硬件的场景。C 是面向过程的编程语言,提供了更为直接的操作方式。
  • C++ 语言:在 C 的基础上增加了面向对象编程特性,并提供了更强大的功能,如模板、异常处理、智能指针等。适合大型项目、需要更高抽象层次的应用。

C++ 比 C 更复杂,但也更强大,适用于更广泛的场景,尤其是当需要面向对象设计时。

堆和栈的区别(这个可能是想问数据结构堆和栈的区别,回答成了内存堆和栈的区别)

“堆” 和 “栈” 这两个概念在不同的上下文中有不同的含义。这里分别解释两种常见的情况:数据结构堆和栈以及内存堆和栈的区别。

1. 数据结构中的堆和栈

栈(Stack)

  • 定义:栈是一种线性数据结构,遵循后进先出(LIFO)原则,即最后压入栈的数据最先被弹出。
  • 操作:栈通常支持两个基本操作:
    • push:将元素压入栈。
    • pop:将栈顶元素弹出。
  • 应用:栈常用于实现递归、函数调用、深度优先搜索等。
  • 特点:栈的内存分配和回收由编译器自动管理,当栈空间用完时会出现栈溢出。

堆(Heap)

  • 定义:堆是一种完全二叉树,可以是最大堆(父节点大于或等于子节点)或最小堆(父节点小于或等于子节点)。堆通常用来实现优先队列。
  • 操作:堆支持两个基本操作:
    • 插入:将元素插入堆中,保持堆的性质。
    • 删除:删除堆顶元素(最大堆的最大元素或最小堆的最小元素),并对堆进行调整。
  • 应用:堆常用于实现优先队列、堆排序等。
  • 特点:堆的结构是一颗二叉树,堆的元素存储在一个数组中,通过下标运算可以快速找到子节点和父节点。

2. 内存中的堆和栈

栈(Stack)

  • 定义:在计算机内存中,栈是一块连续的内存区域,主要用于存储局部变量、函数参数、返回地址等信息。栈是一种由操作系统管理的后进先出(LIFO)数据结构。
  • 特点
    • 内存由操作系统自动管理,分配和回收非常快。
    • 栈大小有限,一旦栈空间用完会发生栈溢出。
    • 局部变量和函数调用会使用栈内存。
    • 栈内存是由操作系统分配的,当函数执行结束时,栈内存自动释放。

堆(Heap)

  • 定义:堆是另一块用于动态内存分配的内存区域。不同于栈,堆内存由程序员手动管理,使用 mallocnew 等函数进行分配,使用 freedelete 来释放。
  • 特点
    • 堆的内存分配不需要遵循后进先出的规则,因此可以在堆中存储大小不定、生命周期不确定的对象。
    • 堆的空间比栈大,但分配和释放相对较慢。
    • 堆内存的分配需要由程序员手动管理,容易发生内存泄漏和碎片问题。
    • 堆中的数据可以在函数调用结束后仍然存在,且可以被多个函数共享。

堆和栈的区别总结

特性
内存分配方式 由操作系统自动分配和回收 由程序员手动分配和回收
分配速度 较快 较慢
内存空间大小 通常较小,受限于操作系统 较大,可以动态扩展
生命周期 局部变量在栈中,函数返回后自动释放 动态分配的内存可以存活至手动释放
管理方式 后进先出(LIFO) 无顺序,按需分配
内存溢出 栈溢出(栈空间用完) 堆溢出(堆空间用完)
用途 存储局部变量、函数调用信息 存储动态分配的内存(如对象、数组等)
线程安全 每个线程都有自己的栈 多线程环境下需要同步管理堆内存

简要总结

  • 数据结构中的栈和堆:栈是一种后进先出的线性数据结构,通常用于递归和函数调用。堆是一种完全二叉树结构,常用于实现优先队列等。
  • 内存中的栈和堆:栈用于存储局部变量、函数调用等信息,由操作系统自动管理,内存分配速度快;堆用于存储动态分配的对象或数组,由程序员手动管理,内存分配较慢但灵活,生命周期更长。

深拷贝和浅拷贝

深拷贝浅拷贝是对象复制的两种方式,它们之间的主要区别在于是否复制对象内部的动态分配的资源(例如指针或堆内存)。

浅拷贝(Shallow Copy)

  • 定义:浅拷贝是指创建一个新对象,但新对象的内容只是原对象的引用或指针。也就是说,浅拷贝不会复制原对象的成员变量,而是将指针的值(内存地址)复制到新对象。因此,原对象和新对象会共享某些资源。
  • 特点
    • 拷贝对象的基本数据类型(如整数、浮点数等)。
    • 指针成员变量的值(即内存地址)会被复制,导致新旧对象指向同一块内存区域。
    • 修改新对象的指针成员,会影响到原对象,反之亦然。
    • 浅拷贝操作较为简单和高效,但可能导致共享资源的修改引发不期望的结果。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class MyClass {
public:
int a;
int *b;
MyClass(int val) {
a = val;
b = new int(val);
}
// 浅拷贝构造函数
MyClass(const MyClass& other) {
a = other.a;
b = other.b; // 直接拷贝指针
}
~MyClass() {
delete b;
}
};

int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 浅拷贝
cout << obj1.b << " " << obj2.b << endl; // 输出相同地址
return 0;
}
  • 结果:obj1obj2共享同一个b指针,它们指向同一块内存。

深拷贝(Deep Copy)

  • 定义:深拷贝是指创建一个新对象,并且复制原对象的所有成员变量(包括动态分配的内存)。换句话说,深拷贝会复制对象内部所有的资源,确保原对象和新对象没有共享资源。
  • 特点
    • 对象成员的基本数据类型会被逐一复制。
    • 对于指针类型成员变量,深拷贝会分配新的内存并复制指针指向的内容,而不仅仅是复制指针的值(即地址)。
    • 修改新对象的指针成员不会影响原对象,反之亦然。
    • 深拷贝的操作相对复杂且消耗资源,但确保对象之间互不干扰。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class MyClass {
public:
int a;
int *b;
MyClass(int val) {
a = val;
b = new int(val);
}
// 深拷贝构造函数
MyClass(const MyClass& other) {
a = other.a;
b = new int(*other.b); // 分配新的内存并复制数据
}
~MyClass() {
delete b;
}
};

int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 深拷贝
cout << obj1.b << " " << obj2.b << endl; // 输出不同地址
return 0;
}
  • 结果:obj1obj2各自有一份独立的b指针,它们指向不同的内存地址。

深拷贝和浅拷贝的区别总结

特性 浅拷贝(Shallow Copy) 深拷贝(Deep Copy)
内存分配 只复制指针或引用,指向相同内存地址。 为每个对象的成员变量分配新的内存。
资源共享 新旧对象共享动态内存资源。 新旧对象拥有独立的资源,不共享内存。
修改影响 修改新对象的指针成员可能影响原对象。 修改新对象的指针成员不会影响原对象。
实现复杂度 简单,执行速度较快。 复杂,执行速度较慢。
使用场景 当对象之间不需要独立的资源时(如浅拷贝适合小型数据结构)。 当对象之间需要完全独立的资源时(如深拷贝适合复杂对象)。

总结

  • 浅拷贝:对象和其动态资源共享,适用于资源不需要独立的场景。
  • 深拷贝:对象和其动态资源完全独立,适用于需要完全隔离和独立操作的场景。

violate关键字

在 C++ 中并没有 violate 关键字。可能你是想问 volatile 关键字,下面是 volatile 关键字的解释:

volatile 关键字

volatile 是 C++ 中的一个关键字,用来修饰变量,表示该变量的值可能会在程序中某些不可预测的方式下发生变化。它告诉编译器该变量的值可能会在外部因素的影响下发生变化,因此编译器在对该变量的优化时需要小心,不能假设该变量的值是固定不变的。

使用场景:

  1. 硬件寄存器:在嵌入式系统中,硬件寄存器的值可能会被外部硬件设备修改,使用 volatile 来确保编译器每次都从内存中读取变量的最新值,而不是从寄存器缓存中读取。
  2. 信号处理函数:在多线程或异步编程中,volatile 可以用于防止编译器缓存变量的值,以保证每次访问该变量时都能读取到最新的值。

作用:

  • 防止优化:通常编译器会对代码进行优化,假设某些变量的值不会发生变化。但如果这些变量实际上会被其他线程或外部硬件修改,那么编译器的优化就可能会导致错误。使用 volatile 可以防止编译器对这些变量进行优化。
  • 强制每次读取:每次访问该变量时,都会从内存中读取,而不会使用缓存或寄存器中的值。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

volatile int flag = 0;

void signal_handler() {
flag = 1; // 假设是由硬件或外部中断修改的
}

int main() {
while (flag == 0) {
// 循环等待 flag 被修改
}
cout << "Flag changed to 1!" << endl;
return 0;
}

在上面的例子中,flag 可能会被外部硬件或中断信号修改,因此需要声明为 volatile,这样编译器每次都会从内存中读取 flag 的最新值。

注意:

  • volatile 只会影响编译器的优化行为,它并不能保证线程间的同步。因此,在多线程环境下,volatile 不能替代互斥锁或原子操作。如果需要线程安全的操作,应该使用 std::atomic 或其他同步机制。

总结:

  • volatile 用来防止编译器对变量进行优化,确保每次访问该变量时都从内存中读取最新的值。
  • 适用于处理外部硬件、信号处理、或者多线程中的共享变量(但不适合线程同步)。

协程和线程的区别,相比于线程有什么优势?为什么用协程而不是子线程?怎么做到比线程使用资源更少的?

协程和线程的区别

  1. 线程(Thread)

    • 线程是操作系统调度的基本单位,每个线程有自己的栈空间和独立的执行上下文。
    • 线程间是并发执行的,通常由操作系统调度来分配 CPU 时间。
    • 线程的创建、销毁、切换的开销比较大,尤其是上下文切换时,操作系统需要保存和恢复线程的状态。
  2. 协程(Coroutine)

    • 协程是一种用户态的轻量级线程,其执行由程序自己控制,通常协程在同一个线程中切换。
    • 协程的调度是由程序中的某个框架或库(如 async/awaitboost::asio)控制,而非操作系统。
    • 相较于线程,协程在创建、销毁和切换时的开销非常小,因为它们不需要操作系统的上下文切换,只需要在程序中保存和恢复少量的状态(例如栈指针)。

协程相较于线程的优势

  1. 资源开销小

    • 线程需要操作系统的支持和管理,每个线程都有独立的栈空间,且每个线程的调度、上下文切换都需要操作系统的介入,造成较大的内存和时间开销。
    • 协程仅通过程序调度,不需要操作系统的干预,且每个协程使用的栈通常比线程小很多,因此内存开销非常低。可以在同样的资源下,创建大量协程。
  2. 上下文切换快

    • 线程上下文切换需要保存当前线程的状态,并切换到另一个线程的状态,这个过程涉及到内核的干预,开销大。
    • 协程的上下文切换是由程序本身控制的,通常只是简单的栈指针的保存和恢复,因此切换速度更快,几乎不需要操作系统的干预。
  3. 并发高效

    • 线程的并发受限于操作系统调度器的能力,且线程的数量过多时,操作系统的调度开销也会变得明显。
    • 协程可以在一个线程中调度成千上万个任务(甚至更多),由于协程不需要操作系统的调度支持,它们能够在单个线程中模拟出大量的并发任务。
  4. 更高效的 I/O 操作

    • 线程通常需要等待 I/O 操作完成,例如文件读写或网络请求,这时 CPU 资源就处于空闲状态。
    • 协程可以在等待 I/O 的时候将控制权交回给调度器,从而不浪费线程资源,能够更高效地处理大量的 I/O 操作。

为什么使用协程而不是线程?

  • 高并发:在高并发的场景下,例如处理大量 I/O 操作的任务,使用协程可以极大地提高程序的吞吐量。相比之下,使用线程需要更多的内存和上下文切换的开销,且操作系统可能无法调度大量线程,导致性能下降。
  • 资源占用:线程资源开销较大,尤其在需要启动大量并发任务时,操作系统和硬件资源可能难以承受。而协程的开销极小,可以在同样的资源下运行更多的任务。
  • 避免复杂的线程同步问题:使用协程时,由于它们在同一个线程中执行,因此避免了多线程编程中常见的线程同步问题(如死锁、竞态条件等),从而降低了编程复杂度。

如何做到比线程使用资源更少?

  1. 协程的调度:协程在程序内由用户控制调度,避免了操作系统对线程的调度和上下文切换。因为操作系统不需要参与,协程的上下文切换非常高效。

  2. 轻量级栈:每个协程通常使用非常小的栈空间,可能只有几 KB,而线程栈通常是几 MB。因此,协程相比线程在内存上的消耗要小得多。

  3. 并发管理:线程的并发性有限,创建和销毁线程需要操作系统的干预,而协程由程序控制,创建销毁的开销几乎为零。程序可以在同一线程内调度多个协程,这样就节省了大量的线程资源。

什么时候应该用协程?

  • I/O 密集型操作:如 Web 服务、网络请求、文件读写等,协程可以有效避免线程等待 I/O 完成时的阻塞。
  • 高并发场景:如微服务架构中的并发请求,使用协程可以在一个线程中处理成千上万的请求,而不必为每个请求创建一个线程。
  • 实时性要求较低的场景:协程需要在单线程内调度大量任务,因此适合于那些实时性要求不是特别严格的场景。

总结

协程比线程更加轻量,开销小,尤其在高并发、I/O 密集型的场景下,协程能够充分利用单核甚至多核 CPU 的资源,避免了线程创建和销毁的开销,从而提高系统的性能和响应速度。

什么是大根堆?怎么维护堆?

什么是大根堆?

大根堆(Max Heap)是一种完全二叉树(Complete Binary Tree),具有以下性质:

  1. 完全二叉树:树是满的,或者说每一层除了最后一层外都完全填满,且所有的节点都在最左边。
  2. 堆的性质:对于树中的每一个节点,其值都大于或等于其左右子节点的值,即每个父节点的值都不小于其子节点的值。根节点的值是最大的,因此叫做“大根堆”。

大根堆的特点

  • 根节点最大,每次插入一个新元素时,会通过上浮操作保持大根堆性质。
  • 移除根节点时,会通过下沉操作将堆的结构恢复为大根堆。

维护大根堆

维护大根堆主要有两种操作:插入删除,都涉及到堆的结构调整。

1. 插入操作(插入新元素)

插入操作时,会将新的元素插入到堆的最后一个位置,并通过上浮操作恢复大根堆的性质:

  • 将新元素插入到堆的末尾。
  • 比较新插入的元素与父节点的值,如果新元素大于父节点的值,则交换两者。
  • 重复这个过程,直到新元素不再大于父节点或者达到根节点。

上浮操作是插入元素后逐步将该元素上移,直到堆保持大根堆性质。

2. 删除操作(删除根节点)

删除根节点时,通常会将堆的最后一个元素移到根节点位置,并通过下沉操作恢复堆的性质:

  • 把根节点的值与堆的最后一个节点交换。
  • 移除堆的最后一个元素。
  • 然后对新的根节点进行下沉操作。将根节点与它的最大子节点交换,直到堆的结构恢复为大根堆。

下沉操作是将根节点的元素下移,直到它的值不小于任何子节点的值。

3. 堆的构建(建堆)

从一个无序的数组构建堆时,可以通过下沉操作来实现。一般的步骤是:

  • 从最后一个非叶子节点开始(即 n/2 - 1),依次对每个节点执行下沉操作,直到根节点。
  • 下沉操作会根据大根堆的性质,将每个子树调整为堆。

时间复杂度

  • 插入操作的时间复杂度为 **O(log n)**,因为在最坏情况下,需要进行 log n 次上浮操作。
  • 删除操作的时间复杂度为 **O(log n)**,因为删除根节点后需要进行最多 log n 次的下沉操作。
  • 构建堆的时间复杂度为 **O(n)**,通过下沉操作从最后一个非叶子节点开始处理。

应用

  • 优先队列:大根堆可以用于实现优先队列,支持高效的插入和删除最大元素操作。
  • 堆排序:通过反复从堆中取出最大元素,进行排序。

总结

大根堆是一种有效的完全二叉树,维护了一个堆结构(每个父节点的值大于或等于子节点),常用于实现优先队列和堆排序。插入操作和删除操作都需要通过上浮和下沉来维护堆的性质,时间复杂度为 O(log n)。

http的头有哪些类型

HTTP 请求和响应头(HTTP headers)是指在 HTTP 请求和响应中携带的元数据,用来传递关于请求或响应的额外信息。HTTP 头部分为很多种类型,每种类型有特定的目的和用途。

以下是一些常见的 HTTP 头类型:

1. 请求头(Request Headers)

请求头包含了客户端发送给服务器的附加信息,如客户端的属性、请求的内容类型等。常见的请求头有:

  • Host:指定服务器的主机名和端口号(对于虚拟主机非常重要)。
  • User-Agent:标识发出请求的浏览器或客户端软件。
  • Accept:客户端可处理的内容类型(如 text/htmlapplication/json 等)。
  • Accept-Encoding:客户端支持的内容编码方式(如 gzipdeflate)。
  • Accept-Language:客户端首选的语言。
  • Authorization:客户端身份验证信息(如 Basic Auth 或 Bearer Token)。
  • Connection:指示是否保持当前连接(如 keep-aliveclose)。
  • Cookie:客户端发送的存储在本地的 cookie。
  • Referer:指定请求页面的 URL。
  • If-Modified-Since:指定请求的资源自某个时间后是否修改。
  • Range:请求获取资源的部分内容,通常用于断点续传。

2. 响应头(Response Headers)

响应头包含服务器返回给客户端的附加信息。常见的响应头有:

  • Content-Type:指定响应体的媒体类型(如 text/htmlapplication/json)。
  • Content-Length:指定响应体的大小,以字节为单位。
  • Server:服务器软件的信息(如 Apache、nginx 等)。
  • Location:用于重定向响应时,指定新的 URL。
  • Cache-Control:控制缓存策略(如 no-cacheprivatemax-age 等)。
  • Set-Cookie:设置 cookie。
  • WWW-Authenticate:在需要身份验证时,服务器提供的认证信息。
  • Expires:指定响应内容的过期时间(通常与缓存控制有关)。
  • ETag:资源的标识符(通常用于缓存验证)。

3. 通用头(General Headers)

通用头可以出现在请求和响应中,提供一般性的元信息。常见的通用头有:

  • Cache-Control:指定缓存控制机制。
  • Connection:指示是否持久连接。
  • Date:表示消息发送的日期和时间。
  • Transfer-Encoding:指示消息体的传输编码方式(如 chunked)。

4. 实体头(Entity Headers)

实体头描述消息体的属性,可以在请求和响应中都出现。常见的实体头有:

  • Content-Type:描述实体数据的类型。
  • Content-Length:描述实体数据的长度。
  • Content-Encoding:描述实体数据的编码方式(如 gzip)。
  • Content-Language:描述实体数据的语言。

5. 安全和代理头(Security and Proxy Headers)

  • Strict-Transport-Security(HSTS):指示客户端应该仅通过 HTTPS 访问该网站。
  • X-Content-Type-Options:防止浏览器进行 MIME 类型嗅探。
  • X-Frame-Options:防止网页被嵌入到 iframe 中,减少点击劫持攻击的风险。
  • X-XSS-Protection:启用或禁用浏览器的 XSS 防护。

总结

HTTP 头可以分为多个类型,主要包括请求头、响应头、通用头、实体头和一些安全和代理相关的头。每个头类型承载着不同的信息,用来在客户端和服务器之间传递元数据。理解和使用这些头是构建和优化 HTTP 请求与响应的重要部分。

rpc调用流程,序列化工具,如何加入http协议

RPC(远程过程调用)是允许程序调用另一个地址空间(通常是不同机器上的程序)中的函数或方法的一种协议。RPC 调用流程的基本步骤如下:

RPC 调用流程

  1. 客户端调用:客户端应用程序发起对服务器端方法的调用。此时客户端并不知道服务器的具体实现,它像调用本地方法一样调用远程方法。

  2. 序列化请求:客户端在调用函数时,将函数的参数和相关信息序列化成一种标准的格式,通常是二进制或者文本格式。常见的序列化工具包括 Protobuf、Thrift、JSON、XML 等。

  3. 发送请求:客户端将序列化后的数据通过网络发送到服务器端。这通常使用某种网络协议,例如 HTTP 或自定义的协议。

  4. 服务器接收请求:服务器接收到请求后,首先对数据进行反序列化,恢复成原始的数据和方法调用。

  5. 执行方法:服务器解析请求数据,调用对应的本地方法,并用结果进行回应。

  6. 序列化响应:服务器将方法的执行结果(可能是数据或状态)进行序列化,然后将其通过网络发送回客户端。

  7. 客户端接收响应:客户端接收到响应后,反序列化数据,得到最终结果。

  8. 完成调用:客户端使用反序列化后的结果,继续执行程序。

序列化工具

序列化工具用于将对象转换为适合在网络上传输的格式。在 RPC 中,序列化工具的作用是将函数参数或返回值转换为标准格式以便进行网络传输。常见的序列化工具包括:

  1. **Protobuf (Protocol Buffers)**:

    • 是 Google 提供的一种轻量级、高效的序列化工具,支持多语言(C++, Java, Python 等)。
    • 数据通过定义 .proto 文件进行结构化,可以生成序列化和反序列化代码。
    • 高效的二进制格式,适合性能要求高的场景。
  2. Thrift

    • 由 Apache 提供,支持多语言并且提供了跨平台的功能。
    • 提供类似 Protobuf 的功能,能够进行高效的序列化与反序列化。
  3. JSON

    • 轻量级的数据交换格式,适合文本传输。常用于 Web API 中。
    • 优点是易于理解和调试,但相对较慢和占用带宽较多。
    • 比如使用 JSON-RPC 协议进行远程调用。
  4. XML

    • 常用于 Web 服务中(如 SOAP)。
    • 比 JSON 占用更多带宽,解析较为复杂,但依然是很多老旧系统的主流格式。

如何加入 HTTP 协议

在 RPC 系统中,如果你希望使用 HTTP 协议进行通信,可以按以下方式设计:

  1. HTTP 作为传输协议

    • 你可以使用 HTTP 协议传输序列化后的数据。即将 RPC 请求包装为 HTTP 请求,将序列化数据放入请求体中,发送到服务器端。
    • 服务器接收到 HTTP 请求后,读取请求体,将其反序列化,调用相应的服务方法,并将结果通过 HTTP 响应返回给客户端。
  2. RESTful RPC

    • 另一种流行的方式是使用 RESTful 风格的 API,通过 HTTP 协议进行通信。
    • 在这种情况下,RPC 方法可以通过 HTTP 请求的不同路径和 HTTP 动作(如 GET、POST)来表示。
    • 请求参数可以通过 URL 或者请求体传递,数据一般采用 JSON 或 XML 进行序列化。
  3. gRPC(基于 HTTP/2)

    • gRPC 是 Google 开发的高性能 RPC 框架,基于 HTTP/2 协议,并使用 Protobuf 作为默认的序列化格式。
    • gRPC 利用 HTTP/2 的多路复用、流控制、服务器推送等特性,提供了高效的、低延迟的通信。
  4. WebSocket

    • 对于需要双向通信的 RPC,可以使用 WebSocket 协议,它可以实现持久连接,允许双向的消息交换。
    • 在 WebSocket 上面,可以使用 JSON、Protobuf 等格式进行数据传输。

总结

  • RPC 调用的流程包括客户端发起请求、序列化数据、网络传输、服务器处理请求、序列化响应、发送回客户端以及反序列化数据。
  • 常见的序列化工具有 Protobuf、Thrift、JSON 和 XML,选择合适的工具主要取决于性能需求和应用场景。
  • HTTP 协议可以用作 RPC 的传输协议,常见的方式有基于 HTTP 的 RESTful RPC、gRPC(使用 HTTP/2)、以及 WebSocket 协议。

客户端如何mysql防止注入攻击

防止 MySQL 注入攻击(SQL Injection)是确保应用程序安全性的一项重要任务。SQL 注入攻击通过在 SQL 查询中插入恶意的 SQL 代码,使得攻击者能够操作数据库,执行非预期的查询、插入、更新或删除数据。

在客户端开发中,虽然很多防注入的安全措施在服务器端进行,但客户端仍然可以通过正确的编码和调用方法来减少 SQL 注入的风险。

客户端防止 MySQL 注入攻击的措施

  1. 使用预编译语句(Prepared Statements)

    • 预编译语句是防止 SQL 注入的最有效方式。通过将 SQL 查询的结构与用户输入分离,确保输入不被当作 SQL 代码执行。
    • 使用预编译语句时,查询和参数会先由数据库服务器解析执行,而用户输入的参数被当作普通数据处理,而不是 SQL 代码。
    • 例如,在 C++ 中,通常会使用 MySQL 提供的 API 或 ORM(如 libmysqlclientMySQL Connector/C++)来执行预编译查询。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // C++ MySQL Prepared Statement example
    MYSQL_STMT *stmt;
    stmt = mysql_stmt_init(conn);

    const char *query = "SELECT * FROM users WHERE username = ? AND password = ?";
    mysql_stmt_prepare(stmt, query, strlen(query));

    // Binding the input parameters
    MYSQL_BIND bind[2];
    memset(bind, 0, sizeof(bind));

    bind[0].buffer_type = MYSQL_TYPE_STRING;
    bind[0].buffer = username;
    bind[0].buffer_length = strlen(username);

    bind[1].buffer_type = MYSQL_TYPE_STRING;
    bind[1].buffer = password;
    bind[1].buffer_length = strlen(password);

    mysql_stmt_bind_param(stmt, bind);

    mysql_stmt_execute(stmt);

    这样即使用户的输入包含恶意 SQL 代码,MySQL 也会将其视为普通数据,不会执行任何潜在的危险操作。

  2. 使用 ORM(对象关系映射)框架

    • 使用 ORM 框架(例如 HibernateEntity FrameworkDjango ORM)可以在更高层次上避免 SQL 注入,因为 ORM 通常会自动使用参数化查询,而不是直接拼接 SQL 字符串。
    • ORM 是通过封装 SQL 查询,避免直接构造 SQL 语句的方式来减少 SQL 注入的风险。
  3. 输入验证和过滤

    • 在客户端进行输入验证可以大大减少无效或恶意输入传到服务器。
    • 确保输入的数据类型、长度、范围和格式符合预期。例如,用户名应该是字母和数字组合,密码应该遵循特定规则,日期字段应该是有效的日期格式。
    • 在提交到数据库之前,对所有用户输入进行严格的验证和清洗(例如通过正则表达式)。
  4. 转义用户输入的特殊字符

    • 如果不能使用预编译语句或 ORM,则可以通过对用户输入中的特殊字符(如引号 '、双引号 "、分号 ;、注释符号 -- 等)进行转义来减少 SQL 注入的风险。转义这些字符可以防止它们在 SQL 查询中被解析为 SQL 语法的一部分。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    std::string escape_sql(const std::string& input) {
    std::string escaped;
    for (char ch : input) {
    if (ch == '\'' || ch == '\"' || ch == ';') {
    escaped += '\\'; // 添加反斜杠转义
    }
    escaped += ch;
    }
    return escaped;
    }
  5. 最小权限原则

    • 确保数据库用户仅具有执行必要操作的权限。例如,如果某个用户只需要读取数据,则不要给该用户执行插入或删除的权限。通过限制数据库账户的权限,可以降低 SQL 注入带来的潜在危害。
  6. 错误处理

    • 在客户端和服务器端,应当避免将数据库的错误信息直接返回给用户。错误信息可能泄露数据库的结构或其他敏感信息。
    • 使用通用的错误消息,而不是具体的数据库错误信息。这样可以防止攻击者根据错误消息推测出数据库的结构或其他安全漏洞。
  7. 使用 Web 应用防火墙(WAF)

    • Web 应用防火墙(WAF)可以帮助检测和阻止 SQL 注入攻击。虽然这主要是一个服务器端的防护手段,但客户端可以依赖 WAF 来增强安全性,防止常见的注入攻击。

总结

防止 SQL 注入的关键是不要直接将用户输入拼接到 SQL 查询中。最有效的防护措施是使用 预编译语句ORM,它们自动为用户输入添加必要的安全检查。除此之外,客户端可以通过 输入验证和过滤转义特殊字符最小权限原则 来增强安全性。

了解过跨域吗,如何处理

是的,跨域(Cross-Origin)问题通常出现在前端开发中,尤其是在一个网站(域名、协议、端口)访问另一个网站的资源时(例如通过AJAX发起请求)。由于浏览器的同源策略,跨域请求会被限制。同源策略要求:只有在协议、域名和端口都相同的情况下,才能进行数据的访问和操作。

跨域的常见问题

跨域的主要问题是浏览器会阻止从一个域名发起的请求访问另一个域名的资源。这种限制是为了防止恶意网站窃取用户的敏感信息。

跨域请求的几种常见处理方式

  1. CORS(Cross-Origin Resource Sharing)

    • CORS 是一种跨源资源共享机制,允许服务器通过 HTTP 头部信息告知浏览器,哪些外部域可以访问它的资源。
    • 服务器通过设置 Access-Control-Allow-Origin 等响应头来控制允许跨域请求的源。例如,可以在服务器响应中设置:
      1
      Access-Control-Allow-Origin: *
      这样可以允许所有源访问资源,或者设置特定的域名:
      1
      Access-Control-Allow-Origin: http://example.com
    • 对于 带凭证的请求(如 cookies 或 HTTP 认证信息),需要在服务端允许:
      1
      Access-Control-Allow-Credentials: true
    • 如果是 预检请求(对于 PUTDELETE 或自定义请求头等方法),浏览器会发送一个 OPTIONS 请求,服务器需要返回相应的头部来告知浏览器是否允许真正的请求。
  2. JSONP (JSON with Padding)

    • JSONP 是一种跨域解决方案,它通过动态创建 <script> 标签来绕过浏览器的同源策略,因为 <script> 标签没有跨域限制。
    • 在服务器端,响应的数据通常是一个函数调用,并且数据以该函数作为参数返回。
    • JSONP 的缺点是只能发起 GET 请求,并且不能处理 HTTP 响应状态码(如 404、500 等)。
    • 示例:
      1
      2
      3
      4
      function handleData(data) {
      console.log(data);
      }
      <script src="http://example.com/data?callback=handleData"></script>
  3. 代理服务器

    • 在一些情况下,客户端可以通过设置一个代理服务器,来间接访问跨域的资源。客户端的请求先发到代理服务器,然后由代理服务器代为发送请求到目标服务器,再把响应返回给客户端。
    • 例如,可以在前端开发时配置一个代理(例如 webpack-dev-server 中的代理功能),在请求过程中通过代理服务器来避免跨域。
    • 示例:在 webpack 配置中:
      1
      2
      3
      4
      5
      devServer: {
      proxy: {
      '/api': 'http://example.com'
      }
      }
      这样,http://localhost:3000/api 请求会被代理到 http://example.com/api,从而避免跨域。
  4. WebSockets

    • WebSocket 是一个建立在 TCP 协议上的双向通信协议,浏览器与服务器通过 WebSocket 建立连接后,数据可以双向流动,不受同源策略的限制。
    • WebSocket 可用于跨域通信,但它并不是适合所有类型的请求。它适用于实时双向通信的应用,如聊天室或实时数据更新等。
  5. Iframe + PostMessage

    • PostMessage 是一种用于不同源的跨文档消息传递的 API。通过在 iframe 中嵌入不同源的内容,父页面和嵌入的页面可以通过 postMessage 来传递信息。
    • 例如,父页面向 iframe 中的页面发送消息:
      1
      iframe.contentWindow.postMessage('message', 'http://other-domain.com');
    • iframe 中的页面通过 window.addEventListener('message', ...) 接收消息。

总结

处理跨域的主要方式是 CORS,它是目前最标准和安全的解决方案。在跨域访问较为简单的情况下,可以考虑使用 JSONP代理服务器,但这些方法有一定的局限性。而对于实时通信,可以使用 WebSocket,也可以通过 iframe + postMessage 进行跨域通信。

在前端开发中,处理跨域问题时,我们通常会依赖于后端的配合,设置正确的 CORS 头信息。

客户端开发的框架 客户端开发的工具,以及开发流程

在客户端开发中,选择框架、工具和开发流程通常根据项目需求和目标平台的不同有所不同。以下是一些常见的客户端开发框架、工具以及开发流程:

1. 客户端开发框架

  • Qt: Qt 是一个跨平台的应用程序开发框架,支持 Windows、Linux、macOS 和嵌入式平台。Qt 提供了一个广泛的库,帮助开发者快速构建图形用户界面(GUI)和后台服务。你提到试用过 Qt,它常用于开发桌面应用程序和嵌入式系统。

  • Electron: 基于 JavaScript、HTML 和 CSS 的跨平台桌面应用框架。它使用 Chromium 和 Node.js 引擎,适合前端开发者,可以构建跨平台的桌面应用。常见应用如 Visual Studio Code 和 Slack 都是基于 Electron 开发的。

  • WPF (Windows Presentation Foundation): WPF 是 Microsoft 提供的一种用于构建 Windows 应用程序的框架,支持数据绑定、模板和可扩展的 UI 控件,适用于开发 Windows 桌面应用。

  • Flutter: Flutter 是 Google 提供的一款跨平台 UI 工具包,主要用于开发 Android 和 iOS 的应用,但它也支持桌面和 Web。通过使用 Dart 编程语言,开发者可以构建高性能的应用。

  • React Native: 用于开发 Android 和 iOS 移动应用的跨平台框架,使用 JavaScript 和 React 构建用户界面。对于有 Web 前端开发经验的开发者来说,这是一个不错的选择。

  • SwiftUI: 如果你专注于 macOS 和 iOS 应用的开发,SwiftUI 是 Apple 提供的现代化 UI 框架,简化了用户界面的构建。

2. 客户端开发工具

  • IDE(集成开发环境)

    • Qt Creator: 对于 Qt 开发,Qt Creator 是官方推荐的 IDE,提供了代码编辑、调试、GUI 设计等功能。
    • Visual Studio: 适用于 C++、C#、.NET 开发,强大的调试功能和丰富的插件支持。
    • Xcode: 用于 macOS 和 iOS 开发,支持 Swift 和 Objective-C。
    • Android Studio: Android 应用开发的官方 IDE,支持 Kotlin 和 Java。
    • IntelliJ IDEA: 主要用于 Java 开发,但支持多种编程语言。适合 Web 前端和后端开发。
  • 调试工具

    • gdb: GNU 调试器,适用于 C++ 调试。
    • Valgrind: 用于检测内存泄漏的工具。
    • Wireshark: 网络协议分析工具,适合调试网络通信问题。
    • Qt Designer: 用于设计 Qt 应用的图形界面,生成 .ui 文件。
  • 版本控制工具

    • Git: 最常用的版本控制系统,GitHub、GitLab 和 Bitbucket 都是基于 Git 的平台。
    • SVN (Subversion): 一种集中式版本控制系统。
  • 构建工具

    • CMake: Qt、C++ 项目中常用的构建工具,支持跨平台构建。
    • Makefile: 用于自动化构建和管理项目。

3. 客户端开发流程

  1. 需求分析与设计

    • 确定应用的目标用户和平台。
    • 设计 UI 和 UX,构思应用的功能和架构。
  2. 选择框架与工具

    • 根据项目需求和目标平台,选择合适的开发框架和工具(如 Qt、Electron、WPF 等)。
  3. 环境搭建

    • 配置开发环境,安装必要的 IDE 和工具(如 Qt Creator、Visual Studio 等)。
    • 设置版本控制系统,创建项目仓库。
  4. UI 设计与编码

    • 使用 UI 设计工具(如 Qt Designer)设计界面,或直接在代码中构建界面(如使用 Qt 的 QWidgets 或 Qt Quick)。
    • 开始编码实现核心功能。
  5. 调试与测试

    • 使用 IDE 自带的调试工具进行调试,发现和修复 bug。
    • 编写单元测试,确保功能的正确性。
  6. 打包与发布

    • 将应用打包成可执行文件(如 Qt 的 .exe.app)。
    • 为目标平台(如 Windows、macOS、Linux)进行打包和优化。
  7. 维护与更新

    • 发布后根据用户反馈修复 bug,添加新功能。
    • 定期更新依赖库,保持应用的兼容性。

总结

客户端开发框架、工具和流程会因项目需求和开发平台而异。你提到试用过 Qt,它是一个非常适合桌面应用的框架,支持跨平台开发。如果你需要开发移动应用或 Web 应用,可能会选择其他框架,如 React Native 或 Flutter。开发流程从需求分析到发布,再到维护和更新,涉及的环节非常多,需要开发者根据具体需求选择合适的工具和框架。

浏览器缓存

浏览器缓存是指浏览器将之前加载过的网页资源(如 HTML 文件、CSS 文件、JavaScript 文件、图片等)保存在本地,以减少重复请求,提高加载速度和降低网络带宽使用。

浏览器缓存的类型

  1. 强缓存 (Strong Cache)
    强缓存是指浏览器在请求资源时,首先检查本地缓存的资源是否符合条件,如果符合条件则直接使用缓存,不会发起请求。强缓存通过 HTTP 头部的 Cache-ControlExpires 来控制。

    • Cache-Control: 是缓存控制的核心,常见的值有:

      • no-cache: 每次请求时都必须向服务器验证资源是否过期。
      • no-store: 不缓存任何内容。
      • public: 任何缓存服务器都可以缓存该资源。
      • private: 只有客户端缓存,不允许共享缓存。
      • max-age=<seconds>: 指定资源的过期时间,以秒为单位。
    • Expires: 指定资源过期的时间戳,浏览器会根据该时间戳判断缓存是否有效。但它存在一个问题,Expires 的时间是一个绝对时间,无法根据实际情况动态计算。

  2. 协商缓存 (Conditional Cache)
    当强缓存失效时,浏览器会通过协商缓存来判断资源是否需要更新。协商缓存是通过 Last-ModifiedETag 头部进行判断的。

    • Last-Modified: 服务器告诉浏览器资源的最后修改时间。浏览器会在下一次请求时使用 If-Modified-Since 头部发送这个时间戳给服务器,服务器根据时间戳来判断资源是否被修改过,如果没有修改则返回 304 状态码。

    • ETag: 是一个资源的唯一标识符,通常是一个哈希值,表示资源内容的唯一性。浏览器会在下一次请求时将 ETag 值发送给服务器,服务器根据 If-None-Match 判断资源是否有更新。

浏览器缓存的工作流程

  1. 请求资源

    • 浏览器向服务器发送 HTTP 请求,要求获取某个资源。
  2. 查看缓存

    • 浏览器根据缓存策略检查本地缓存,如果符合强缓存条件,则直接使用缓存,不再向服务器请求。
  3. 发起协商缓存请求

    • 如果强缓存失效,浏览器会发送带有 If-Modified-SinceIf-None-Match 的请求,询问服务器该资源是否有变化。
  4. 服务器响应

    • 如果服务器判断资源没有变化,返回 304 状态码,并不返回资源内容;如果资源有变化,返回新的资源和 200 状态码。
  5. 更新缓存

    • 如果返回的是 200 状态码,浏览器将新的资源保存到本地缓存中,以便下次使用。

浏览器缓存的优势

  • 提高加载速度: 缓存可以减少重复请求,节省时间,提升用户体验。
  • 减少网络带宽消耗: 资源缓存后,避免每次都从服务器请求相同的资源,减少了带宽消耗。
  • 降低服务器负载: 缓存减少了对服务器的请求量,减轻了服务器压力。

浏览器缓存的缺点

  • 缓存失效问题: 当资源更新时,可能需要强制刷新缓存。否则用户会看到旧的资源。
  • 缓存占用磁盘空间: 浏览器需要在本地存储缓存数据,占用一定的磁盘空间。
  • 版本控制难题: 当资源更新频繁时,可能需要更复杂的缓存管理策略。

缓存清理

浏览器缓存通常会在以下情况下自动清理:

  • 缓存过期(Cache-ControlExpires 头部设置了有效期)。
  • 浏览器缓存达到最大存储限制。
  • 用户手动清理浏览器缓存。

通过合理配置缓存策略,可以有效利用浏览器缓存提高性能,同时避免缓存问题对用户体验的影响。

二叉搜索树

二叉搜索树(Binary Search Tree,简称 BST)是一种具有特殊性质的二叉树数据结构,在该树中,每个节点的左子树中的所有节点的值都小于该节点的值,右子树中的所有节点的值都大于该节点的值。具体来说,对于每个节点:

  • 左子树的值 < 当前节点的值。
  • 右子树的值 > 当前节点的值。

二叉搜索树的特点

  1. 每个节点最多有两个子节点:即每个节点至多有一个左子节点和一个右子节点。
  2. 有序性:对于任何一个节点,其左子树的节点值都小于该节点的值,右子树的节点值都大于该节点的值。这保证了中序遍历二叉搜索树时,得到的节点值是按升序排列的。
  3. 查找效率:二叉搜索树的查找效率较高,平均时间复杂度为 O(log N),最坏情况下为 O(N)(当树退化为链表时)。

二叉搜索树的基本操作

  1. 插入节点:插入时,首先从根节点开始,逐层比较插入值与当前节点的值的大小关系,决定向左子树还是右子树继续插入,直到找到一个空位置插入新节点。

  2. 查找节点:查找时,首先从根节点开始,逐层比较查找值与当前节点的值的大小关系,决定向左子树还是右子树继续查找,直到找到目标节点或者遍历完所有节点。

  3. 删除节点:删除节点时有三种情况:

    • 节点没有子节点:直接删除该节点。
    • 节点只有一个子节点:用该节点的唯一子节点替代该节点。
    • 节点有两个子节点:找到该节点的右子树中最小的节点(或左子树中最大的节点),将其值复制到要删除的节点,并删除该最小节点或最大节点。
  4. 遍历

    • 中序遍历:从左子树 -> 根节点 -> 右子树遍历树,得到升序的节点值。
    • 前序遍历:根节点 -> 左子树 -> 右子树。
    • 后序遍历:左子树 -> 右子树 -> 根节点。

二叉搜索树的优缺点

优点

  • 查找、插入、删除的时间复杂度较低,理想情况下为 O(log N),能保证快速的查找性能。
  • 有序性:能够方便地进行有序遍历(如中序遍历得到升序排列)。

缺点

  • 树的平衡性问题:在插入数据时,如果数据是按升序或降序排列的,二叉搜索树会退化为链表,导致查找、插入、删除的时间复杂度退化为 O(N)。
  • 不适用于某些极端数据分布:例如,当输入数据是已经排序的情况下,二叉搜索树的效率会降低。

如何保持二叉搜索树的平衡

为了避免二叉搜索树退化成链表,通常采用平衡二叉搜索树(如 AVL 树、红黑树等)来保持树的平衡,确保其高度始终保持在 O(log N) 级别,从而保证查找、插入、删除等操作的效率。

AVL 树:自平衡的二叉搜索树,要求每个节点的左右子树高度差不超过 1,插入和删除操作时进行旋转操作来保持平衡。

红黑树:也是一种自平衡的二叉搜索树,维护了一些额外的属性,确保树的高度不会过大,从而提供对数级别的查找效率。

总结

二叉搜索树是一个非常有用的数据结构,具有良好的查找性能和有序性,但需要注意它的平衡性问题。在实际应用中,通常会使用平衡二叉搜索树(如 AVL 树、红黑树)来确保效率。

红黑树了解吗

是的,红黑树(Red-Black Tree)是一种自平衡的二叉查找树,具有以下性质,保证了其高度不会过大,从而能提供对数级别的查找、插入和删除操作。

红黑树的基本性质

红黑树具有以下五个基本性质(这些性质保证了树的平衡性):

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色
  3. 所有叶子节点(NIL节点)是黑色
  4. 如果一个红色节点的父节点或子节点存在,那么它的子节点必须是黑色(即没有两个相邻的红色节点)。
  5. 从任何一个节点到其每个叶子节点的路径上,必须包含相同数量的黑色节点。这个数量被称为该节点的“黑色高度”。

红黑树的结构特点

  • 二叉查找树:红黑树是二叉查找树,因此它具有二叉查找树的基本性质:每个节点的左子树的值都小于节点的值,右子树的值都大于节点的值。

  • 平衡性:红黑树通过控制树的高度,确保查找、插入和删除操作的时间复杂度维持在 O(log N),避免了普通二叉搜索树在最坏情况下(比如输入是已排序的元素)退化成链表的问题。

红黑树的插入和删除

  • 插入操作

    • 插入的节点总是红色的,这样不会破坏黑色高度的平衡。
    • 插入后,可能会出现红色节点与父节点是红色的情况,这时需要通过“旋转”和“着色”操作来恢复红黑树的性质。
    • 旋转操作有左旋和右旋两种方式。通过旋转,可以改变树的结构,从而调整节点的相对位置,保证树的平衡。
  • 删除操作

    • 删除一个节点后,可能会破坏红黑树的平衡,特别是删除黑色节点后,会导致路径上的黑色高度不一致,需要通过旋转和重新着色来恢复平衡。
    • 与插入操作类似,删除后也需要进行一系列的修正操作,保证红黑树的性质依然成立。

红黑树的优点

  1. 高效的查找、插入和删除:红黑树能够保证查找、插入和删除操作的时间复杂度为 O(log N),这比普通的二叉查找树更加高效,特别是对于一些大量数据的情况。

  2. 自平衡:红黑树是自平衡的,在进行插入和删除时,自动通过旋转和调整颜色来保持平衡,避免了树的退化问题。

  3. 适用于实时应用:由于插入和删除的复杂度都为 O(log N),因此它常用于对时间要求较高的系统(比如数据库系统)。

红黑树的缺点

  1. 实现复杂度较高:相比于普通的二叉查找树或AVL树,红黑树的实现相对复杂,尤其是在插入和删除时,需要进行多次旋转和着色操作来维持树的平衡。

  2. 内存开销:每个节点除了存储数据外,还需要存储一个颜色属性(红或黑),因此比普通的二叉树节点内存开销稍大。

红黑树与其他自平衡树(如 AVL 树)比较

  1. 平衡因子

    • AVL 树:严格的平衡条件,任何节点的左右子树高度差不能超过 1。
    • 红黑树:较宽松的平衡条件,允许存在一些不平衡,但通过其五个性质保证了树的高度不会过大。
  2. 插入和删除的调整

    • AVL 树:插入和删除时可能需要多次旋转,插入后的平衡调整较为复杂。
    • 红黑树:插入和删除时通常需要较少的旋转,虽然每个节点的调整可能涉及颜色改变,但整体操作较为简洁。
  3. 查找性能

    • AVL 树:由于严格的平衡要求,AVL 树的查找操作稍微比红黑树更高效,特别是在树的高度较大时。
    • 红黑树:查找性能稍逊,但差距不大。
  4. 应用场景

    • AVL 树:适用于查询频繁、更新较少的场景,因其查找效率较高。
    • 红黑树:适用于插入和删除操作频繁的场景,比如数据库的索引和操作系统的调度算法中,红黑树通常被用作底层实现。

总结

红黑树是一种重要的自平衡二叉搜索树,它保证了树的高度平衡,避免了退化成链表的情况,使得查找、插入、删除操作的时间复杂度始终保持在 O(log N) 的水平。红黑树通过五个基本性质保证了树的平衡性,广泛应用于数据库、文件系统等需要频繁插入和删除操作的场景中。

对称加密与非对称加密应用场景

对称加密和非对称加密的应用场景

对称加密(Symmetric Encryption)非对称加密(Asymmetric Encryption) 是两种常见的加密方法,它们在许多应用中各自有不同的优势和适用场景。

1. 对称加密

对称加密是指加密和解密使用相同的密钥。常见的对称加密算法有 AESDES3DES 等。

应用场景:
  • 数据加密:对称加密用于需要大量数据加密和高效性能的场景,如 磁盘加密(BitLocker、FileVault)、文件加密(例如,压缩包加密)、数据库加密等。

  • HTTPS中的数据传输:在 SSL/TLS 协议中,虽然使用了非对称加密来交换密钥,但一旦建立了安全通道,实际的数据传输会使用对称加密算法进行加密。对称加密在这种场景下用于高速加密和解密数据传输。

  • 虚拟专用网络(VPN):VPN中为了保障数据安全,通常使用对称加密来保护数据的机密性和完整性,尤其是在长时间的数据传输过程中。

  • 无线通信:在 WPA2(Wi-Fi Protected Access 2)WEP 等无线加密协议中,通常使用对称加密来保护无线网络的数据传输。

优势:
  • 加密和解密速度快,适合大量数据传输。
  • 计算复杂度低,处理速度较非对称加密高。
缺点:
  • 密钥的分发和管理较为复杂,必须确保密钥的安全性。
  • 密钥传输过程中容易被窃取(如果没有保护密钥的安全方式)。

2. 非对称加密

非对称加密是指加密和解密使用不同的密钥。常见的非对称加密算法有 RSAECCDSA 等。

应用场景:
  • SSL/TLS协议:用于 HTTPS 等网络协议中,非对称加密用于服务器和客户端之间的密钥交换,在加密通道建立时确保数据传输的安全性。

  • 电子邮件加密:非对称加密可用于 PGP(Pretty Good Privacy)和 S/MIME(Secure/Multipurpose Internet Mail Extensions)协议中,提供邮件内容的加密和签名。

  • 数字签名:非对称加密广泛用于数字签名,确保数据的来源和完整性。比如,在软件发布时,开发者会使用私钥签名软件包,用户用公钥验证软件包的真实性。

  • 身份验证:非对称加密可用于身份认证中,利用公钥加密和私钥解密的机制,确保用户身份的唯一性和安全性,广泛应用于基于公钥基础设施(PKI)的身份认证和授权系统。

  • 区块链和加密货币:在比特币等加密货币中,使用非对称加密来控制账户的私钥和公钥,以确保交易的安全和验证。

优势:
  • 密钥交换安全:非对称加密的最大优势是密钥可以公开传输,公钥可以公开发送,私钥始终保密。
  • 数据签名:非对称加密可以用私钥进行签名,使用公钥验证签名,确保数据的完整性和身份的可靠性。
缺点:
  • 计算速度较慢,加密和解密操作比对称加密复杂。
  • 密钥管理复杂:公钥和私钥的管理和存储需要更加谨慎。

对称加密与非对称加密的结合

在实际应用中,往往将对称加密和非对称加密结合使用,取其优点。比如在 SSL/TLS 中:

  1. 非对称加密 用于初始的 密钥交换。客户端和服务器通过非对称加密算法交换密钥。
  2. 一旦密钥交换完成,使用 对称加密 来保护数据的传输。

这种结合方式既保证了密钥的安全传输,又能确保数据传输的高效性。

总结

  • 对称加密:用于数据量大且对加密/解密速度要求较高的场景,但需要解决密钥管理和分发问题。
  • 非对称加密:用于密钥交换、身份验证、数字签名等需要公开密钥和私钥的场景,安全性较高,但加密/解密速度较慢。

HTTP怎么实现断点续传?

HTTP 断点续传的实现

断点续传(Resume Download)是指在文件传输中断后,从中断的位置继续下载,避免从头开始下载整个文件。这种功能通常在下载大文件时非常重要,特别是在网络不稳定的情况下。

HTTP 协议通过 Range 请求头实现了断点续传功能,允许客户端请求文件的部分内容,而不是从头开始请求整个文件。下面是实现断点续传的具体过程。

1. 客户端请求部分数据(Range 请求)

当客户端想要下载文件的一部分时,它会在 HTTP 请求头中加入 Range 字段来指定所需的文件部分。Range 字段指明了客户端希望从文件的哪个位置开始接收数据。

请求头示例:

1
2
3
GET /largefile.zip HTTP/1.1
Host: example.com
Range: bytes=1000-1999

这个请求表示客户端希望从 largefile.zip 文件中的第 1000 字节到第 1999 字节之间的数据。如果请求成功,服务器将返回指定范围的数据。

  • bytes=1000-1999:表示请求从第 1000 字节到第 1999 字节的文件部分。

请求头中的 Range 字段可以有几种格式:

  • bytes=0-499: 从第 0 字节到第 499 字节。
  • bytes=500-999: 从第 500 字节到第 999 字节。
  • bytes=1000-: 从第 1000 字节开始,直到文件末尾。

2. 服务器响应指定范围的数据

服务器接收到带有 Range 请求头的请求后,会根据请求的范围返回对应的文件部分。服务器必须支持范围请求并能够处理这种请求。

响应头示例:

1
2
3
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-1999/5000
Content-Length: 1000
  • 206 Partial Content:表示服务器成功返回部分内容。
  • Content-Range: bytes 1000-1999/5000:表示服务器返回的是从第 1000 字节到第 1999 字节的数据,文件的总大小是 5000 字节。
  • Content-Length: 1000:表示返回的部分数据的大小是 1000 字节。

3. 继续请求剩余数据

如果客户端已经下载了一部分文件,它可以继续发送带有 Range 请求头的请求,获取剩余的文件数据。例如,如果客户端已经下载了文件的第 0 到 499 字节,它可以发送请求获取下一个部分的数据。

请求头示例:

1
2
3
GET /largefile.zip HTTP/1.1
Host: example.com
Range: bytes=500-999

4. 服务器的状态码

  • 206 Partial Content:表示请求范围内的部分内容成功返回。
  • 416 Range Not Satisfiable:如果客户端请求的范围无效或超出了文件的大小范围,服务器会返回 416 错误码。客户端可以根据文件的实际大小调整请求的范围。

5. 断点续传的实现步骤

  1. 客户端:计算已下载的字节数,并通过 Range 请求头告知服务器要下载的部分。
  2. 服务器:根据客户端的请求,返回对应范围的文件部分,并附带 Content-Range 响应头。
  3. 客户端:接收数据并将其保存到本地,继续请求剩余的文件部分,直到文件下载完成。

总结

HTTP 断点续传的实现原理主要依赖于 Range 请求头和 Content-Range 响应头,允许客户端请求文件的指定部分内容,服务器返回该部分数据。当下载过程中发生中断时,客户端可以从上次下载中断的位置继续请求文件,从而实现高效的断点续传。

HTTP有哪些缓存的机制?

HTTP 缓存机制主要通过使用各种 HTTP 头部来缓存静态资源,减少客户端和服务器之间的重复请求,提高资源加载效率,减少网络带宽的消耗。常见的 HTTP 缓存机制包括以下几种:

1. 缓存控制:Cache-Control

Cache-Control 是 HTTP/1.1 引入的缓存控制头,允许客户端和服务器控制缓存的行为。它可以指定多个缓存指令,决定资源是否缓存、缓存多久、缓存何种内容等。

常见的 Cache-Control 指令:

  • public:资源可以被任何缓存(浏览器缓存、代理缓存等)缓存。
  • private:资源只能由客户端缓存,不允许在共享缓存中缓存。
  • no-cache:不直接缓存资源,但可以缓存验证信息(如 ETag 或 Last-Modified)。每次请求时,客户端必须验证缓存是否过期。
  • no-store:完全不缓存资源。
  • **max-age=**:指定资源可以被缓存的最大时间(以秒为单位)。
  • must-revalidate:缓存过期后,必须重新验证资源。
  • proxy-revalidate:与 must-revalidate 类似,但只影响代理缓存。

例子:

1
Cache-Control: public, max-age=3600

表示该资源可以被任何缓存缓存,且最多缓存 3600 秒(即 1 小时)。

2. 最后修改时间(Last-Modified)

Last-Modified 头部告诉客户端资源最后修改的时间,客户端在请求资源时可以根据该时间判断缓存是否有效,避免不必要的资源下载。

例子:

1
Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT

3. 条件请求(If-Modified-Since)

If-Modified-Since 是客户端请求头的一部分,用于向服务器询问资源是否发生变化。如果自从 Last-Modified 时间以来资源没有变化,服务器将返回 304 Not Modified 状态码,表示客户端的缓存仍然有效。

例子:

1
If-Modified-Since: Wed, 21 Oct 2020 07:28:00 GMT

如果自该时间以来资源没有变化,服务器会返回 304 状态码。

4. 实体标签(ETag)

ETag 是服务器给资源生成的一个唯一标识符,通常基于资源的内容生成。客户端通过 If-None-Match 请求头将 ETag 值发送给服务器,服务器可以比较 ETag 值来判断资源是否修改。如果没有变化,服务器返回 304 Not Modified。

例子:

1
ETag: "5d8c72a6c2f2d"

客户端请求时:

1
If-None-Match: "5d8c72a6c2f2d"

如果 ETag 一致,则返回 304,客户端可以继续使用缓存的资源。

5. Expires(过期时间)

Expires 是 HTTP/1.0 中引入的缓存头,指定缓存资源的过期时间。与 Cache-Controlmax-age 类似,但 Expires 使用绝对日期/时间,表示资源的过期时间。

例子:

1
Expires: Wed, 21 Oct 2020 07:28:00 GMT

如果当前时间超过 Expires 指定的时间,缓存资源将被视为过期。

6. Pragma

Pragma 是 HTTP/1.0 中引入的缓存控制指令,常用于 HTTP/1.0 请求中。最常用的值是 no-cache,用于告诉缓存不使用缓存中的资源。

例子:

1
Pragma: no-cache

7. Vary

Vary 头部告诉缓存代理服务器在决定是否使用缓存时,应该考虑哪些请求头。常用的请求头有 Accept-EncodingUser-Agent 等。如果响应的缓存是基于某些请求头字段的,Vary 可以帮助避免缓存错误的资源。

例子:

1
Vary: Accept-Encoding

这表示缓存不同的 Accept-Encoding 值(如 gzip 和 deflate),以便返回不同的压缩格式。

8. Cache-Control 和 Expires 的关系

Cache-ControlExpires 都控制缓存的生命周期,但它们的优先级不同。Cache-Control 是 HTTP/1.1 的标准,优先级更高,而 Expires 是 HTTP/1.0 的标准,在 HTTP/1.1 中也仍然有效。

总结

HTTP 的缓存机制通过多个头部控制资源的缓存行为,常用的有:

  • Cache-Control:控制缓存策略。
  • Last-ModifiedIf-Modified-Since:基于时间戳的缓存机制。
  • ETagIf-None-Match:基于内容的缓存验证。
  • Expires:设置资源的过期时间。
  • Vary:指定哪些请求头字段影响缓存。

这些机制的目的是减少重复请求,提高缓存效率,提升页面加载速度,同时确保缓存的资源是最新的。

304状态码是什么?

304 状态码是 HTTP 协议中的一种响应状态码,表示未修改(Not Modified)。当客户端向服务器请求一个资源时,服务器可以返回 304 状态码,表示自从客户端上次请求以来,资源没有被修改过,客户端可以继续使用本地缓存的副本,而不需要重新下载资源。

使用场景:

304 状态码通常出现在条件请求的过程中。客户端在请求资源时,带上了一个条件(比如上次访问资源的 Last-Modified 时间戳,或者 ETag 标签)。如果服务器认为资源没有发生变化,它就会返回 304 状态码,告诉客户端可以使用本地缓存的副本。

常见的请求和响应流程:

  1. 客户端请求
    客户端请求资源时,服务器会返回资源以及一个 Last-ModifiedETag 响应头,用于标识该资源的版本。

    1
    2
    HTTP/1.1 200 OK
    Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
  2. 客户端带条件请求
    客户端下次请求资源时,使用 If-Modified-SinceIf-None-Match 头部,带上上次获取的 Last-ModifiedETag

    1
    HTTP/1.1 304 Not Modified
  3. 服务器响应
    如果资源未修改,服务器返回 304 状态码,并且不包含实体内容(资源的实际数据)。客户端会继续使用缓存的资源。

304 的特点:

  • 节省带宽:避免了重复下载相同的资源,提高了网络效率。
  • 客户端缓存:当服务器返回 304 时,客户端无需重新下载资源,只需使用本地缓存。
  • 快速响应:因为没有返回资源的实际内容,响应速度更快。

例子:

  1. 客户端请求资源

    1
    2
    GET /images/logo.png HTTP/1.1
    Host: www.example.com
  2. 服务器返回资源和 Last-Modified

    1
    2
    3
    HTTP/1.1 200 OK
    Content-Type: image/png
    Last-Modified: Wed, 21 Oct 2020 07:28:00 GMT
  3. 客户端使用 If-Modified-Since 请求

    1
    2
    3
    GET /images/logo.png HTTP/1.1
    Host: www.example.com
    If-Modified-Since: Wed, 21 Oct 2020 07:28:00 GMT
  4. 服务器返回 304 状态码

    1
    HTTP/1.1 304 Not Modified

总结:

304 状态码的目的是为了提高缓存效率,减少不必要的数据传输。它常用于配合 Last-ModifiedETag 进行条件请求,表示资源没有变化,客户端可以继续使用之前缓存的内容。