字节跳动 醒图 AI 应用客户端开发实习一面:TCP/UDP、HTTPS 与 C++ 多态
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
先拷打实习经历和项目,然后八股。
- TCP 和 UDP 的区别?
- TCP 怎么保证可靠的?
- TCP 和 UDP 的适用场景?
- HTTP 和 HTTPS 的区别?
- HTTPS 的过程?
- HTTPS 是对称加密还是非对称加密?
- 讲一下常用排序算法。
- 为什么快排比较快?
- 堆排序的思路?
- 为什么快排比堆排序厉害?
- 索引有什么用?
- 索引的数据结构是什么?
- B+ 树做索引为什么快?
- C++ 多态是怎么实现的?
- 进程与线程的区别?
- 进程之间通信方式?
- 讲一下三个智能指针。
算法题:最短路径和。
《参考解析》
TCP 与 UDP 的区别:TCP 面向连接、可靠、有序、有流量控制与拥塞控制,以字节流交付,头部 20 字节起;UDP 无连接、不保证到达与顺序、没有流控,以数据报交付,头部 8 字节。TCP 的可靠是用序列号 + 确认 + 超时重传 + 滑动窗口 + 校验和这一套换来的,代价是延迟和队头阻塞;UDP 什么都不保证,换来的是低延迟和自由。
TCP 靠什么保证可靠:① 序列号与确认(ACK)——每个字节都有编号,接收方回 ACK 告知已收到哪里;② 超时重传——发送方起定时器,超时未收到确认就重发,RTO 按 RTT 自适应估算;③ 快速重传——收到三个重复 ACK 立即重发,不等超时;④ 校验和——检测传输中的比特错误;⑤ 滑动窗口与流量控制——接收方通告可用窗口,防止发送过快压垮接收端;⑥ 拥塞控制——慢启动、拥塞避免、快重传快恢复,按网络状况调整发送速率;⑦ 连接管理——三次握手确认双方收发能力与初始序列号,四次挥手保证双向数据都发完。还要说明 TCP 保证的是「字节流可靠有序」,不保证应用层消息边界,粘包要靠应用层协议自己划。
适用场景:TCP 用于对完整性要求高、能容忍一点延迟的场景——HTTP / HTTPS、文件传输、邮件、数据库连接、SSH。UDP 用于对实时性敏感、丢了可以重发或直接丢弃的场景——音视频通话、直播、在线游戏、DNS 查询、QUIC(HTTP/3)在 UDP 上自建可靠性。
HTTP 与 HTTPS 的区别:HTTPS 就是 HTTP 加了一层 TLS。差别在三处:加密(HTTP 明文传输,中间任何一跳都能看到和篡改;HTTPS 对内容加密)、身份认证(HTTPS 用 CA 签发的证书证明「你连的确实是这个域名」,防中间人冒充)、完整性(TLS 的 MAC / AEAD 保证内容没被篡改)。默认端口 HTTP 80、HTTPS 443。代价是握手增加 RTT、加解密消耗 CPU。
HTTPS 握手过程:TLS 1.2 完整握手是 2-RTT:ClientHello(版本、密码套件列表、客户端随机数、SNI)→ ServerHello(选定套件、服务端随机数)+ Certificate(证书链)→ 客户端验证证书(CA 是否可信、域名是否匹配、有效期、吊销状态)→ 密钥交换(RSA 加密 pre-master,或 ECDHE 交换参数)→ 双方用随机数导出会话密钥 → ChangeCipherSpec + Finished 校验握手完整性 → 开始对称加密传输。TLS 1.3 精简到 1-RTT(重连支持 0-RTT),只保留前向安全的 ECDHE 类密钥交换,证书也加密传输。
对称还是非对称:两者都用,分工不同。非对称加密(RSA / ECDHE)只用在握手阶段——协商出会话密钥、验证服务端身份,因为它能解决「双方事先没有共享密钥」的问题,但计算慢。对称加密(AES-GCM / ChaCha20-Poly1305)用于握手之后的所有数据传输,因为快一到两个数量级。所以标准答法是「用非对称协商密钥,用对称加密数据」。ECDHE 的额外价值是前向安全——私钥将来泄露也解不开历史流量。
常用排序算法:按复杂度分三档。O(n²)——冒泡(相邻交换,稳定)、插入(局部有序时极快,稳定,是小数组的实用选择)、选择(每次选最小,不稳定)。O(n log n)——归并(分治 + 合并,稳定,需要 O(n) 额外空间)、快排(选 pivot 分区后递归,平均 O(n log n)、最坏 O(n²),不稳定,原地)、堆排序(建堆后反复取堆顶,O(n log n) 稳定复杂度,不稳定,原地)。线性——计数排序、桶排序、基数排序,适用于值域有限或可分解的整数。工程实现(如 introsort)是混合策略:快排为主,递归过深退化成堆排序,小区间切插入排序。
快排为什么快:三个原因。① 常数因子小——原地交换、循环体极简、顺序访问内存对 CPU 缓存和分支预测友好;② 平均复杂度 O(n log n) 且实践中常数低,分区过程一次线性扫描就完成划分;③ 递归结构可并行、可分块,对大数组的局部性好。它的弱点是 pivot 选得差会退化到 O(n²)(已排序数组用首元素做 pivot 就是典型),所以工程上用随机 pivot 或三数取中缓解。
堆排序思路:先把数组原地建成大顶堆(从最后一个非叶节点 n/2 - 1 开始向前做下沉 sift-down,O(n) 建堆),然后循环 n-1 次:把堆顶(当前最大值)与末尾元素交换,堆大小减一,对新堆顶做一次下沉恢复堆性质。最终数组升序。整体 O(n log n),原地,但常数比快排大。
快排为什么比堆排序快:虽然渐近复杂度相同,实际快一个明显量级,原因是内存访问模式。快排是顺序扫描数组、访存局部性极好,缓存命中率高;堆排序的 sift-down 是沿着堆的层级跳跃访问(下标 i 的父子在 2i+1 / 2i+2),跨度大、缓存不友好,几乎每次比较都可能 cache miss;此外堆排序的交换次数多、分支预测难。另一个角度是快排的分区把问题切成两个子问题后各自连续处理,而堆排序每次只归位一个元素。所以库实现普遍以快排为主,堆排序只在快排递归过深时兜底(保证最坏 O(n log n))。
索引的作用:把「全表扫描」换成「按有序结构定位」。具体收益是查询变快(从 O(n) 到 O(log n))、排序与分组可以借索引顺序免掉额外排序、唯一索引还能当约束用。代价是写入变慢(每次 INSERT / UPDATE / DELETE 都要维护索引结构)、占额外存储、优化器选错索引时反而更慢。所以索引不是越多越好,要按查询模式设计,尤其注意复合索引的字段顺序(最左前缀)。
索引的数据结构:MySQL InnoDB 用 B+ 树,哈希索引(Memory 引擎、或 InnoDB 的自适应哈希)用于等值查询。B+ 树的特点是:非叶节点只存键、不存数据,所以一个节点能塞下更多键、树更矮(通常 3~4 层就能覆盖千万级数据);所有数据都在叶子节点且叶子之间用双向链表相连,范围查询和排序可以顺着链表扫,不用回树;每个节点大小对齐磁盘页(16KB),一次 IO 读一整页。
B+ 树做索引为什么快:核心是把随机 IO 次数压到很低。① 树矮——非叶节点只放键和指针,扇出大(一个 16KB 页能放几百上千个键),千万级数据的树高只有 34 层,一次查询最多 34 次页读取,其中非叶层通常已在 buffer pool 里缓存,真正落地 IO 往往只有一两次;② 叶子节点链表让范围查询只需定位起点再顺序扫,不用回树;③ 节点大小与磁盘页对齐,一次读就是一页,没有额外开销;④ 相比 B 树,B+ 树把数据都放叶子,非叶节点能存更多键,进一步降低树高;相比二叉搜索树,扇出大得多,层数少一个量级,IO 次数也就少一个量级。
C++ 多态的实现:分编译期和运行期。编译期多态靠函数重载、模板(静态决议,编译时确定调哪个)。运行期多态靠虚函数:类里有虚函数时,编译器给这个类生成一张虚函数表(vtable),表里按声明顺序放各虚函数的地址;每个对象里插入一个指向该表的指针(vptr),通常在对象内存布局的开头。调用虚函数时通过 vptr 找到 vtable、再按固定偏移取出函数地址调用——这就是动态绑定。派生类重写虚函数时,自己的 vtable 里对应槽位被替换成派生类的实现;基类指针指向派生类对象时,vptr 指向派生类的 vtable,所以调用落到派生类实现上。构造和析构期间 vptr 会随构造层次切换,所以构造函数里调虚函数不会多态。基类析构函数必须是虚的,否则通过基类指针 delete 派生类对象是未定义行为。
进程与线程的区别:进程是资源分配的基本单位(独立地址空间、文件描述符表、信号处理),线程是 CPU 调度的基本单位,同进程内的线程共享地址空间、全局变量、打开的文件,但各自有独立的栈、寄存器上下文和线程局部存储。所以线程间通信成本低(直接读写共享内存)但要自己做同步,进程间隔离强、崩溃不互相影响但通信要走内核。切换成本上线程远低于进程(进程切换要换页表、刷 TLB)。一个进程至少有一个线程。
进程间通信方式:① 管道(匿名管道用于父子进程,单向;命名管道 FIFO 可用于无亲缘进程);② 消息队列(内核维护的消息链表,有类型、可按类型取);③ 共享内存(最快,多进程映射同一块物理内存,但必须配信号量之类的同步机制);④ 信号量(主要用于同步与互斥,本身不传数据);⑤ 信号(异步通知,只能传一个编号);⑥ Socket(可跨主机,本地也可用 Unix domain socket,性能高于 TCP 回环);⑦ 文件(最原始的共享方式,靠锁协调)。共享内存 + 信号量是本地高性能 IPC 的经典组合。
三个智能指针:unique_ptr——独占所有权,不可拷贝只可移动(std::move 转移),零额外开销(和裸指针一样大),默认首选;用 std::make_unique 创建。shared_ptr——共享所有权,内部有控制块维护引用计数,计数归零时释放对象;拷贝会增加计数、有原子操作开销,循环引用会导致内存泄漏。weak_ptr——不增加引用计数的弱引用,指向 shared_ptr 管理的对象,用之前要 lock() 提升成 shared_ptr 并判空;它存在的意义就是打破 shared_ptr 的循环引用(典型场景是父子互指、观察者模式里的回调)。经验规则:默认 unique_ptr,确实需要共享才用 shared_ptr,出现双向关系时一侧改 weak_ptr。