1. 值传递和引用传递的区别?
- 值传递:函数调用时触发一次参数拷贝,修改参数不影响原始值。大对象拷贝开销大,会复制整个对象,效率低。
- 引用传递:函数调用时传递的是参数别名,不触发拷贝,效率高,但修改直接影响原始值。
什么场景用引用?① 避免大对象拷贝(std::vector、std::string) ② 需要修改实参原始值。
什么场景用值传递?① 基础类型(int、char、double) ② 拷贝成本低,值传递更简洁。
扩展知识
- 只读大对象参数,优先
const T&:既避免拷贝开销,又防止意外修改原始值。 - 底层机制对比与代码示例:C++ 基础-初级
2. C 和 C++ 的区别?
- 编程范式:C 是面向过程语言,通过函数组织逻辑;C++ 是多范式语言,核心是面向对象(类与对象),也支持泛型、过程式编程。
- 继承与多态:C++ 支持继承、虚函数、多态;C 语言没有这些概念。
- 函数重载:C++ 支持同名函数按参数类型/个数重载;C 不支持,函数名必须唯一。
- 模板:C++ 支持模板(泛型编程),可实现编译期多态;C 语言无模板。
- 内存管理:C++ 用
new/delete,还支持智能指针;C 用malloc/free,手动管理且无 RAII。 - 标准库:C++ STL 丰富得多(容器、算法、
std::string等),C 只有少量基础库。
扩展知识
- C++ 特有机制:RAII(资源获取即初始化)、移动语义(
std::move)、lambda 表达式、右值引用。 - 详细对比表与代码示例:C++ 基础-初级
3. C++ 中 sizeof 和 strlen 的区别?
- sizeof 是运算符(编译期求值),strlen 是函数(
<cstring>,运行期求值)。 sizeof返回字节数(数组含\0),任何类型/表达式都可用;strlen返回字符数(不含\0,遇到\0停止),只用于 C 风格字符串。
经典陷阱:sizeof 对数组返回整个数组字节数,对指针只返回指针大小(4/8 字节)——这是高频考点。
扩展知识
sizeof的参数不会被求值(sizeof(a+b)不真正执行)。- 数组退化为指针时
sizeof的变化、strlen遇\0提前停止:C++ 基础-初级
4. C++ 中 extern 有什么作用?extern “C” 有什么作用?
- extern:声明变量/函数在其他文件定义,当前文件只是引用,用于跨文件共享(避免重复定义,违反 ODR)。
- extern “C”:让 C++ 编译器按 C 的链接规则处理,不做名称修饰(name mangling)。
为什么需要?C++ 为支持函数重载会修饰函数名(add(int,int) → _Z3addii),而 C 不修饰(_add),所以 C++ 调用 C 库时链接不上。extern "C" 就是告诉编译器”按 C 的方式链接”。
扩展知识
- 跨平台写法:用
__cplusplus宏包裹,让同一头文件可被 C 和 C++ 同时包含。 - 典型场景:C++ 调用 C 库(OpenSSL、SQLite)、提供 C 兼容 API(FFI)。
- 名称修饰详解:C++ 基础-初级
5. 什么是左值和右值?有什么区别?
一般可以从两个方向来区分:
方向一:能不能放在等号左边
- 左值:可以放到赋值运算符左边的东西
- 右值:不可以放到赋值运算符左边的东西
方向二:能不能取地址、有没有名字
- 左值:可以取地址,并且有名字的东西(通常是变量)
- 右值:不能取地址,没有名字的东西(通常是常量、临时变量)
扩展知识
- C++11 引入了右值引用(
T&&),是移动语义的基础,核心目的是窃取临时对象资源,避免无意义深拷贝。 const左值引用可以绑定右值(延长临时对象生命周期)。- 详细辨析与
std::move用法:C++ 基础-中级
6. 什么是移动语义和完美转发?
移动语义和完美转发都是 C++11 引入的新特性,核心是优化资源管理:
移动语义:
- 常规的资源管理是拷贝别人的资源(深拷贝,开销大)
- 移动语义是转移资源所有权,而不是拷贝资源,性能更高
- 通常用于大对象(
std::string、std::vector等),通过移动构造函数或移动赋值运算符实现 std::move的作用是把左值转成右值引用,从而触发移动语义
完美转发:
- 在模板函数中,把参数以原始的值类别(左值还是右值)原封不动地转发给另一个函数
- 使用
std::forward配合转发引用(T&&)实现,避免转发过程中丢失右值属性
扩展知识
- 移动语义的基础是右值引用——没有右值引用就无法区分”可以偷资源的临时对象”
- 转发引用在日常中也可以叫万能引用
- 移动语义 vs 拷贝语义的性能对比、
std::move注意事项:C++ 基础-中级
7. 什么是 C++ 的列表初始化?
C++11 引入了列表初始化,核心语法是用花括号 {} 来初始化变量或对象:
- 语法简洁,用
{}替代传统的()或=初始化 - 适用范围广:内置类型、用户自定义类型(类/结构体)、容器(
vector、map等)都能用 - 防止类型窄化:编译期就能拦截精度丢失的隐式转换(如
double转int),括号初始化做不到 - 可以接受任意长度的初始化列表,配合
std::initializer_list实现灵活的构造函数
扩展知识
- 类型窄化的具体示例与
std::initializer_list用法:C++ 基础-中级
8. C++ 中三种智能指针的使用场景?
C++ 智能指针主要用于管理动态分配的内存,避免内存泄漏。C++11 引入了三种:
std::unique_ptr(独占所有权):
- 同一时间只能有一个
unique_ptr指向对象,不支持拷贝,只支持移动 - 销毁时自动释放所管理的对象
- 使用场景:① 对象只有一个所有者时;② 自动管理非堆资源(文件句柄、互斥锁等);③ 替代裸
new/delete的首选方案
std::shared_ptr(共享所有权):
- 多个
shared_ptr可指向同一对象,内部用引用计数管理生命周期,最后一个释放时才真正销毁对象 - 使用场景:① 多个所有者共享同一个对象;② 需要通过复制或赋值传递所有权
std::weak_ptr(弱引用):
- 不拥有对象所有权,指向由
shared_ptr管理的对象,不增加引用计数 - 使用场景:① 解决
shared_ptr的循环引用问题;② 观察某对象但不需要拥有它 - 注意:
weak_ptr必须和shared_ptr搭配使用,访问前需要先升级为shared_ptr(lock())
扩展知识
- 引用计数原理、控制块(Control Block)结构、循环引用问题详解:C++ STL
make_shared为什么比new高效(一次内存分配 + 异常安全)
9. C++11 中有哪些常用的新特性?
C++11 新特性几乎是面试必问话题,主要可以从以下几个方面回答:
auto类型推导:编译器自动推导变量类型,简化代码,尤其配合模板、lambda 使用- 智能指针:
unique_ptr、shared_ptr、weak_ptr,取代裸指针,自动管理内存,避免泄漏 - RAII(Resource Acquisition Is Initialization):用对象生命周期管理资源(文件、锁、内存等),结合智能指针实现异常安全
std::thread:原生线程库,不再需要依赖平台 API(如 pthread),简化多线程编程- 左值右值与移动语义:右值引用(
T&&)、std::move、移动构造函数,避免不必要的深拷贝 std::function和 Lambda 表达式:支持闭包语法,配合std::function实现函数封装和回调
扩展知识
auto的限制、lambda 捕获列表的完整用法、std::function底层开销等:C++ 基础-中级
10. C++ 中 static 的作用?什么场景下用到 static?
C++ 中 static 有三种主要用法,分别对应不同场景:
1. 修饰局部变量:
- 变量存储在静态存储区,生命周期贯穿整个程序运行期间
- 只在第一次执行到声明处时初始化一次,后续函数调用不会重新初始化
- 使用场景:需要保留函数调用之间的状态(如计数器、缓存等)
2. 修饰全局变量或函数:
- 限制作用域为当前文件内部(内部链接属性),其他文件不可见
- 使用场景:避免不同文件之间的命名冲突,封装实现细节
3. 修饰类的成员变量或函数:
- 属于类本身而非某个对象,所有实例共享同一份
static成员变量 static成员函数没有this指针,可以在没有类实例的情况下调用- 使用场景:① 统计类的实例数量;② 实现单例模式;③ 工具方法(不依赖对象状态)
扩展知识
- static 局部变量的线程安全问题、static 成员的类外初始化、单例模式实现:C++ 基础-中级
11. C++ 中 const 的作用?谈谈你对 const 的理解?
const 的核心作用是声明不可变性——告诉编译器和阅读代码的人”这个值不该被修改”。但 const 的用法远不止”常量”这么简单,它贯穿了 C++ 的类型系统:
- 常变量:声明变量为常量,初始化后不可修改,编译期进行安全检查
- const 成员函数:承诺不修改对象状态,只读操作,允许
const对象调用 - const 指针和引用:区分”指向常量的指针”和”常量指针”,控制访问权限
- const 参数/返回值:防止函数修改传入数据,表达 API 契约
面试时建议从”常变量 → const 指针 → const 引用 → const 成员函数”由浅入深展开,最后总结 const 的本质是编译期的只读承诺。
扩展知识
- const 指针的四种形态辨析(谁不能改 vs 指向谁不能改)
- const 成员函数与
mutable成员变量 - const 的正确理解:不是”不变”,而是”不通过此接口修改”
constexpr与 const 的区别(C++11 编译期常量)- 详细讲解与代码示例:C++ 基础-中级
12. #define 和 const 的区别?
从定义常量的角度来看,C++ 中优先使用 const,原因如下:
#define是预处理指令:在预处理阶段进行纯文本替换,不进行类型检查,编译器看不到宏名,调试困难const是 C++ 关键字:在编译阶段确定类型和值,具有类型安全的特点,编译器可以进行类型检查
除此之外,const 还具有作用域控制(遵循 C++ 作用域规则)、可被调试器查看等优势,而 #define 宏全局可见直到 #undef,容易产生命名污染。
但 #define 仍有使用场景:条件编译(#ifdef / #ifndef)、函数式宏(带参数的宏),这些 const 无法替代。
扩展知识
#define宏的陷阱:无括号导致的运算优先级问题、函数式宏的多次求值问题constexpr作为 const 的升级版(真正的编译期常量)- 现代 C++ 替代方案:inline 函数替代函数式宏、enum class 替代数值宏
- 详细对比表与代码示例:[[C++ 基础-中级#
#define和 const 的区别?]]
13. C++ 中 inline 的作用?它有什么优缺点?
inline 的作用是建议编译器将函数调用替换为函数体本身,以减少函数调用的开销,与宏的效果类似,但具有类型安全。核心目的是提高频繁调用的小型函数的执行效率。
优点:
- 降低函数调用开销——省去了参数压栈、跳转、返回等指令
- 函数体较小时,可以显著提升执行效率
缺点:
- 容易导致代码膨胀——每次调用都内联展开,可执行程序体积变大,尤其当 inline 函数体较大且被多次调用时
inline只是对编译器的建议,编译器可以选择忽略(递归函数、虚函数等场景编译器通常不会内联)
扩展知识
- inline 函数的编译原理(调用点展开、链接器优化)
- inline 解决头文件重复定义问题的妙用(C++17 inline 变量)
- 现代编译器自动内联优化(
-O2/-O3下的 LTO) - 详细讲解与代码示例:C++ 基础-中级
14. C++ 中数组和指针的区别?
数组和指针虽然都能用于访问一组数据,但本质上是完全不同的概念,主要区别体现在四个方面:
1)内存分配:
- 数组:编译器在栈上为所有元素分配连续的内存空间,数组名代表这块内存的起始地址
- 指针:指针本身只占用一个内存单元(4/8 字节),存储的是一个地址,可通过
new/malloc动态分配内存
2)固定与动态大小:
- 数组:大小在声明时就确定,必须是常量,无法在运行时改变
- 指针:指向的内存可以在运行时动态分配和释放,灵活性更好
3)类型安全性:
- 数组:声明时绑定具体类型,编译器在访问时可以进行类型检查
- 指针:可以重新赋值指向不同类型地址,频繁的指针类型转换(如
int*转void*)容易引起类型不匹配
4)运算操作:
- 数组:数组名是首元素的常量指针,不能直接进行
++/--等算术运算 - 指针:可以直接进行递增、递减等算术运算,灵活访问不同位置
扩展知识
- 数组名退化为指针的机制(函数参数中的
T[]等价于T*) sizeof对数组和指针的不同结果std::array和std::vector作为现代 C++ 的替代方案- 详细讲解与代码示例:C++ 基础-初级
15. C++ 中野指针和悬挂指针的区别?
野指针和悬挂指针都是”指向非法内存”的指针,但产生原因不同:
- 野指针(Wild Pointer):指针从未被初始化,指向随机/未知地址。如
int* p;后直接*p = 5;。预防:声明即初始化(int* p = nullptr;)。 - 悬挂指针(Dangling Pointer):指针曾经有效,但指向的内存已失效。典型来源:①
delete/free后未置空继续使用;② 返回局部变量的地址;③ 容器扩容导致指针/迭代器失效。预防:delete后置nullptr,或直接用智能指针。
扩展知识
- 一句话区分:野指针是「没初始化」,悬挂指针是「指向的内存被释放了」。
- 三个悬挂指针常见来源的代码示例与预防:C++ 基础-中级
16. C++ 中 volatile 关键字的作用?
volatile 告诉编译器:这个变量的值可能在程序控制之外被改变,因此每次访问都必须直接从内存读取,禁止编译器用寄存器缓存值做优化。
- 与
const相反:const承诺”程序不改”,volatile警告”编译器别假设它不变”。 - 使用场景:硬件寄存器(内存映射 I/O)、信号处理函数共享的标志位、中断服务程序(ISR)共享变量。
扩展知识
- 关键区分:
volatile≠ 线程安全。它只保证单个读写不被优化,但不保证原子性、不提供内存屏障,不能替代std::atomic/std::mutex。 volatile与std::atomic的定位差异:C++ 基础-中级
17. C++ 中 explicit 和 final 的作用?
两个关键字,分别在禁止隐式转换和禁止继承/重写上发挥作用:
explicit:修饰构造函数(或转换运算符),禁止隐式调用,只允许显式构造。防止 int 被悄悄转成自定义类型这类意外的隐式转换。经验法则:除非明确需要隐式转换,单参数构造函数一律加 explicit。
final(C++11):① 修饰类,禁止该类被继承;② 修饰虚函数,禁止该函数被子类重写。还能帮助编译器做去虚化(devirtualization)优化。
扩展知识
explicit修饰转换运算符(explicit operator bool())final与override的配合使用- 代码示例与使用场景:C++ 基础-高级
18. 什么是内存对齐?为什么要内存对齐?
内存对齐:数据在内存中的存放地址必须是其大小的整数倍,编译器会在结构体成员之间/之后插入**填充字节(padding)**来满足对齐要求。
为什么要对齐:
- 硬件要求:很多 CPU 架构要求数据必须对齐,否则异常或无法读取
- 性能:CPU 按”字”(4/8 字节)读写内存,未对齐的
int横跨两个字边界时需两次内存访问,效率减半 - 原子性:很多平台的原子操作要求数据对齐
本质是空间换时间——插入 padding 浪费少量内存,换取更快的访问速度。
扩展知识
- 合理排列成员顺序(大的在前)能减少 padding
alignas(C++11)、#pragma pack改变默认对齐- 内存布局示意与示例:C++ 基础-高级
19. C++ 中四种类型转换的使用场景?
C++ 四种命名的类型转换,各有分工:
static_cast:编译期常规转换——基本类型转换、类层次向上转换、void*转具体指针、枚举转换。最常用、较安全。dynamic_cast:类层次中安全的向下转换,依赖 RTTI(需要虚函数),转换失败返回nullptr(指针)或抛异常(引用)。const_cast:唯一能移除/添加const、volatile的转换,常用于适配不带 const 的老接口。reinterpret_cast:位模式重解释,不改变二进制位(指针 ↔ 整数、不相关指针类型),最危险、平台相关。
扩展知识
- 使用原则:首选最小权力的转换,能用
static_cast就不用reinterpret_cast dynamic_cast失败返回nullptr、四种转换对比表:C++ 基础-高级
20. 什么是多态?简单介绍下 C++ 的多态?
多态指同一个接口,针对不同对象类型表现出不同行为。C++ 有两类:
编译期多态(静态):函数重载、模板、运算符重载,编译期确定,无运行时开销。
运行期多态(动态):虚函数 + 继承 + 基类指针/引用,运行期通过虚函数表动态绑定到实际对象的实现。
运行期多态的三个必要条件:① 继承 ② 虚函数(virtual + override)③ 通过指针或引用调用(值传递会对象切片,丢失多态)。
扩展知识
- 值传递切片问题、虚析构函数的重要性
- 代码示例与必要条件详解:C++ 基础-高级
21. C++ 中虚函数的原理?
虚函数运行期多态依赖两个机制:
- 虚函数表(vtable):每个含虚函数的类有一张虚表,存储该类所有虚函数的地址。派生类重写虚函数 = 用派生类版本替换虚表中的对应条目。虚表每个类一份。
- 虚函数指针(vptr):每个对象内部隐藏一个
vptr,指向它所属类的虚表。每个对象一份,构造函数自动初始化。
动态绑定过程:通过基类指针调用虚函数时,运行期 对象 → vptr → vtable → 函数地址,从而调到实际对象的实现。
扩展知识
- 虚析构函数(
delete基类指针不泄漏派生类资源)、纯虚函数(抽象类)、虚继承(菱形继承) - 虚表结构示意图与代码演示:C++ 基础-高级
22. C++ 中构造函数可以是虚函数吗?
不能,编译器直接报错。核心原因:
- 虚函数依赖 vptr,而 vptr 在构造函数期间才建立——对象还没构造完,vptr 没就绪,无法虚分发(“先有鸡还是先有蛋”)。
- 构造时对象类型编译期已知——
new Dog()一定调Dog的构造,不存在”运行期才知道调用哪个”的不确定性,而虚函数正是解决运行期不确定性的。
对比析构函数:析构发生在对象生命末期、vptr 已就绪,且 delete 基类指针 需要运行期知道实际类型,所以析构函数可以是(而且应该是)虚函数。
扩展知识
- 一正一反对比:构造函数不能虚 vs 析构函数应该虚
- 需要”多态构造”时用工厂模式 / 虚克隆(virtual clone)
- 详细讲解:C++ 基础-高级
23. C++ 的函数重载?它的优点是什么?和重写有什么区别?
- 函数重载:同一作用域内,函数名相同但参数列表不同(个数/类型/顺序),编译器编译期根据实参决定调用哪个。
- 优点:① 提高可读性(同一操作同一名字)② 接口统一 ③ 编译期多态(无运行时开销)。
- 易错点:仅返回类型不同不能构成重载。
与重写(override)的区别:重载是同作用域、参数不同、编译期绑定;重写是派生类重写基类虚函数、参数相同、运行期绑定(需 virtual + override)。
扩展知识
- 重载 vs 重写的完整对比表(作用域/参数/绑定时机):C++ 基础-初级
24. C++ 中 struct 和 class 的区别?
唯一本质区别是默认访问权限:
struct默认 public(成员和继承都是)class默认 private(成员和继承都是)
除此之外两者能力完全等价(都能有成员函数、构造析构、继承、多态、模板)。习惯上 struct 用于纯数据结构(POD、配置项),class 用于封装对象(有行为和不变量)。
扩展知识
- 默认继承权限也随之不同(struct public 继承 / class private 继承)
- 详细讲解:C++ 基础-初级
25. C++ 中 struct 和 union 的区别?如何使用 union 做优化?
- struct:各成员各自独立占内存,所有成员同时有效,大小 = 各成员之和 + padding。
- union:各成员共享同一块内存,同一时刻只有一个成员有效,大小 = 最大成员的大小。
union 做优化:利用”多个字段不会同时使用”的特性,让互斥字段共享内存来节省空间(如网络消息、嵌入式受限内存)。
扩展知识
- union 陷阱:写入新成员会覆盖旧成员、读取非激活成员是 UB
- 现代替代:
std::variant(C++17 类型安全) - 消息结构优化示例:C++ 基础-初级
26. C++ 中 new 和 malloc 的区别?delete 和 free 的区别?
一条主线记住全部:new = malloc + 构造函数,delete = 析构函数 + free。
new vs malloc:
new是运算符,malloc是库函数new返回类型安全的T*,malloc返回void*(需强转)new自动算大小,malloc手动sizeofnew失败抛std::bad_alloc异常,malloc失败返回NULLnew自动调构造函数,malloc不调;new可重载,malloc不可
delete vs free: delete 自动调析构函数、delete[] 释放数组;free 只释放内存不调析构。不能混用(new 配 delete,malloc 配 free)。
扩展知识
- 混用是未定义行为
- 现代 C++ 推荐智能指针而非裸
new/delete - 详细对比表与代码:C++ 基础-初级
27. C++ 什么场景下需要用到移动构造函数和移动赋值运算符?
类管理堆资源时深拷贝昂贵,移动构造/赋值只需转移资源指针(原对象置空),几乎零成本。典型场景:
- 不可拷贝只能移动的对象:
std::unique_ptr、std::thread、std::mutex - 容器扩容/插入临时对象:
vector::push_back(临时对象)触发移动 std::move显式转移所有权:明确不再使用原对象时- 函数返回局部大对象:配合 RVO/移动避免拷贝
引出 Rule of Three/Five/Zero:Rule of Zero 说明”不手动管理资源就不需要手写移动”。
扩展知识
- Rule of Three / Five / Zero 的完整含义
- 移动构造代码示例:C++ 基础-中级
28. 什么是 C++ 的运算符重载?
为自定义类型定义运算符(+、<、<<、[] 等)的行为,让对象像内置类型一样使用,语法是 operator@。典型:复数 +、流 <<、容器 []。
规则限制:
- 不能重载不存在的运算符(
**)、不能改变操作数个数 - 至少一个操作数是自定义类型
- 不能重载:
.、::、.*、?:、sizeof、typeid - 只能做成员函数:
=、[]、()、->
扩展知识
- 流插入运算符
operator<<的友元写法 - 详细讲解与规则:C++ 基础-中级
29. C++ 中 using 和 typedef 的区别?
两者都定义类型别名,但 using(C++11)更强:
- 语法更直观:
using 别名 = 类型;(赋值风格)vstypedef 类型 别名;(反直觉) - 支持模板别名:
template<typename T> using Vec = std::vector<T>;✅,而 typedef 做不到(需 struct 包装)
现代 C++ 推荐用 using。
扩展知识
- typedef 只能用 struct 包装模拟模板别名
- 函数指针等复杂类型的可读性对比:C++ 基础-中级
30. C++ 中 enum 和 enum class 的区别?
enum class(C++11 强类型枚举)对传统 enum 有三大改进:
- 作用域隔离:枚举值在枚举内部,需
Color::Red,防命名冲突(enum会泄漏到外层作用域) - 类型安全:不隐式转
int,需static_cast(enum会隐式转) - 可指定底层类型:
enum class Status : uint8_t {...},节省内存
现代 C++ 推荐用 enum class。
扩展知识
- 命名冲突、隐式转换的代码对比
- 底层类型指定示例:C++ 基础-中级
31. C++ 中析构函数一定要是虚函数吗?
不一定,取决于是否作为多态基类:
- 必须是虚函数:当类会被”基类指针 delete 派生类对象”时。否则
delete基类指针时静态绑定只调基类析构,派生类资源泄漏(未定义行为)。 - 不必是虚函数:类不是基类(
final类、纯数据类),或不会通过基类指针 delete 派生对象——加virtual还引入 vptr 开销。
经验法则:有虚函数的类,析构也一定要虚;可用 protected 非虚析构防止基类指针 delete。
扩展知识
- 与”构造函数不能虚”形成对比(构造不能虚、析构该虚)
- 资源泄漏场景代码:C++ 基础-高级
32. 什么是 C++ 中的虚继承?
用于解决菱形继承(B、C 继承 A,D 继承 B、C)的数据冗余和二义性问题:
- 问题:普通继承下,D 中有两份 A 的成员,访问
d.x二义性 - 解法:B、C 用
virtual继承 A,D 中只保留一份 A 的成员
代价:① vbptr 额外间接开销 ② 对象布局更复杂 ③ 虚基类由最派生类负责初始化。 实践上优先用设计避免菱形继承(组合/接口),除非确实需要。
扩展知识
- 菱形结构示意、最派生类初始化虚基类
- 详细讲解:C++ 基础-高级
33. C++ 中 this 指针的作用?
this 是成员函数内部的隐含指针,指向调用该成员函数的当前对象。三大作用:
- 区分同名局部变量和成员变量:
this->age = age;(左边成员、右边形参) - 返回对象自身实现链式调用:
return *this; - 隐式访问成员:成员函数内访问成员实际是
this->member
特性:this 是常量指针(T* const),不能在静态成员函数中使用(静态函数无 this)。
扩展知识
- 链式调用示例、this 的隐藏参数本质:C++ 基础-初级
34. C++ 中 memcpy 和 memmove 有什么区别?
两者都是内存拷贝函数,核心区别是是否处理内存重叠:
- memcpy:不处理重叠,src/dest 重叠时是未定义行为;但更快
- memmove:保证重叠时也正确(判断拷贝方向或用临时缓冲区);略慢
选择:确定不重叠用 memcpy,可能重叠(字符串插入、数组移位)用 memmove。
扩展知识
- memmove 通过判断 dest/srce 相对位置选拷贝方向
- 重叠场景示例:C++ 基础-初级
35. C++ 的 strcpy 和 memcpy 有什么区别?
- 用途:strcpy 拷贝字符串,memcpy 拷贝任意内存(含二进制)
- 结束条件:strcpy 遇
\0停止,memcpy 按指定字节数拷贝 - 长度要求:strcpy 自动找
\0(不需传长度),memcpy 必须手动传字节数 \0处理:strcpy 会拷贝\0,memcpy 不关心\0
安全:两者都有溢出风险,strcpy 更危险(无长度限制),现代用 std::string 替代。
扩展知识
- 含
\0的二进制数据只能用 memcpy - 详细对比:C++ 基础-初级
36. C++ 中类定义里 delete 关键字和 default 关键字的作用?
= default:显式要求编译器生成默认实现(默认构造/拷贝/析构等)。常用于自定义了某个构造函数后,把被抑制的默认构造”找回来”。= delete:显式禁止某个函数的使用。常用于:① 禁止拷贝(= delete拷贝构造/赋值)② 禁止特定隐式转换 ③ 禁止特定模板实例化。
扩展知识
- 禁止拷贝的类(Mutex)、delete 掉 double 构造函数防隐式转换
- 详细讲解:C++ 基础-中级
37. 什么是 C++ 中的 RAII?它的使用场景?
RAII(资源获取即初始化):把资源生命周期绑定到对象生命周期——构造获取资源、析构释放资源,对象销毁即自动释放。
优势:① 自动释放防泄漏 ② 异常安全(栈展开调析构)③ 代码简洁。
使用场景:智能指针(内存)、文件流(文件句柄)、lock_guard/unique_lock(互斥锁)、jthread(线程)、数据库连接等。
扩展知识
- 智能指针、锁的 RAII 示例
- 详细讲解:C++ 基础-中级
38. C++ 中 vector 的原理?resize 和 reserve 的区别?size 和 capacity 的区别?
- vector 原理:堆上连续内存的动态数组,容量不够时倍数扩容(重新分配 + 移动旧元素,导致迭代器失效)。
- size vs capacity:size 是当前元素个数,capacity 是已分配内存能容纳的个数(size ≤ capacity)。
- resize vs reserve:
resize(n)改变元素个数(会构造新元素);reserve(n)只预留容量(不构造元素,size 不变)。
建议:已知数量先 reserve 避免反复扩容。
扩展知识
- 三指针内部结构、扩容迭代器失效
- 详细讲解:C++ STL
39. C++ 中 deque 的原理?它内部是如何实现的?
deque 是双端队列,内部是分段连续:一块中控 map(指针数组) + 多个固定大小缓冲区。数据分散在缓冲区里,逻辑连续。
- 头尾插入 O(1):直接操作对应缓冲区,满了加新缓冲区(不像 vector 头插要移动所有元素)
- 随机访问 O(1):先算缓冲区(
i / 缓冲区大小)、再算偏移(i % 缓冲区大小),两次寻址 - 扩容只加缓冲区,不移动旧元素、迭代器更稳定
扩展知识
- map + 缓冲区示意图、与 vector 对比表
- 详细讲解:C++ STL
40. C++ 中 map 和 unordered_map 的区别?分别在什么场景下使用?
- map:底层红黑树,有序,查找/插入/删除 O(log n),键只需
<比较。 - unordered_map:底层哈希表,无序,平均 O(1)(最坏 O(n)),键需哈希函数 +
==。
场景:需要有序遍历/范围查询(lower_bound)用 map;只做快速查找不关心顺序用 unordered_map。
扩展知识
- 自定义类型做 unordered_map 键需提供哈希函数
- 详细对比:C++ STL
41. C++ 中 list 的使用场景?
std::list 是双向链表:任意位置插入/删除 O(1)(已知迭代器),随机访问 O(n)。
使用场景:① 频繁在中间插入/删除(且已知迭代器)② 插入/删除不使其他迭代器失效(区别于 vector)。
注意:节点分散、缓存局部性差,遍历慢——很多场景下 vector 反而更快。需随机访问用 vector,头尾操作用 deque。
扩展知识
- list 实际性能(缓存不友好)
- 详细讲解:C++ STL
42. C++ 的 function、bind、lambda 都在什么场景下会用到?
三者都是”把可调用对象当一等公民”:
- lambda:就地定义匿名函数(闭包),最常用——算法回调(sort 比较器)、捕获局部变量。
- std::function:类型擦除的可调用对象包装器,统一存储函数指针/lambda/函数对象——回调函数、策略模式、事件处理、任务队列。
- std::bind:绑定/重排参数(预绑定、换序)——但现代已被 lambda 取代。
扩展知识
- 三者配合(function 存 lambda、bind 适配签名)
- 详细讲解:C++ 基础-中级
43. 介绍一下 C++ 的模板,使用模板有哪些优缺点?
模板是泛型编程机制:写一份代码,编译器针对不同类型编译期生成对应版本(函数模板 + 类模板)。
优点:① 代码复用 ② 类型安全 ③ 零运行时开销(编译期多态,非虚函数动态分发)④ 泛型算法(STL 基于模板)。
缺点:① 编译时间增加 ② 代码膨胀(每类型一份代码)③ 错误信息晦涩 ④ 调试困难。
扩展知识
- 模板 vs 虚函数(编译期 vs 运行期多态)
- 详细讲解:C++ 基础-中级
44. C++ 中函数模板和类模板有什么区别?
两点核心区别:
- 实例化方式:函数模板根据实参自动推导模板参数(
add(1,2)无需写<int>);类模板通常需显式指定(Box<int>,C++17 CTAD 后也可自动推导)。 - 特化能力:函数模板只能全特化,类模板支持偏特化(如
Pair<T,T>特化两参数相同的情况)。
扩展知识
- 函数模板无偏特化(需重载模拟)、类模板偏特化示例
- 详细讲解:C++ 基础-中级
45. C++ 中可以使用 delete this 吗?
可以,但非常危险、有严格前提。合法前提(必须全部满足):
- 对象必须是
new分配的(堆上)——栈对象/成员对象delete this是 UB delete this后不能再访问任何成员(this 已悬空)- 不能二次 delete
典型场景:引用计数归零自动释放(shared_ptr 内部、COM 的 Release())。现代 C++ 用智能指针替代手写 delete this。
扩展知识
- delete this 后 this 变悬挂指针
- 详细讲解:C++ 基础-高级
46. C++ 中堆内存和栈内存的区别?
- 栈:编译器自动分配释放,速度快,容量小(默认约 1~8MB),无碎片
- 堆:程序员手动 new/delete,速度慢,容量大(受虚拟内存限制),有碎片
- 栈存局部变量、函数参数;堆存大对象、动态大小、需跨作用域存活的数据
- 栈作用域结束自动回收;堆忘记 delete 会内存泄漏
- 典型问题:栈溢出、内存泄漏
扩展知识
- 栈分配只需移动栈指针,因此比堆查找空闲块快得多
- 详细讲解:C++ 基础-初级
47. C++ 的栈溢出是什么?
- 定义:栈上分配的内存超过栈最大容量,导致崩溃(段错误)
- 常见原因一:函数内声明超大局部数组
- 常见原因二:递归过深或无限递归,栈帧不断累积
- 解决方案:大对象改堆分配(new / std::vector)、递归转迭代
- 栈溢出是运行时错误,编译能通过
扩展知识
- 编译器通常支持调整栈大小(如 -Wl,—stack)
- 详细讲解:C++ 基础-初级
48. C++ 中为什么要使用 nullptr 而不是 NULL?
- NULL 是宏,本质是 0(或 (void*)0),会被当成整数
- nullptr 是 C++11 关键字,类型是 std::nullptr_t,类型安全
- 函数重载时 NULL 可能误匹配 int 版本,nullptr 精确匹配指针版本
- 模板推导时 NULL 被推导为 int,nullptr 推导为 std::nullptr_t
- 结论:C++ 中一律用 nullptr
扩展知识
- nullptr 可隐式转换为任意指针类型
- 详细讲解:C++ 基础-初级
49. C++ 中 include <a.h> 和 include "a.h" 有什么区别?
- 两者都是预处理指令,把文件内容原样插入当前文件
- 区别在查找路径顺序:尖括号只查系统/标准库目录
- 双引号先查当前源文件目录,再查系统目录
- 习惯:标准库用尖括号,自定义头文件用双引号
扩展知识
- 编译器 -I 参数可添加额外的头文件搜索目录
- 详细讲解:[[C++ 基础-初级#C++ 中 include
<a.h>和 include"a.h"有什么区别?]]
50. C++ 是否可以 include 源文件?
- 技术上可以,include 是文本替换,不区分扩展名
- 但工程上强烈不推荐,是坏实践
- 危害:源文件同时被编译又包含时,导致重复定义(链接错误)
- 正确做法:头文件放声明,源文件放定义
- 特例:模板和 inline 函数因需完整定义,可放头文件
扩展知识
- 重复定义违反 ODR(单一定义规则)
- 详细讲解:C++ 基础-初级
51. C++ 中命名空间有什么作用?如何使用?
- 作用:避免命名冲突,把全局标识符按逻辑分组
- 定义:namespace X { … },访问用 X::name
- 使用方式:限定访问 X::name、using 声明、using 指令
- 头文件里避免 using namespace std,防止污染包含者的命名空间
- 匿名命名空间内部符号有内部链接,可替代 static
扩展知识
- 命名空间支持嵌套和别名(namespace A::B、namespace AB = A::B)
- 详细讲解:C++ 基础-初级
52. C++ 如何调用 C 语言的库?
- 核心障碍:C++ 名称修饰(name mangling)导致符号不匹配
- 第一步:头文件用 extern “C” 包裹,让 C++ 按 C 规则链接
- 第二步:用 gcc 编译 C 源码为 .o,再用 g++ 链接进 C++ 程序
- __cplusplus 宏让同一头文件同时被 C/C++ 包含
- 不做 extern “C” 包裹会报未定义引用(undefined reference)
扩展知识
53. 指针和引用的区别是什么?
- 指针是存地址的变量,引用是变量的别名
- 引用必须初始化、不能为空、绑定后不可改指向
- 指针可不初始化(野指针风险)、可为 nullptr、可改指向
- 引用解引用无需 *,指针需要 *
- 指针支持多级(int**),引用无多级
- sizeof 引用得对象大小,sizeof 指针得指针大小
扩展知识
- 引用底层常由指针实现(编译器行为,标准未强制)
- 详细讲解:C++ 基础-初级
54. C++ 中为什么 new 和 delete 一定要配对使用?
- new = 分配内存 + 调构造函数,delete = 调析构函数 + 释放内存
- 配对才能保证对象生命周期完整,构造/析构正确执行
- new 配 delete,new[] 必须配 delete[]
- 数组不用 delete[] 是未定义行为,通常只析构第一个元素
- 不能与 malloc/free 混用
扩展知识
55. C++ 中 malloc 申请的内存,可以用 delete 释放吗?
- 不可以,malloc 配 free,new 配 delete,不能混用
- malloc 分配的内存用 delete 释放是未定义行为(UB)
- 原因:malloc 不调构造函数、delete 会调析构,语义不匹配
- 同样,new 分配的内存也不能用 free 释放
- 混用可能崩溃、资源泄漏或内存损坏
扩展知识
- delete 会调用析构函数,而 malloc 分配的对象未经过构造
- 详细讲解:C++ 基础-初级
56. 什么是 C++ 的回调函数?为什么需要回调函数?
- 回调是把一个函数(或可调用对象)作为参数传给另一个函数,由后者在合适时机反过来调用它
- C++ 实现方式:函数指针、
std::function、lambda、函数对象(现代推荐后两者) - 动机一:解耦调用方与被调用方,库不知道业务逻辑
- 动机二:事件驱动 / 异步通知(定时器、网络、信号)
- 动机三:框架扩展点,让库代码调用用户代码(如
std::sort比较器)
扩展知识
- 经典示例:
std::sort传 lambda 作比较器、std::find_if传谓词 - 详细讲解:C++ 基础-中级
57. 什么是大端序?什么是小端序?
- 字节序是多字节数据在内存中的存储顺序
- 大端(Big-Endian):高字节存低地址(符合书写习惯,如
12 34 56 78) - 小端(Little-Endian):低字节存低地址(如
78 56 34 12) - 网络字节序是大端;x86/ARM 通常是小端
- 跨端传输要
htonl/htons/ntohl/ntohs转换
扩展知识
- 可用
union或char*读首字节判断本机大小端 - 详细讲解:C++ 基础-中级
58. C++ 中什么是深拷贝?什么是浅拷贝?写一个标准的拷贝构造函数?
- 浅拷贝只复制指针值(地址),多个对象共享同一份资源,可能 double free
- 深拷贝重新分配内存并复制内容,每个对象拥有独立资源
- 编译器默认拷贝构造/赋值是浅拷贝(成员逐一复制)
- 标准深拷贝构造四要素:重新分配 → 复制内容 →(赋值时)释放旧资源 + 防自赋值
- 引出 Rule of Three:析构/拷贝构造/拷贝赋值要写就一起写
扩展知识
- 现代 C++ 更推荐 Rule of Zero(智能指针/容器)或 Rule of Five(补移动语义)
- 详细讲解:C++ 基础-中级
59. C++ 中友元类和友元函数有什么作用?
- 友元(friend)允许外部函数/类访问本类的 private/protected 成员,是封装的”受控例外”
- 三类:友元函数、友元类、友元成员函数
- 最典型场景:
operator<<流插入重载(需访问私有成员) - 三条性质:不传递、不继承、不是成员函数(无 this)
- 破坏封装,应尽量少用
扩展知识
- 其他场景:类间紧密协作(迭代器访问容器)、单元测试访问私有状态
- 详细讲解:C++ 基础-中级
60. 介绍下 C++ 程序从编写到可执行的整个过程?
- 四阶段:预处理 → 编译 → 汇编 → 链接
- 预处理:宏展开、
#include插入、条件编译、删注释(产出.i) - 编译:语法/语义分析,生成汇编代码(产出
.s) - 汇编:汇编转机器码,生成目标文件(产出
.o) - 链接:符号解析 + 地址重定位,合并目标文件和库,生成可执行文件
扩展知识
- 语法错误发生在编译期;未定义引用/重复定义发生在链接期
- g++ 分步:
-E→-S→-c→ 链接 - 详细讲解:C++ 基础-中级
61. 什么是 C++ 中的 auto 和 decltype?
- 两者都是 C++11 类型推导关键字
auto:根据初始化表达式推导变量类型,会丢弃引用和顶层 constdecltype:推导表达式类型(不求值),完整保留引用和 constauto常用于简化变量声明、范围 for;decltype常用于模板尾置返回类型- 注意
decltype((x))得引用、auto丢 const 的细节
扩展知识
- 尾置返回类型:
auto add(T t, U u) -> decltype(t + u) - 详细讲解:C++ 基础-中级
62. 什么情况下会出现内存泄漏?如何避免内存泄漏?
- 内存泄漏:动态分配的内存没释放且没有指针再指向它,无法回收
- 典型场景:new 后忘 delete、异常路径提前退出、容器存裸指针、shared_ptr 循环引用、指针被覆盖
- 避免一:RAII + 智能指针(unique_ptr/shared_ptr/容器),根本方案
- 避免二:shared_ptr 循环引用用 weak_ptr 打破
- 检测工具:ASan(
-fsanitize=address)、valgrind
扩展知识
- 异常路径导致的泄漏最隐蔽:资源获取放构造函数、释放放析构(RAII)才能异常安全
- 详细讲解:C++ 基础-中级
63. 平时开发 C++ 程序处理错误是使用 try-catch 还是错误码方式?
- 不是二选一,按错误性质分工
- 预期内、可恢复的错误 → 错误码/返回值(现代用
std::optional、std::expected) - 意外、难以就地处理的错误 → 异常(自动传播、异常安全)
- 构造函数没有返回值,只能靠异常报构造失败
- 析构函数禁止抛异常(否则可能
std::terminate)
扩展知识
- 异常正常路径零开销、抛出路径有开销;错误码每次调用都要判断
- 详细讲解:C++ 基础-中级
64. C++ 中模板的实现一定要写在头文件中吗?
- 结论:不一定要写头文件,但绝大多数情况推荐写在头文件
- 根本原因:模板是编译期实例化,编译器在实例化处必须看到完整定义
- 分离到 .cpp 会导致使用处无法实例化 → 链接时找不到符号
- 解决方案:定义放头文件(常用);或 .cpp 里显式实例化(需枚举所有类型);C++20 模块
- 显式实例化局限:不适用于任意类型的泛型库(如 STL)
扩展知识
- 显式实例化语法:
template int add<int>(int, int); - 详细讲解:C++ 基础-中级
65. 请介绍一下 C++ 的返回值优化?
- RVO 是编译器优化:函数返回局部对象时,直接在调用者内存上构造,省掉拷贝/移动
- RVO(返回临时对象)vs NRVO(返回命名局部变量)
- C++17 起对”返回未命名纯右值”强制拷贝消除(guaranteed copy elision)
- 返回局部对象时不要写
std::move,可能阻碍 NRVO - 与移动语义共同解决”返回大对象”的拷贝开销
扩展知识
- 强制拷贝消除下,拷贝/移动被 delete 的类也能
return T(); - 详细讲解:C++ 基础-中级
66. C++ 动态库和静态库的区别?
- 静态库:链接期把库代码复制进可执行文件,体积大、无外部依赖、升级需重编译
- 动态库:运行期加载,体积小、可多进程共享、替换库即可升级
- Linux 后缀:静态
.a、动态.so;Windows:.lib/.dll - 动态库编译需
-fPIC(位置无关代码) - 动态库运行期存在找不到库的风险(
LD_LIBRARY_PATH/rpath/版本冲突)
扩展知识
- 生成:
ar rcs libmath.a ...与g++ -shared -o libmath.so ... - 详细讲解:C++ 基础-中级
67. C++ 中如何实现一个单例模式?
- 单例保证类只有一个实例并提供全局访问点
- 硬性要求:私有化构造函数、禁止拷贝/赋值、提供静态获取方法
- 饿汉式:启动即创建,天然线程安全但可能浪费
- 懒汉式:按需创建,但要处理线程安全
- 推荐 Meyers 单例:局部 static 变量 + C++11 magic static 保证线程安全初始化
扩展知识
- 双重检查锁是 C++11 之前的老写法,现已不推荐
- 详细讲解:C++ 基础-中级
68. C++ 成员变量的初始化顺序是固定的吗?
- 是固定的,但顺序是声明顺序,而不是初始化列表书写顺序
- 基类先于派生类成员初始化
- 同一类内成员按声明顺序初始化
- 陷阱:用声明靠后的成员初始化声明靠前的成员,会读到未初始化值
- 编译器
-Wreorder可对顺序不一致发出警告
扩展知识
- 反例:
Bad() : b(10), a(b)中 a 先声明,a(b) 先执行但 b 尚未初始化 - 详细讲解:C++ 基础-中级
69. C++ 中未初始化和已初始化的全局变量放在哪里?全局变量定义在头文件中有什么问题?
- 已初始化全局变量在
.data(数据段),初值存入可执行文件 - 未初始化(或零初始化)全局变量在
.bss,不占文件空间、运行时清零 - 头文件直接定义全局变量 → 每个翻译单元一份 → 链接期重复定义错误
- 正确做法:头文件
extern声明 + 单处.cpp定义 - C++17 可用
inline变量直接在头文件定义
扩展知识
- 未初始化全局/静态变量默认值为 0
- 详细讲解:C++ 基础-中级
70. C++ 函数调用的原理是什么?什么是栈帧?
- 每次函数调用在调用栈上分配一块内存,即栈帧(Stack Frame)
- 栈帧内容:参数、返回地址、保存的寄存器、局部变量
- 调用过程:压参数 →
call压返回地址 → 建立栈帧分配局部变量 →ret弹返回地址 - 栈向下增长,rsp 指向栈顶、rbp 指向栈帧底
- 局部变量随栈帧销毁而失效 → 不能返回局部变量地址;递归过深 → 栈溢出
扩展知识
- 嵌套调用是先进后出的栈结构,对应调用链
- 详细讲解:C++ 基础-中级
71. C++ new 操作的背后都经历了什么?
- new 两步:① 调
operator new分配原始内存(默认封装 malloc)→ ② 调用构造函数初始化 new失败抛std::bad_alloc;new (std::nothrow)失败返回 nullptr- 构造函数抛异常时,
operator new会回滚调用operator delete回收内存 new[]额外记录元素个数,故必须配delete[]operator new可被重载(自定义内存池),malloc不可
扩展知识
72. 请介绍 C++ 多态的实现原理?
- 多态实现分两类:编译期多态靠重载/模板静态决议,运行期多态靠虚函数动态绑定。
- 运行期多态底层是 vtable + vptr:每个类一张虚表存虚函数地址,每个对象一个 vptr 指向虚表。
- 重写虚函数 = 用派生类版本替换虚表条目;调用走 vptr → 虚表 → 函数地址的间接寻址。
- 编译期多态零运行期开销,运行期多态多一次间接查表开销。
扩展知识
- 编译期多态:函数重载、模板、运算符重载;运行期多态:虚函数 + 继承 + 基类指针/引用。
- 值传递会切片丢失多态,必须用指针/引用触发动态绑定。
- 详细讲解:C++ 基础-高级
73. C++ 如何进行性能优化?
- 方法论先行:先 profile 定位热点,再优化,别盲目。
- 算法/数据结构优先:降复杂度(哈希替代有序表)、缓存友好(连续内存)。
- 内存:引用传参避免拷贝、移动语义转移资源、reserve 预留容量、emplace_back 原地构造。
- 语言特性:constexpr 编译期求值、final 去虚化、模板。
- 编译选项:-O2/-O3、-march=native、LTO、PGO;并发:多线程/原子/减小锁粒度。
扩展知识
- 拷贝是 C++ 最大隐形开销;并发注意伪共享与锁竞争。
- 过早优化是万恶之源,每次改动后重新测量验证。
- 详细讲解:C++ 基础-高级
74. C++ QT 中信号和槽的原理是什么?
- 本质是观察者模式:对象间松耦合通信,发送者与接收者互不知晓。
- 原理:moc 预编译 + 元对象系统(QMetaObject),记录信号/槽名字与索引。
- connect 建立”信号 → 接收者 → 槽”映射,发射信号时查表调用。
- 连接类型分 Direct(同线程同步)与 Queued(跨线程异步,走事件循环)。
- 对比回调:更松耦合、支持多对多、天然跨线程,但比直接调用有开销。
扩展知识
- 信号只有声明无实现(moc 生成);Q_OBJECT 宏必须存在。
- 用函数指针式 connect(&Sender::signal)可在编译期校验签名。
- 详细讲解:C++ 基础-高级
75. 什么场景下用继承?什么场景下使用组合?
- 原则:优先组合而非继承(Composition over Inheritance)。
- 判据 is-a / has-a:is-a 用继承,has-a 用组合。
- 继承 = 白盒复用、编译期静态、耦合强;组合 = 黑盒复用、运行期可替换、耦合弱。
- 需要多态 + 稳定层次用继承;功能装配/易变场景用组合。
扩展知识
- 错误示范:仅为复用功能而继承(Car 继承 Engine)导致语义错乱、破坏封装。
- 组合可运行期替换内部组件(如注入不同 Engine 实现)。
- 详细讲解:C++ 基础-高级
76. C++ 中为什么要使用 std::array?它有什么优点?
- std::array 是固定大小的数组容器(C++11),封装 C 风格数组,大小编译期确定
- 不会退化为指针,保留数组语义,可用 .size() 拿到大小
- 支持标准库算法(begin/end)与 C++17 结构化绑定,可整体拷贝赋值
- 局部 std::array 在栈上分配,零动态内存开销,性能等同原生数组
- 相比 vector:固定大小、无堆分配、更轻量;相比 C 数组:更安全、接口更丰富
扩展知识
- at() 有边界检查(越界抛 std::out_of_range),operator[] 无检查
- 详细讲解:C++ STL
77. 请介绍 C++ 中 shared_ptr 的原理?shared_ptr 线程安全吗?
- 原理:shared_ptr 通过控制块里的强引用计数管理生命周期,计数归零时释放对象
- 引用计数是原子操作,多个线程各自拷贝/析构不同的 shared_ptr 副本是安全的
- shared_ptr 对象本身不是线程安全的:多线程同时读写同一个 shared_ptr 对象是数据竞争
- 多线程读写同一个 shared_ptr 需加锁,或用 std::atomic_load / std::atomic_store
- 一句话:引用计数安全,对象本身不安全
扩展知识
78. 请介绍 C++ 中 weak_ptr 的原理?
- weak_ptr 是弱引用,指向 shared_ptr 管理的对象但不增加强引用计数
- 不影响对象生命周期:对象可被正常释放,weak_ptr 只观察对象是否存活
- 使用前必须 lock() 升级为 shared_ptr;对象已销毁时 lock() 返回空 shared_ptr
- 核心用途是打破 shared_ptr 循环引用,防止内存泄漏
- 弱引用计数归零后控制块才释放,weak_ptr 依赖控制块判断对象存活
扩展知识
- lock() 原子地检查强引用计数是否归零,非零则安全返回 shared_ptr
- 详细讲解:C++ STL
79. C++ 中 shared_from_this 的作用是什么?它有什么优点?
- 作用:让类在成员函数内安全获得指向自身 this 的 shared_ptr
- 通过继承 std::enable_shared_from_this
,调用 shared_from_this() - 解决痛点:直接用 shared_ptr
(this) 会新建第二个控制块,导致 double free - 优点:返回的 shared_ptr 与已有 shared_ptr 共享同一控制块,引用计数一致
- 前提:对象必须已被 shared_ptr 管理,否则抛 std::bad_weak_ptr
扩展知识
- enable_shared_from_this 内部通常靠一个 weak_ptr 记住控制块
- 详细讲解:C++ STL
80. C++ 的 std::string 内部使用的是堆内存还是栈内存?
- 答案取决于长度:短字符串用 SSO(小字符串优化)存对象内部,长字符串才在堆上
- SSO:string 对象内部预留小缓冲区(约 15 字节,实现相关),短串零堆分配
- 超过 SSO 阈值才 new 一块堆内存存数据
- 好处:大部分字符串都很短,SSO 避免堆分配、提升缓存局部性
- SSO 是实现约定非标准强制,但主流实现都支持
扩展知识
- .data() 返回连续缓冲区是 C++11 起标准保证的
- 详细讲解:C++ STL
81. C++ 中 vector 的 push_back 和 emplace_back 有什么区别?
- push_back 接受已构造好的对象,拷贝/移动到末尾
- emplace_back 接受构造参数,在 vector 内存中原地构造
- emplace_back 基于完美转发,省去一次临时对象的拷贝/移动,性能更优
- 传入现成对象时两者等价,emplace_back 优势仅在传构造参数时
- 注意 emplace_back 参数不能引用 vector 自身元素(扩容时可能悬空)
扩展知识
- 复杂/昂贵类型(如 string、自定义类)差距最明显
- 详细讲解:C++ STL
82. C++ 的迭代器和指针有什么区别?
- 指针是内建类型、本质是内存地址;迭代器是”泛化的指针”
- 指针本身是随机访问迭代器的一种,vector 迭代器常就是指针
- 迭代器用统一接口屏蔽不同容器实现差异(连续/链表/树)
- 不同迭代器类别能力不同:list 迭代器只能 ++,不能 +n
- 价值:解耦算法与容器,同一算法作用于多种容器
扩展知识
- 迭代器分五类:输入/输出/前向/双向/随机访问
- 详细讲解:C++ STL
83. 请介绍下 std::sort 的原理?底层是快速排序吗?
- 不是纯快排,而是内省排序 introsort(快排 + 堆排 + 插入排序的混合)
- 主力快排(平均 O(n log n)),递归过深切堆排兜底(避免最坏 O(n²))
- 小区间用插入排序(常数小、缓存友好)
- 保证平均和最坏都是 O(n log n)
- 不稳定排序、要求随机访问迭代器;stable_sort 用归并、稳定但可能多占内存
扩展知识
- 快排选 pivot 常三数取中,进一步降低退化概率
- 详细讲解:C++ STL
84. C++17 有哪些新特性?
- 定位是 C++11 之后的”补强版”,不颠覆范式、专注实用
- 结构化绑定:
auto [x, y] = pair/tuple/struct一次解包 if constexpr:编译期分支,未选中分支不实例化,取代部分enable_if- 三个值语义容器:
std::optional(可有值)、std::variant(类型安全 union)、std::any(任意类型) std::string_view:不拥有内存的只读视图,避免拷贝- CTAD(类模板实参推导)+ inline 变量 + 折叠表达式
(args + ...)
扩展知识
- 还有
[[nodiscard]]、std::filesystem、并行算法、RVO 强制拷贝消除 - 详细讲解:C++ 基础-中级
85. C++20 有哪些新特性?
- 定位是继 C++11 之后最大的一次升级,核心是”语言四大件”
- concepts:编译期谓词约束模板参数,报错友好
- ranges:管道式(
|)组合算法,重写 STL 接口 - coroutines:协程,
co_await/co_yield/co_return挂起/恢复 - modules:
import取代#include,解决头文件/ODR/编译慢 - 实用特性:
std::span、三路比较<=>、std::format
扩展知识
- 还有
std::jthread、std::source_location、[[likely]]/[[unlikely]]、constexpr大幅增强 - 详细讲解:C++ 基础-中级
86. std::move 和 std::forward 的本质区别?
- 两者都只是 cast(类型转换),运行时零开销,本身不移动/转发任何东西
std::move:无条件转右值,本质static_cast<T&&>std::forward:有条件转换,左值保持左值、右值保持右值forward的”条件”来自模板参数T的引用折叠(左值→T=int&,右值→T=int)move要remove_reference避免引用折叠成左值引用
扩展知识
- 转发函数里错用
std::move会把左值误判为右值、调用错误的移动/拷贝 - 详细讲解:C++ 基础-中级
87. 模板的特化和偏特化?SFINAE 是什么?如何用 enable_if 做重载约束?
- 全特化:所有参数指定具体类型;偏特化:只约束部分参数或形态
- 函数模板只能全特化(偏特化用重载模拟),类模板两者皆可
- SFINAE:替换失败不是错误——候选替换失败时剔除候选、继续尝试,全部失败才报错
enable_if:条件为真暴露type,否则无type触发 SFINAEenable_if三种位置:返回值类型 / 模板参数 / 函数参数
扩展知识
- C++20 concepts 是对 enable_if 的更友好替代;
void_t可探测成员 - 详细讲解:C++ 基础-中级
88. constexpr、consteval、constinit 的区别?
- 共同点是编译期求值,区别在”强制性”
constexpr:变量/函数可以编译期求值,函数可在运行期退化普通函数consteval:立即函数,必须编译期求值,传运行期值直接编译错误constinit:变量必须编译期初始化,但之后可变(不是 const)constinit解决 static 初始化顺序问题
扩展知识
constexpr变量是 const,constinit变量不是 const;consteval只能用于函数- 详细讲解:C++ 基础-中级
89. type_traits 常用工具?如何实现一个编译期类型判断?
- 三类工具:判断
is_integral/is_pointer/is_same、变换remove_reference/add_pointer/decay、使能enable_if/conditional - C++17 加
_v(值)和_t(类型)别名 - 实现编译期类型判断:通用模板 false + 偏特化 true
- 现场写
is_pointer:template<typename U> struct is_pointer<U*>偏特化为 true - 探测成员用
std::void_t + decltype + declval
扩展知识
- 标准库
is_integral等就是”通用 false + 逐个偏特化 true”实现 - 详细讲解:C++ 基础-中级
90. C++14 有哪些新特性?
- 定位是 C++11 的小版本、补丁式完善
- 泛型 lambda:参数用
auto,本质是匿名函数模板 std::make_unique:补上 C++11 缺失的对应物- 变量模板:
template<typename T> constexpr T pi = ... - 返回类型推导:
auto直接推导,无需尾置返回类型 [[deprecated]]弃用标记 + 二进制字面量/数字分隔符
扩展知识
- 还有
std::shared_timed_mutex、放松 constexpr 限制 - 详细讲解:C++ 基础-中级
91. lambda 的底层原理?闭包如何实现?捕获列表的本质?
- lambda = 编译器生成的匿名类(闭包类型)+ 重载
operator() - 捕获列表里的变量 → 闭包类的成员变量
- 值捕获 = 拷贝成员,引用捕获 = 引用成员
operator()默认 const,加mutable才能改值捕获成员- 无捕获 lambda 可隐式转函数指针,有捕获则不能
扩展知识
- 引用捕获的 lambda 在变量生命周期结束后调用会悬空(异步回调典型坑)
- 详细讲解:C++ 基础-中级
92. C++20 concept 约束是什么?如何定义和使用?
- concept 是约束模板参数的编译期谓词
- 定义三种方式:常量表达式 /
requires表达式 / 组合 - 使用四种写法:requires 子句、作模板参数、
auto简写、泛型 lambda - 相比 enable_if:可读性好、报错友好、可命名复用、天然支持重载
- 底层仍是 SFINAE 思想,是 enable_if 的语法糖化
扩展知识
requires表达式可检查{ a + b } -> convertible_to<int>这类复合要求- 详细讲解:C++ 基础-中级
93. 变参模板(parameter pack)与折叠表达式?
- 参数包:
typename... Args(类型包)、Args... args(值包) - C++11 处理靠递归:递归函数 + 终止函数,一层层”剥开”
- C++17 折叠表达式
(args + ...)把递归简化为一行 - 四种形式:一元/二元 × 左折叠/右折叠
sizeof...(args)求参数个数;参数包可用于完美转发
扩展知识
- 右折叠
(p op ...)从右往左结合,左折叠(... op p)从左往右结合 - 详细讲解:C++ 基础-中级
94. 动态库 ABI 兼容性如何保证?
- ABI 是二进制接口(布局/mangling/调用约定),API 是源码接口
- 破坏 ABI:增删/重排成员、改虚表、改函数签名、换编译器
- 保证核心:
extern "C"+ 不透明指针,把内部实现完全隐藏 - Pimpl 惯用法:对外只暴露一个指针,成员变动不影响对象大小
- 保持编译器/标准库版本一致 + 符号版本控制 + ABI 检查工具
扩展知识
- C++ 没有跨编译器统一 ABI,所以 Linux 上 C++ 库常以 C 接口导出
- 详细讲解:C++ 基础-中级
95. 内存池如何设计?对比 new/delete 有什么优势?
new/delete每次都走malloc,有系统调用开销 + 内存碎片 + 分配慢- 内存池核心思想:一次大分配 + 内部快速复用,把系统调用和碎片收敛到池边界内
- Arena:顺序分配、整体释放(
reset重置),适合生命周期一致场景 - 对象池:预分配一批对象,空闲链表复用,省去反复构造/析构
- 定长块池:按固定大小切块,空闲链表管理,O(1) 分配,专治同尺寸小块
- 优势:O(1) 分配、碎片可控、连续内存缓存友好、可整体释放
扩展知识
- 空闲链表节点可直接复用内存块本身,不额外占内存
- 可与 73. C++ 如何进行性能优化? 的内存管理维度呼应
- 详细讲解:C++ 基础-高级
96. 空类为什么 sizeof 是 1?虚继承下的对象内存布局?
- 空类
sizeof == 1而非 0:C++ 标准要求不同对象必须有不同地址 - 空基类优化(EBO):空类作基类可不占字节,作成员则按 1 字节算
- 含虚函数的空类有
vptr,sizeof == 8(64 位),不再是空类 - 虚继承布局靠 vbptr(虚基类指针):每个虚继承分支带一个 vbptr 记录虚基类偏移
- 虚基类成员只有一份,访问多一次间接;由最派生类初始化
扩展知识
- 对象布局是编译器相关的,答题说明是”简化示意”
- 关联 18. 什么是内存对齐?为什么要内存对齐? 与 32. 什么是 C++ 中的虚继承?
- 详细讲解:C++ 基础-高级
97. 常见设计模式(工厂、策略、观察者)在 C++ 中如何实现?
- 三模式分别解决:创建解耦(工厂)、算法可替换(策略)、一对多通知(观察者)
- C++ 实现共性:抽象接口用虚函数,配合继承或组合
- 工厂:静态/实例方法返回
unique_ptr<基类>,调用方只依赖抽象接口 - 策略:策略接口 + 组合注入,
setStrategy运行期换算法 - 观察者:主题维护观察者列表,
attach注册、notify逐一通知 - 工厂靠继承,策略/观察者靠组合(呼应组合优于继承原则)
扩展知识
- Qt 信号槽本质就是观察者模式
- 关联 75. 什么场景下用继承?什么场景下使用组合?
- 详细讲解:C++ 基础-高级
98. PIMPL 惯用法的作用与实现?
- PIMPL = Pointer to IMPLementation:私有成员全搬到内部
Impl类,外层只留unique_ptr<Impl> - 头文件只前置声明
struct Impl;,不暴露任何实现细节 - 三大作用:编译防火墙、降低编译依赖、ABI 稳定
- 改实现只重编译
.cpp,不波及所有使用者;增删私有成员不破坏公开 ABI - 代价:一次堆分配 + 一层间接调用;
unique_ptr要求析构/移动在.cpp定义
扩展知识
- 不完整类型(Impl)下
unique_ptr无法就地析构,故析构函数要在.cpp中 - 需拷贝时手写深拷贝,或改
shared_ptr获得浅拷贝 - 详细讲解:C++ 基础-高级
99. 内存泄漏如何检测?AddressSanitizer / valgrind 的原理?
- 两条线:预防靠 RAII/智能指针(治本),检测靠 ASan/Valgrind
- ASan:编译插桩(
-fsanitize=address)+ 影子内存 + 红区,越界/已释放立即报错 - ASan 快(约 2 倍)、信息全(堆栈行号),但需重编译
- Valgrind(Memcheck):动态二进制翻译,逐指令插桩模拟,无需重编译
- Valgrind 慢(10–30 倍),用 V 位/A 位元数据检测未初始化、越界、不匹配释放、泄漏
扩展知识
- LeakSanitizer 是 ASan 的泄漏检查子集
- 越界读写、未初始化内存也能查,不止内存泄漏
- 详细讲解:C++ 基础-高级
100. SIMD 向量化优化?使用过哪些 intrinsic 或库?
- SIMD = 单指令多数据流:一条指令同时处理多个数据(SSE 4 个、AVX 8 个 float)
- 三层手段:自动向量化(
-O3 -march=native,零成本首选)→ intrinsic(_mm_/_mm256_)→ 高层库(Eigen/xsimd) - 自动向量化前提:无数据依赖、内存连续、循环次数可数
- intrinsic 示例:
_mm_loadu_ps+_mm_add_ps+_mm_storeu_ps一次算 4 个 float - 注意点:内存对齐(
alignas)、指令集分派(#ifdef __AVX__)、先 profile 再优化
扩展知识
-fopt-info-vec-all可查看编译器向量化报告- 关联 73. C++ 如何进行性能优化? 的编译器优化维度
- 详细讲解:C++ 基础-高级
101. 分支预测、CPU 流水线对性能的影响?
- 流水线让多条指令重叠执行;分支指令让 CPU 需猜测走向,等结果会暂停流水线
- 分支预测失败要冲刷流水线、丢弃投机结果,代价约 10–20 时钟周期
- 有序数据预测几乎全中(快),随机数据约 50% 失败(慢数倍)
- 优化①:
likely/unlikely(__builtin_expect)提示编译器,冷路径放远 - 优化②:无分支(branchless)写法消除分支;优化③:多累加器打破数据依赖链
扩展知识
- 现代 CPU 瓶颈常在分支/缓存/依赖,而非算力本身
- 关联 73. C++ 如何进行性能优化? 与 18. 什么是内存对齐?为什么要内存对齐?
- 详细讲解:C++ 基础-高级
102. std::optional、std::variant、std::any 的区别与使用场景?
std::optional<T>:可空单值,表达”可能有值也可能没有”,替代哨兵值/空指针std::variant<Ts...>:类型安全 union,同一时刻是列出的多个类型之一,用 index 记录当前类型,无堆分配std::any:任意类型的类型擦除,运行时可换成任意类型,通常堆分配- 取值方式:optional 用
*/value_or,variant 用get/visit,any 用any_cast(类型不匹配抛 bad_any_cast) - 选择原则:可空用 optional、有限已知类型用 variant、运行时未知用 any
扩展知识
optional::value()无值抛bad_optional_access;value_or()给默认值更安全variant内存 ≈ 最大成员 + tag,类型集合编译期固定;可用std::monostate表示”空”状态any只能存可拷贝构造对象,any_cast需精确匹配原始类型- 详细讲解:C++ STL
103. std::string_view 的原理?使用时有什么坑?
string_view是只读、非拥有的视图,内部只有”指针 + 长度”,不拷贝数据- 构造、传递、substr 都零拷贝、零堆分配,substr 是 O(1)
- 适合做函数参数,统一接收
std::string/const char*/字面量而不拷贝 - 坑一:悬垂引用——不拥有数据、不延长生命周期,指向的临时对象析构后悬空
- 坑二:不保证
\0结尾;坑三:不能直接传给需要const char*的 C 接口
扩展知识
- 典型大小 16 字节(8 字节指针 + 8 字节长度)
- 传给
printf需显式%.*s+.data()+ 长度,或用std::string(sv)转回拥有型 - 不要从临时
std::string返回string_view,也不要在底层被修改后继续用 view - 详细讲解:C++ STL
104. 数组指针和指针数组有什么区别?
- 区别看运算符优先级:
[]优先级高于*,括号改变结合顺序。 int* arr[5]是指针数组——本质是「数组」,含 5 个int*元素。int (*p)[5]是数组指针——本质是「指针」,指向整个int[5]。- 用法:指针数组常构建「长度可不同的二维数组(锯齿数组)」;数组指针常作二维数组形参
int (*matrix)[N]。 - 记忆口诀:先看括号,
(*p)先指针后下标;无括号则[]先下标后指针。
扩展知识
- 二维数组名
int grid[2][3]退化后是指向int[3]的数组指针,正好匹配int (*p)[3]。 - 数组名退化为
int*的机制,是理解数组指针的前提。 - 详细讲解:C++ 基础-初级
105. C++ 中位域(bit field)是什么?有什么用途?
- 位域:在 struct/union 中按位指定成员宽度,语法
unsigned int a : 3;。 - 作用是把多个小字段压缩到同一个字节/字,节省内存。
- 四大用途:节省内存、网络协议头解析、硬件寄存器映射、标志位/权限集合。
- 三个坑:位域不能取地址、布局平台/编译器相关、建议用无符号类型。
扩展知识
- 位域是「内存对齐/struct/union」话题里按位压缩的补充手段,跨平台协议解析时布局不可移植需谨慎。
- 详细讲解:C++ 基础-初级
106. 函数指针、函数对象、std::function 有什么区别?
- 三者都能表示「可调用对象」,抽象层次递进。
- 函数指针:指向函数的指针,无状态、最底层,适合 C 风格回调。
- 函数对象:重载
operator()的类对象,能携带状态、可被内联、性能最好。 - std::function:类型擦除的包装器,能统一装函数指针/函数对象/lambda/bind 结果,但有间接调用开销。
- 选型:要状态+性能用函数对象,要统一接口/异构存储用 std::function,简单回调用函数指针。
扩展知识
- 与 lambda 底层原理(闭包类 + operator())一脉相承;std::function 的类型擦除开销使其不如函数对象可内联。
- 详细讲解:C++ 基础-中级
107. C++ 中 set 和 multiset 的原理和使用场景?
- 两者底层都是红黑树(平衡二叉搜索树),元素有序,查找/插入/删除 O(log n)
- 唯一区别:
set键唯一(自动去重),multiset键可重复 set用于”去重 + 有序”、快速判存在、范围查询;multiset用于”有序 + 允许重复”multiset用count统计出现次数、equal_range拿相等区间
扩展知识
multiset的erase(value)删掉所有等于 value 的元素,erase(find(value))只删一个- 与
map相比:set 只存 key(value 就是 key),map 存 key-value 对 - 详细讲解:C++ STL
108. C++ 中各种容器的迭代器失效场景总结?
- 连续容器
vector:扩容全部失效,中间插入/删除之后全部失效 - 分段连续
deque:两端操作迭代器失效但引用不失效,中间操作全部失效 - 节点式
list/map/set:只有被删除的迭代器失效,插入不影响其他 - 哈希容器
unordered_*:rehash全部失效,删除只失效被删者 - 用
reserve预留容量避免 vector 扩容和 rehash 失效;erase用返回值继续遍历
扩展知识
- 失效后的迭代器继续使用是未定义行为
deque两端插入不使引用/指针失效(元素本身不搬移)- 详细讲解:C++ STL
109. STL 空间配置器 allocator 的作用?它如何管理内存?
- 作用:把内存分配/释放(
allocate/deallocate)与对象构造/析构(construct/destroy)分离 - 经典实现 SGI STL 二级配置器:>128 字节用
malloc,≤128 字节用内存池 + 16 条自由链表 - 内存池/自由链表减少小额内存的
malloc调用与内存碎片 vector::reserve只分配内存不构造对象,正是依赖这个分离设计
扩展知识
- 现代
std::allocator大多退化为对operator new/delete的封装,但分离思想仍是考点 - 二级配置器按 8 字节对齐分 16 档(8/16/24…128 字节)
- 详细讲解:C++ STL