网易 软件开发-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_maplist 维护访问顺序,头部是最新访问,尾部是最久未使用;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++ 常考面试题总结 文章被收录于专栏

本专栏系统梳理C++方向, 大中厂高频高频面试考点 , 内容皆来自真实面试经历,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力.

全部评论

相关推荐

点赞 评论 收藏
分享
求好运眷顾🙏🏻:翻译:面试前没盘点好hc一下面太多了,现在在排序回去等通知
点赞 评论 收藏
分享
05-06 20:02
武汉大学 Python
不知道怎么取名字_:肯定会有的,加油好事多磨的啊
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务