字节跳动-后端开发实习生-C++ 一面
1. 自我介绍
2. 项目介绍
3. 进程和线程的本质区别是什么,为什么线程切换通常更轻
答案:进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程有自己独立的虚拟地址空间、文件描述符表、页表等资源;同一进程里的多个线程共享代码段、数据段、堆、打开的文件等资源,但每个线程有自己独立的栈、寄存器上下文和线程局部存储。线程切换通常比进程切换轻,是因为同进程线程切换时,不需要像进程切换那样频繁切换地址空间和页表,缓存和 TLB 的破坏通常也更小。但“轻”不代表没成本,线程切换依然涉及内核调度、寄存器保存恢复、可能的锁竞争和缓存失效。
4. 进程通信方式有哪些,怎么选
答案:常见的进程通信方式有管道、命名管道、消息队列、共享内存、信号、套接字、信号量、内存映射文件。如果是父子进程、数据量不大、单向流式通信,管道足够;如果是本机高吞吐、低拷贝场景,共享内存更合适;如果需要跨机器,通常还是 socket;如果更关注消息边界和异步解耦,可以考虑消息队列。共享内存性能高,但同步复杂;消息队列使用方便,但吞吐和延迟通常不如共享内存;socket 通用性最好,代价是协议处理和拷贝链路更长。工程里选型一般不会只看“最快”,还要看可维护性、可靠性和故障恢复难度。
5. 线程之间共享哪些内存,真正的问题为什么是可见性和同步
答案:同一进程里的线程共享代码段、全局变量、静态变量、堆内存、文件描述符等资源。但每个线程的栈空间、寄存器和线程局部存储是独立的,所以局部变量通常不会天然共享。面试里问“线程是否有共享内存”,更准确的说法应该是:线程运行在同一个进程地址空间里,天然共享进程级资源,但共享不等于安全。真正麻烦的点在于数据竞争、内存可见性和执行顺序。一个线程改了某个共享变量,另一个线程什么时候看见、看见的是不是中间状态,都需要同步原语来保证。
代码:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> ready{0};
int data = 0;
void writer() {
data = 42;
ready.store(1, memory_order_release);
}
void reader() {
while (ready.load(memory_order_acquire) == 0) {}
cout << data << endl;
}
int main() {
thread t1(writer), t2(reader);
t1.join();
t2.join();
return 0;
}
6. 用户态和内核态的区别是什么,为什么要区分
答案:用户态和内核态的核心区别在于权限级别不同。用户态程序权限受限,不能直接操作硬件、页表、中断和敏感系统资源;内核态拥有更高权限,可以访问设备、调度进程、管理内存、处理系统调用。之所以要区分,是为了安全性和稳定性。应用程序如果都能直接操作硬件或系统关键资源,任何一个 bug 都可能把整个系统拖垮。应用从用户态切到内核态,一般通过系统调用、异常或中断完成,这个切换本身也有成本,所以高性能场景里会尽量减少不必要的系统调用。
7. 栈和堆的生命周期有什么区别,栈在什么情况下销毁
答案:栈上的对象通常跟随作用域或函数调用生命周期存在,进入作用域时分配,离开作用域时自动释放;堆上的对象由程序员或资源管理对象显式控制,生命周期更灵活。栈的回收通常不需要逐块管理,函数返回时栈帧整体弹出,所以效率高;堆内存则由分配器管理,适合动态大小和跨作用域对象。面试里说“栈什么时候销毁”,更准确地讲,是某个栈帧在函数返回、线程退出或者异常展开离开作用域时被回收。如果是线程栈,那会在线程结束时整体释放;如果是某个函数里的局部对象,会在离开该函数或作用域时析构。
8. 虚拟内存和页表你怎么理解,它和进程隔离有什么关系
答案:每个进程看到的地址空间通常是连续、独立的虚拟地址空间,但真实内存是物理内存。页表的作用是建立虚拟地址到物理地址的映射,CPU 访问内存时会先做地址翻译。进程隔离本质上依赖的是不同进程拥有不同的页表映射,同样的虚拟地址在不同进程里可以映射到完全不同的物理页。这样一个进程默认不能直接访问另一个进程的地址空间,除非通过共享内存、内核映射或特权机制建立共享关系。这也是为什么进程切换比线程切换重,因为进程切换通常伴随地址空间切换。
9. MySQL 事务你了解吗
答案:事务本质上是一组操作,要么全部成功,要么全部失败回滚。MySQL 里事务常说 ACID,分别是原子性、一致性、隔离性、持久性。原子性一般依赖 undo log,一致性是事务执行前后数据状态满足约束,隔离性通过锁和 MVCC 保证,持久性则依赖 redo log。在实际业务里,事务最常见的价值是保证跨多条 SQL 的一致更新,比如扣库存、写订单、更新状态这些动作必须作为一个整体成功。
10. MySQL 的隔离级别和 MVCC 是怎么配合的
答案:MySQL 常见隔离级别有读未提交、读已提交、可重复读和串行化。InnoDB 默认是可重复读,它通过 MVCC 和锁共同实现事务隔离。MVCC 的核心思想是“读历史版本”,普通一致性读不一定加锁,而是结合 undo log 和 Read View 去判断当前事务能看到哪个版本的数据。这样能降低读写冲突,提高并发性能。但如果是当前读,比如 select ... for update、update、delete,还是会加锁。面试里如果继续追,通常会问幻读、间隙锁、next-key lock 这些。
11. 进程切换和线程切换的开销主要来自哪里
答案:切换开销主要来自上下文保存恢复、调度器参与、缓存失效和可能的地址空间切换。线程切换时需要保存寄存器、程序计数器、栈指针等上下文;如果跨 CPU 核,还会带来缓存局部性破坏。进程切换除了这些,还经常涉及页表切换、TLB 刷新或部分失效,因此通常更重。真正影响性能的很多时候不是“切没切”,而是切换频率太高,比如线程数失控、锁竞争激烈、任务过细导致调度成本高于执行成本。
12. 常见的死锁场景有哪些,如何避免
答案:最典型的死锁场景是两个线程拿锁顺序相反:线程 A 先拿锁 1 再等锁 2,线程 B 先拿锁 2 再等锁 1。除此之外,递归调用中重复拿非递归锁、持锁等待外部回调、多个模块间锁顺序不统一,也很容易出问题。避免死锁最有效的方法通常是统一加锁顺序、减少锁粒度、避免锁嵌套、使用 std::lock 或层级锁设计。如果线上已经卡住,一般会通过线程栈、锁等待信息、core dump 来定位是谁在相互等待。
代码:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
mutex m1, m2;
void work1() {
lock(m1, m2);
lock
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
