目录
前言:面试官真正在考什么
一、手写代码:算法基本功
1.1 非递归二分查找
1.2 从数组中找出第二大的数
二、C/C++ 语言核心
2.1 `struct` 与 `class` 的区别
2.2 C 中 `static` 的作用
2.3 C++ 中 `extern "C"` 的作用
2.4 多态的实现机制
2.5 C/C++ 程序的内存布局
2.6 宏:由数组名求长度
三、Linux 系统与 IO
3.1 僵尸进程如何产生
3.2 如何避免僵尸进程
3.3 文本去重:`sort` / `uniq` / `awk` / `sed`
3.4 `epoll`:作用、优势与创建参数
四、架构题:海量用户并发登录要考虑什么
4.1 传输协议选择
4.2 负载均衡与分片
4.3 线程与进程模型
五、总结
前言:面试官真正在考什么
腾讯这类后台 / C++ 岗的面试,常见节奏是:
(1). 先手写一段代码,如二分、链表、快排、atoi 等,看编码习惯与边界意识;
(2). 再挖语言与系统细节,如static、虚表、内存布局、epoll、进程模型;
(3). 最后抬到架构层,最常见的如协议选择、负载均衡、线程模型。
下面按这个认知顺序重组原题,每一题给出:推荐答法 + 追问点 + 常见坑 。
一、手写代码:算法基本功
1.1 非递归二分查找
有序数组上的二分查找是面试热身题。重点不在背模板,而在:循环条件、区间收缩、溢出与失败返回值。
非递归二分查找示意
图 1:每次比较array[mid]与目标值,将搜索区间收缩到一半,直至找到或区间为空。
// 数组递增有序;找到返回下标,失败返回 -1 int binarySearch(const int* array, int len, int value) { if (array == nullptr || len <= 0) { return -1; } int low = 0; int high = len - 1; while (low <= high) { // 避免 (low + high) 溢出 int mid = low + (high - low) / 2; if (array[mid] == value) { return mid; } if (value > array[mid]) { low = mid + 1; // 右半区 } else { high = mid - 1; // 左半区 } } return -1; }面试加分点:
同类常考手写:快排、链表反转、atoi / 字符串转整数。
1.2 从数组中找出第二大的数
次优思路:堆排序
建大顶堆后做两次取堆顶,时间复杂度 \(O(n\log n)\)。能写出堆调整说明功底扎实????不是本题期望答案。面试官通常会追问:能否 \(O(n)\)?
堆相关下标(完全二叉树、数组下标从 0 起):
左孩子:2 * i + 1,右孩子:2 * i + 2
最后一个非叶子节点:len / 2 - 1
推荐答案:一遍扫描,维护最大值与次大值
核心是以两个变量换掉第二次全表扫描,真正做到 \(O(n)\)、额外空间 \(O(1)\)。
// 返回第二大的数;无法求出时返回 INT_MIN(也可约定返回 -1,需与面试官对齐) #include <climits> int getSecondMax(const int* array, int len) { if (array == nullptr || len < 2) { return INT_MIN; } int maxVal = array[0]; int second = INT_MIN; bool hasSecond = false; for (int i = 1; i < len; ++i) { if (array[i] > maxVal) { second = maxVal; maxVal = array[i]; hasSecond = true; } else if (array[i] < maxVal) { // 严格小于最大值,才可能成为「第二大」 if (!hasSecond || array[i] > second) { second = array[i]; hasSecond = true; } } // array[i] == maxVal:跳过,避免「第二大」等于最大值(视题目是否允许重复) } return hasSecond ? second : INT_MIN; }和「冒泡 / 选择排两遍」的对比
冒泡或简单选择各扫一遍拿到最大、次大,本质是 \(O(2n)\),常数更大,且不满足「只要 \(O(n)\)」的表述偏好;
一遍维护 max / second,比较次数约 \(n\) 量级,才是标准答法。
必须主动提的边界
(1). len < 2;
(2). 全部元素相等 → 没有严格意义上的第二大;
(3). 负数、重复值、最大值出现多次。
二、C/C++ 语言核心
2.1 `struct` 与 `class` 的区别
在 C++ 里二者几乎等价,默认访问权限与默认继承权限不同:
补充两点常被追问:
(1). 模板类型参数写法是 template<typename T> 或 template<class T>,不能写成 template<struct T>;
(2). 习惯上:struct 多表示「纯数据聚合」,class 多表示「有不变式与封装的类型」——这是风格约定,不是语言强制。
2.2 C 中 `static` 的作用
分场景记,比背定义更稳:
(1) 修饰局部变量
存储期:静态存储期,生命周期贯穿整个程序;
作用域:仍只在定义它的函数 / 块内可见;
只初始化一次。
(2) 修饰全局变量 / 函数(C 与 C++ 文件作用域)
给予内部链接:仅当前翻译单元可见,避免与其他 .c/.cpp 符号冲突;
修饰函数同理,限制作用域为当前文件。
C++11 起还有「静态局部变量的线程安全初始化」等细节,资深岗可能追问。
2.3 C++ 中 `extern "C"` 的作用
告诉编译器:按 C 的规则做名字修饰(name mangling)与链接。
原因:C++ 支持函数重载,符号名会编码参数类型;C 不支持重载,符号更「直白」。例如 void foo(int, int):
C 侧常见类似 _foo;
C++ 侧可能变成 _foo_int_int 一类 mangled name(具体格式与编译器相关)。
典型用途:
#ifdef __cplusplus extern "C" { #endif void c_api_init(void); #ifdef __cplusplus } #endif用于:C++ 调用 C 库、导出给 C 调用的接口、动态库稳定 ABI。
2.4 多态的实现机制
一句话:动态多态靠虚函数表(vtable)+ 虚表指针(vptr)完成运行时绑定。
C++ 虚函数表与动态绑定
图 2:对象内嵌vptr指向所属类的虚表;通过基类指针调用虚函数时,按对象真实类型跳转到对应实现。
口述框架建议:
(1). 类若声明虚函数,编译器为该类生成虚表,表项是函数地址;
(2). 每个对象多一个隐藏的 vptr,构造时指向正确虚表;
(3). Base* p = new Derived; 调用 p->foo() 时,查 p 所指对象的虚表,执行 Derived::foo;
(4). 析构函数通常也应为虚函数,否则经基类指针 delete 会只析构基类部分。
可延伸:多重继承下的多张虚表、虚继承与 vbptr、纯虚函数与抽象类、override / final。
2.5 C/C++ 程序的内存布局
进程地址空间(用户态)常见分层如下(地址由低到高):
程序内存布局示意
图 3:典型用户态布局——代码与静态数据在低址侧,堆向上增长,栈向下增长;中间为可映射区域。
常考追问:栈溢出、堆碎片、new 失败策略、智能指针与 RAII、内存泄漏排查。
2.6 宏:由数组名求长度
#define ARRAY_LEN(array) (sizeof(array) / sizeof((array)[0]))原理:sizeof(array) 为整段字节数,sizeof(array[0]) 为单元素字节数,二者相除得元素个数。
坑 :该宏只对真正的数组名有效。一旦数组退化为指针(函数参数传递),sizeof(array) 变成指针大小,结果错误。C++ 更推荐:
template <typename T, std::size_t N> constexpr std::size_t arrayLen(const T (&)[N]) noexcept { return N; }三、Linux 系统与 IO
3.1 僵尸进程如何产生
在 UNIX / Linux 中:子进程已退出,但父进程尚未 wait / waitpid 回收其退出状态时,该子进程进入僵尸态(Z)。内核仍保留最小进程表项(PID、退出码等),等待父进程读取。
僵尸进程产生与回收
图 4:子进程退出后若无人 wait,会残留为僵尸;父进程回收或由 init 接管后才能彻底释放。
3.2 如何避免僵尸进程
3.3 文本去重:`sort` / `uniq` / `awk` / `sed`
题目:删除文本文件中的重复行。
# 方法一:排序并去重(输出有序) sort -u file # 方法二:先排序再 uniq(可配合 -k/-t 按字段) sort file | uniq # 方法三:sort + awk(相邻去重) sort file | awk '{ if ($0 != line) print; line = $0 }' # 方法四:sort + sed(模式空间两行比较) sort file | sed '$!N; /^\(.*\)\n\1$/!P; D'说明:
uniq 只去掉相邻重复,所以通常要先 sort;
若要求「保序去重、不排序」,可用 awk '!seen[$0]++' 。
sed 版简要拆解:N 读入下一行拼进模式空间;正则匹配两行相同;!P 表示不匹配时打印第一行;D 删到换行并重新循环——保证模式空间里始终是「当前待比较的两行」。
3.4 `epoll`:作用、优势与创建参数
epoll 是 Linux 上对大批量 fd 做 IO 多路复用的机制,是 select / poll 的增强版:大量连接、少量活跃时,CPU 利用率通常明显更好。
epoll 与 select 对比
图 5:select 侧多在用户态/内核间反复拷贝并线性扫描;epoll 维护兴趣列表与就绪队列,取事件更接近只处理就绪者。
与 `select` 的核心差异
创建接口:
int epoll_create(int size); // Linux 2.6.8 后 size 被忽略,但仍需 >0 int epoll_create1(int flags); // 推荐;flags 可为 0 或 EPOLL_CLOEXEC 等注意:
创建出的 epfd 本身也占一个 fd,用完必须 close;
EPOLL_CLOEXEC:exec 时自动关闭,避免 fd 泄漏给新镜像;
旧资料里 EPOLL_NONBLOCK 用于 epoll_create1 的说法不准确——非阻塞通常设在被监听的业务 fd 上,ET 模式下尤其常见。
ET 使用口诀:配合非阻塞 IO,一次事件尽量读写到 EAGAIN,否则可能「漏事件」。
四、架构题:海量用户并发登录要考虑什么
题目往往开放:若你是架构师,如何支撑 QQ 量级用户并发登录?不必复述产品内幕,按协议 → 接入 → 状态 → 扩展 → 线程模型分层答即可。
海量登录并发架构示意
图 6:客户端经负载均衡进入鉴权网关,账号按分片落到后端服务;长连接保活与登录短连接可分层设计。
4.1 传输协议选择
口述时可强调:可靠性既可以在传输层(TCP),也可以在应用层补齐(UDP 之上自建可靠语义)。
4.2 负载均衡与分片
短时海量登录冲击的是鉴权与账号存储:
(1). 接入层:L4/L7 负载均衡,无状态网关水平扩展;
(2). 账号分片:按 QQ 号哈希或尾号分片 ,把热点打散到多组服务与库;
(3). 缓存:热点票据、风控规则放 Redis 等,减轻 DB;
(4). 限流与降级:验证码、排队、只读降级,防止雪崩。
4.3 线程与进程模型
服务端处理连接,现代常见是:
- 多进程 / 多线程 + IO 多路复用(epoll):一个线程处理大量连接事件;
- CPU 密集(加解密、风控)再丢到线程池;
- 进程适合做隔离与优雅重启,线程适合共享缓存与更轻的上下文切换。
一律用线程服务每个登录过于绝对;更完整的答法是:事件驱动处理连接,工作线程池处理业务,并说明共享数据需要锁或无锁结构。
五、总结
以上腾讯题覆盖了后台 C++ 面试的典型剖面:能写对代码、能讲清机制、能画对架构图。复习时不要只背答案——对每一题准备一个追问应答:复杂度能否再降?有没有边界?线上如何观测与降级?把这三问答顺,现场发挥会稳很多。