网易 软件开发-C++ 实习 二面+HR面
1. 自我介绍,介绍一下项目背景和主要负责的功能
2. C++ 对象内存布局中,虚函数、多继承和虚继承分别会带来什么变化
答案:普通类对象一般按照成员声明顺序布局,中间可能因为对齐产生 padding。如果类里有虚函数,主流编译器通常会给对象加一个虚表指针,虚表里存放虚函数地址。对象大小会因此增加一个指针大小,虚函数调用也会多一次间接寻址。
多继承时,一个派生类对象内部会包含多个基类子对象。如果多个基类都有虚函数,对象里可能会有多个虚表指针。派生类指针转成不同基类指针时,地址可能发生偏移调整。虚继承主要解决菱形继承中公共基类重复的问题,但它会引入额外的虚基类偏移信息,对象布局和访问成本都会更复杂。
面试里如果让判断对象大小,不能只把成员大小简单相加,还要考虑 vptr、对齐、多继承基类子对象和虚继承额外结构。
代码:
#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;
cout << "sizeof(A): " << sizeof(A) << endl;
cout << "sizeof(B): " << sizeof(B) << endl;
cout << "sizeof(C): " << sizeof(C) << endl;
A* pa = &obj;
B* pb = &obj;
cout << "C*: " << &obj << endl;
cout << "A*: " << pa << endl;
cout << "B*: " << pb << endl;
}
3. 无锁队列如何实现,无锁和有锁的核心区别是什么,C++ 内存序有什么作用
答案:无锁队列通常依赖原子变量和 CAS,而不是 mutex。它的目标不是完全没有竞争,而是避免线程因为锁阻塞在内核态。有锁队列逻辑简单,临界区由 mutex 保护,正确性更容易保证;无锁队列要自己处理并发读写、内存可见性、ABA、队列满和队列空这些问题。
如果是单生产者单消费者,可以用环形数组加两个原子下标实现,生产者只更新 tail,消费者只更新 head。这里内存序很关键:生产者写入数据后,用 release 发布 tail;消费者 acquire 读取 tail,保证看到 tail 更新时,也能看到对应槽位的数据。如果是多生产者或多消费者,复杂度会明显增加,通常需要 CAS 抢槽位,甚至给每个槽位加序号。
代码:
#include <atomic>
#include <array>
using namespace std;
template <typename T, size_t N>
class SPSCQueue {
private:
array<T, N> buf_;
atomic<size_t> head_{0};
atomic<size_t> tail_{0};
public:
bool push(const T& val) {
size_t t = tail_.load(memory_order_relaxed);
size_t next = (t + 1) % N;
if (next == head_.load(memory_order_acquire)) {
return false;
}
buf_[t] = val;
tail_.store(next, memory_order_release);
return true;
}
bool pop(T& val) {
size_t h = head_.load(memory_order_relaxed);
if (h == tail_.load(memory_order_acquire)) {
return false;
}
val = buf_[h];
head_.store((h + 1) % N, memory_order_release);
return true;
}
};
4. 内存池怎么设计,项目里为什么要用内存池
答案:内存池的核心是提前申请一大块内存,然后按照固定大小或不同规格切分成小块,后续对象申请和释放都在池内完成,减少频繁 malloc/free 的系统开销和内存碎片。在多人协同白板实时状态同步服务里,操作事件对象非常多,比如一次拖拽可能产生很多增量操作。如果每条操作都直接走堆分配,高峰期会产生大量小对象分配,影响延迟稳定性,所以可以对固定大小的事件对象使用对象池。
设计时要注意几个点:池内对象的构造析构不能省略;多线程访问要做分片或线程本地缓存,避免一个全局锁成为瓶颈;释放对象时要防止重复释放;如果对象大小差异很大,可以按 size class 分多个池。内存池不是为了省所有内存,而是为了降低分配抖动和提升局部性。
代码:
#include <vector>
#include <memory>
using namespace std;
template <typename T>
class ObjectPool {
private:
vector<T*> freeList_;
vector<unique_ptr<T>> storage_;
public:
template <typename... Args>
T* create(Args&&... args) {
if (!freeList_.empty()) {
T* p = freeList_.back();
freeList_.pop_back();
new (p) T(forward<Args>(args)...);
return p;
}
storage_.push_back(make_unique<T>(forward<Args>(args)...));
return storage_.back().get();
}
void destroy(T* p) {
if (!p) return;
p->~T();
freeList_.push_back(p);
}
};
5. LRU 缓存怎么实现,多线程版本怎么做
答案:LRU 的核心是“最近使用的放前面,最久未使用的放后面”。常见实现是 list + unordered_map。list 维护访问顺序,头部是最新访问,尾部是最久未使用;unordered_map 负责根据 key 快速找到链表节点。get 时把节点移动到头部,put 时如果超过容量,就删除尾部节点。
多线程版本最简单的做法是给整个 LRU 加一把 mutex,正确但并发度一般。更高并发的做法是分片 LRU:按 key hash 到不同 shard,每个 shard 有自己的锁、map 和 list。这样不同 key 大概率落到不同分片,减少锁竞争。如果缓存读多写少,还可以考虑读写锁,但 LRU 的 get 本身也会修改访问顺序,所以读操作并不完全是只读。
代码:
#include <list>
#include <unordered_map>
#include <mutex>
using namespace std;
class LRUCache {
private:
int cap_;
list<pair<int, int>> lst_;
unordered_map<int, list<pair<int, int>>::iterator> mp_;
mutex mtx_;
public:
explicit LRUCache(int cap) : cap_(cap) {}
int get(int key) {
lock_guard<mutex> lk(mtx_);
auto it = mp_.find(key);
if (it == mp_.end()) return -1;
lst_.splice(lst_.begin(), lst_, it->second);
return it->second->second;
}
void put(int key, int val) {
lock_guard<mutex> lk(mtx_);
auto it = mp_.find(key);
if (it != mp_.end()) {
it->second->second = val;
lst_.splice(lst_.begin(), lst_, it->second);
return;
}
if ((int)lst_.size() == cap_) {
auto old = lst_.back();
mp_.erase(old.first);
lst_.pop_back();
}
lst_.push_front({key, val});
mp_[key] = lst_.begin();
}
};
6. 项目中如果房间状态很多,如何设计状态快照和增量日志
答案:多人协同白板里不能每次用户重连都从头回放所有操作,否则房间越活跃,恢复成本越高。比较合理的做法是“快照 + 增量日志”。服务端定期把房间完整状态生成一个快照,比如所有图形对象、层级关系、文本内容和版本号。快照之后的操作按递增序列号记录成增量日志。客户端重连时带上自己最后确认的版本,服务端根据版本选择直接补增量,或者下发最近快照再补后续日志。
难点在于快照生成不能阻塞正常操作。可以使用 copy-on-write 或者在房间线程内切出一致性视图,再交给后台线程压缩落盘。另外每条增量操作要具备幂等性,客户端重复收到同一个操作时不能重复应用。
代码:
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;
struct Operation {
uint64_t seq;
string object
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.