网易 软件开发-C++ 实习一面
1. 自我介绍
2. 线上 C++ 服务出现偶发崩溃,core 文件里栈已经被破坏,怎么定位
答案:栈被破坏时不能只依赖 bt,因为调用链可能已经不可信。一般会先看崩溃信号,比如 SIGSEGV、SIGABRT、SIGBUS,再看寄存器、崩溃地址和附近内存。如果怀疑是越界写、use-after-free 或 double free,我会优先用 ASan 复现;如果线上不能开 ASan,就增加关键对象的 canary、对象 ID、构造析构日志,配合灰度流量复现。
还可以从最近一次合法日志、线程状态、堆分配器报错、异常请求入手。对于高并发服务,栈破坏经常来自数组越界、协议长度没校验、异步回调访问已释放对象,或者跨线程使用非线程安全容器。如果 core 无法直接定位,我会构造最小复现,然后用 ASan/UBSan/TSan 和压测一起跑。
代码:
ulimit -c unlimited gdb ./gateway core.xxx (gdb) info registers (gdb) x/32gx $rsp (gdb) thread apply all bt (gdb) disassemble
3. C++ 对象内存布局在单继承、多继承、虚继承下有什么区别
答案:单继承下,如果基类有虚函数,派生类对象通常包含一份虚表指针,基类子对象在派生类对象的起始位置附近。通过基类指针调用虚函数时,会根据对象里的 vptr 找到对应虚表。多继承下,一个派生类对象里可能包含多个基类子对象,每个有虚函数的基类子对象都可能有自己的 vptr。把派生类指针转换成第二个基类指针时,指针地址可能需要调整。
虚继承主要解决菱形继承中基类子对象重复的问题。它会引入虚基类表或类似机制,对象布局更复杂,访问虚基类成员时可能需要额外偏移计算。所以虚函数、多继承、虚继承都会影响对象大小、指针转换和 ABI,工程里如果不是确实需要,不建议设计过深的继承层次。
代码:
#include <iostream>
using namespace std;
struct A {
virtual void fa() {}
int a;
};
struct B {
virtual void fb() {}
int b;
};
struct C : public A, public B {
int c;
};
int main() {
C obj;
A* pa = &obj;
B* pb = &obj;
cout << &obj << endl;
cout << pa << endl;
cout << pb << endl; // 多继承下这里可能和 &obj 不同
}
4. shared_ptr 的控制块里一般有什么,为什么不能用同一个裸指针构造两个 shared_ptr
答案:shared_ptr 不只是保存对象地址,它还关联一个控制块。控制块里一般有强引用计数、弱引用计数、删除器、分配器等信息。多个 shared_ptr 共享同一个控制块,才能正确维护引用计数。
如果对同一个裸指针分别构造两个 shared_ptr,会产生两个独立控制块。两个控制块都以为自己拥有这个对象,最后强引用计数分别归零时,会对同一个对象执行两次 delete,导致 double free。所以对象要么用 make_shared 创建,要么确保裸指针只交给一个 shared_ptr 接管,后续都从这个 shared_ptr 拷贝。
代码:
#include <memory>
using namespace std;
struct Obj {};
int main() {
Obj* raw = new Obj();
shared_ptr<Obj> p1(raw);
// shared_ptr<Obj> p2(raw); // 错误:会产生第二个控制块,导致重复释放
shared_ptr<Obj> p2 = p1; // 正确:共享同一个控制块
}
5. enable_shared_from_this 的作用和使用坑
答案:enable_shared_from_this 用来让对象在成员函数里安全地拿到管理自己的 shared_ptr。它的典型场景是异步回调里需要延长当前对象生命周期,比如连接对象发起异步读写时,回调执行前对象不能被释放。
坑在于,只有对象已经被 shared_ptr 管理之后,才能调用 shared_from_this()。如果对象是栈对象,或者在构造函数里调用 shared_from_this(),通常会抛 bad_weak_ptr 或产生未定义行为。所以一般通过工厂函数创建对象,构造完成后再调用初始化逻辑。
代码:
#include <memory>
#include <iostream>
using namespace std;
class Session : public enable_shared_from_this<Session> {
public:
static shared_ptr<Session> create() {
auto p = shared_ptr<Session>(new Session());
p->init();
return p;
}
void init() {
auto self = shared_from_this();
cout << "init session\n";
}
private:
Session() = default;
};
6. 无锁 MPSC 队列怎么设计,和 SPSC 队列难点有什么不同
答案:SPSC 队列只有一个生产者和一个消费者,生产者只写 tail,消费者只写 head,通常用环形数组加 acquire/release 就能实现。MPSC 是多个生产者、单个消费者,难点在于多个生产者会同时竞争写入位置,必须用 CAS 或 fetch_add 分配槽位。还要处理槽位还没写完但 tail 已经推进的问题,否则消费者可能读到未初始化数据。
一种常见做法是每个槽位加 sequence number。生产者先通过 CAS 抢到位置,再写数据,最后发布序号;消费者只有看到对应序号已经发布,才读取数据。这类结构对内存序要求很高,工程里如果不是瓶颈明确,通常会先用成熟库或者加锁队列。
代码:
#include <atomic>
#include <array>
using namespace std;
template <typename T, size_t N>
class MPSCQueue {
struct Cell {
atomic<size_t> seq;
T data;
};
array<Cell, N> buffer_;
atomic<size_t> tail_{0};
size_t head_{0};
public:
MPSCQueue() {
for (size_t i = 0; i < N; ++i) {
buffer_[i].seq.store(i, memory_order_relaxed);
}
}
bool push(const T& value) {
size_t pos = tail_.load(memory_order_relaxed);
while (true) {
Cell& cell = buffer_[pos % N];
size_t seq = cell.seq.load(memory_order_acquire);
intptr_t diff = (intptr_t)seq - (intptr_t)pos;
if (diff == 0) {
if (tail_.compare_exchange_weak(pos, pos + 1,
memory_order_relaxed)) {
cell.data = value;
cell.seq.store(pos + 1, memory_order_release);
return true;
}
} else if (diff < 0) {
return false;
} else {
pos = tail_.load(memory_order_relaxed);
}
}
}
bool pop(T& value) {
Cell& cell = buffer_[head_ % N];
size_t seq = cell.seq.load(memory_order_acquire);
intptr_t diff = (intptr_t)seq - (intptr_t)(head_ + 1);
if (diff == 0) {
value = cell.data;
cell.seq.store(head_ + N, memory_order_release);
++head_;
return true;
}
return false;
}
};
7. C++ 内存序里 relaxed、acquire、release、seq_cst 分别解决什么问题
答案:memory_order_relaxed 只保证原子变量本身操作是原子的,不保证和其他内存访问的顺序关系,适合计数、统计这类不依赖同步关系的场景。memory_order_release 通常用于写线程发布数据,保证 release 之前的写入不会被重排到 release 之后。memory_order_acquire 通常用于读线程获取数据,保证 acquire 之后的读取不会被重排到 acquire 之前。acquire 和 release 配合,可以建立跨线程的 happens-before 关系。
memory_order_seq_cst 是最强的顺序一致性,语义简单但可能更贵。写无锁结构时,不能只想着“都用 relaxed 更快”,如果没有正确的同步关系,消费者可能看到标志位已经更新,但数据本身还没对它可见。
8. 虚函数调用为什么在构造函数和析构函数里不会表现出正常多态
答案:构造对象时,基类部分先构造,派生类部分还没有构造完成。此时如果基类构造函数调用虚函数,只会调用基类版本,因为派生类对象还不完整。析构时顺序相反,派生类部分先析构,进入基类析构函数时,派生类部分已经不再可靠,因此虚函数也不会分发到派生类版本。
这个规则是为了避免访问尚未构造或已经析构的派生类成员。工程里如果需要对象初始化后执行可扩展逻辑,通常会使用工厂函数,构造完成后显式调用 init(),不要在构造函数里依赖虚函数分发。
9. epoll 在多线程 Reactor 模型里怎么设计连接分发
答案:常见做法是一个主 Reactor 负责监听 fd 和 accept,新连接建立后,通过轮询或 hash 分配给某个子 Reactor。每个子 Reactor 运行在固定线程里,维护自己的 epoll 实例和连接集合。连接一旦绑定到某个子 Reactor,后续读写、定时器和状态修改都尽量在这个线程内完成,避免多个线程同时操作同一个连接对象。
主线程把新 fd 交给子线程时,可以用 eventfd 或 pipe 唤醒子 Reactor。子 Reactor 收到通知后,从队列取出新
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.