腾讯 云平台架构 一面

1. 自我介绍

2. 平时写代码多吗,平时主要用 C++ 做什么

3. C++11 和 C++17 里你觉得比较重要的特性有哪些

答案:C++11 我觉得比较关键的是右值引用、移动语义、智能指针、lambda、线程库、原子操作、auto、范围 for、nullptr。这些特性基本把现代 C++ 的使用方式奠定下来了。C++17 里我更常用的是结构化绑定、if constexpr、折叠表达式、std::optionalstd::variantstd::string_view、并行算法里的一些思想。如果是实际工程里最有感知的,我会优先说移动语义、智能指针、lambda、原子和 string_view,因为这些直接影响写法和性能。

代码:

#include <iostream>
#include <optional>
#include <string_view>
using namespace std;

optional<int> parse(string_view s) {
    if (s.empty()) return nullopt;
    return stoi(string(s));
}

int main() {
    if (auto v = parse("123"); v.has_value()) {
        cout << *v << endl;
    }
    return 0;
}

4. STL 了解哪些,vectorunordered_map 的扩容机制怎么理解

答案:STL 我会从容器、迭代器、算法、函数对象、分配器这几块来讲。容器里常见的像 vectordequelistmapunordered_mapsetpriority_queuevector 的核心是连续内存和按容量扩张,扩容时通常会重新申请更大的空间,把旧元素搬过去,然后释放原空间,所以会带来迭代器失效。unordered_map 的核心是哈希桶,元素增多到一定程度后会 rehash,把原有元素重新分布到新的桶数组里。它的平均查找复杂度高,但极端碰撞时会退化。这题如果继续问,往往会追到底层内存布局、失效规则、负载因子和为什么实际工程里很多场景 vector 会比链表更快。

5. 详细讲一下你的项目,是否基于开源组件,是否基于 TCP,目的是什么

6. epollselectpoll 的区别是什么,LT 和 ET 又是什么

答案:selectpoll 本质上都需要每次把关注的 fd 集合交给内核,然后由内核遍历检查哪些就绪;fd 多的时候,线性扫描成本会很明显。epoll 把“注册感兴趣的 fd”和“等待活跃事件”拆开了,内核维护关注集合和就绪队列,等待时只返回活跃 fd,所以更适合高并发连接场景。LT 是水平触发,只要 fd 仍然处于可读可写状态,后续还会继续通知;ET 是边缘触发,只在状态从“不就绪”变成“就绪”的边缘时通知一次,要求应用自己把数据尽量读完或写完。如果继续深挖,往往会问红黑树、就绪链表和 ET 模式下的正确读写循环。

7. 用 epoll 的 ET 模式时,为什么一般一定要配合非阻塞

答案:因为 ET 模式只在状态变化时通知一次,如果你收到可读事件后没有把数据读到 EAGAIN,后面可能就收不到新的提醒了。如果 fd 还是阻塞的,读的时候一旦卡住,整个线程就可能被挂住,这和 ET 想要提高事件处理效率的目标是冲突的。所以 ET 模式基本都会和非阻塞 IO 配套使用:收到事件后循环读,直到返回 EAGAINEWOULDBLOCK 为止,写也是类似思路。

代码:

char buf[4096];
while (true) {
    ssize_t n = read(fd, buf, sizeof(buf));
    if (n > 0) {
        // 处理数据
    } else if (n == 0) {
        // 对端关闭
        close(fd);
        break;
    } else {
        if (errno == EAGAIN || errno == EWOULDBLOCK) {
            break;
        }
        close(fd);
        break;
    }
}

8. ET 模式下如果有一部分数据处理到了用户队列里,这次没读完,下次还能读到吗

答案:能不能再收到通知,要看你到底有没有把内核缓冲区的数据读空。如果你只是把部分数据拷到了用户层队列里,但 socket 接收缓冲区里还有没读走的数据,那么在 ET 模式下通常不会再次提醒,因为“就绪状态”并没有发生新的边缘变化。所以 ET 模式不是说“读一点先放着”,而是要求你这次事件来了之后尽量把内核里的数据读干净。应用层可以后处理,但内核态到用户态这一步最好一次拉空。这也是为什么 ET 模式常常配合环形缓冲区、协议解析状态机和分阶段处理。

9. 当前服务端连接很多,但处理能力有限,临时不想继续读,怎么做保护

答案:这类场景本质上是背压问题。如果用户层任务队列已经堆积,继续从 socket 读数据只会把压力从内核缓冲区搬到用户缓冲区,最后导致内存继续膨胀。比较常见的做法是暂停关注读事件,等队列消费到一定水位后再重新注册 EPOLLIN,或者主动做连接级别限流。再往系统层看,还可以做最大连接数限制、每连接读写配额、超时淘汰、优先级调度、拒绝新连接或者让客户端指数退避重试。如果业务允许,严重过载时也可以直接丢弃低优先级请求,而不是把所有连接都拖死。

10. TCP 里的滑动窗口、接收窗口、拥塞窗口分别是什么关系

答案:滑动窗口是一个更泛的概念,用来控制发送方当前允许“在路上”的未确认数据量。接收窗口是接收方根据自己缓冲区剩余空间告诉发送方“你最多还能发多少”;拥塞窗口则是发送方根据网络拥塞情况自己控制的发送额度。真正发送时,通常受两者共同限制,可以简单理解成有效窗口大致取决于接收窗口和拥塞窗口中更小的那个。所以一个连接发不快,不一定是网络差,也可能是对端处理不过来把接收窗口收小了,或者发送方因为丢包把拥塞窗口降下来了。

11. TCP 的包和 MTU、MSS、IP 分片之间是什么关系

答案:MTU 是链路层一次能承载的最大帧载荷大小,最常见以太网 MTU 通常是 1500 字节。IP 层的数据报如果超过路径上的 MTU,就可能发生分片;而 TCP 为了尽量避免 IP 分片,会根据 MSS 来控制单个 TCP 报文段的数据部分大小。MSS 可以简单理解成在当前路径下,一个 TCP 段里数据部分尽量不要超过的大小,通常和 MTU、IP 头、TCP 头长度有关。真实工程里一般都希望尽量避免分片,因为分片一旦丢一个,整个 IP 报文重组都会失败,代价比较高。

12. TCP 粘包和拆包是怎么回事,怎么解决

答案:粘包和拆包不是 TCP 出错,而是 TCP 是字节流,没有消息边界。发送方写两次,接收方可能一次读到,也可能分多次读到。如果上层协议需要“完整的一条消息”,就必须自己设计边界。常见办法是固定长度、分隔符、或者最常用的“长度字段 + 消息体”。真正的关键不只是知道方案,而是要写出能处理半包、粘包、恶意长度和缓冲区扩展的状态机。如果是高性能服务,协议解析通常会和连接对象自己的读缓冲区绑定,而不是每次新建字符串做拼接。

代码:

struct Header {
    uint32_t len;
};

bool try_parse(Buffer& buf) {
    if (buf.size() < sizeof(Header)) return false;
    auto* h = reinterpret_cast<const Header*>(buf.data());
    if (buf.size() < sizeof(Header) + h->len) return false;

    // 取出一条完整消息
    process(buf.data() + sizeof(Header), h->len);
    buf.consume(sizeof(Header) + h->len);
    return true;
}

13. 零拷贝了解吗,常见方式有哪些

答案:零拷贝的核心不是“绝对没有任何拷贝”,而是尽量减少用户态和内核态之间不必要的数据复制,以及减少 CPU 参与数据搬运。常见方式有 sendfilemmapsplice。例如文件发网络时,传统路径可能是磁盘到内核、内核到用户、用户再回到 socket 缓冲区;而 sendfile 可以让内核直接把文件内容搬到 socket 发送路径,减少一次或多次用户态参与。它特别适合大文件传输、静态资源服务和日志转发这类场景,但如果应用层还要改写内容、加密、压缩,那就不一定能直接享受到完整收益。

代码:

#include <sys/sendfile.h>
#include <fcntl.h>
#include <unistd.h>

int main() {
    int in_fd = open("a.log", O_RDONLY);
    int out_fd = 1; // 示例:输出到标准输出
    off_t offset = 0;
    sendfile(out_fd, in_fd, &offset, 4096);
    close(in_fd);
    return 0;
}

14. TCP Keepalive 和应用层心跳有什么区别

答案:Keepalive 是 TCP 协议栈层面的保活机制,主要用于探测连接是否还活着,默认周期通常比较长,而且更多是“活性探测”,并不一定适合业务实时性要求。

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

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

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

全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

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