畅唐网络 游戏开发-C++ 一面
1、实习这段经历你主要做了什么,最大的收获是什么
2、一个程序运行时,加载的是实际的物理地址还是虚拟地址
答案:程序运行时,CPU 执行层面看到的首先是 虚拟地址,不是直接拿物理地址来跑。现代操作系统一般都采用虚拟内存机制,每个进程拥有自己的虚拟地址空间,代码段、堆、栈、共享库这些区域在进程视角下都是连续或者相对独立的虚拟地址。
虚拟地址最终会通过页表映射到物理地址,地址转换通常还会借助 TLB 来加速。所以对用户程序来说,平时接触的指针地址、函数地址、本地变量地址,本质上都是虚拟地址。只有在 MMU 做地址转换后,才真正落到物理内存上。
3、进程切换的时候做了什么工作
答案:进程切换本质上是 CPU 从当前进程的执行现场切到另一个进程的执行现场。这个过程里操作系统通常要保存当前进程的上下文,再恢复目标进程的上下文。
这里的上下文不只是寄存器值,还包括程序计数器、栈指针、页表基址、调度状态、内核栈信息,有些架构下还会涉及浮点寄存器、SIMD 寄存器这些扩展状态。如果切换的是不同进程,地址空间也会跟着切换;如果是线程切换但线程属于同一进程,地址空间通常不变,只需要切执行流上下文。
所以进程切换的成本往往比用户态函数调用高很多,真正昂贵的地方既包括寄存器保存恢复,也包括地址空间切换、TLB 失效以及缓存局部性被破坏。
4、上下文切换的时候,虚拟地址映射到物理地址的关系会发生改变吗
答案:如果切换的是 不同进程,那虚拟地址到物理地址的映射关系通常会改变,因为每个进程有自己的页表。即使两个进程里看到同样的虚拟地址,比如都叫 0x400000,它们也完全可能映射到不同的物理页。
切换时操作系统会切换页表基址寄存器,之后同样的虚拟地址会按新进程的页表重新解释。这也是为什么进程之间默认不能直接访问彼此内存。如果访问的虚拟页当前没有映射,或者对应物理页不在内存,就会触发缺页中断。缺页中断和上下文切换不是一回事,但它们都可能导致 CPU 从当前执行流陷入内核。
如果切换的是同一进程内的线程,一般虚拟地址映射关系不变,因为它们共享地址空间。
5、CPU 的执行调度算法有了解吗,说说这些算法
答案:常见调度算法大概有这几类:先来先服务、短作业优先、优先级调度、时间片轮转、多级反馈队列。
先来先服务实现简单,但对短任务不友好;短作业优先平均等待时间可能更优,但需要预估任务长度,现实里不太稳定;优先级调度适合区分任务重要程度,但可能导致低优先级饥饿;时间片轮转适合分时系统,公平性比较好;多级反馈队列是工程上更常见的折中方案,会根据进程行为动态调整优先级,兼顾交互性和吞吐。
Linux 里如果说普通进程调度,通常会提到 CFS,也就是 Completely Fair Scheduler。它不是简单轮转,而是尽量让每个可运行任务获得“相对公平”的 CPU 时间,用虚拟运行时间来组织调度顺序。
6、进程间通信有哪些方式,它们各自的优劣势是什么
答案:进程间通信常见有管道、命名管道、消息队列、共享内存、信号、套接字、信号量这些。
如果只是父子进程之间做简单单向通信,匿名管道很方便,但能力有限;命名管道可以让无亲缘关系进程通信,不过本质上还是偏流式;消息队列适合按消息边界传输,编程模型清晰,但吞吐一般不如共享内存;共享内存速度最快,适合大数据量场景,但同步复杂,需要自己处理并发一致性;信号更适合做异步通知,不适合承载大量数据;套接字最通用,本机和跨机器都能用,扩展性最好,但开销相对更高。
所以如果数据量很大而且是同机多进程,通常优先考虑共享内存;如果更关注通用性和边界清晰,套接字会更稳妥。
7、设计一个基类,要求必须通过 std::shared_ptr 管理,禁止栈分配和裸 new,同时又能安全拿到自己的 shared_ptr,怎么设计
答案:这个场景的关键点有两个,一个是 限制对象创建方式,另一个是 安全自引用。
限制创建方式通常会把构造函数设成受保护或者私有,并且提供静态工厂函数返回 std::shared_ptr。这样外部既不能直接栈上构造,也不能随便 new。安全自引用则一般依赖 std::enable_shared_from_this,这样对象在已经被 shared_ptr 托管后,成员函数内部可以通过 shared_from_this() 安全拿到指向自身的 shared_ptr,用于异步回调或延长生命周期。
这里有个边界要注意:对象如果还没进入 shared_ptr 控制,就调用 shared_from_this(),会出问题。所以必须保证对象确实是通过工厂函数创建,并立即交给 shared_ptr 托管。
代码:
#include <iostream>
#include <memory>
#include <functional>
using namespace std;
class Base : public enable_shared_from_this<Base> {
public:
using Ptr = shared_ptr<Base>;
virtual ~Base() = default;
static Ptr create() {
struct MakeSharedEnabler : public Base {};
return make_shared<MakeSharedEnabler>();
}
void asyncUse(function<void(Ptr)> executor) {
executor(shared_from_this());
}
protected:
Base() = default;
private:
Base(const Base&) = delete;
Base& operator=(const Base&) = delete;
};
int main() {
auto p = Base::create();
p->asyncUse([](shared_ptr<Base> self) {
cout << "use count = " << self.use_count() << endl;
});
}
8、可被继承的基类如果用基类指针指向子类对象,delete 的时候会发生什么
答案:如果基类析构函数是虚函数,那么通过基类指针 delete派生类对象时,会先调用派生类析构,再调用基类析构,整个析构链是完整的。如果基类析构函数不是虚函数,那么通过基类指针删除派生类对象时,可能只
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.
