C++八股文面经
目录
说明:重点在于收集问题,答案内容较简洁,此答案仅供参考。
1、c和c++最大的区别
C:纯过程式编程语言,以函数和数据结构为核心,通过函数调用组织代码,强调 “怎么做”(流程)。 C++:多范式语言,核心是面向对象编程(OOP),支持封装、继承、多态,将数据和操作绑定为 “类”,强调 “做什么”(对象交互)。
C:适合底层开发,追求极致性能和硬件控制: 操作系统内核、嵌入式系统、驱动程序、高性能服务器等。 C++:适合复杂应用开发,兼顾性能与抽象能力: 大型应用程序(如浏览器、游戏引擎)、高性能计算、图形界面开发等。 总结
2、c可以实现c++中的对象吗
虽然 C 语言没有原生支持面向对象编程(OOP)的语法(如 class、public/private、virtual 等),但可以通过结构体、函数指针和一些编程技巧模拟 C++ 中的 “对象” 特性,包括封装、继承和多态。
3、如何理解this指针
本质是 C++ 编译器为成员函数隐式添加的一个参数,用于标识当前调用函数的对象实例,使得成员函数能准确访问该对象的数据。
底层原理:this 指针并非对象本身的一部分(不占用对象的内存空间),而是编译器在调用成员函数时,通过参数传递给函数的。其传递方式依赖于编译器和调用约定(如 __thiscall)。
C++ 编译器会将成员函数编译为类似 “带额外参数的全局函数”,这个额外参数就是 this 指针。
4、你觉得你c++能力100分 能评到多少分
核心是既要展现自信,又要体现对自身能力的清醒认知,避免过度自负或过度谦虚。回答的关键在于:给出具体分数(通常 70-80 分较合适)+ 说明评分依据(结合自身优势和待提升点)+ 体现持续学习的态度。
推荐回答框架: “如果满分是 100 分,我会给自己打 75 分左右。 理由是: 优势方面:我对 C++ 的核心语法(如面向对象特性、模板、STL)和底层原理(如虚函数表、智能指针实现、内存管理)有系统理解,能独立完成中等复杂度的项目开发(比如曾用 C++ 实现过 XX 功能 / 项目,涉及多线程同步、STL 容器优化等),也能解决开发中遇到的常见问题(如内存泄漏排查、模板编译报错分析)。
不足方面:在某些深入场景(如极端性能优化、C++20 + 新特性的工程化落地、大型项目的架构设计经验)上,我的实践还不够充分,相比资深工程师还有提升空间。 这个分数对我来说,既是对现有能力的肯定,也提醒自己还有 25 分的进步空间 —— 我最近在深入学习 XX(如协程、内存池设计),希望能不断填补这些短板。”
5、写一个不会内存泄漏的程序,你的设计思想是什么
核心思想是通过系统化的机制消除 “手动管理资源” 的风险,让资源的生命周期与对象 / 作用域自动绑定,从根源上避免 “忘记释放”“重复释放”“释放后使用” 等问题。
RAII 思想通过将资源绑定到对象的生命周期,让资源在对象创建时获取、在对象销毁时(离开作用域或被析构)自动释放,彻底避免手动释放的疏漏。
动态内存泄漏(new 后未 delete)是最常见的内存泄漏类型。解决方案是用智能指针完全替代裸指针,让指针的生命周期与内存绑定。
内存泄漏常发生在 “资源分配与释放逻辑分散在代码各处” 的场景(例如在函数 A 中 new,却在函数 B 中 delete,中间流程复杂时易遗漏)。设计时应做到:
- “谁分配,谁释放”:资源的分配和释放逻辑尽量在同一作用域或同一类中,避免跨函数 / 跨模块传递裸指针。
- 用容器代替零散内存:优先使用 std::vector/std::list 等 STL 容器存储批量数据,容器会统一管理内部元素的内存,避免手动 new[]/delete[]。
一些危险的编码习惯会间接导致内存泄漏,需通过规范杜绝:
- 禁用 void* 与强制类型转换:void* 会丢失类型信息,增加指针误用风险(如释放时类型不匹配)。
- 避免 “野指针”:指针赋值后及时初始化(用 nullptr),不访问已释放的内存。
- 禁止 “内存泄漏温床” 的语法:如 goto 跳过多重释放逻辑、delete 后未置空指针等。
6、如果项目中遇到内存泄漏,你是如何进行排查的
一、第一步:快速 “冻结” 现场,保存关键信息
- Linux 环境:用 gcore 生成 core dump(进程内存全量快照)
- 进程资源使用:top -p 1234(Linux)或任务管理器(Windows),记录内存占用(RES/VSS)、线程数、CPU 使用率。
- 系统状态:free -m(内存总量 / 剩余)、vmstat(换页情况)、netstat(网络连接数)—— 排除系统资源不足导致的间接泄漏(如内存碎片化)。
- 业务场景:记录当前程序正在处理的任务(如用户输入、请求 ID、数据量),偶发泄漏常与特定输入 / 并发量相关。
二、第二步:实时分析当前内存分布,锁定异常对象 通过工具解析内存快照,统计 “数量异常多” 或 “总大小异常大” 的对象类型,这些往往是泄漏的源头。
在 gdb 中执行以下操作:
- 查看堆内存使用概况:info proc mem(确认堆区是否异常增长)。
- 检查全局 / 静态容器:若程序中有全局缓存(如 std::map<Key, Value>),打印其大小(p global_map.size()),判断是否未清理。
- 查找自定义类的实例:若有 MyClass 类,用 info variables 找到其构造函数地址,结合内存搜索(find 命令)统计实例数量(正常逻辑下数量应稳定,泄漏时会持续增长)。
Linux:用 lsof -p 1234 列出进程打开的文件 / 网络句柄,若发现大量 REG(文件)或 IPv4(网络连接)句柄且状态为 CLOSE_WAIT(未正常关闭),可能是资源泄漏导致的内存占用。
三、第三步:结合代码逻辑,分析泄漏根源
7、说说你对c++中STL理解
STL 的核心思想是将数据的存储(容器) 和数据的操作(算法) 解耦,通过迭代器(Iterator) 作为两者的 “桥梁”,使得同一算法可以作用于不同容器,同一容器也可以适配不同算法。这种设计带来了极强的灵活性:
- 例如,std::sort 算法可以对 std::vector、std::array、std::deque 等多种容器进行排序,只要它们提供符合要求的迭代器;
- 反之,std::vector 可以被 std::find、std::for_each、std::count 等数十种算法操作。
STL 由五大核心组件构成,它们相互配合,形成完整的泛型编程体系:
- 容器(Containers):管理数据存储的 “数据结构”
- 迭代器(Iterators):容器与算法的 “接口”
- 算法(Algorithms):操作数据的 “通用函数”
- 适配器(Adapters):“包装” 现有组件,改变接口
- 适配器(Adapters):“包装” 现有组件,改变接口
8、lambda表达式使用需要注意什么
值捕获([x])会在 lambda 创建时复制变量的当前值(快照),后续外部变量的修改不会影响 lambda 内部的副本。 引用捕获([&x])不会复制变量,而是直接引用外部变量。若 lambda 的生命周期超过被引用变量的生命周期,会导致悬垂引用(访问已销毁的内存)。
9、C++类的大小由什么决定
遵循 “只计算实际存储的数据,忽略成员函数” 的基本原则,同时特殊情况(如虚函数、继承)会影响最终大小。
- 类的大小首先由所有非静态成员变量的大小决定,静态成员变量不计算在内(静态成员属于类本身,存储在全局区,而非对象中)。
- 为了提高 CPU 访问效率,编译器会对成员变量进行内存对齐(按一定规则在成员间插入空白字节),这会导致类的实际大小可能大于成员变量的总和。
- 如果类中包含虚函数(或继承了包含虚函数的类),编译器会为类添加一个虚函数表指针(vptr),用于实现多态。
- 虚函数表指针的大小为 4 字节(32 位环境)或 8 字节(64 位环境);
- 无论类中有多少个虚函数,都只需要一个虚函数表指针。
10、设计模式的原则
设计模式的核心原则(通常称为 “面向对象设计原则”)是一套指导软件设计的思想准则,目的是提高代码的可复用性、可维护性和灵活性。这些原则是设计模式的基础,多数模式都遵循或组合了这些原则。最经典的是SOLID 五大原则,此外还有一些常用补充原则。
一、SOLID 五大核心原则
- 单一职责原则(Single Responsibility Principle - SRP) 定义:一个类 / 模块应该只有一个引起它变化的原因(即只负责一项职责)。 意义:减少类的复杂度,降低修改风险(修改一个职责时不会影响其他职责)。 反例:一个 User 类同时负责 “用户信息管理” 和 “用户登录验证”,当登录逻辑变化时,可能影响信息管理功能。 正例:拆分为 UserInfo(管理信息)和 UserAuthenticator(处理登录)两个类。
- 开放 - 封闭原则(Open-Closed Principle - OCP) 定义:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。 意义:通过扩展已有代码来适应新需求,而非修改原有代码,避免引入新 bug。 实现方式:依赖抽象(如接口或抽象类),通过子类继承或实现接口来扩展功能。
- 里氏替换原则(Liskov Substitution Principle - LSP) 定义:子类对象必须能够替换掉所有父类对象,且不改变程序的正确性。 意义:保证继承关系的合理性,确保多态逻辑正确(父类引用指向子类对象时行为一致)。 反例:正方形(Square)继承自长方形(Rectangle),但修改正方形的 “宽” 会同时改变 “长”,违反长方形的行为契约,导致替换后逻辑错误。 要求:子类不得违反父类的前置条件(如输入参数范围)和后置条件(如返回值约束)。
- 接口隔离原则(Interface Segregation Principle - ISP) 定义:客户端不应该被迫依赖它不需要的接口(即接口应最小化,避免臃肿)。 意义:防止 “胖接口” 导致的依赖冗余,减少客户端与无用接口的耦合。 反例:一个 Worker 接口包含 work()、eat()、sleep() 方法,让 “机器人” 类实现时,被迫实现它不需要的 eat() 和 sleep()。 正例:拆分为 Workable(work())、Eatable(eat())、Sleepable(sleep())三个小接口,客户端按需实现。
- 依赖倒置原则(Dependency Inversion Principle - DIP) 定义:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。 意义:降低模块间的耦合,使得低层模块变化时,高层模块无需修改。 反例:高层的 PaymentService 直接依赖低层的 WeChatPay 类,当需要支持 Alipay 时,必须修改 PaymentService。 正例:定义 Payment 抽象接口,PaymentService 依赖 Payment,WeChatPay 和 Alipay 实现该接口,新增支付方式时无需修改高层。
二、其他重要原则
- 迪米特法则(Law of Demeter - LoD,最少知识原则) 定义:一个对象应该对其他对象保持最少的了解(只与直接朋友通信,不访问朋友的朋友)。 意义:减少对象间的耦合,降低系统复杂度。 示例:A 依赖 B,B 依赖 C,则 A 不应直接调用 C 的方法(需通过 B 间接访问)。
- 合成复用原则(Composite Reuse Principle - CRP) 定义:优先使用对象组合(has-a),而非继承(is-a)来实现代码复用。 意义:避免继承带来的强耦合(子类依赖父类实现),组合更灵活(可动态替换组件)。 示例:Car 类通过组合 Engine、Wheel 等对象实现功能,而非继承 EngineCar 等子类。
- 开闭原则的补充:优先使用抽象类和接口 抽象是实现 “开放 - 封闭” 的基础,通过抽象定义稳定的接口,具体实现留给子类或实现类,从而隔离变化。
总结:原则的核心目标 所有设计原则的最终目的都是 “降低耦合,提高内聚”:
- 内聚:一个模块 / 类内部的功能应高度相关(如单一职责)。
- 耦合:模块 / 类之间的依赖应尽可能弱(如依赖倒置、接口隔离)。
这些原则不是 “必须严格遵守的规则”,而是 “权衡利弊的指导”。实际开发中需根据场景灵活应用,避免过度设计(如为了遵循原则引入不必要的复杂度)。设计模式则是这些原则的具体体现 —— 例如,工厂模式体现了依赖倒置,装饰器模式体现了开放 - 封闭。
11、c++中如何选择容器使用
选择容器前,先回答以下问题,缩小范围:
- 数据访问方式:需要随机访问([]或at())还是只能顺序访问?
- 插入 / 删除位置:主要在尾部、头部还是中间位置操作?
- 是否需要排序:数据是否需要保持有序,或需要范围查询(如 “找大于 x 的元素”)?
- 是否需要键值对:是存储单一元素(如int),还是键值映射(如id→name)?
- 性能优先级:优先考虑时间效率(访问 / 插入速度)还是空间效率(内存占用)?
12、单继承下虚函数表数量?多继承为什么会有多个虚函数表及对应表头指针?
详情见:D:\interview\Storage\c++\class\类的大小计算 如果父类有虚函数,则有多少虚函数父类就有多少个虚函数表指针。 另外自身的虚函数表指针跟最后继承的类指针会合并,即单继承中,父类有虚函数,子类也有虚函数,则虚函数表指针为同一个,然后做相应的位移。
13、虚函数相比普通函数的性能开销?
4.虚函数重写的时机? 5.什么是右值引用? 6.move 的操作过程? 7.string 类型的移动构造做了哪些事情? 8.forward 函数?为什么不用forward会变成左值? C++ 的 RAII 机制核心是什么? 10.RAII 如何配合异常处理的流程? 11.dynamic_cast、static_cast 的区别? 12.设计模式的原则? 13.单例模式怎么实现? 14.观察者模式的应用场景? 15.工厂模式的分类及作用? 16.STL 的空间分配器是怎么设计的? 17.STL 是怎么调用 allocator 的?(如 vector