腾讯 微信客户端-C++ 二面

1、你做的高性能日志系统里最有挑战的是什么

答案:高性能日志系统里最有挑战的部分通常不是把日志写出去,而是在高并发场景下同时兼顾吞吐、时延和落盘可靠性。

如果每条日志都直接加锁写文件,线程一多就会在锁和系统调用上卡住,所以一般会把日志写入分成前台生产和后台刷盘两段。前台线程只负责把日志快速写入缓冲区,后台线程批量落盘。这样可以把大量小写合并成少量顺序写,减少系统调用和磁盘抖动。

真正难的地方在于几个细节。第一是缓冲区切换时机,切得太频繁会增加刷盘压力,切得太慢又会拖高日志可见延迟。第二是满载场景下的回压策略,如果生产速度远大于消费速度,日志队列迟早会堆爆,这时要决定阻塞业务线程、丢弃低优先级日志,还是降级打印。第三是崩溃前的数据保全,如果进程异常退出,缓冲区里还有没刷出去的数据,要考虑刷盘策略、刷盘周期和可靠性之间怎么平衡。

如果继续做优化,还会把时间格式化、线程 ID 拼接、字符串拼装这些高频开销提前处理,减少真正写日志路径上的动态分配和重复格式化。

2、项目拷打

3、无锁队列在高并发下是怎么工作的

答案:无锁队列主要依赖原子操作维护共享状态。在并发环境下,多个线程不会通过互斥锁串行进入临界区,而是通过 CAS 去尝试推进 head 或 tail 指针。

如果是单生产者单消费者队列,结构会简单很多,因为 push 和 pop 的修改职责天然分开。生产者只推进 tail,消费者只推进 head,不需要复杂的竞争协调。如果是多生产者多消费者队列,就要考虑 ABA、内存回收、伪共享和内存序的问题。比如一个节点出队以后,不能立刻释放,因为其他线程可能还保留着旧指针;CAS 成功后,新写入的节点内容也必须对其他线程及时可见。

代码:

#include <atomic>
using namespace std;

template <class T>
struct Node {
    T value;
    atomic<Node*> next{nullptr};
    Node(const T& v) : value(v) {}
};

template <class T>
class SPSCQueue {
public:
    SPSCQueue() {
        Node<T>* dummy = new Node<T>(T{});
        head_ = tail_ = dummy;
    }

    void push(const T& v) {
        Node<T>* node = new Node<T>(v);
        tail_->next.store(node, memory_order_release);
        tail_ = node;
    }

    bool pop(T& out) {
        Node<T>* next = head_->next.load(memory_order_acquire);
        if (!next) return false;
        out = next->value;
        delete head_;
        head_ = next;
        return true;
    }

private:
    Node<T>* head_;
    Node<T>* tail_;
};

4、乐观锁怎么理解,mutex 和自旋锁分别适合什么场景

答案:乐观锁的思路是先假设并发冲突不严重,修改共享数据时再做校验。如果状态没变,就提交;如果变了,就重试或者回滚。CAS 就是这种思路最常见的底层实现。

mutex 更适合锁持有时间不确定、临界区可能较长、甚至可能阻塞的场景。线程拿不到锁时通常会被挂起,让出 CPU。自旋锁更适合临界区很短、锁能很快释放的场景。拿不到锁的线程不睡眠,而是在用户态循环等待,避免线程切换。

如果竞争比较激烈,自旋锁会浪费大量 CPU;如果临界区很短但访问特别频繁,mutex 又可能在调度开销上显得偏重。

5、线程切换时到底切了什么

答案:线程切换时,操作系统需要先保存当前线程的执行现场,再恢复下一个线程之前保存的现场。

这里面包括通用寄存器、程序计数器、栈指针、状态寄存器,还可能包括浮点寄存器、SIMD 寄存器以及线程局部存储相关状态。除了 CPU 现场,还要切换线程在调度器里的状态,比如运行态、阻塞态、时间片、优先级等。

如果是同一个进程内的线程切换,地址空间一般不变;如果是不同进程之间切换,还会涉及页表切换和 TLB 干扰,代价更高。

6、栈上的对象是怎么析构的

答案:栈上对象的生命周期由作用域控制。编译器在生成代码时,会把析构调用插入到作用域结束的位置。正常路径上,执行流走到作用域末尾时,会按对象构造的逆序调用析构函数。

如果中途抛异常,编译器会配合异常展开机制,在栈展开过程中自动调用已经构造成功对象的析构函数,所以局部对象在异常场景里也能正常释放。

代码:

#include <iostream>
using namespace std;

class A {
public:
    A(const char* name) : name_(name) {
        cout << "construct " << name_ << 

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

C++ 常考面试题总结 文章被收录于专栏

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

全部评论
愿意试试多多吗,看我住叶,进度随时帮看
点赞 回复 分享
发布于 03-12 20:18 上海

相关推荐

03-07 13:19
门头沟学院 Java
字节again,感谢节子天天给我发面试机会1.&nbsp;拷打项目就只问了几句,主要是对业务场景提出了质疑,感觉我的回答他没理解,我也没太理解他想听啥,然后就糊弄过去了2.&nbsp;八股1.&nbsp;Java和python,c++等语言的区别2.&nbsp;java的特性(回答了继承封装多态)3.&nbsp;额外解释了一下什么是多态4.&nbsp;Java有哪些集合,解释一下底层数据结构(说了一下hashmap,&nbsp;arraylist,linkedlist)5.&nbsp;Array&nbsp;list如何扩容6.&nbsp;哈希map如何扩容?7.&nbsp;哈希map和hash&nbsp;table的区别,和con&nbsp;currentash&nbsp;map的区别,既然table和current&nbsp;hash&nbsp;map都是线程安全,为什么使用current&nbsp;hash&nbsp;map不用table8.&nbsp;MySQL和redis的区别9.&nbsp;既然MySQL内存和磁盘都能存储,为什么使用redis不用MySQL?为什么red&nbsp;is快?除了基于内存外有别的原因吗?10.&nbsp;hive表和mysql的区别,为什么hive表可以存储巨量数据11.&nbsp;你知道memory&nbsp;cache吗?(。。。理解错题意了,以为是问的技术或某个软件,和redis一样,面完了才缓过来是个技术概念,当时傻不拉几的说是c#里的一个类)12.&nbsp;Http的长连接和短连接13.&nbsp;为什么http&nbsp;传输层是用TCP不用udp?14.&nbsp;除了http&nbsp;1.0和1.1外,还了解别的版本吗?15.&nbsp;Http有什么状态码(啊啊啊这个记错了,500是internal&nbsp;server&nbsp;error,记成bad&nbsp;request了)16.&nbsp;大语言模型了解哪些?(说了一个agent)17.&nbsp;解释一下agent的作用(顺便扯到了mcp)算法题:实现指定下标的链表删除,就传一个index,然后删除列表的这个位置(简单题秒了)感觉面试官技术栈应该不是Java,就全程听我瞎扯呼,问的答上来的九成,算法题也撕的简单,面试体验也很好,就是最后给挂了。。
想摸鱼不想干活:woc过了,之前发了感谢问卷,还以为挂了呢,结果周一给我打电话又说过了
点赞 评论 收藏
分享
评论
1
6
分享

创作者周榜

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