Cpp面筋

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

  1. char *p = "Hello world"; → p 指向H(下标 0)
  2. p++;p++; → p +=2,现在指向l(下标 2)
  3. int *pint = (int *)p; 强制转成 int 指针(32 位系统 int 占 4 字节)
  4. pint++;:int 指针自增,地址 + 4 字节。pint 原本指向下标 2,+4 → 指向下标 6,字符w
  5. p = *(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 种问题

  1. 脏读 :一个事务读到另一个事务未提交的数据,如果对方回滚,读到无效脏数据
  2. 不可重复读 :同一个事务内,同一行数据,前后两次读,结果不一样。中间被别的事务 update 提交修改。(重点:update 修改行)
  3. 幻读 :同一个事务,同样查询条件,前后读,行数变多 / 变少,别的事务 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 个条件

  1. 互斥条件:锁同一时刻只能一个事务占有
  2. 占有且等待:事务拿到一把锁,再去申请另一把锁,不释放已有锁
  3. 不可剥夺:锁不能被强行抢走,只能持有者主动释放
  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 采用混合加密 机制,非对称加密 用来安全地交换"会话密钥",而对称加密则用来高效地加密实际传输的数据。

加密过程

  1. 客户端发起请求:浏览器发送支持的加密算法列表和一个随机数给服务器。

  2. 服务器响应 :服务器选定加密算法,并发送数字证书(内含公钥)和另一个随机数。

  3. 验证证书 :浏览器用内置的 CA 根证书公钥验证服务器证书的签名和域名,确认服务器身份。

  4. 生成会话密钥 :浏览器生成一个 pre-master secret ,用服务器的公钥加密后发送。

  5. 计算会话密钥 :服务器用私钥解密 得到 pre-master secret,双方再结合之前的两个随机数,独立计算出相同的会话密钥。

  6. 加密通信 :握手完成,之后双方使用这个会话密钥(对称加密) 来加密和解密所有应用数据,效率高且安全。

18.SQL分类

  1. DDL 数据定义语言 Define:库、表、索引结构create / alter / drop / truncate
  2. DML 数据操纵语言 Manipulate:表里增删改数据insert / update / delete / select
  3. DCL 数据控制语言 Control :权限管理grant授权、revoke回收权限
  4. TCL 事务控制语言 Transaction :事务commit提交、rollback回滚、savepoint保存点

MySQL 内部执行顺序

  1. FROM

  2. JOIN ... ON:完成多表关联,生成中间大结果集

  3. WHERE:对关联完的数据过滤

  4. GROUP BY 分组

  5. 聚合函数计算

  6. HAVING 过滤分组结果

  7. SELECT 投影列、生成别名

  8. ORDER BY排序

  9. 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);     // 调用③

编译器选择函数的过程分三步:

  1. 候选函数集 :同一作用域中所有同名、可见的函数

  2. 可行函数集:实参数目匹配(考虑默认参数),且每个实参都能隐式转换到形参类型

  3. 最佳可行函数:转换序列最优者

当编译器无法选出唯一的最佳函数 时,产生二义性错误(编译错误,而非运行时错误)。

避免方案:

  1. 显式类型转换 :f(static_cast<double>('a'));

  2. 调整重载设计:避免转换等级相同的重载

  3. 避免默认参数与重载混用

  4. 使用命名空间或不同函数名

  5. 强制转换指针/引用:明确 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密集任务,多线程的区别

  1. CPU密集型:大量运算,CPU一直处于满载。

线程数建议≈CPU物理核心数;线程开太多,上下文切换开销暴涨,性能下降。

  1. 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 的典型应用(需要可靠、有序、完整传输)

  1. HTTP / HTTPS(Web 浏览):网页、图片、API 请求要求数据完整有序到达,丢一个字节页面就坏了,必须用 TCP。

  2. 文件传输(FTP、SFTP、scp):文件内容不能有缺失或错序,依赖 TCP 的重传和按序交付。

  3. 电子邮件(SMTP、POP3、IMAP):邮件正文和附件必须完整送达。

  4. 远程登录(SSH、Telnet):交互式命令要求按序、可靠传输。

UDP 的典型应用(追求低延迟、能容忍少量丢包,或需要广播/组播)

  1. DNS 查询:一次请求一个响应,报文小、要求快,UDP 开销小;丢包可由应用层重试。(大响应或 DNSSEC 等场景会改用 TCP)

  2. 实时音视频(VoIP、视频会议、直播、RTP):宁可丢几帧也不愿因重传导致卡顿,低延迟优先。

  3. 在线游戏:位置、动作状态频繁更新,旧数据过期即无意义,重传反而有害。

  4. DHCP、SNMP、TFTP:DHCP 需要广播发现服务器;TFTP 是轻量文件传输,靠应用层自己做确认。

  5. QUIC(HTTP/3 的底层):基于 UDP 在用户态实现可靠传输,兼顾低延迟与可控性。

33.为什么TCP可靠

TCP通过有连接、序列号、超时重传、拥塞控制、滑动窗口、校验和的机制。

  1. 握手建立连接,同步序号。

  2. 数据分段编号(SEQ),发送并启动定时器。

  3. 接收方校验和验证 → 按序缓存 → 回累积 ACK。

  4. 发送方收到 ACK 确认;没收到就超时重传或快速重传。

  5. 接收方去重、重排,按序交给应用。

  6. 全程用滑动窗口控制接收方压力,用拥塞控制控制网络压力。

  7. 结束时四次挥手,确保双方数据都处理完。

序号 + 确认应答(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 字节流不保留消息边界;处理办法是在应用层协议里用固定长度、分隔符或长度字段定义边界,接收方用缓冲区循环解析、拼够一条再处理,同时做好最大长度校验和缓冲区上限。 实际项目里最常用的是"长度字段 + 缓冲区状态机"。

接收方维护一个缓冲区 + 状态机:

  1. recv 到的数据先追加到缓冲区,不假设一次读全。

  2. 循环尝试解析:

    • 缓冲区不够头部长度 → 退出等下次;

    • 解析出长度,校验不超过最大消息长度(防恶意超长导致内存耗尽);

    • 缓冲区不够 body 长度 → 退出等下次;

    • 够 → 取出一条完整消息,从缓冲区移除,交给业务处理,继续循环。

  3. 注意长度字段本身也可能被拆包,所以要先保证能读满头部。

  4. 缓冲区要设上限,防止攻击;解析用循环而不是递归,避免一次读大量数据时栈问题。

相关推荐
道尔柯南24 分钟前
C++ string 类详解:从常用接口到深拷贝,彻底掌握字符串处理
android·java·c++
十二4 小时前
【洛谷题解】P1083 [NOIP 2012 提高组] 借教室(二分答案 + 差分)
数据结构·c++·算法·蓝桥杯
D e re k ZH6 小时前
C++ 从零入门(一):基础语法与程序结构
数据结构·c++·算法
zjxtxdy7 小时前
C++面向对象程序开发-类与对象
开发语言·c++
東隅已逝,桑榆非晚9 小时前
算法6. 双指针法-查找总价格为目标的两个商品
c++·学习·算法
qq_4069810310 小时前
c++拷贝构造函数
开发语言·c++
HUANGZHENXUAN12311 小时前
最大子矩阵和:从 O(n⁴) 到 O(n³) 的列压缩 + Kadane
c语言·前端
H.莓飛12 小时前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio
Rabitebla12 小时前
【Linux操作系统】进程篇(十一):进程程序替换——从底层原理到 exec 函数族
linux·服务器·c++·笔记·学习·算法