度小满 客户端开发-C++ 一面
1. 实习期间做了什么
2. 项目拷打
3. 一个程序运行时,加载的是实际的物理地址还是虚拟地址
答案:用户态程序看到和使用的通常是虚拟地址,不是物理地址。现代操作系统基本都基于虚拟内存机制运行,进程拿到的是自己的独立虚拟地址空间。CPU 执行访存时,会通过页表把虚拟地址翻译成物理地址,最终再访问内存。
这样做的好处很明显:
- 每个进程都有相对独立的地址空间
- 提高安全性,进程之间默认隔离
- 便于内存管理,比如分页、换页、按需分配
- 程序不用关心真实物理内存布局
所以从程序员视角看,代码段、堆、栈这些地址都是虚拟地址。真正落到物理内存上,是 MMU 配合页表完成的事情。
4. 进程切换的时候做了什么工作
答案:进程切换本质上是操作系统把 CPU 从当前进程交给另一个进程执行。这个过程中需要保存当前进程上下文,再恢复下一个进程上下文。常见工作包括:
- 保存当前进程 CPU 寄存器状态
- 保存程序计数器、栈指针等执行现场
- 切换内核栈
- 切换页表基址,进入下一个进程地址空间
- 更新调度器相关状态
- 必要时刷新 TLB 或依赖 ASID / PCID 降低刷新代价
如果从开销角度看,进程切换之所以贵,不只是“多了几次函数调用”,而是:
- 要切地址空间
- cache / TLB 局部性会被破坏
- 调度和恢复现场本身也有成本
5. 上下文切换的时候,虚拟地址映射到物理地址的关系会发生改变吗?
答案:会,而且这正是进程隔离的重要体现。不同进程虽然可能看到一样的虚拟地址,比如都访问 0x12345678,但它们通过各自页表映射到的物理地址通常并不一样。所以当进程切换时,CPU 使用的页表上下文也会切换,虚拟地址到物理地址的映射关系自然会变化。
不过这个问题还有两个细节:
- 内核空间在很多系统里通常有一部分是共享映射,不是全部都变
- 如果访问的虚拟页当前没有有效映射,才会触发缺页中断,不是说每次切换都会缺页
缺页中断常见发生场景有:
- 页根本没分配
- 页被换出到磁盘
- 写时复制触发新页分配
- 权限不匹配
进程切换会切换地址空间映射上下文;缺页中断是访问某个虚拟页时发现映射无效或不满足访问条件才发生的。
6. TLB 是干什么的?进程切换时它为什么会影响性能?
答案:TLB 可以理解为页表项的高速缓存。因为每次做虚拟地址到物理地址转换,如果都去查多级页表,开销会很大,所以 CPU 会把最近用过的地址映射缓存到 TLB 里。
TLB 的价值主要是:
- 减少地址翻译开销
- 降低多级页表遍历代价
- 提升访存性能
进程切换时,性能容易受到影响,原因是:
- 新进程的地址空间和旧进程不同
- 原来缓存的 TLB 项可能不能继续直接用
- 如果 TLB 命中率下降,后续访存需要重新走页表遍历
它和上下文切换的性能抖动直接相关。
7. CPU 的调度算法有了解吗?重点说说现代系统里更实用的调度思路
答案:经典调度算法当然包括:
- 先来先服务
- 短作业优先
- 优先级调度
- 时间片轮转
- 多级反馈队列
但现代通用操作系统里,更有代表性的其实是尽量在公平性、响应性和吞吐之间做平衡。比如 Linux 里的 CFS,核心思想不是传统意义上“每人固定时间片轮着跑”,而是让每个任务尽量获得公平的虚拟运行时间。
可以把它理解成:
- 每个任务都维护一个虚拟运行时间
- 调度器优先选虚拟运行时间最小的任务运行
- 权重高的任务增长更慢,因此能获得更多 CPU
- 常用红黑树维护可运行任务集合
这样做的优势是:
- 公平性更好
- 对交互型任务响应更自然
- 比简单轮转更适合复杂负载
调度算法本质是在不同目标之间做取舍,没有绝对最优,只有更适合当前系统目标的策略。
8. 进程间通信有哪些方式?分别有什么优劣势?
常见 IPC 方式包括:
- 管道
- 命名管道
- 消息队列
- 共享内存
- 信号量
- socket
- 信号
- 内存映射文件
最好顺手讲一下优缺点。
管道 / 命名管道:
- 优点:简单,适合字节流传输
- 缺点:语义偏简单,扩展性一般
消息队列:
- 优点:按消息边界组织,适合异步通信
- 缺点:系统限制较多,吞吐不一定最高
共享内存:
- 优点:速度快,适合大块数据交换
- 缺点:同步复杂,容易出并发问题
socket:
- 优点:最通用,本机和跨机都能用
- 缺点:相比共享内存有额外协议和拷贝成本
信号:
- 优点:轻量,适合通知
- 缺点:不适合承载复杂数据
9. 设计一个基类,要求它和派生类必须通过 std::shared_ptr 管理,并且成员函数里能安全拿到自己的 shared_ptr,怎么设计?
答案:
- 禁止外部栈上创建、禁止裸
new - 对象内部还要能安全拿到指向自己的
shared_ptr
标准思路通常是:
- 基类继承
std::enable_shared_from_this<Base> - 构造函数和析构函数做受限访问,避免外部随意创建
- 提供静态工厂函数返回
shared_ptr - 成员函数里通过
shared_from_this()获取共享所有权 - 异步回调里优先捕获
weak_ptr或正确的shared_ptr
不能在对象还没被 shared_ptr 接管前调用 shared_from_this(),否则会出问题。另外,如果想强行禁止栈分配,常见做法就是把构造函数设成 protected/private,只允许工厂方法创建。
代码:
#include <memory>
#include <iostream>
class Base : public std::enable_shared_from_this<Base> {
public:
using Ptr = std::shared_ptr<Base>;
virtual ~Base() = default;
std::shared_ptr<Base> self() {
return shared_from_this();
}
protected:
Base() = default;
};
class Derived : public Base {
public:
using Ptr = std::shared_ptr<Derived>;
static Ptr create() {
struct MakeSharedEnabler : public Derived {};
return std::make_shared<MakeSharedEnabler>();
}
void asyncSafeUse() {
auto me = std::static_pointer_cast<Derived>(shared_from_this());
std::cout << "safe
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

查看78道真题和解析