CPP 综合
一些CPP面试内容,有时间再整理
详细解释 C++ 中 “对象模型” 的核心概念(如虚函数表、内存布局、this 指针),并分析: ① 虚函数调用的核心流程; ② 多重继承下虚函数表的结构; ③ 为什么空类的大小是 1 字节?
0.首先 OOP有三大特性 封装 继承 和 多态,封装的底层原理就是内存布局,所有访问控制比如访问 private 还是 public 仅在编译阶段由编译器检查,底层本质上是结构体; 非静态数据成员直接存放在对象体内,也就是这个结构体; static成员变量、静态和非静态的成员函数存放在代码段; 成员函数和普通的全局函数没有什么本质区别,只不过编译器会隐式添加一个 this 指针参数,成员函数调用时,编译器会再隐式传入一个 this 指针,this指向当前对象的首地址。 多态的底层原理就是虚函数表,主要通过 vtable和vptr实现。 vtable是编译器为每个拥有虚函数的类创建一个静态数组,里面存放着该类所有虚函数的地址,一个类一个表 vptr 是编译器为每个对象的内存头部插入一个隐藏的指针,指向该类对应的 vtable, 每个有虚函数的对象都有
1.虚函数调用的核心流程:通过基类指针调用 base_ptr->foo(), 根据指针 base_ptr 找到对象内vptr, 通过 vptr 找到该对象实际所属类的vtable, 根据函数在表中的索引找到对应的函数地址,然后调转到该地址执行代码。 2.多重继承下虚函数表的结构:派生类对象内部通常会有多个个 vptr,分别指向对应的虚表 3.保证每一个独立的对象在内存里都有独立的地址,如果是0空对象数组所有地址就一样了
对比 C++11/14/17/20 的核心特性 ① 说明特性的设计初衷; ② 分析底层实现逻辑(如 lambda 表达式的闭包原理、std::optional 的内存布局); ③ 结合项目案例说明使用该特性后,在性能 / 可读性 / 安全性上的具体提升(可提供直观效果描述)。
1、Lambda 部分情况不用额外写函数了,使用如sort或回调函数时,必须编写独立的函数。这导致代码阅读时需要在不同位置跳转,并且难以捕获局部上下文Context,需要传参 Lambda 表达式本质上是编译器生成的匿名类 异步任务回调很方便
2、智能指针 解决 C++ 内存泄漏和悬空指针问题。实现 RAII unique_ptr: 内部仅包含一个原始指针,其大小等同于原始指针 禁用了拷贝构造和赋值,只允许移动,确保所有权唯一。 shared_ptr: 内部包含两个指针,一个指向对象,一个指向堆上的控制块。控制块包含引用计数(原子操作)和弱引用计数。
安全性上 在异常发生时,栈展开会自动析构智能指针,防止资源泄漏。
3、constexpr 将计算从运行移至编译时。提高运行时性能,减少启动时间。
编译器在编译阶段解释执行代码,计算出结果,并将结果直接硬编码到二进制文件的 .text 或 .data 段中。
4、auto 少写点类型名 auto的推断机制类似于C++的模板参数推断,可以看作是编译器将auto变量看作一个函数参数,传入模板函数,从而推断出模板参数的实际类型 对于一些巨长的名字可太有用了
5、Concepts C++20解决模板编程天书错误的问题,用于约束模板参数的类型。 Concept 是编译期的布尔表达式,编译器在实例化模板前,先检查类型是否满足 Concept。如果不满足,直接从重载候选集中剔除,并给出清晰的“约束不满足”错误。
内存管理是 C++ 的核心考点: ① 详细说明堆、栈、全局 / 静态存储区、常量存储区的内存分配机制、生命周期及访问效率差异; ② 列举 4 种常见内存泄漏场景(含智能指针使用不当的情况); ③ 说明 Valgrind 或简单内存检测工具的使用方法,以及你在项目中如何通过 “编码规范 + 手动排查” 减少内存问题。
1、 栈 自动分配 生命周期是函数结束时自动释放,访问效率最高 堆 手动分配 程序员控制生命周期 访问效率较低 全局/静态区 编译器分配 全生命周期 访问效率较高 常量区 编译器分配 全生命周期 访问效率较高
2、 智能指针的循环引用 基类析构函数非虚 容器中的指针指向的元素未清理 异常导致的内存泄漏 非RAII
3、 检测可用Valgrind 或者 ASan,ASan更快,但是需要处理编译,要加到CI上 编码规范:严禁裸写的 new 和 delete,必须使用智能指针
深入分析 STL 容器: ① vector 的扩容机制(扩容因子、内存拷贝成本、reserve 与 resize 的区别),如何避免频繁扩容? ② map(红黑树)与 unordered_map(哈希表)的底层实现核心逻辑,对比在 “百万级数据查找、插入、删除” 场景下的性能差异; ③ 项目中遇到 STL 容器性能瓶颈时,你是如何通过替换容器类型或优化遍历方式解决的?
1、 GCC 2 倍, 内存拷贝成本高, 开辟新空间, 搬迁元素,如果对象没有移动构造函数,会强制走拷贝构造, 析构旧对象并释放内存 reserve 只分配内存,不创建对象(Size 不变)。 resize 既分配内存,又创建对象。 如何避免频繁扩容,估计数据量,如果可以也可以放大扩容因子
2、 map:红黑树 一种自平衡二叉搜索树。Key 有序。节点在内存中是分散的(通过指针连接)。 unordered_map:哈希表 通常是数组 + 链表, Key 无序。通过 Hash 函数计算索引。
百万级数据查找、插入、删除 均是unordered_map哈希表快,但是如果Rehash,或者有大量hash冲突,就不行了,map可以范围查找,速度也可以更均衡, 两者都存在cache miss 的情况, 可以第三方用 swiss table 优化下
3、 用排序数组替代 map 做只读查找 开放寻址法哈希表 swiss table
并发编程深度解析: ① C++11 线程库与 pthread 的关联,线程创建 / 销毁的核心流程; ② 互斥锁(mutex)、读写锁(shared_mutex)的实现原理及适用场景; ③ 原子操作(std::atomic)的基本用法,项目中如何通过原子操作或简单锁机制实现并发安全。
1、 在 Linux上,thread 是对 pthread 的封装
创建: 用户态:调用 std::thread 构造函数 pthread_create 内核态:调用 clone 系统调用 初始化:内核分配内核栈 用户态分配用户栈,然后跳转到线程入口函数
销毁: 自然结束:线程函数返回,调用 pthread_exit 如果线程是 joinable 状态,它会变成僵尸线程,直到主线程调用join() 如果线程是 detached 状态,退出时自动释放资源
2、 mutex (互斥锁)
互斥锁不再是单纯的互斥锁,先尝试用原子操作修改锁的状态。如果成功,直接获得锁,无系统调用开销,速度快。如果 CAS 失败,才调用 futex 系统调用,将线程挂起。
读写锁(shared_mutex)用于读多写少 内部维护两个状态:读者计数 写者标志 读锁:如果无写者,读者计数 +1,立即进入;否则等待。 写锁:必须等待读者计数归零且无其他写者,才能进入。
3、基本操作:load, store fetch_add, exchange。
二、场景化实战 服务器开发场景:设计一个支持 “万级并发 TCP 连接” 的网关核心模块(C++ 实现),要求: ① 选择合适的 IO 模型(epoll LT/ET)并说明理由; ② 设计简单线程模型(如主从 Reactor),描述核心流程; ③ 解决粘包 / 拆包、半包问题的具体方案; ④ 如何保证连接断开时的资源安全释放(避免文件描述符泄漏)。
1、 选择:epoll ET 边缘触发
主要是可以减少系统调用,ET下,状态变化仅通知一次。相比LT在缓冲区有数据时不断触发,ET 能减少 epoll_wait的返回次数。 在极高负载下可以减少内核与用户态之间的上下文切换开销。
需要使用 非阻塞 IO O_NONBLOCK,必须循环读取直到返回EAGAIN
新系统内核也可以尝试io_uring,异步IO
2、主从 Reactor
主 Reactor 负责监听套接字。epoll_wait监控 accept事件, 建立新连接后,选择一个 Sub Reactor,并将 fd 分发给它
Sub Reactor 多个
- 每个 Sub Reactor 维护自己的epoll fd。
- 负责已连接套接字的所有 IO 事件。
- 负责逻辑处理。
3、 解决粘包 / 拆包与半包问题
TCP 是面向字节流的协议,没有消息边界。必须在应用层设计协议格式。 主要是 固定包头 + 长度字段
4、资源安全释放
使用智能指针管理连接对象。在对象析构函数中执行 close(fd)。 read返回 0(对端关闭)或 -1 且非 EAGAIN时,主动调用 close。
跨模块 / 跨语言协作: ① 说明 Protobuf 的核心序列化原理,对比 JSON 的性能差异; ② 设计 C++ 与 Java/Python 的简单跨语言调用方案,如何解决数据类型兼容性问题; ③ 项目中如何保证接口调用的可靠性(如超时重试)?举例说明你解决的跨语言调用异常问题。
1、 序列化原理 Protobuf 不存储字段名,只存储字段编号 Varint 和 ZigZag 做压缩
比JSON小,也比json快 JSON 包含大量字段名字符串,并且需要进行复杂的字符串解析,要保证安全,性能没有办法太高