「高频面试题」小米C++后端/嵌入式/客户端岗位高频面试题
适用岗位:C++开发工程师(后端/嵌入式/客户端等)
复习重点:C++底层原理、现代C++特性、STL源码机制、内存管理、多线程与网络、常见手撕算法选自:《C++八股文2026版》 & 学员投稿
第一部分:C++基础语法与关键字(高频必问)
1. 指针和引用的区别
-
定义与绑定:指针是一个变量,存储地址,可重新赋值指向其他地址,可为
nullptr;引用是变量的别名,定义时必须初始化,且终身绑定同一个对象。 -
内存占用:指针在32/64位系统下占4/8字节,有独立存储空间;引用在语义上不占用额外存储(底层通常也是地址,但视为原变量的直接别名)。
-
操作安全性:指针存在空指针、野指针风险,支持指针算术运算;引用更安全,使用起来像原变量本身。
-
函数传参:指针传参可通过解引用修改原值,也可修改指针指向;引用传参直接映射原变量,效率高且语法简洁。
2. static关键字的作用(多场景)
-
局部变量:生命周期延长至程序结束,但作用域不变,函数退出后变量值保留。
-
全局变量/函数:作用域限定在当前编译单元(.cpp文件),防止多文件链接时符号冲突(内部链接属性)。
-
类成员变量:属于类本身,所有对象共享一份,必须在类外单独初始化(C++17后可用
inline在类内初始化)。 -
类成员函数:不依赖于具体对象,只能访问静态成员变量或其他静态函数,不能调用非静态成员。
3. const关键字的完整用法(新增重点)
-
指针与引用:
-
const int* p(指向常量的指针):不能通过p修改所指对象的值,但p本身可以指向其他地址。 -
int* const p(常量指针):p本身不能指向其他地址,但可以通过p修改所指对象的值。 -
const int* const p:两者都不能修改。
-
-
成员函数:
void func() const表示该函数不会修改对象的非mutable成员变量,可被常量对象调用。const和非const版本可以重载(如operator[])。 -
mutable成员变量:在const成员函数中仍可被修改,常用于锁、缓存计数器等场景。
4. struct和class的区别
-
默认访问权限:
struct默认为public,class默认为private。 -
设计意图:
struct多用于纯数据聚合(兼容C语言),class用于完整的面向对象封装(继承、多态、RAII等)。 -
模板类型参数(重要修正):
class和typename在模板类型参数中完全等价;struct不能直接用于模板类型参数的声明(template <struct T>是非标准写法)。
5. malloc/free和new/delete的区别
-
语言特性:
malloc/free是C库函数,new/delete是C++运算符(可重载)。 -
内存大小计算:
malloc需手动传入字节数,new由编译器自动计算。 -
构造函数/析构函数:
malloc只分配原始内存,不调用构造函数;new分配后调用构造函数。free只释放内存,不调用析构函数;delete先析构再释放。 -
数组操作:
delete[]会依次调用数组中每个元素的析构函数;delete只调用第一个,对数组使用delete会导致未定义行为(内存泄漏或崩溃)。
第二部分:面向对象与多态(深挖底层)
6. 多态的实现原理(虚函数表机制)
-
分类:编译时多态(重载、模板)和运行时多态(虚函数、继承)。
-
核心数据结构:每个包含虚函数的类都有一个虚函数表(vtable),存储该类所有虚函数的实际地址。
-
对象布局:每个对象内部有一个虚表指针(vptr),指向所属类的虚表。对象构造时vptr被初始化。
-
调用过程:通过基类指针/引用调用虚函数时,程序运行时根据对象的vptr找到虚表,再定位到正确的函数地址(动态绑定)。
7. 析构函数为什么必须是虚函数(重要)
-
如果基类析构函数不是虚函数,用基类指针
delete派生类对象时,只会调用基类析构函数,派生类中的资源(如堆内存、文件句柄)无法释放,造成资源泄漏。 -
将基类析构函数声明为
virtual后,会先调用派生类析构函数,再调用基类析构函数,确保完整的对象清理。
8. 构造函数为什么不能是虚函数
-
依赖vptr:虚函数调用依赖对象的vptr,而vptr是在构造函数初始化列表执行期间才被设置的。
-
逻辑矛盾:构造顺序是从基类到派生类,如果构造函数是虚的,在基类构造时无法确定派生类的具体信息,导致无法完成多态构造。
-
语义不符:构造函数的作用是“创建对象”,而虚函数的含义是“在运行时根据对象实际类型调用函数”,对象都还没创建完,虚调用没有意义。
9. 纯虚函数和虚函数的区别
-
有无实现:虚函数可以有默认实现(可选重写);纯虚函数用
=0声明,通常不提供实现(但C++允许给纯虚函数提供定义,极少使用)。 -
对类的影响:包含纯虚函数的类成为抽象类,无法实例化。派生类必须重写所有纯虚函数,否则派生类也是抽象类。
-
设计目的:虚函数用于提供默认行为;纯虚函数用于定义接口契约(类似Java的Interface)。
第三部分:内存管理(避坑指南 + 现代C++)
10. 堆和栈的区别
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 管理方式 | 编译器自动分配/释放 | 程序员手动管理(new/delete) |
| 空间大小 | 较小(通常几MB,取决于系统/编译器) | 较大(受系统虚拟内存限制) |
| 访问速度 | 快(CPU直接支持) | 慢(涉及内存分配器算法) |
| 碎片问题 | 无碎片(后进先出) | 容易产生外部碎片 |
| 分配方式 | 连续分配 | 链表/空闲链表管理,不保证连续 |
11. 内存对齐(字节填充)(新增高频考点)
-
对齐规则:每个成员按其自身大小对齐(如
int按4字节对齐,double按8字节对齐);结构体的整体对齐大小为其最大成员大小的整数倍。 -
示例:
struct A { char c; int i; };在32位下sizeof(A)通常为8(c后面填充3个字节),而非5。 -
为什么要对齐:CPU访问内存时按字长(4/8字节)读取,未对齐的数据需要两次读取并拼接,降低效率。
-
手动控制:
#pragma pack(n)可以指定对齐值(如网络协议数据包常用#pragma pack(1)紧凑排列)。
12. 智能指针(C++11)
-
unique_ptr:独占所有权,禁止拷贝(拷贝构造被delete),支持移动语义(std::move)。内部无引用计数,开销极小。 -
shared_ptr:共享所有权,基于引用计数(控制块)。计数归零时自动释放对象。存在循环引用风险。 -
weak_ptr:弱引用,不增加引用计数。循环引用打破机制详解:对象A持有B的shared_ptr,B持有A的weak_ptr。A析构时B的计数减1,B正常释放;B释放后,A中的weak_ptr自动置空,形成完整释放链。使用时需lock()提升为shared_ptr。 -
循环引用案例:两个类互相持有对方的
shared_ptr,导致双方引用计数永远为1,无法释放。解决方案:将其中一方改为weak_ptr。
13. 移动语义与完美转发(C++11)(新增现代C++核心)
-
左值与右值:左值有持久地址(如变量名),右值无持久地址(如临时对象、字面量、
std::move的结果)。 -
std::move的本质:无条件将左值强制转换为右值引用,使其可以匹配移动构造函数或移动赋值运算符,从而“窃取”资源(如堆内存),避免拷贝。 -
std::forward(完美转发):在泛型函数中,保持参数的原始引用类型(左值保持左值,右值保持右值),常用于工厂函数和容器emplace_back的实现。 -
性能收益:移动语义使大对象(如
vector、string)传参和返回时开销极低,是C++11性能优化的关键。
14. 内存泄漏的定位与解决
-
定义:动态分配的内存(堆)未被释放,且指针丢失,导致内存无法被回收。
-
常见场景:
new/malloc后未对应delete/free;异常抛出时跳过释放代码;循环引用导致shared_ptr无法释放。 -
解决手段:
-
优先使用智能指针(RAII思想)。
-
使用工具检测:Valgrind(Linux)、AddressSanitizer(ASAN)、Visual Studio诊断工具。
-
代码审查确保
new/delete配对,或使用std::unique_ptr<T[]>处理数组。
-
第四部分:STL容器与迭代器(高频源码级)
15. vector和list的区别
-
底层结构:
vector是动态数组(连续内存),list是双向链表(不连续节点)。 -
随机访问:
vector支持O(1)随机访问,list仅支持顺序遍历O(n)。 -
插入/删除:
vector尾部操作O(1),中间操作需移动元素O(n);list任意位置插入/删除O(1)(前提是已知迭代器位置)。 -
内存开销:
vector可能预留多余容量(capacity),list每个节点额外存储前后指针(8/16字节开销)。
16. map/set与unordered_map/unordered_set的区别
-
底层实现:
map/set为红黑树(平衡二叉搜索树),元素自动排序(升序);unordered_*为哈希表,元素无序。 -
时间复杂度:红黑树增删查改均为
O(log n);哈希表平均O(1),最坏O(n)(大量哈希冲突时)。 -
适用场景:需要有序遍历、范围查找时用
map;追求极致查找速度、不关心顺序时用unordered_map。 -
键的类型要求:红黑树要求键支持
<运算符;哈希表要求键支持std::hash和==运算符。
17. 迭代器失效问题及解决方案(修正版)
-
vector:push_back仅在触发扩容(重新分配内存)时使所有迭代器失效;若不扩容,仅end()迭代器失效。insert和erase会导致插入/删除位置及其之后的所有迭代器失效。 -
list:insert不影响已有迭代器;erase仅使被删迭代器失效,其余保持有效。 -
deque:头部/尾部插入可能使所有迭代器失效,但元素地址不变(视具体实现)。 -
解决策略:利用
erase/insert的返回值(返回新的有效迭代器);遍历删除时使用it = vec.erase(it)写法;避免在循环中做插入操作。
第五部分:操作系统与多线程(必考硬核)
18. 进程和线程的区别
-
资源维度:进程是资源分配的最小单位(独立虚拟地址空间、文件描述符等);线程是CPU调度的最小单位,线程共享所属进程的资源。
-
切换开销:进程切换需刷新TLB、切换页表,开销大;线程切换仅需切换寄存器上下文,开销小。
-
通信方式:进程间通信(IPC)复杂(管道、共享内存、Socket等);线程间通信天然共享内存,但需加锁同步。
19. 死锁的四个必要条件与避免方法
-
互斥(Mutual exclusion):资源不能被共享,一次只能被一个进程占用。
-
持有并等待(Hold and wait):进程持有至少一个资源,并正在等待其他进程占有的资源。
-
不可抢占(No preemption):已分配的资源不能被强行剥夺。
-
循环等待(Circular wait):多个进程形成环路,每个进程等待下一个进程占有的资源。
-
避免策略:破坏任一条件。常用方法——资源有序分配法(破坏循环等待)、设置超时机制(破坏等待条件)、使用银行家算法(预防死锁)。
20. 进程间通信(IPC)方式
-
管道(Pipe/FIFO):半双工,用于父子进程或同主机进程。
-
消息队列(Message Queue):存储在系统内核,容量有限,支持消息类型过滤。
-
共享内存(Shared Memory):最快IPC,直接映射物理内存,需配合信号量或互斥锁同步。
-
信号量(Semaphore):计数器,用于同步与互斥,本质上不是传输数据,而是协调访问。
-
Socket:最通用,支持跨主机TCP/UDP通信。
21. select、poll和epoll的区别(IO多路复用)
-
select:fd数量限制为FD_SETSIZE(通常1024/2048);每次调用需将fd集合从用户态拷贝到内核态;采用轮询方式,效率随fd数量线性下降。 -
poll:使用链表存储fd,无数量限制;但仍需拷贝和全量轮询,效率与select类似。 -
epoll(Linux专属):基于事件驱动(红黑树+就绪链表)。epoll_wait只返回就绪的fd,无需轮询。支持边缘触发(ET)和水平触发(LT)。重要补充:ET模式需配合非阻塞fd使用,若一次read未读完数据,下次不再通知,可能漏读,因此必须循环读取直至EAGAIN。适合高并发、长连接场景。
第六部分:网络基础与系统交互
22. HTTP和HTTPS的区别
-
传输层:HTTP默认80端口,HTTPS默认443端口。
-
安全层:HTTP明文传输,无加密;HTTPS = HTTP + SSL/TLS加密层,提供数据机密性、完整性校验和服务器身份认证(数字证书)。
-
性能:HTTPS初次握手(SSL/TLS握手)耗时较长,需加解密计算,略慢于HTTP,但现已被广泛采用(尤其是涉及隐私的场景)。
23. 访问www.baidu.com背后发生了什么(经典全链路)
-
DNS解析:本地Hosts -> 递归/迭代DNS查询,将域名解析为IP地址。
-
TCP三次握手:客户端与服务器建立TCP连接(SYN -> SYN+ACK -> ACK)。
-
发送HTTP请求:构建HTTP报文(GET /),通过TCP连接发送。
-
服务器处理:Nginx/Tomcat等后端服务处理请求,可能涉及数据库查询、缓存等。
-
服务器响应:返回HTTP状态码(200)、响应头及HTML资源。
-
浏览器渲染:解析HTML、CSS、JS,加载静态资源(图片、JS等)。
-
TCP四次挥手:关闭连接(或保持长连接复用)。
第七部分:设计模式(新增常考模块)
24. 单例模式(线程安全实现)
-
懒汉式(双重检查锁定 DCLP):在C++11之前存在内存屏障问题(指令重排可能导致获取未初始化对象),C++11之后可用
std::atomic和std::memory_order正确实现,但复杂度高。 -
Meyers Singleton(最推荐):利用C++11标准的局部静态变量初始化线程安全特性。
class Singleton { public: static Singleton& getInstance() { static Singleton instance; // C++11保证线程安全 return instance; } Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete; private: Singleton() {} }; -
饿汉式:程序启动时即创建实例,线程安全但可能造成启动延迟和资源浪费(即使未使用也会创建)。
第八部分:手撕代码(高频题型)
面试中通常会要求现场写代码,难度中等(LeetCode Hot 100级别),重点考察思路清晰度、边界条件处理和时间/空间复杂度优化。
重要技巧:熟练使用STL算法(std::reverse、std::sort、std::unique等)可减少手写冗余代码,体现对标准库的熟悉,但面试官可能追问其复杂度(如std::sort为O(n log n))。
高频题目类型汇总
-
字符串处理:翻转字符串(原地,注意
std::reverse)、判断数字回文(不用转字符串)、最长无重复子串(滑动窗口)。 -
数组操作:双指针(去重、两数之和)、滑动窗口(最大/最小子数组)、合并两个有序数组(从后往前填充)。
-
链表:反转链表(迭代/递归)、判断环形链表(快慢指针)、合并两个有序链表(递归/迭代)。
-
二叉树:后序遍历(递归+非递归(双栈法))、层序遍历(BFS队列)、求二叉树的最大深度、判断平衡二叉树(后序遍历剪枝)。
-
动态规划:爬楼梯(斐波那契,注意
long long防溢出)、最大子序和(Kadane算法)。 -
数学逻辑:判断素数(优化到
sqrt(n))、正整数各位相加直到个位数(数根,直接(n-1)%9+1即可)。
备考冲刺建议
-
深挖原理:不能停留于“会用”,要理解STL底层数据结构(如红黑树特点、
vector扩容因子通常为2或1.5)、智能指针控制块结构(包含强引用计数、弱引用计数、删除器等)。 -
手写练习:务必在自己电脑上编译调试高频手撕题,注意处理空指针、负值等边界,关注编译器的警告信息。
-
项目结合:准备1-2个能体现C++11/14/17特性的项目,准备好“为什么用这个特性”以及“遇到了什么坑”的回答(例如为什么用
shared_ptr而非裸指针,std::move在哪处优化了性能)。 -
反问准备:面试最后通常有反问环节,建议从“技术栈迭代周期”、“团队当前面临的技术挑战”等角度提问,展现积极思考。
祝你面试顺利!这份文档涵盖了基础、进阶和实战细节,反复温习一定能大幅提升通过率。💪
更多推荐


所有评论(0)