- 自我介绍
- 资源监控具体是怎么实现的,是通过观测线程还是什么
- 监控的采样频率是怎样确定的
- 卡顿检测具体是怎么做的,200ms 是怎样确定的,包括3级
- 流畅性优化讲一下,有没有具体导致他卡死的场景
- 卡顿检测的数据有吗,什么场景卡死的情况最多
- 流畅性优化这里会有同步性问题吧
- Sg flow 自更新冲突是怎么解决的
- Memory 是怎么写入的
- 杀戮尖塔2 为什么要用多agent
- 领域知识是用的rag吗
- 讲讲c++的auto,有哪些情况不能用
- 讲讲数组和链表
- 设计模式的四大原则
- 讲讲观察者模式,然后观察者模式的一个升级?
- 讲讲单例模式,单例模式和 一个类的所有成员函数和变量都改成 static的,这两者有什么区别
- 讲讲智能指针
- unique_ptr 和裸指针的区别,他是怎么实现不能拷贝只能移动的
- weak_ptr 是怎么解决循环引用问题的,弱引用计数作用是什么
- 手撕,数组模拟队列,包括扩容、push,pop,拿队头
非项目相关问题参考答案(对应第 12—20 题)
以下按原题号补充;第 1—11 题涉及个人经历和项目事实,保留原题,不代填具体实现或数据。
12. C++ 的 auto:哪些情况不能用?
**面试回答:**auto 让编译器从初始化表达式推导类型,不是运行时动态类型,也不是“任意类型”。如 auto x = 1; 推导为 int;auto& r = x; 保留引用;const auto& r = x; 可只读绑定。普通 auto 按值推导会去掉顶层 const 和引用,数组/函数通常退化为指针;想保留精确表达式类型可考虑 decltype(auto),但也要留意返回引用的生命周期。
**不能直接用或不适合用:**① auto x; 无初始化器,无法推导;② auto x = {1, 2}; 推成 std::initializer_list<int>,auto x{1}; 在 C++17 起推成 int,混合类型列表可能推导失败,不能想当然地当成某种容器;③ 重载函数名没有唯一目标类型时不能直接 auto f = overloaded;;④ 非静态数据成员不能只声明成 auto member = …;(静态成员等规则另论);⑤ 普通函数形参 void f(auto x) 需 C++20 的简写函数模板,旧标准应使用模板参数。即使语法允许,遇到单位、所有权、精度或隐式转换容易混淆时,显式类型更清楚。
13. 数组和链表
**面试回答:**数组元素连续存储,按下标访问 O(1),缓存局部性好;固定数组长度不可变,动态数组如 std::vector 扩容时需申请新内存并搬移元素。链表节点分散,以指针连接;已拿到插入位置时插入/删除可为 O(1),但按下标查找是 O(n),而且有指针额外开销、分配和缓存未命中。
**对比与选型:**vector 尾部追加均摊 O(1),单次扩容 O(n);在中间插入/删除要移动后续元素,O(n)。链表即便改指针是 O(1),从头找到目标节点通常仍是 O(n)。游戏客户端按帧遍历、随机索引通常优先连续容器;只有稳定迭代器/引用、频繁已定位节点的插删等明确需求,才考虑 std::list(它也不支持 O(1) 随机访问)。
14. 设计模式的“四大原则”
先明确口径:“四大原则”不是统一标准名称。若面试官指面向对象常讲的四项,可以回答:单一职责(一个类只有一个主要变化原因)、开闭原则(扩展功能尽量不修改稳定代码)、里氏替换(子类放到父类预期位置仍满足契约)、依赖倒置(高层策略依赖抽象,而非具体实现)。例如资源导入:调用方依赖 Importer 接口,新增格式实现接口而不是在调用方堆满分支。
完整的 SOLID 还有接口隔离(客户端不依赖用不到的方法);中文设计原则清单还常含迪米特法则与合成复用原则。如果对方说的是“设计模式四原则”的特定教材版本,应先确认是哪四项,不把某一种列表说成唯一标准;也不要为遵守原则而过度抽象。
15. 观察者模式与“升级”
**观察者模式:**一个主题持有多个观察者的订阅关系;状态变化时通知它们,观察者各自处理,如资源发生变化后分别刷新预览、失效缓存。要能订阅/取消订阅,注意通知期间对象销毁、回调重入、订阅列表修改和线程安全;同步回调也可能阻塞主题。
**“升级”的一种常见理解是发布—订阅 / 事件总线:**发布者不直接持有具体观察者,通过按事件类型分发的中介让发布者与订阅者进一步解耦,还可选择队列异步处理。但“升级”不是标准的唯一答案,面试时可以确认是否指事件总线、消息队列或响应式流。异步带来顺序、重复投递、背压、取消订阅及生命周期问题;对简单一对多通知不必引入总线。
16. 单例与“全 static 类”有何区别?
**单例:**类有且只允许一个受控实例,并提供全局访问入口。例如私有构造、删除拷贝/赋值,通过函数内静态局部对象返回引用;C++11 起其初始化是线程安全的(实例内部可变状态仍需自行同步)。实例能实现接口、拥有普通成员和构造/析构生命周期,也便于用抽象和依赖注入做测试。
**全 static 类:**静态成员属于类而非某个对象,没有实例身份;静态函数没有 this,不能作为虚函数,不能借多态替换一个实例服务。静态数据一样可能是全局共享可变状态,但初始化、资源释放和测试替换方式与单例对象不同。单例也会形成隐藏全局依赖,能把服务显式传入时通常更易测试;不是说单例必然优于静态工具函数。
17. 智能指针
**核心是用 RAII 表达所有权:**std::unique_ptr 独占对象,离开作用域销毁,不能拷贝、可以移动;std::shared_ptr 共享所有权,拷贝增加强引用计数,最后一个拥有者释放对象;std::weak_ptr 不拥有对象,需通过 lock() 获取临时 shared_ptr 后才能安全访问。优先 make_unique / make_shared,按语义选择而不是默认全部 shared_ptr;不拥有对象时可传引用或裸指针,但必须保证被指向对象存活。
shared_ptr 的控制块维护引用计数,计数操作的线程安全不等于对象本身线程安全。不要从同一个裸指针各造一个独立 shared_ptr,否则可能重复释放;也不要把裸指针的 get() 结果误当成拥有所有权。涉及数组、文件句柄等要匹配适当的删除器。
18. unique_ptr 与裸指针;如何做到只移动不拷贝?
**区别:**unique_ptr<T> 是独占拥有者,析构时按删除器释放资源,可通过 get() 借出不拥有的 T*;裸指针本身只存地址,既可用于观察也可能由约定承担所有权,但类型不自动表达谁释放、何时释放。两者都可能为空,解引用前要遵守非空与生命周期约束。
**实现机制:**类显式将拷贝构造和拷贝赋值声明为 = delete;移动构造/移动赋值接管内部指针及删除器,使源指针不再拥有资源(典型效果是变为 nullptr)。因此 auto b = std::move(a); 可以转移所有权,auto b = a; 编译失败。std::move 只把表达式转换为可移动的值类别,本身并不移动;赋给已有 unique_ptr 时原有资源会先被妥善释放。
19. weak_ptr 如何解决循环引用?弱计数起什么作用?
**循环示例:**A 持有 shared_ptr<B>,B 又持有 shared_ptr<A>,外部指针释放后彼此仍使强计数非零,两个对象都无法析构。把不拥有的一侧改成 weak_ptr(例如子节点指向父节点)后,不增加对方强计数;外部最后一个 shared_ptr 销毁时对象即可释放。weak_ptr 不能凭空打破任意环:必须把环上的至少一条共享拥有边改为非拥有关系。
**控制块两类计数:**强计数决定被管理对象何时销毁;弱计数用于追踪 weak_ptr 与控制块生命周期。强计数归零后对象销毁,但有 weak_ptr 时控制块仍需保留,供 expired()/lock() 判断是否过期;弱引用也释放后,控制块才可回收。lock() 安全地尝试提升为临时 shared_ptr;成功才访问对象,不要仅凭先前一次 expired() 检查就认为稍后仍存活。
20. 手撕:数组实现可扩容队列
**思路:**用循环数组,head_ 指向队头、size_ 记录元素数、容量由 vector 大小给出;队尾位置是 (head_ + size_) % capacity。满了先扩容为两倍,按逻辑顺序复制到新数组的 [0, size_),再把 head_ 设为 0。push 尾部写入,pop 前进队头,front 读队头;空队列访问/弹出应明确报错。
#include <cstddef>
#include <stdexcept>
#include <vector>
class IntQueue {
std::vector<int> data_{4};
std::size_t head_ = 0;
std::size_t size_ = 0;
void grow() {
std::vector<int> next(data_.size() * 2);
for (std::size_t i = 0; i < size_; ++i)
next[i] = data_[(head_ + i) % data_.size()];
data_.swap(next);
head_ = 0;
}
public:
bool empty() const { return size_ == 0; }
std::size_t size() const { return size_; }
void push(int value) {
if (size_ == data_.size()) grow();
data_[(head_ + size_) % data_.size()] = value;
++size_;
}
void pop() {
if (empty()) throw std::out_of_range("queue is empty");
head_ = (head_ + 1) % data_.size();
--size_;
}
int front() const {
if (empty()) throw std::out_of_range("queue is empty");
return data_[head_];
}
};push 均摊 O(1)、扩容当次 O(n);pop/front O(1),空间 O(n)。例如依次 push(1), push(2), pop(), push(3),front() 是 2;连续 push 至扩容后仍保持先入先出。若面试允许直接用标准库,生产代码可优先使用 std::queue,但手撕题要说明环形下标和扩容复制。