Momenta C++ 智驾 一面 面经

1. 自我介绍

2. vector 的底层实现原理是什么?扩容时发生了什么?

答:vector 本质是一块连续的堆内存,维护三个指针:指向数据起始位置、指向当前末尾、指向分配内存的末尾,分别对应 sizecapacity

扩容过程:

  • size == capacity 时,继续 push_back 会触发扩容
  • 申请一块新的更大的内存(通常是原来的 1.5 倍或 2 倍,不同实现不同)
  • 把原来的元素全部移动(或复制)到新内存
  • 释放旧内存
  • 更新内部指针

关键点:

  • 扩容后所有迭代器、指针、引用全部失效,因为底层内存地址变了
  • 扩容是 O(n) 操作,但均摊下来每次 push_back 是 O(1)
  • 如果提前知道大小,用 reserve 预分配,避免多次扩容
  • shrink_to_fit 可以释放多余容量,但不保证一定生效

3. newmalloc 的区别是什么?deletefree 能混用吗?

答:区别:

类型

C 标准库函数

C++ 运算符

返回值

void*

,需要强转

对应类型的指针

构造函数

不调用

调用

失败处理

返回

NULL

抛出

std::bad_alloc

大小

手动指定字节数

自动计算

不能混用:

  • 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 之前,对象传递和返回都是复制,对于持有大量资源的对象(比如 vectorstring)代价很高
  • 移动语义允许"偷走"源对象的资源,而不是复制一份,源对象变成一个有效但未指定状态(通常是空)

移动构造函数和移动赋值运算符:

// 移动构造:把 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++八股文全集 文章被收录于专栏

本专栏系统梳理C++技术面试核心考点,涵盖语言基础、面向对象、内存管理、STL容器、模板编程及经典算法。从引用指针、虚函数表、智能指针等底层原理,到继承多态、运算符重载等OOP特性从const、static、inline等关键字辨析,到动态规划、KMP算法、并查集等手写实现。每个知识点以面试答题形式呈现,注重原理阐述而非冗长代码,帮助你快速构建完整知识体系,从容应对面试官提问,顺利拿下offer。

全部评论

相关推荐

03-17 23:54
黑龙江大学 Java
来个白菜也好啊qaq:可以的,大厂有的缺打手
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

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