Momenta C++ 智驾 一面 面经
1. 自我介绍
2. vector 的底层实现原理是什么?扩容时发生了什么?
答:vector 本质是一块连续的堆内存,维护三个指针:指向数据起始位置、指向当前末尾、指向分配内存的末尾,分别对应 size 和 capacity。
扩容过程:
- 当
size == capacity时,继续push_back会触发扩容 - 申请一块新的更大的内存(通常是原来的 1.5 倍或 2 倍,不同实现不同)
- 把原来的元素全部移动(或复制)到新内存
- 释放旧内存
- 更新内部指针
关键点:
- 扩容后所有迭代器、指针、引用全部失效,因为底层内存地址变了
- 扩容是 O(n) 操作,但均摊下来每次
push_back是 O(1) - 如果提前知道大小,用
reserve预分配,避免多次扩容 shrink_to_fit可以释放多余容量,但不保证一定生效
3. new 和 malloc 的区别是什么?delete 和 free 能混用吗?
答:区别:
类型 |
C 标准库函数 |
C++ 运算符 |
返回值 |
,需要强转 |
对应类型的指针 |
构造函数 |
不调用 |
调用 |
失败处理 |
返回
|
抛出
|
大小 |
手动指定字节数 |
自动计算 |
不能混用:
malloc分配的内存用delete释放:未定义行为,delete会尝试调用析构函数,但对象从未被构造过new分配的内存用free释放:析构函数不会被调用,可能导致资源泄漏,内存管理器的元数据也可能不兼容new[]必须用delete[],new必须用delete,混用同样是未定义行为
4. 说一下你理解的 RAII,结合智能指针讲一下
答:RAII(Resource Acquisition Is Initialization):资源的生命周期绑定到对象的生命周期,在构造函数里获取资源,在析构函数里释放资源,利用 C++ 对象离开作用域自动析构的特性,保证资源不泄漏。
智能指针是 RAII 的典型应用:
unique_ptr:
- 独占所有权,同一时刻只有一个
unique_ptr指向某个对象 - 不可复制,只能移动(
std::move) - 零开销,和裸指针性能相同
- 适合明确的单一所有权场景
shared_ptr:
- 共享所有权,内部维护引用计数
- 复制时引用计数加一,析构时减一,减到零时释放资源
- 有额外的控制块内存开销和引用计数的原子操作开销
- 注意循环引用问题:A 持有 B 的
shared_ptr,B 持有 A 的shared_ptr,两者引用计数永远不为零,内存泄漏
weak_ptr:
- 不增加引用计数,用于打破循环引用
- 使用前需要
lock()升级为shared_ptr,如果对象已销毁则返回空
5. 多态是怎么实现的?虚函数表的结构是什么样的?
答:C++ 多态通过虚函数表(vtable)实现:
- 每个含有虚函数的类,编译器为其生成一张虚函数表,表里存放该类所有虚函数的函数指针
- 每个该类的对象,在内存布局的最开头有一个隐藏的虚指针(vptr),指向该类的虚函数表
- 调用虚函数时,通过对象的 vptr 找到虚函数表,再通过函数在表中的偏移找到实际函数地址,完成调用
派生类的情况:
- 派生类有自己的虚函数表
- 如果重写了基类的虚函数,表中对应位置替换为派生类的函数指针
- 如果没有重写,继承基类的函数指针
- 派生类新增的虚函数追加在表的末尾
性能影响:
- 虚函数调用比普通函数多一次间接寻址(通过 vptr 找表,再找函数地址)
- 无法内联,因为编译期不知道实际调用哪个函数
- 在自动驾驶等对性能敏感的场景,热路径上要谨慎使用虚函数
6. std::move 做了什么?移动语义解决了什么问题?
答:std::move 本身不移动任何东西,它只是一个类型转换,把左值强制转换为右值引用,告诉编译器"这个对象可以被移动"。
移动语义解决的问题:
- 在 C++11 之前,对象传递和返回都是复制,对于持有大量资源的对象(比如
vector、string)代价很高 - 移动语义允许"偷走"源对象的资源,而不是复制一份,源对象变成一个有效但未指定状态(通常是空)
移动构造函数和移动赋值运算符:
// 移动构造:把 other 的内部指针偷过来,other 置空
MyClass(MyClass&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
实际场景:
- 函数返回局部对象时,编译器会优先使用 RVO(返回值优化),其次使用移动语义,避免不必要的复制
- 把对象放入容器时,如果对象是临时值,用移动而不是复制
std::move用完之后不要再使用被移动的对象,状态未定义
7. 讲一下 const 的几种用法,const 成员函数里能修改成员变量吗?
答:const 的用法:
const int a:常量,不可修改const int* p:指向常量的指针,不能通过 p 修改值,但 p 本身可以指向别处int* const p:常量指针,p 不能指向别处,但可以通过 p 修改值const int* const p:两者都不能改const成员函数:函数声明末尾加const,承诺不修改对象的成员变量,this指针变为const T*
const 成员函数里修改成员变量:
- 普通成员变量:不能修改,编译报错
mutable修饰的成员变量:可以修改,mutable专门用于在const函数里需要修改的场景,比如缓存、互斥锁、引用计数- 通过
const_cast去掉 const 强行修改:未定义行为,不要这样做
8. 进程和线程的区别?线程间同步有哪些方式?
答:区别:
- 进程是资源分配的基本单位,有独立的地址空间、文件描述符、信号处理等
- 线程是 CPU 调度的基本单位,同一进程内的线程共享地址空间和大部分资源,但有独立的栈、寄存器、线程局部存储
- 进程间切换开销大(需要切换页表等),线程间切换开销小
- 进程间通信需要专门的 IPC 机制,线程间可以直接读写共享内存,但需要同步
线程间同步方式:
互斥锁(mutex):保护临界区,同一时刻只有一个线程进入
std::mutex mtx; std::lock_guard<std::mutex> lock(mtx); // RAII,自动释放
条件变量(condition_variable):线程等待某个条件成立
std::condition_variable cv;
cv.wait(lock, []{ return ready; }); // 等待
cv.notify_one(); // 通知
原子操作(atomic):对简单类型的无锁操作,比互斥锁轻量
std::atomic<int> counter{0};
counter.fetch_add(1); // 原子加一
读写锁(shared_mutex):允许多个读者同时读,写者独占
信号量(C++20 的 std::semaphore):控制并发访问数量
9. 内存泄漏怎么排查?用过哪些工具?
答:排查思路:
代码层面:
- 检查每个
new是否有对应的delete,优先用智能指针代替裸指针 - 检查异常路径,异常抛出时是否跳过了
delete - 检查循环引用,
shared_ptr互相持有 - 检查容器里存放的裸指针,容器清空时是否释放了指针指向的内存
工具:
Valgrind(Linux):
valgrind --leak-check=full ./program- 能检测内存泄漏、越界访
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。


查看29道真题和解析