1.std::find 和 STL 容器中 find 的区别和效率
两者的核心区别在于设计目的 :std::find 是通用算法 ,适用于任何提供迭代器的容器;STL 容器的成员 find 则是为了利用容器的内部结构实现更高效的查找。
效率对比
std::find :线性搜索,复杂度 O(n)。它只是从头到尾依次比较,没有任何捷径。
容器的成员 find:效率取决于容器类型:
-
关联容器(set、map 等) :底层通常是红黑树,查找复杂度 O(log n) 。以一百万个元素的 set 为例,
std::find最坏情况需要约 1,000,000 次比较,而set::find最多只需约 40 次。 -
无序关联容器(unordered_set、unordered_map) :基于哈希表,平均复杂度 O(1),最坏情况 O(n)。
为什么会有这种差异
STL 的设计原则是:只有当成员函数比通用算法更快时,容器才提供自己的 find。
对于 vector、list 这类线性容器,成员 find 和 std::find 都是 O(n),所以它们不提供 成员 find,直接用 std::find 即可。而 set、map 有更优的树形或哈希结构,成员 find 能利用这些结构跳着找,效率远高于从头扫到尾的 std::find。
2.STL 容器三大类
一、序列容器(Sequence Containers)
序列容器不自动排序,元素的位置由插入顺序和操作决定。
| 容器 | 特点 |
|---|---|
vector |
动态数组,尾部插入高效,随机访问 O(1) |
deque |
双端队列,头尾插入高效,随机访问 O(1) |
list |
双向链表,任意位置插入删除 O(1),不支持随机访问 |
forward_list |
单向链表,比 list 更省空间 |
array |
固定大小数组,C++11 引入 |
string |
字符序列,本质类似 vector |
这些容器不会自动排序。即使你插入的元素恰好有序,那也是巧合,容器本身不维护顺序。
二、有序关联容器(Ordered Associative Containers)
有序关联容器会根据键 自动排序,底层通常是红黑树(一种平衡二叉搜索树)。
| 容器 | 特点 |
|---|---|
set |
键唯一,自动按升序排列 |
multiset |
键可重复,自动排序 |
map |
键值对,键唯一,按键排序 |
multimap |
键值对,键可重复,按键排序 |
核心特征:
-
插入、删除、查找都是 O(log n)。
-
遍历时元素始终按键有序。
-
可以自定义比较函数,比如
std::greater<int>实现降序。
三、无序关联容器(Unordered Associative Containers)
无序关联容器基于哈希表实现,C++11 引入。
| 容器 | 特点 |
|---|---|
unordered_set |
键唯一,无序 |
unordered_multiset |
键可重复,无序 |
unordered_map |
键值对,键唯一,无序 |
unordered_multimap |
键值对,键可重复,无序 |
核心特征:
-
平均查找、插入、删除是 O(1),最坏情况 O(n)。
-
遍历顺序不确定,取决于哈希函数和桶的分布。
-
需要为键类型提供哈希函数和相等比较。
四、对比总结
| 维度 | 序列容器 | 有序关联容器 | 无序关联容器 |
|---|---|---|---|
| 是否自动排序 | 否 | 是(按键) | 否 |
| 底层结构 | 数组/链表 | 红黑树 | 哈希表 |
| 查找效率 | O(n) | O(log n) | 平均 O(1) |
| 遍历顺序 | 插入顺序 | 按键有序 | 不确定 |
| 典型容器 | vector、list | set、map | unordered_set、unordered_map |
3.char a[] = "12345", *p = a; printf("%d\n", *(p+5)); 输出什么?
字符串字面量 "12345" 末尾会自动追加一个空字符 '\0' ,所以数组长度是 6,不是 5。
p = a 后,p 指向数组首元素 a[0]。
p + 5 表示指针向后移动 5 个元素,指向 a[5]。
*(p+5) 就是取出 a[5] 的值,即 '\0'。'\0' 的 ASCII 码值是 0 。%d 会把这个字符按整数输出,所以打印出 0。
4.获取字符串长度
| 选项 | 是否是获取长度的成员函数 | 说明 |
|---|---|---|
capacity() |
否 | 返回容量,不是长度 |
size() |
是 | 返回实际字符数 |
strlen() |
否 | C 库函数,非成员函数,不包含\0 |
length() |
是 | 返回实际字符数,与 size() 等价 |
size() 和 length() 的关系两者完全等价,返回同一个值,没有性能差异。
之所以有两个名字,是历史原因:length() 更符合字符串的语义 ,size() 则是为了和其他 STL 容器(如 vector、map)保持接口一致 。C++ 标准明确规定 size() 和 length() 的行为相同。
5.柔性数组
柔性数组是结构体末尾的未指定大小数组,不占结构体空间,通过一次 malloc 把头部和变长数据放在连续内存里。它的核心价值是减少内存分配次数(减少内存碎片)、保证内存连续、简化释放逻辑,常用于协议包、缓冲区等变长数据场景。
cpp
struct S {
int len;
int data[]; // 柔性数组,必须是最后一个成员
};
柔性数组的作用
1. 实现变长数据结构
可以在一次分配中创建"**头部 + 变长数据"**的整体,数据长度在运行时决定。
2. 内存连续,缓存友好
结构体头部和数据在同一块连续内存中,访问时局部性好,CPU 缓存命中率高。
3. 只需一次 malloc 和一次 free
相比"结构体里放一个指针,再单独分配数据"的做法,柔性数组减少了内存分配次数,也简化了释放逻辑
6.应用层协议
UDP 典型应用:DNS、DHCP、SNMP、TFTP
TCP 典型应用:FTP、SMTP、HTTP、HTTPS、Telnet
- DNS :域名解析,优先使用 UDP(53 端口);当报文长度超出 UDP 限制时,会改用 TCP。所以 DNS 会使用 UDP。
- FTP:只使用 TCP(21 控制端口,20 数据端口)
- SMTP:邮件发送,只使用 TCP(25 端口)
- HTTPS:加密网页,只使用 TCP(443 端口)
7.extern "C" 是什么?有什么作用?
extern "C" 是 C++ 中的一种链接指示(linkage specification) ,用来告诉 C++ 编译器:这段代码要按照 C 语言的规则进行编译和链接。它最常见的用途是让 C++ 代码能够正确调用 C 语言编写的函数,或者让 C 代码能够调用 C++ 编写的函数。
8.char指针强转后++
注意是什么类型的指针++,指针++是加指针类型的长度。
cpp
void foo2()
{
char *p = "Hello world";
p++; p++; // p 向后移动2次
int *pint = (int *)p;
pint++;
p = *(char **)pint;
}
字符串:H e l l o w o r l d
下标:0 1 2 3 4 5 6 7 8 9 10
char *p = "Hello world";→ p 指向H(下标 0)p++;p++;→ p +=2,现在指向l(下标 2)int *pint = (int *)p;强制转成 int 指针(32 位系统 int 占 4 字节)pint++;:int 指针自增,地址 + 4 字节。pint 原本指向下标 2,+4 → 指向下标 6,字符wp = *(char **)pint;取出这个地址的值,也就是字符w
9.Socket 编程中常用的系统调用
| 系统调用 | 作用 | 适用场景 |
|---|---|---|
socket() |
创建套接字,返回文件描述符 | 客户端、服务端 |
bind() |
将套接字绑定到本地地址和端口 | 服务端 |
listen() |
将套接字设为监听状态,准备接受连接 | 服务端 |
accept() |
从已完成连接队列中取出一个连接 | 服务端 |
connect() |
主动向服务端发起连接 | 客户端 |
send() / write() |
发送数据 | 双方 |
recv() / read() |
接收数据 | 双方 |
close() |
关闭套接字 | 双方 |
shutdown() |
半关闭连接(只关读或只关写) | 双方 |
10.迭代器类型
vector/deque → 随机访问迭代器Random Access Iterator
list → 双向迭代器Bidirectional iterator
forward_list → 前向迭代器Forward iterator
11.主机同网段通信和跨网段通信流程
| 项目 | 同网段通信 | 跨网段通信(同一物理交换机) |
|---|---|---|
| IP 地址 | 源 IP、目的 IP 全程不变 | 源 IP、目的 IP 全程不变 |
| 帧目的 MAC | 目标主机 MAC | 第一跳是网关 MAC,网关再封装为目的主机 MAC |
| 是否经过网关 | 不经过 | 必须经过网关(三层设备) |
| ARP 作用 | 获取目标主机 MAC | 获取网关 MAC,网关再获取目标主机 MAC |
| 转发层级 | 二层交换机转发 | 二层交换机 + 三层路由共同完成 |
12.BGP 的路由选择规则是怎样的?
当 BGP 收到到达同一目的地的多条路由时,会按照一套严格的优先级顺序进行比较,最终选出一条最优路由。这套规则的核心原则是:如果通过前面的属性已经能选出唯一最优,就不再比较后面的属性。 BGP 选路是一套逐级比较、先胜即止 的决策过程。最常用的"干预手段"集中在前几步:Weight(本地私有)、Local_Pref(AS 内统一策略)、AS_Path(影响跨 AS 选路)和 MED(影响入站流量)。其余步骤更多是在属性完全相同时的"兜底"规则。
BGP 选路规则中,协议首选值(PrefVal)和本地优先级(Local_Pref)是数值越大越优,其余大多数属性是数值越小或长度越短越优。
13.无重复字符的最长子串(LeetCode 3)
给定一个字符串(数组),找出其中不含有重复字符的最长子串的长度 。 例:
abcabcbb→ 3;bbbbb→1;pwwkew→3
滑动窗口(双指针)+ 哈希集合,时间复杂度 O (n),空间 O (min (m,n))
cpp
#include <iostream>
#include <unordered_set>
#include <string>
using namespace std;
int lengthOfLongestSubstring(string s) {
unordered_set<char> window;
int left = 0; // 窗口左边界
int maxLen = 0;
for(int right = 0; right < s.size(); right++){
// 如果当前字符已经在窗口中,不断移动左指针,移除重复字符
while(window.count(s[right])){
window.erase(s[left]);
left++;
}
window.insert(s[right]);
maxLen = max(maxLen, right - left + 1);
}
return maxLen;
}
int main() {
string str = "abcabcbb";
cout << lengthOfLongestSubstring(str) << endl;
return 0;
}
14.事务隔离级别
事务:
- A 原子性:事务要么全成,要么全失败回滚,undo log 实现
- C 一致性:业务数据前后合理,原子 + 隔离 + 持久共同保证
- I 隔离性 :多个事务并发,互相之间的可见程度,隔离级别解决
- D 持久性:提交修改永久保存,redo log 实现
并发事务会出现的 3 种问题
- 脏读 :一个事务读到另一个事务未提交的数据,如果对方回滚,读到无效脏数据
- 不可重复读 :同一个事务内,同一行数据,前后两次读,结果不一样。中间被别的事务 update 提交修改。(重点:update 修改行)
- 幻读 :同一个事务,同样查询条件,前后读,行数变多 / 变少,别的事务 insert/delete 提交。(重点:新增、删除行)
简单记:
脏读:读到未提交
不可重复读:行内容变了 (update)
幻读:行数变了 (insert/delete)
4 种隔离级别(MySQLInnoDB)
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | InnoDB 实现方式 |
|---|---|---|---|---|
| 读未提交 Read uncommitted | 允许 | 允许 | 允许 | 无锁,直接读 |
| 读已提交 Read committed (RC) | 禁止 | 允许 | 允许 | 快照读,普通 select 快照;行锁 |
| 可重复读 Repeatable‑Read (RR) MySQL 默认 | 禁止 | 禁止 | 禁止 | MVCC + Next‑Key Lock 临键锁 |
| 串行化 Serializable | 禁止 | 禁止 | 禁止 | 全部 select 加共享读锁,退化为串行执行,性能最差 |
15.死锁
两个或者多个事务,互相持有对方需要的锁,又互相等待对方释放锁,循环等待,谁都无法继续执行。
死锁必要 4 个条件
- 互斥条件:锁同一时刻只能一个事务占有
- 占有且等待:事务拿到一把锁,再去申请另一把锁,不释放已有锁
- 不可剥夺:锁不能被强行抢走,只能持有者主动释放
- 循环等待:事务之间形成环路等待。
16.io 多路复用的方法和底层数据结构
| 维度 | select | poll | epoll |
|---|---|---|---|
| 底层结构 | 位图数组 | pollfd 数组 | 红黑树 + 就绪链表 |
| fd 数量限制 | 1024 | 无 | 无 |
| 拷贝 fd 集合 | 每次调用都拷贝 | 每次调用都拷贝 | 仅注册时拷贝 |
| 扫描方式 | 线性扫描 O(n) | 线性扫描 O(n) | 回调,O(1) |
| 触发模式 | LT | LT | LT + ET |
| 跨平台 | 是 | 是 | Linux 专用 |
| 适用场景 | 少量连接 | 少量连接 | 高并发 |
水平触发(LT): 只要 fd 还有数据,每次 epoll_wait 都会通知。(默认)
边缘触发(ET): 只在状态变化时通知一次,必须一次性读完数据,通常配合非阻塞 I/O。
cpp
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
struct pollfd {
int fd; // 文件描述符
short events; // 关注的事件
short revents; // 实际发生的事件
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int epoll_create(int size); // 创建 epoll 实例
int epoll_ctl(int epfd, int op, int fd, // 注册/修改/删除 fd
struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, // 等待事件
int maxevents, int timeout);
17.http各版本区别,http和https的区别,以及https怎么加密?
各版本区别
**HTTP/0.9:**仅支持 GET,无 Header,只能传 HTML。
**HTTP/1.0:**增加 POST/HEAD 方法、请求头、状态码,支持多媒体。无连接(每次都要新建TCP连接)、队头阻塞
HTTP/1.1:长连接 (连接复用)、管道化 、支持 Host 头(虚拟主机)、断点续传。仍有队头阻塞(响应必须按序返回)
HTTP/2.0:二进制分帧 、多路复用 (一个连接并行多个请求)、头部压缩(HPACK)、服务端推送。消除了应用层的队头阻塞,但是TCP层仍然存在队头阻塞。
HTTP/3.0:基于UDP的QUIC协议,解决队头阻塞。
**队头阻塞:**一条连接上,前面一个请求没处理完,后面所有请求都被卡住排队等待。
二进制分帧:把原来完整的 HTTP 请求 / 响应消息 ,切割成多个二进制小片段,叫做帧 (frame) ,在同一个 TCP 连接里交错传输。
HTTP 与 HTTPS 的区别
HTTPS= HTTP + TLS/SSL(HTTP与TCP之间的加密层)
https加密传输,可验证身份,防篡改,需要CA证书,端口443.
HTTPS 采用混合加密 机制,非对称加密 用来安全地交换"会话密钥",而对称加密则用来高效地加密实际传输的数据。
加密过程
-
客户端发起请求:浏览器发送支持的加密算法列表和一个随机数给服务器。
-
服务器响应 :服务器选定加密算法,并发送数字证书(内含公钥)和另一个随机数。
-
验证证书 :浏览器用内置的 CA 根证书公钥验证服务器证书的签名和域名,确认服务器身份。
-
生成会话密钥 :浏览器生成一个 pre-master secret ,用服务器的公钥加密后发送。
-
计算会话密钥 :服务器用私钥解密 得到 pre-master secret,双方再结合之前的两个随机数,独立计算出相同的会话密钥。
-
加密通信 :握手完成,之后双方使用这个会话密钥(对称加密) 来加密和解密所有应用数据,效率高且安全。
18.SQL分类
- DDL 数据定义语言 Define:库、表、索引结构
create / alter / drop / truncate - DML 数据操纵语言 Manipulate:表里增删改数据
insert / update / delete / select - DCL 数据控制语言 Control :权限管理
grant授权、revoke回收权限 - TCL 事务控制语言 Transaction :事务
commit提交、rollback回滚、savepoint保存点
MySQL 内部执行顺序
-
FROM
-
JOIN ... ON:完成多表关联,生成中间大结果集
-
WHERE:对关联完的数据过滤
-
GROUP BY 分组
-
聚合函数计算
-
HAVING 过滤分组结果
-
SELECT 投影列、生成别名
-
ORDER BY排序
-
LIMIT
sql
SELECT c.name, COUNT(s.id) stu_cnt
FROM class c
LEFT JOIN student s
ON c.id = s.class_id
WHERE c.id > 1
GROUP BY c.id,c.name
HAVING COUNT(s.id)>=2
ORDER BY stu_cnt DESC
LIMIT 2;
ON:多表拼接的时候做条件,影响参与连接的行。WHERE:拼接完成后对整体结果过滤,会过滤掉 NULL。
三种连接:INNER JOIN 内连接 只保留两边匹配上的数据。LEFT JOIN 左连接(左外连接) 左边表全部保留;右边匹配不到,右边字段填NULL.RIGHT JOIN 右连接 右边表全部保留,左边匹配不到填 NULL.
19.C++:基类构造、基类析构里面调用虚函数,不会发生动态多态,只会调用本类版本的虚函数。
对象构造顺序:先基类构造,再派生类构造
基类构造函数执行(此时派生类对象还没有构造完成,虚表指向基类版本)
基类析构函数执行(派生部分已经析构,虚表切回基类)
**构造函数不允许声明为 virtual。**析构函数推荐写成虚函数,当基类指针指向派生类对象,delete 基类指针时,可以触发派生类析构,防止内存泄漏。
20.秩查询(Rank Query)
在一个数据结构中查询某个元素的排名 或名次。
支持秩查询:这个数据结构能够高效地回答某个元素的排名,或者查询第 k 小的元素是谁。
操作:rank(x)查询元素x的排名(比x小的元素个数),select(k)查询第k小的元素是什么。
支持秩查询的数据结构:
1. 平衡二叉搜索树(红黑树、AVL 树)
在树的每个节点上维护子树大小,就能在 O(log n) 时间内计算 rank 和 select。
-
rank(x):从根往下走,累加左子树大小。 -
select(k):从根往下走,根据左子树大小决定往左还是往右。
2. 树状数组(Fenwick Tree)
常用于动态排名问题。如果元素值域不大,可以用树状数组维护每个值出现的次数,然后:
-
rank(x)= 前缀和sum(1..x-1) -
select(k)= 树状数组上二分查找第 k 个前缀和
3. 线段树
和树状数组类似,每个节点维护区间内元素个数,支持 rank 和 select。
4. 跳表(Skip List)
每层维护节点数量和跨度,也能在 O(log n) 内完成 rank 和 select。
5. 有序数组
直接用二分查找,rank(x) 是 O(log n),但插入和删除是 O(n),只适合静态数据。
6. 数据库中的索引
B+ 树索引天然支持按顺序扫描和定位,某些数据库还支持 RANK() 窗口函数。
21.全局 map 拆分成多把分片锁,为什么会出现"死锁 + 读到未完成更新"?什么样的设计会导致这种故障?
把全局 map 拆成多把分片锁(sharded lock)本意是降低锁竞争,但分片锁场景下的死锁,通常源于跨分片操作 和加锁顺序不一致。
死锁:
1.需要同时锁多个分片,但加锁顺序不固定
最典型的是扩容(rehash)或跨分片迁移。比如从 4 个分片扩到 8 个,需要把旧分片的数据搬到新分片,这时要同时持有两个分片的锁。
2.业务操作需要原子地访问多个 key,而 key 落在不同分片(读写侧使用不同分片映射)同一个 key,读写可能落到不同分片,于是需要同时持有两把锁,且顺序可能交叉。
3.分片锁之上还有全局锁,且获取顺序混乱
避免死锁的常见做法:
-
固定加锁顺序:按分片编号从小到大加锁,所有线程遵守同一顺序。
-
避免跨分片原子操作:重新设计数据划分,让需要原子操作的 key 落在同一分片。
-
用 trylock + 回退:拿不到就释放已持有的锁,稍后重试。
-
用一把全局锁保护元数据(如扩容),分片锁只保护数据读写。
读到未完成更新:
- 写操作只锁了部分步骤
- 读操作没有加锁,或用了错误的同步方式
- 分片之间数据有关联,但更新不是原子的
避免读到未完成更新的常见做法:
-
读写都加锁:读也持有分片锁,保证看到完整状态。
-
用不可变数据 + 原子指针替换:写操作构造完整新对象,最后用原子指针一次性替换,读线程要么看到旧完整版,要么看到新完整版。
-
用 RCU(Read-Copy-Update):读无锁,写复制后延迟释放,保证读到的始终是完整版本。
-
明确同步边界:让加锁范围覆盖整个逻辑写操作,包括关联的计数、索引等。
-
用内存屏障或原子操作:防止编译器和 CPU 重排导致读到中间状态。
22.二维数组初始化和传递
cpp
vector<vector<int>> a(m,vector<int>(n));
//m行n列
//双层for循环赋值
//传参
func(vector<vector<int>>& mat){}
//普通数组
const MAXN = 105;
int a[m][n]//mn是常数
void func(int p[][MAXN],int m,int n);//列数必须写
23.索引的约束
索引的"约束"核心有两点:唯一索引/主键索引是唯一性和主键完整性的实现机制 ;同时索引的创建受键长、列类型、NULL 语义、数量和维护成本等规则限制。
| 约束类型 | 与索引的关系 | 说明 |
|---|---|---|
| 主键约束(PRIMARY KEY) | 自动创建唯一索引(聚簇或非聚簇) | 主键列不允许 NULL,且值唯一 |
| 唯一约束(UNIQUE) | 自动创建唯一索引 | 允许多个 NULL(多数数据库),但非 NULL 值必须唯一 |
| 外键约束(FOREIGN KEY) | 被引用列必须有索引(主键/唯一索引),外键列建议建索引 | 索引本身不强制外键,但外键检查依赖被引用列的索引 |
也就是说,唯一索引本身就是一种约束:它保证索引键值的唯一性。主键 = 唯一索引 + NOT NULL。
24.函数重载及二义性
函数重载是指在同一作用域内定义多个同名函数 ,但它们的参数列表不同(参数个数、类型或顺序不同)。编译器根据调用时实参的个数和类型来选择最合适的函数。
重载的条件
-
函数名相同
-
参数列表不同(个数、类型、顺序至少一项不同)
-
返回值类型不能作为重载依据
-
仅返回类型不同不构成重载
cpp
int add(int a, int b); // ① int,int
double add(double a, double b); // ② double,double
int add(int a, int b, int c); // ③ 三个参数
add(1, 2); // 调用①
add(1.5, 2.5); // 调用②
add(1, 2, 3); // 调用③
编译器选择函数的过程分三步:
-
候选函数集 :同一作用域中所有同名、可见的函数
-
可行函数集:实参数目匹配(考虑默认参数),且每个实参都能隐式转换到形参类型
-
最佳可行函数:转换序列最优者
当编译器无法选出唯一的最佳函数 时,产生二义性错误(编译错误,而非运行时错误)。
避免方案:
-
显式类型转换 :
f(static_cast<double>('a')); -
调整重载设计:避免转换等级相同的重载
-
避免默认参数与重载混用
-
使用命名空间或不同函数名
-
强制转换指针/引用:明确 const 或类型
25.map 和unordered_map 的区别
1.底层结构
map:红黑树,元素有序;增删查 O(logn)
unordered_map:哈希表,元素无序;平均O(1),冲突最坏O(n)
2.缓存
map:节点分散,CPU缓存命中率差
unordered_map:哈希桶连续,缓存表现更好
3.多线程
STL两者都不是线程安全,多读可以,有写必须外部加锁。
map 修改调整树结构
unordered_map 写可能触发rehash,全部迭代器失效,风险更高
26.CPU密集任务 和 IO密集任务,多线程的区别
- CPU密集型:大量运算,CPU一直处于满载。
线程数建议≈CPU物理核心数;线程开太多,上下文切换开销暴涨,性能下降。
- IO密集型:大部分时间在等待网络、磁盘IO,CPU空闲。
可以设置多于核心数的线程;线程大部分时间阻塞等待IO,用来提升整体吞吐。
本质:CPU密集瓶颈在CPU算力;IO密集瓶颈在IO等待。
27.寻找公共祖先
①两个数组求公共元素(交集)
1.哈希集合:一个数组存入set,遍历第二个数组查找,时间O(n),空间O(n)
2.排序+双指针:排序之后双指针扫描,时间O(nlogn),空间O(1)(不计输出)
②二叉树两个节点最近公共祖先 LCA
递归DFS:从当前节点开始向下对比
如果当前节点为空,或者等于A、B,返回当前节点。递归左右子树。
左右返回都不为空,当前就是LCA
一边非空返回那一边,两边空返回null。
28.三级模式结构 和二级映像(映射)
三层结构:数据库系统的三级模式
| 层次 | 名称 | 面向对象 | 作用 |
|---|---|---|---|
| 外层次 | 外模式(子模式/用户模式) | 最终用户、应用程序 | 描述用户能看到的数据视图,一个数据库可有多个外模式 |
| 概念层 | 模式(逻辑模式/概念模式) | 全体数据 | 描述全部数据的逻辑结构、联系、完整性约束,全库只有一个 |
| 内层 | 内模式(存储模式/物理模式) | 存储系统 | 描述数据在磁盘上的物理存储结构、存取方式、索引组织,全库只有一个 |
对应关系:外模式---模式---内模式,从用户视图到全局逻辑再到物理存储,逐层抽象。
两层映射:二级映像
| 映射 | 连接 | 保证 |
|---|---|---|
| 外模式/模式映像 | 外模式 ↔ 模式 | 逻辑独立性:模式改变时,调整映像即可,应用程序(外模式)不变 |
| 模式/内模式映像 | 模式 ↔ 内模式 | 物理独立性:内模式(存储结构、索引、文件组织)改变时,调整映像即可,逻辑模式不变 |
这两层映射是三级模式结构的"粘合剂",也是数据独立性的实现机制:
-
外模式/模式映像 → 逻辑独立性
-
模式/内模式映像 → 物理独立性
29.NULL和nullptr的区别
nullptr 是有类型的空指针常量(C++11),NULL 通常只是整数 0 的宏。
nullptr 不能被隐式转换为整数,避免了"空指针被当成 0 参与算术"的隐患。
| 场景 | 用 NULL |
用 nullptr |
|---|---|---|
| 函数重载 | 可能选错重载或二义 | 明确匹配指针 |
| 模板推导 | 推导成整数 | 推导成 nullptr_t |
| 赋给整数 | 合法 | 编译错误 |
| 现代 C++ 代码 | 不推荐 | 推荐 |
-
nullptr可以隐式转换为任意指针类型和bool(值为false),也可以和指针比较。 -
sizeof(nullptr)等于sizeof(std::nullptr_t),通常与指针大小相同,但标准不保证。 -
在 C 语言里没有
nullptr,只能用NULL(C23 引入了nullptr关键字,但语义与 C++ 略有不同)。 -
C++ 中应优先使用
nullptr,NULL主要为了兼容旧代码。
30.RAII是什么?除了智能指针还能用到哪里?比如文件管理?
RAII(Resource Acquisition Is Initialization,资源获取即初始化)本质是:把资源的生命周期绑定到对象的生命周期上------对象构造时获取资源,对象析构时释放资源。
std::unique_ptr / shared_ptr 是"管理堆内存"的 RAII 封装。但 RAII 的适用范围远不止内存,凡是有"获取---释放"配对操作的系统资源都适用。
RAII 的其他典型应用
| 资源类型 | 获取 / 释放 | RAII 封装 |
|---|---|---|
| 互斥锁 | lock() / unlock() |
std::lock_guard、std::unique_lock、std::scoped_lock |
| 堆内存 | new / delete |
unique_ptr、shared_ptr |
| 文件 | fopen / fclose、open / close |
fstream、自定义 File |
| 数据库连接/事务 | connect / disconnect、BEGIN / COMMIT/ROLLBACK |
连接池句柄、Transaction 类 |
| 网络 socket | socket / close |
Socket 类 |
| 动态库 | dlopen / dlclose |
Library 类 |
| GUI 资源 | CreatePen / DeleteObject(Win32 GDI) |
unique_ptr + 删除器 |
| 内存映射 | mmap / munmap |
MappedFile 类 |
| 作用域退出时执行代码 | --- | scope_guard / std::unique_ptr<T, Deleter> 的 lambda 删除器 |
| 性能计时 | 构造记开始,析构记结束 | ScopedTimer |
| 异常安全的状态回滚 | 构造保存旧值,析构恢复 | ScopedRollback |
31.函数的声明和定义的区别,次数?
声明是"告诉编译器有这么个东西",定义是{"真正把它实现出来"}
声明可以多次(同作用域内签名一致),定义只能一次(ODR,inline/模板/static 等例外)。声明让编译器"认识"函数,定义让链接器"找到"函数;调用处要先有声明,链接时要恰好一个定义。
-
调用函数前至少要有声明(否则 C++ 报未声明;C 里隐式声明已废弃/禁止)。
-
链接时需要恰好一个定义(对非 inline、非模板的外部链接函数)。
-
只声明不定义 → 链接错误(undefined reference)。
-
只定义不声明、但调用点在定义之前 → 编译错误(C++ 要求先声明后使用)。
32.TCP和UDP的主要区别,二者各举至少两个应用的例子
| 维度 | TCP | UDP |
|---|---|---|
| 连接性 | 面向连接,通信前要三次握手,结束后四次挥手 | 无连接,直接发数据报,不建立/拆除连接 |
| 可靠性 | 可靠:确认应答(ACK)、超时重传、序号、去重、按序交付 | 不可靠:不保证到达、不保证顺序、不重传、可能丢包/重复 |
| 传输方式 | 面向字节流,把数据看成无边界的字节序列,需应用层自己划分消息边界 | 面向数据报,保留消息边界,一次发送对应一个数据报 |
| 流量/拥塞控制 | 有滑动窗口(流量控制)和拥塞控制(慢启动、拥塞避免等) | 没有,发送速率由应用自己控制 |
| 头部开销 | 头部至少 20 字节(含选项可更大) | 头部固定 8 字节 |
| 连接对象 | 一对一(点对点) | 支持一对一、一对多、多对多(广播/组播) |
| 顺序性 | 保证数据按序到达 | 不保证顺序 |
| 典型特点 | 慢但稳,开销大 | 快但不可靠,开销小、实时性好 |
一句话概括:TCP 用可靠性换开销,UDP 用可靠性换效率和实时性。
TCP 的典型应用(需要可靠、有序、完整传输)
-
HTTP / HTTPS(Web 浏览):网页、图片、API 请求要求数据完整有序到达,丢一个字节页面就坏了,必须用 TCP。
-
文件传输(FTP、SFTP、scp):文件内容不能有缺失或错序,依赖 TCP 的重传和按序交付。
-
电子邮件(SMTP、POP3、IMAP):邮件正文和附件必须完整送达。
-
远程登录(SSH、Telnet):交互式命令要求按序、可靠传输。
UDP 的典型应用(追求低延迟、能容忍少量丢包,或需要广播/组播)
-
DNS 查询:一次请求一个响应,报文小、要求快,UDP 开销小;丢包可由应用层重试。(大响应或 DNSSEC 等场景会改用 TCP)
-
实时音视频(VoIP、视频会议、直播、RTP):宁可丢几帧也不愿因重传导致卡顿,低延迟优先。
-
在线游戏:位置、动作状态频繁更新,旧数据过期即无意义,重传反而有害。
-
DHCP、SNMP、TFTP:DHCP 需要广播发现服务器;TFTP 是轻量文件传输,靠应用层自己做确认。
-
QUIC(HTTP/3 的底层):基于 UDP 在用户态实现可靠传输,兼顾低延迟与可控性。
33.为什么TCP可靠
TCP通过有连接、序列号、超时重传、拥塞控制、滑动窗口、校验和的机制。
-
握手建立连接,同步序号。
-
数据分段编号(SEQ),发送并启动定时器。
-
接收方校验和验证 → 按序缓存 → 回累积 ACK。
-
发送方收到 ACK 确认;没收到就超时重传或快速重传。
-
接收方去重、重排,按序交给应用。
-
全程用滑动窗口控制接收方压力,用拥塞控制控制网络压力。
-
结束时四次挥手,确保双方数据都处理完。
序号 + 确认应答(ACK):可靠传输的基础
TCP 把应用数据看成字节流 ,给每个字节编号。每个报文段的头部带 SEQ(本段第一个字节的序号) ,接收方回复 ACK(期望收到的下一个字节序号)。
发送方: SEQ=1000, 长度=500 → 接收方
接收方: ACK=1500 → 发送方 (1000~1499 都收到了,下次要 1500)
-
ACK 是累积确认:ACK=1500 表示 1500 之前的所有字节都收到了。
-
发送方收到 ACK 才知道数据真正到达,否则认为可能丢了。
-
序号还让接收方能识别重复 (已收到的序号再收到就丢弃)和重排乱序。
超时重传:丢了就再发
发送方每发一个段就启动重传定时器(RTO,Retransmission Timeout)。如果在 RTO 内没收到对应 ACK,就重传该段。
-
RTO 不是固定值,而是根据 RTT(往返时间)动态估算(Jacobson/Karels 算法),避免设得太短误重传、太长恢复慢。
-
超时后通常还会指数退避(RTO 翻倍),避免网络拥塞时雪上加霜。
这解决了"数据段丢失"和"ACK 丢失"两种情况。
快速重传:不等超时,提前补发
只靠超时重传恢复太慢。TCP 还有快速重传 :如果发送方连续收到 3 个重复 ACK(说明接收方一直没收到某个段,但后面的段到了),就立即重传缺失段,不等 RTO 到期。
接收方收到 1,2,4,5 → 一直回 ACK=3(重复 ACK)
发送方收到 3 个 ACK=3 → 立即重传段 3
配合 SACK(选择性确认),接收方还能告诉发送方"我收到了哪些不连续区间",发送方只重传真正缺失的部分,效率更高。
校验和:检测传输错误
TCP 头部有 16 位校验和,覆盖头部 + 数据 + 伪头部(源/目的 IP 等)。接收方校验失败就丢弃该段,不回复 ACK,发送方靠重传恢复。这防止了数据在链路中比特翻转后被当成正确数据交付。
(注意:TCP 校验和是较弱的校验,不能防恶意篡改,安全性靠 TLS 等上层。)
去重与按序交付
-
接收方维护接收缓冲区 和期望序号。收到乱序段先缓存,等缺失段补齐后按序交给应用。
-
收到已确认过的序号(重复段)直接丢弃,保证应用不会读到重复数据。
-
这保证应用看到的是无重复、按序、完整的字节流。
滑动窗口:流量控制(别压垮接收方)
接收方在 ACK 里带窗口大小(Window),告诉发送方"我还能接收多少字节"。发送方据此限制在途未确认数据量,不超过接收方缓冲能力。
-
接收方缓冲快满时窗口变小甚至为 0,发送方暂停。
-
窗口为 0 时发送方会定期发窗口探测,防止死锁。
这解决的是接收方处理不过来的问题,与网络拥塞无关。
拥塞控制:别压垮网络
流量控制管接收方,拥塞控制管网络。发送方维护拥塞窗口(cwnd) ,实际发送量取 min(cwnd, 接收窗口)。经典算法包括:
-
慢启动:cwnd 从小开始,每 RTT 翻倍,快速探知网络容量。
-
拥塞避免:到阈值后线性增长,谨慎试探。
-
快速重传 / 快速恢复:检测到丢包时减小 cwnd 并恢复,避免网络崩溃。
-
现代还有 CUBIC、BBR 等算法。
丢包被 TCP 视为拥塞信号,主动降速,这是 TCP 能在互联网上"友好共存"的关键,也间接提升了整体可靠性(网络不因过载而大面积丢包)。
连接管理:三次握手与四次挥手
-
三次握手:双方交换初始序号(ISN)并确认对方收发能力,确保连接建立时双方状态一致,防止历史重复连接请求造成错误连接。
-
四次挥手:双方各自确认数据发完再关闭,配合 TIME_WAIT 状态,保证最后 ACK 能到达、旧连接报文不在新连接中混淆。
-
连接期间双方维护状态机(序列号、窗口、定时器),任何异常状态都有对应处理。
34.什么是粘包和拆包,怎么处理粘包
粘包:发送方连续发小包,Nagle 算法合并;接收方缓冲区大或读取不及时,一次读出多条。
拆包:消息大于 MSS 或发送/接收缓冲区,TCP 分段;网络 MTU 限制导致分片到达。
TCP 是面向字节流 的,不保留消息边界。发送方多次 send 的数据,接收方可能一次 recv 读到多条(粘包 ),也可能一条消息分多次才读完(拆包/半包 )。所以严格说,粘包拆包不是 TCP 的缺陷,而是应用层没有自己划清消息边界导致的现象。UDP 面向数据报、保留边界,一般没有这个问题。
粘包拆包的本质是 TCP 字节流不保留消息边界;处理办法是在应用层协议里用固定长度、分隔符或长度字段定义边界,接收方用缓冲区循环解析、拼够一条再处理,同时做好最大长度校验和缓冲区上限。 实际项目里最常用的是"长度字段 + 缓冲区状态机"。
接收方维护一个缓冲区 + 状态机:
-
recv到的数据先追加到缓冲区,不假设一次读全。 -
循环尝试解析:
-
缓冲区不够头部长度 → 退出等下次;
-
解析出长度,校验不超过最大消息长度(防恶意超长导致内存耗尽);
-
缓冲区不够 body 长度 → 退出等下次;
-
够 → 取出一条完整消息,从缓冲区移除,交给业务处理,继续循环。
-
-
注意长度字段本身也可能被拆包,所以要先保证能读满头部。
-
缓冲区要设上限,防止攻击;解析用循环而不是递归,避免一次读大量数据时栈问题。