2 C++基础-面试题
2.1 C++基本知识
2.1.1 C vs C++
#include "..."和#include <...>的区别?
#include "..."优先使用本地,再去标准库搜索。
#include <...>直接搜索标准头文件。
【必问】C语言 vs C++有什么区别?
-
面向对象 vs 过程式
-
C 是过程式语言,以函数和结构体为核心,没有类、继承、多态等面向对象特性。
-
C++ 是多范式语言,支持面向对象(类、继承、多态)、泛型(模板)和过程式编程。
-
-
内存管理
-
C 依赖
malloc/free手动管理内存,容易出错(如内存泄漏)。 -
C++ 提供
new/delete和智能指针(如std::unique_ptr),更安全且支持 RAII(资源获取即初始化)。
-
-
标准库
-
C 的标准库(如
<stdio.h>、<stdlib.h>)功能较基础,缺乏高级容器和算法。 -
C++ 的标准库(STL)提供
vector、map、algorithm等强大工具,大幅提升开发效率。
-
-
函数与类
-
C 只有函数,没有成员函数、构造函数、析构函数等概念。
-
C++ 支持类、成员函数、运算符重载、友元等面向对象特性。
-
-
异常处理
-
C 没有原生异常机制,通常用错误码(如
errno)处理错误。 -
C++ 提供
try/catch异常处理机制,更符合现代编程习惯。
-
你为什么选择以C++为你使用的语言?为什么不是java?为什么不是go?C++相比其他这些语言优势在哪里?缺点在哪里?
选择 C++ 的原因
我选择 C++ 主要是基于它的 性能、灵活性和控制力。作为服务器开发者,我们经常需要处理高并发、低延迟的场景,而 C++ 能让我直接管理内存和资源,避免 GC(垃圾回收)带来的不可控延迟。比如在游戏服务器、高频交易系统中,C++ 的性能优势非常明显。
为什么不是 Java?
Java 虽然有成熟的生态(JVM、Spring 等),但它的 GC 机制会导致不可预测的停顿,这在实时性要求高的场景(如高频交易、实时通信)是不可接受的。此外,Java 的运行时开销较大,而 C++ 可以更精细地优化内存和 CPU 使用。
为什么不是 Go?
Go 的并发模型(goroutine)确实很优秀,语法也更简单,但它的 性能上限不如 C++,特别是在计算密集型任务上。而且 Go 的泛型(Go 1.18+)和内存管理(GC)仍然不如 C++ 灵活。如果项目对性能要求极高,或者需要直接操作底层(如网络协议栈、硬件交互),C++ 仍然是更好的选择。
C++ 的优势
-
高性能:零成本抽象,接近硬件的控制力。
-
灵活性:支持多范式(面向对象、泛型、过程式)。
-
资源控制:手动管理内存和资源,适合底层开发。
-
跨平台:编译型语言,可移植性强。
C++ 的缺点
-
开发效率较低:手动管理内存容易出错(内存泄漏、悬垂指针)。
-
学习曲线陡峭:模板、RAII 等概念较复杂。
-
编译速度慢:相比 Java/Go,C++ 的编译时间较长。
游戏服务器为什么一般使用C++和go?为什么不是JAVA?
游戏服务器选择 C++ 和 Go 主要考虑 性能、实时性和开发效率,而 Java 在某些方面存在劣势:
-
性能与实时性
-
游戏服务器需要 低延迟、高吞吐,C++ 能直接控制内存和 CPU,避免 GC 停顿(Java 的垃圾回收可能导致不可预测的延迟)。
-
-
资源控制
-
C++ 允许 精细化的内存管理,适合需要频繁创建/销毁对象的场景(如游戏实体)。
-
Java 的 GC 机制仍可能引入不可控延迟。
-
-
部署与运维
-
C++ 编译为二进制,部署简单;而 Java 需要 JVM,增加复杂度。
-
#pragma once和#ifdef有什么区别?
#pragma once简洁,不需要手动维护宏名,减少了出错可能,而且编译速度可能更快,因为编译器可以更高效地处理包含关系。
#ifdef可移植性更好,因为所有编译器都支持。但缺点是需要手动确保宏名唯一,如果宏名冲突会导致保护失效,而且代码量稍多。
2.1.2 内存
【必问】Linux运行C++程序的进程内存布局?
高地址 (High Address)
+--------------------------------------------+
| 命令行参数 & 环境变量 (argv, envp) |
+--------------------------------------------+
| 栈 (Stack) | ← 向低地址增长
+--------------------------------------------+
| 共享库映射区 (mmap) | (如动态库 .so)
+--------------------------------------------+
| 堆 (Heap) | ← 向高地址增长 (malloc/new)
+--------------------------------------------+
| 未初始化数据段 (BSS) | (全局/静态未初始化变量)
+--------------------------------------------+
| 初始化数据段 (Data) | (全局/静态已初始化变量)
+--------------------------------------------+
| 只读数据段(.rodata) | (const 全局/静态数据、虚函数表)
+--------------------------------------------+
| 代码段 (Text/Code) | (程序代码、只读常量)
+--------------------------------------------+
低地址 (Low Address)
-
代码段(Text Segment / Code Segment)
-
程序的可执行代码(CPU 指令)。
-
只读常量(
"Hello")。
-
-
全局区-初始化数据段(Initialized Data)
-
全局变量 和 静态变量(static),如果它们被显式初始化。
-
-
全局区-未初始化数据段(BSS, Block Started by Symbol)
-
全局变量 和 静态变量(static),如果它们未初始化(或初始化为 0)。
-
-
堆(Heap)
-
动态分配的内存(如
malloc、new、new[])。 -
向高地址增长(但具体增长方向取决于系统)。
-
-
共享库映射区(Memory-Mapped Region, mmap)
-
动态链接库(.so 文件) 的代码和数据。
-
内存映射文件(mmap)(如数据库文件映射到内存)。
-
-
栈(Stack)
-
局部变量(函数内部定义的变量)。
-
函数调用时的返回地址、参数、寄存器保存。
-
函数调用栈帧(Stack Frame)。
-
向低地址增长(栈顶指针 ESP/RSP 向下移动)。
-
-
命令行参数 & 环境变量(argv & envp)
-
程序启动时的命令行参数(
argc,argv)。 -
环境变量(
envp,如PATH=/usr/bin)。
-
const修饰的变量都放在全局区吗?
不是。跟const没办毛钱关系。全局区只看是否全局、是否static。
const修饰的全局变量放在全局区。
const局部变量不在全局区。
malloc()是做什么的?
在内存的动态存储区(堆区)中分配一个长度为size的连续空间。并将该空间的首地址作为函数值返回,即此函数是一个指针函数。
由于返回的指针的基类型为 void,应通过显式类型转换后才能存入其他基类型的指针变量,否则会有警告。如果分配不成功,返回空指针(NULL)。
new是做什么的?
利用new关键字,可以将数据开辟到堆区。
堆区开辟的数据,由程序员手动开辟,手动释放,释放利用操作符 delete
利用new创建的数据,会返回该数据对应的类型的指针
【必问】malloc() vs new有什么区别?
malloc() 和 new 都是用于动态内存分配的机制,但它们有本质区别:
-
来源与语言支持
-
malloc()是 C 标准库函数(<stdlib.h>),纯 C 语言使用。 -
new是 C++ 关键字,是语言原生支持的运算符,支持面向对象特性。
-
-
初始化
-
malloc()只分配内存,**不会初始化**(返回的内存内容是未定义的)。 -
new不仅分配内存,还会**调用构造函数**(如果是类对象),确保对象被正确初始化。
-
-
返回值类型
-
malloc()返回void*,需要手动强制转换(如(int*)malloc(sizeof(int)))。 -
new返回对应类型的指针(如new int直接返回int*),无需强制转换。
-
-
失败处理
-
malloc()失败时返回NULL,需要手动检查。 -
new失败时默认抛出std::bad_alloc异常(可通过new (nothrow)禁用异常)。
-
-
内存释放
-
malloc()分配的内存用free()释放。 -
new分配的内存用delete释放(如果是数组则用delete[])。
-
-
扩展性
-
new可以重载(自定义内存分配逻辑),malloc()是标准库函数,无法直接重载。
-
总结:在 C++ 中优先用 new/delete,因为它更安全、更符合语言特性;malloc() 通常只在 C 兼容代码或特殊场景(如自定义内存池)中使用。
2.1.3 引用
【必问】C++引用是什么?做什么的?
给变量起别名。对“别名”做任何修改都等于操作“原名”,因为他们指向的是同一块内存。函数传参时,可以利用引用的技术让形参修饰实参,可以简化指针修改实参。
引用做函数返回值是干嘛用的?需要注意什么?
函数调用作为左值,test02() = 1000;
注意: 不要返回局部变量引用!
引用的本质是什么?为什么引用必须初始化?为什么不能用常量初始化?为什么说引用在初始化后,指向地址不可以改变?
本质:引用的本质在c++内部实现是一个指针常量。
int& ref = a;就是int* const ref = &a;
ref=20;//内部发现ref是引用,自动帮我们转换为:*ref=20;
因为本质是指针,指针必须初始化。
指针初始化必须指向内存而非一个常量,如果必须要指向常量,可以使用常量引用const int& ref = 10;。
因为是指针常量,指针常量指向的地址不能更改。
常量应用的本质是什么?为什么const int& ref = 10;合法?
常量引用的本质:int temp = 10; const int &ref = temp;。
2.1.4 重载
【必问】什么是函数重载?它有什么用?
-
同一个作用域下
-
函数名称相同
-
函数参数
类型不同或者个数不同或者顺序不同 -
如果返回值不同,参数列表相同,那也不是重载。重载和返回值无关。
作用: 函数名可以相同,提高复用性。
C++函数重载调用重载版本的规则是什么?
-
若能完美匹配类型,则匹配。
-
若不能完美匹配,会触发隐式类型转换。
-
若不能完美匹配,在隐式类型转换之后却能匹配多个重载版本,则触发编译报错,提示调用歧义。
C++在函数重载时,参数前加修饰符,例如const,它能构成重载吗?
在C++中,函数重载时参数前的 const 修饰符是否被视为不同参数类型,取决于参数传递的方式:
-
值传递(Pass by Value)
-
不构成重载。值传递时,参数的
const是顶层(top-level)修饰符,仅限制函数内部对副本的修改,不影响调用者的实参。编译器会将这两个函数视为相同参数类型,导致编译错误(重复定义)。
-
-
引用传递(Pass by Reference)
-
构成合法重载。
const修饰符改变引用的类型(从int&到const int&),影响函数匹配规则:-
func(int& x)不能 接受const int或字面量。 -
func(const int& x)可以 接受const int、字面量以及普通int。 -
这个跟
const int& x的底层实现结构有关(可以展开来聊)。
-
-
-
指针传递(Pass by Pointer)
-
构成合法重载。
const修饰符作用于指针指向的内容(底层const),导致参数类型不同(int*vsconst int*)。
-
运算符重载实现的位置有几种?
2种。
在类中修改成员函数,实现运算符重载。
全局函数也可以重载运算符。
<<运算符重载为什么一般不写在类里?
因为这样最终的格式是a.operator<<(cout),即a << cout顺序相反。
++运算符重载,前置++和后置++有何不同?后置++为什么不能返回引用?
-
重载后置++运算符有一个int参数,int代表占位参数,可以用于区分前置和后置递增。
-
前置++返回该类的引用,后置++只返回类。
因为后置++先用一个temp记录当前状态,再++,再返回,返回的temp是一个临时变量。
[]重载的参数类型和返回值类型是什么?
-
参数类型:可以是任意类型(如
int、size_t、string等),但通常为整数或枚举类型。 -
返回类型:
-
通常返回元素的引用(
T&),以支持通过下标修改元素(如obj[0] = 5)。 -
若不需要修改元素,可提供
const重载版本。
-
[]重载要注意什么?[]重载为什么不能作为全局函数重载?
注意:[]重载必须为成员函数:不能作为全局函数重载。
在C++里运算符重载不能单纯作为全局函数重载,主要是因为很多运算符的操作数是类的成员,需要访问类的私有成员。如果作为全局函数重载,它就没办法直接访问这些私有成员,除非把该类的所有相关成员都设成公有,这会破坏封装性。
()重载见仿函数。
2.1.5 类型转换
【必问】C++类型转换有几种,分别是做什么的?
static_cast:用于明确定义的转换,具有编译时检查。
-
基本数据类型间的转换(如
int→double)。 -
类层次结构中的上行转换(派生类指针/引用 → 基类指针/引用)。
-
类层次结构中的下行转换(基类指针/引用 → 派生类指针/引用),但无运行时检查,可能不安全。
-
void*与其他类型指针间的转换。
dynamic_cast:处理多态类型的安全下行转换,依赖 RTTI(运行时类型信息)。
const_cast:修改类型的 const 或 volatile 属性。
reinterpret_cast:低级别位模式重新解释,风险高。
C++中的explicit关键字是做什么用的?
explicit关键字主要是用来防止隐式类型转换的。在C++里,如果一个类的构造函数是单参数的,或者有多个参数但除了第一个参数外其他都有默认值,那么这个构造函数就可以作为隐式类型转换的函数。比如说,有个类A,它有个接受int类型参数的构造函数,在代码里如果有个int类型的值,在需要A类型对象的地方就可以直接使用这个int值,编译器会自动调用构造函数把它转成A类型对象。但有时候这种隐式转换不是我们想要的,可能会带来一些难以发现的错误。这时候我们就可以在构造函数前加上explicit关键字,这样编译器就不会进行隐式类型转换了,必须显式地调用构造函数来创建对象,让代码的意图更加清晰,减少因隐式转换导致的意外行为。
2.1.6 函数
main函数不写返回值会发生什么?
会自动调用exit(0);
std::get<T>是干嘛的?
std::get<T> 是 C++ 标准库中的一个模板函数,主要用于从特定类型的元组(tuple)或类似元组的数据结构中获取第 N 个元素。它是一个 编译期索引 的访问方式,非常类型安全且高效。
C++回调函数使用时应当注意什么?
-
生命周期问题
-
悬空引用:Lambda捕获局部变量的引用,但变量已销毁。
-
修复:通过值捕获(
[=])或传递shared_ptr管理对象。
-
成员函数作为回调
-
成员函数隐含
this指针,需确保对象存活。 -
修复:使用bind或Lambda。
2.1.7 数组
形如const int(&)[2]的函数参数是什么意思?
这是一个 对含有 2 个 int 的数组的常量引用(reference to an array of 2 const ints)。
C++数组之间为什么不可以相互赋值?
在 C++ 中,数组之间不能直接相互赋值,比如这样是不允许的:
int a[5] = {1, 2, 3, 4, 5};
int b[5];
b = a; // ❌ 编译错误:数组不能作为赋值目标
在 C++ 中,数组名在大多数表达式中会退化为指向其首元素的指针(即 &a[0],类型是 int*),但它 本身仍然是一个固定大小的、不可重新绑定的聚合对象。
但更重要的是:C++ 标准规定,数组类型不是一个可赋值的类型(not assignable)。
也就是说,数组类型不支持赋值运算符 = 的直接使用。
C++数组作为函数的参数传入有哪些注意点?
当你将一个数组作为函数参数传递时,它不会以“整个数组对象”的形式传进去,而是退化为指向其首元素的指针。
-
尽管你写的是
int arr[],编译器会把它当作int* arr来处理; -
在函数内部,
sizeof(arr)返回的是指针的大小(比如 4 或 8 字节),而不是数组的总字节大小;
#include的本质是什么?往数组初始化语句里丢一个#include竟然不报错?
#include 是 C / C++ 预处理指令,用于在编译之前,将指定文件的内容直接“插入”到当前源代码文件中,作为源代码的一部分。它由预处理器处理,在正式的编译开始之前执行。当编译器看到 #include <header> 或 #include "file" 时,会暂停当前文件的解析,将指定文件的内容读取进来,并直接插入到 #include 指令所在的位置,然后再继续编译。它本质上是 文本替换/文件包含机制,用来实现代码复用、模块化、分离声明与实现等。
int arr[] = {
#include "data.txt"
}; // 看起来很神奇,竟然可能不报错?"data.txt"其内容恰好是合法的 C++ 数组初始化列表,例如:{1, 2, 3},然后通过 #include 把这个列表“粘贴”到了 int arr[] = { ... }; 中。
但请注意:
-
#include本身并不是“丢进数组初始化语句里的某个表达式”,它是一个 预处理指令,必须在文件的最外层(全局或命名空间作用域,或函数体外等合法位置),不能嵌套在表达式内部,比如:int x = #include "file.txt";// ❌ 错误!不能这样写
一个数组访问a[1]和1[a]为什么是一样的?
在 C 和 C++ 中,a[1] 和 1[a] 是完全等价的,它们都表示访问数组 a 的第 2 个元素(下标为 1 的元素),两者生成的代码通常也是一样的。这不是 bug,而是 C / C++ 语言标准所定义的、合法且一致的行为。
对于指针 p 和整数 i,表达式 p[i] 和 i[p] 是完全等价的,因为它们都等价于 *(p + i)。
2.2 面向对象
2.2.1 封装
【必问】面向对象思想之一的封装是什么?为什么需要封装?
封装就是把数据和操作数据的方法捆绑成一个独立的单元,也就是类。它对外隐藏对象的内部实现细节,只暴露必要的接口供外部使用。
封装的意义:
-
将属性和行为作为一个整体
-
将属性和行为加以权限控制
【必问】类成员的访问权限有几种?分别是什么?
访问权限有三种:
-
public公共权限 public成员 类内可以访问、类外可以访问
-
protected保护权限 protected成员 类内可以访问、非子类的类外不可以访问
-
private私有权限 private成员 类内可以访问、类外不可以访问
在C++里,struct和class有什么区别?
在C++中 struct和class唯一区别就在于默认的访问权限不同。struct 默认权限为公共、class 默认权限为私有。
【必问】构造函数和析构函数是什么?
构造函数: 主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。
析构函数: 主要作用在于对象销毁前系统自动调用,执行一些清理工作。
析构函数为什么不能进行重载?
析构函数不可以有参数,因此不可以发生重载。
为什么不要在析构函数里抛出异常?
-
在析构抛出异常时,这时可能正在处理异常,程序无法同时处理两个异常,导致程序崩溃。
-
其他要释放的资源很可能因此而没有被释放,造成内存泄漏。
正确处理:使用noexcept关键字,并只捕获异常,但不抛出。
为什么构造函数可以抛出异常?
如果构造函数抛出异常,对象被视为从未被成功构造,因此析构函数不会被调用(因为对象不存在)。
如果构造函数在初始化列表或构造函数体内抛出异常,已经构造完成的成员变量和基类子对象会被自动析构。
但是也存在问题,指针类型的成员变量的“自动析构”不会干任何事情,必须手动在析构函数delete,而构造函数抛出异常不会析构,这时候也会内存泄漏的。
解决方法:
-
不要在构造函数中直接管理资源(如裸指针、文件句柄等),而是使用智能指针(
std::unique_ptr、std::shared_ptr)。 -
如果构造函数可能失败,可以采用 工厂模式 或
init()方法进行两段式构造:先构造对象(不执行可能失败的操作)。调用init()方法执行可能失败的操作,如果失败则返回错误码或抛出异常。
为什么不要利用拷贝构造函数初始化匿名对象?
编译器会认为这是对象声明,它会认为你又创建了一遍这个对象,会在编译时发生重定义错误。
C++中拷贝构造函数调用时机有哪几种情况?
-
使用一个已经创建完毕的对象来初始化一个新对象。
-
值传递的方式给函数参数传值。
-
以值方式返回局部对象。
在写一个类时,C++编译器默认会提供哪些函数?什么时候不提供?
-
默认构造函数(无参,函数体为空)
-
默认析构函数(无参,函数体为空)
-
默认拷贝构造函数,对属性进行值拷贝
-
赋值运算符 operator=,对属性进行值拷贝
如果用户定义有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造
如果用户定义拷贝构造函数,C++不会再提供其他构造函数
如果用户定义有参构造函数,C++不再提供默认无参构造,这时候使用无参构造创建对象会发生什么?
编译时报错。
如何判断调用了拷贝构造函数还是重载=号?
如果正在构造,那就是拷贝构造函数。
如果构造完了再用等于号,这才是=符号。
C++类的构造函数必须是public的吗?
C++ 类的构造函数不一定必须是 public,它可以是 public、protected 或 private,具体取决于类的设计需求。
-
public构造函数(最常见)特点
-
外部可直接创建对象。
-
常见于普通类。
-
-
protected构造函数✅ 适用于:基类,禁止外部创建对象,但允许子类调用。
特点
-
外部不能直接创建对象。
-
子类可以继承并调用构造函数。
-
常用于基类,用于强制派生类管理对象实例化。
-
-
private构造函数✅ 适用于:
-
单例模式(Singleton)—— 控制对象创建。
-
外部无法创建对象,必须通过
getInstance()获取唯一实例。 -
常用于单例模式,确保全局只有一个实例。
-
-
静态工厂模式(Factory)—— 限制构造函数的直接调用。
-
外部无法直接构造对象,必须使用
create()方法。 -
适用于 工厂模式(Factory Pattern),隐藏对象的创建逻辑。
-
-
只能在类内或友元函数中创建对象。
-
【必问】深拷贝 vs 浅拷贝有什么区别?浅拷贝需要注意什么?
浅拷贝: 简单的赋值拷贝操作
深拷贝: 在堆区重新申请空间,进行拷贝操作
-
如果在构造函数等地申请了堆区内容,需要最后在析构函数将堆区开辟数据做释放操作。
-
如果在构造函数等地申请了堆区内容,那么默认的拷贝构造函数只是“浅拷贝”,两个对象里的指针指向同一个区域。如果析构函数里释放了该内存,那么当一个对象被销毁时,另一个对象使用该指针就会出错,因为你使用了被释放的内存。另一个对象即使什么都不做也会在析构时出错,因为它尝试释放已经释放了的内存。
类的默认成员初始化、类构造函数初始化列表、类构造函数内部赋值,哪个优先级更高?
在C++中,类构造函数初始化列表的优先级最高,其次是默认成员初始化,最后才是构造函数内部赋值。
构造时使用初始化列表的好处是?
-
提高效率:避免不必要的默认构造 + 赋值操作
-
初始化顺序更清晰、可控,符合成员声明顺序
C++聚合体是什么?
在 C++ 中,聚合体(aggregate)是指一种特殊的类(通常是结构体 struct 或数组),它没有用户定义的构造函数、没有私有/保护的非静态成员、没有基类、没有虚函数,满足一系列简单规则,从而允许使用特殊的初始化方式 —— 聚合初始化(Aggregate Initialization)。
聚合初始化是什么?
如果一个类是聚合体,那么你可以使用一种非常简洁的 大括号初始化方式 来初始化它的成员,无需调用构造函数,直接按顺序或按名字初始化成员变量。
类的前置声明有那些坑?
假设前置声明了类A,B类中使用了类A(而非类A的指针),编译时会出错。只有完整的类型才可以直接用类,否则只能用指针。
正确做法:使用A的指针,因为一个指针大小是固定的。
不过这些指针不要去做任何操作,例如p->getXX();,前置声明除了这个指针之外其他的信息编译器都是无法得知的,所以要把指针指出的操作的函数需要放到cpp文件下实现。
类的三个基本原则是什么?
第一:申请的资源必须在析构时释放掉。
第二:申请的资源必须考虑拷贝构造函数
第二:申请的资源必须考虑重载等于号。重载等于号时先把自己的资源都delete。
【必问】类的静态成员是什么?
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员。
静态成员分为:
-
静态成员变量
-
所有对象共享同一份数据。
-
在编译阶段分配内存。
-
类内声明,类外初始化。
-
-
静态成员函数
-
所有对象共享同一个函数。
-
静态成员函数只能访问静态成员变量。因为非静态变量需要依托于对象存在,而非类本身,所以无法访问。
-
静态成员变量 不属于某个对象上,所有对象都共享同一份数据。
静态成员变量需要在类内声明,类外初始化,那么假设我在声明的同时赋值会发生什么?
在 C++11 之前,类内不能直接为静态成员变量赋值(只能声明)。如果尝试这样写,会编译错误。
在 C++11 及之后,可以在类内直接赋值,但仅限于静态常量整型或枚举类型。
静态变量的初始化必须在全局作用域进行,不能放在某个函数内。除了下面的特殊情况,不过这里的x也不是成员。
class A {
public:
static int& getX() {
static int x = 10; // ✅ 只初始化一次
return x;
}
};
静态成员变量设为private,初始化和访问与public的静态成员变量有何不同?
无论是 public 还是 private,静态成员变量的初始化方式相同,都必须在类外进行初始化。
即使 y 是 private,类外仍然必须进行初始化,但不能直接访问 A::y。
private 静态变量:必须通过成员函数访问。
【必问】空对象占用内存空间为?
1。
原因:C++编译器会给每个空对象也分配一个字节空间,是为了区分空对象占内存的位置。不然两个空对象可能就占有同一个内存空间地址了。
(追问)只有一个非静态int成员,该对象占用内存空间为?
4。
直接分配一个int的大小。
(追问)那静态成员变量呢?
完全不占用对象的空间,就当他没存在过。
(追问)成员函数呢?
静态和非静态函数都不占用对象的空间。
C++中空指针可以调用成员函数吗?
可以,但注意此时this==NULL,所以函数内部暗含this则可能出问题,需要提前判断。
const修饰成员函数会发生什么?
常函数内不可以修改成员属性,除非成员属性声明时加关键字mutable。
const修饰对象会发生什么?
-
声明对象前加const称该对象为常对象
-
常对象内不可以修改成员属性(除非mutable)
-
常对象只能调用常函数
为什么常函数内不可以修改成员属性?
因为函数后加const使得编译器会把this指针前面也加上const:const A * const this;。你无法修改this指出的值。
【必问】C++友元是什么?为什么需要友元?
友元让一个函数或者类访问另一个类中私有成员。
友元的三种实现:
-
全局函数做友元
-
类做友元
-
某个类的成员函数做友元
如何定义只能在堆上或栈上生成对象的类?
让一个类的对象 只能在堆(heap)上创建(只能用 new,不能在栈上创建):
将构造函数设为 private 或 protected,并提供一个静态成员函数返回堆上创建的对象(工厂方法)。
同时,为了防止用户使用 new 直接调用构造函数后还能 delete,你也可以 将析构函数设为 private,这样即使是 new 出来的对象,用户也无法手动 delete,而是要通过你提供的销毁接口。
让一个类的对象 只能在栈(stack)上创建(不能用 new 创建):
重载 operator new 并设为 private 或直接删除它,使得 new 操作不可用。
C++11 开始,我们可以使用 = delete 明确地 禁用某个函数,包括 operator new。
2.2.2 继承
C++继承是什么?为什么需要继承?
派生类可以复用基类的代码,同时还能扩展自己的功能。
我们需要继承主要是为了实现代码复用和建立类之间的层次关系。通过继承可以避免重复编写相同的代码,提高开发效率。
【必问】C++子类继承父类时,使用不同的继承方式,成员访问权限如何被继承?
-
公共继承:父public->子public,父protected->子protected,父private->子不可访问
-
保护继承:父public->子protected,父protected->子protected,父private->子不可访问
-
私有继承:父public->子private,父protected->子private,父private->子不可访问
从父类继承过来的成员,哪些属于子类对象中?父类的私有成员没有被继承吗?
父类中所有非静态成员都会被子类继承下去。父类中私有成员属性是被编译器给隐藏了,因此只是访问不到,但是确实被继承下去了。
【必问】父类、子类、成员对象的构造和析构顺序是什么?
构造顺序:虚父类 → 父类 → 成员对象 → 子类;
析构顺序:子类 → 成员对象(逆序) → 父类 → 虚父类。
(追问)假设有ABCD四个类,B继承自A,C继承自A并包含一个B的对象,D继承自B并包含一个C对象,那么D的构造和析构顺序是什么样的?
-
根据父类 → 成员对象 → 子类的顺序,D继承自B并包含一个C对象,所以B → C → D
-
B继承自A,所以B为A → B
-
C继承自A并包含一个B的对象,所以C = A → B → C,又因为B = A → B,所以C = A → A → B → C
-
所以D = A → B → A → A → B → C → D
析构顺序完全相反。
(追问)为什么虚继承最优先构造?
这是为了确保每个虚基类在派生结构中只被构造一次,从而避免数据冗余。
C++会出现循环构造的问题吗?例如D继承自B,D包含一个B的对象。
是的,但你这么写,编译器会发现,会直接编译报错。
在继承时,父类和子类有同名的成员,优先访问哪个?
优先访问子类。
访问子类同名成员 直接访问即可。
访问父类同名成员 需要加作用域。
访问成员函数也是同理。
(追问)虽然父类和子类的函数重名了,但是参数不一样,会发生重载吗?
不会!
如果子类中出现和父类同名的成员函数,子类的同名成员会隐藏掉父类中所有同名成员函数。
即使父类里有一个函数被重载了1000次,子类的同名函数也会把他全部隐藏。
多继承中,父类中有同名成员出现会发生什么?如何解决?
直接报错:有歧义!
需要加作用域区分。
多继承的构造和析构的顺序是什么?
派生类的构造函数会按照继承列表中基类的顺序依次调用它们的构造函数。
【必问】多继承会造成什么问题?菱形继承是什么?如何解决?
多继承会造成菱形继承问题,B继承A,C继承A,D继承B、C,就会出现菱形继承问题。
同一个基类下面被继承了两份数据而且值还可以不一样,造成资源浪费。且不指名作用域就会直接报错。
利用虚继承可以解决菱形继承的问题。虚继承确保基类仅被实例化一次。派生类通过虚基表(vtable)间接访问虚基类的成员。
【必问】虚基类指针和虚基类表是什么?
-
虚基类指针:每个虚继承的派生类对象中会包含一个指向虚基类的指针,这个指针用于定位虚基类子对象在对象中的位置。
-
虚基类表:每个包含虚基类的类会有一个虚基类表(vbt),表中存储了虚基类子对象相对于当前类对象的偏移量。当通过派生类访问虚基类成员时,编译器会通过虚基类指针和虚基类表找到正确的虚基类子对象位置。
以B虚继承A,C虚继承A,D继承B、C为例,虚基类表里面装了什么?
(1) 类A
-
无虚基类指针(A是最顶层基类,没有更上层的虚基类)
-
无虚基类表(A不需要定位任何虚基类)
(2) 类B
-
虚基类指针:
vbptr_B(指向B的虚基类表) -
虚基类表(B的vbt):
-
存储A在B中的偏移量(即
&B对象 + 偏移量 = &A子对象) -
例如:
offset_A_in_B(假设值为0x10,表示A子对象在B对象中的偏移量为16字节)
-
(3) 类C
-
虚基类指针:
vbptr_C(指向C的虚基类表) -
虚基类表(C的vbt):
-
存储A在C中的偏移量(即
&C对象 + 偏移量 = &A子对象) -
例如:
offset_A_in_C(假设值为0x20,表示A子对象在C对象中的偏移量为32字节)
-
(4) 类D
-
虚基类指针:
vbptr_D(指向D的虚基类表) -
虚基类表(D的vbt):
-
存储B在D中的偏移量(
offset_B_in_D) -
存储C在D中的偏移量(
offset_C_in_D) -
存储A在D中的偏移量(
offset_A_in_D,通过B或C的vbt间接定位)
-
菱形继承造成的类的sizeof是多大?增加了虚继承呢?增加了虚函数呢?
class A {
virtual void foo() {}
int a;
};
class B : virtual A {
virtual void foo1() {}
int b;
};
class C : virtual A {
virtual void foo2() {}
int c;
};
class D : public B, public C {
virtual void foo3() {}
int d;
};
D是多大?
-
看本类是否有虚函数,无论有多少个,如果有则【可能】生成一个vptr_X(8)。如果X的【非虚继承】的父类也拥有虚函数,则此vptr_X并不会生成。D有虚函数,但它的父类B也有虚函数,所以此处不会有任何东西。
-
接着看X的所有【非虚继承】,将他们继承,但不要处理虚继承。D有一个普通继承B:vptr_B(8)+vbptr_B(8)+int_B(4->8),和一个普通继承C:vptr_C(8)+vbptr_C(8)+int_C(4->8)。B和C都有一个虚继承,不要去碰它。
-
接着看本类是否有【虚继承】,无论有多少个,如果有则生成一个vbptr_X(8)。本类没有虚继承。
-
X的其他内容:int_D(4->8)
-
【统一处理】所有【虚继承】,包括父类的【虚继承】,将他们完全继承。虚继承的类里面的内容只出现一次不会重复。虚继承是A,B虚继承自A:vptr_A(8)+int_A(4->8)。
C也虚继承自A:vptr_A(8)+int_A(4->8)。但是!虚继承的类里面的内容只出现一次不会重复!!!
2.2.3 多态
【必问】多态是什么?分为哪两类?为什么需要多态?
多态是C++面向对象编程的核心特性,它允许不同类的对象对同一消息做出不同响应,也就是用统一的接口操作不同的对象。简单说就是"一个接口,多种实现"。
多态主要分为两类:
-
编译时多态(静态多态):通过函数重载和运算符重载实现,在编译阶段就确定了调用哪个函数。
-
运行时多态(动态多态):通过虚函数和继承实现,程序运行时根据对象的实际类型决定调用哪个函数。
我们需要多态主要是因为:
-
提高代码复用性:通过基类接口统一操作不同派生类对象,避免重复代码。
-
增强扩展性:新增派生类时不需要修改现有代码,符合开闭原则。
-
实现更灵活的设计:比如游戏中的不同角色可以共享同一个"移动"接口,但具体实现各不相同。
-
更好的抽象:隐藏具体实现细节,只暴露必要的接口,让代码更易维护和理解。
【必问】覆盖重写 vs 函数重载有什么区别?
-
覆盖重写(override):
-
有父子继承。
-
函数同名同参数,函数签名必须完全一致,包括const修饰符。(返回值也必须相同,除非返回父类指针或引用,子类可以重写为返回子类的指针或引用)
-
父类的这个函数是虚函数。
-
-
函数重载(overload):
-
没有继承关系,只需同一个作用域内(比如同一个类或同一个命名空间)。
-
同名,但是参数必须不同,可以是参数个数、类型或顺序不同。
-
【必问】动态多态,底层原理是什么?
动态多态的底层实现主要依赖虚函数表(vtable)和虚函数指针(vptr)的机制:
-
虚函数表(vtable):每个包含虚函数的类在编译时都会生成一张虚函数表,表中按顺序存储该类所有虚函数的地址。如果子类重写了父类的虚函数,子类的vtable中对应位置会替换成子类的函数地址。
-
虚函数指针(vptr):每个对象在内存中会包含一个隐藏的vptr指针,指向其所属类的vtable。这个指针在对象构造时由构造函数初始化,指向正确的vtable。
-
调用过程:当通过基类指针或引用调用虚函数时:
-
编译器会通过对象的vptr找到对应的vtable
-
然后根据虚函数在vtable中的偏移量找到实际要调用的函数地址
-
最后跳转到该地址执行函数
-
关键点在于:
-
虚函数表是静态的,编译时确定
-
虚函数指针是动态的,运行时根据实际对象类型确定
-
这种机制实现了"运行时多态",让程序可以根据对象的实际类型调用正确的函数版本
【必问】抽象类是什么?抽象类的特点是什么?为什么需要抽象类?
当类中有了纯虚函数(有一个就算),这个类也称为抽象类。纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;。
抽象类特点:
-
无法实例化对象。
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
抽象类提供了更好的抽象层次。它代表了一类事物的共同特性,但不关心具体实现细节。在大型项目中,抽象类能让代码更易于维护和扩展,新增功能时只需添加新的派生类而不需要修改现有代码。
构造函数可以是虚函数吗?
不。
-
对象类型在构造时尚未确定:虚函数调用依赖于虚函数表(vtable),而该表是在构造过程中逐步建立的,且在基类构造期间,vtable 反映的是基类的虚函数,不是派生类的。
-
语言规范明确禁止:C++ 标准明确规定,构造函数不能声明为虚函数。如果你尝试这样做,编译器会报错。
【必问】虚析构是什么?为什么需要虚析构?
析构函数加上virtual就是虚析构。
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。
纯虚析构是什么?虚析构和纯虚析构的区别是什么?
virtual ~A()=0;
如果是纯虚析构,该类属于抽象类,无法实例化对象。
为什么父类的纯虚析构函数也必须实现?(通常在类外)
当通过基类指针删除派生类对象时,会先调用派生类的析构函数,再调用基类的析构函数。如果基类析构函数没有实现,链接时会报错。
构造函数和析构函数都调用某个虚函数,那么他们分别调用父类还是子类的函数?
都会调用父类的函数。在基类构造函数执行期间,派生类部分尚未构造完成,因此此时对象被视为基类类型。在派生类析构函数执行完毕后,派生类部分已经被销毁,此时对象被视为基类类型。
为什么多态时子类要public继承父类?而非private?
如果子类不是 public 继承父类,Base* 不能指向 Derived 对象,因为 Derived 的 Base 部分是私有的,外部无法访问。
虚函数可不可以是private?什么时候用到?
可以。
实现“非虚接口(NVI, Non-Virtual Interface)”惯用法。
这是一种设计模式,核心思想是:
-
基类提供一个 public 的非虚函数作为稳定的对外接口(称为“包装函数”或“调用入口”)
-
实际的实现细节(可能涉及多态)放在 private 或 protected 的虚函数中
【必问】虚函数表的创建时机?虚基类表创建的时机?
虚函数表:
-
虚函数表是一个函数指针数组,其中每个条目指向一个类的虚函数的实现。
-
如果一个类中声明了至少一个虚函数,或者继承了有虚函数的基类,那么该类就拥有一个虚函数表。
-
每个含有虚函数的类(有自己 vtable 的类) 在编译时期由编译器生成一张对应的虚函数表。
-
每个该类的对象 在内存中(通常在对象起始位置)会包含一个隐藏的指针,称为 虚函数表指针(vptr),指向该类的虚函数表。
虚函数表的创建时机:虚函数表是在编译期间由编译器生成的,也就是说,编译器会为每个包含虚函数的类生成一张虚函数表,并确定好其中的函数指针指向哪个具体的虚函数实现(可能是基类的,也可能是派生类重写的)。这张表是静态的,它在程序加载时就已存在。
vptr的创建时机:当一个对象被构造时,在进入构造函数体之前(即成员初始化列表之后,构造函数代码执行之前),编译器会插入代码来设置该对象的 vptr,使其指向当前类对应的虚函数表。
虚基类表:
-
当使用虚继承(virtual inheritance)时,如果一个类从多个路径继承了同一个虚基类,为了避免二义性和数据冗余,C++ 引入了虚基类机制。
-
为了正确找到虚基类子对象在派生类中的偏移地址,编译器会生成一张虚基类表(vbtable,也称为虚基偏移表,或 virtual base table)。
-
每个使用了虚继承的类(通常是派生类,直接虚继承某个基类),编译器会为其生成一张虚基类表,其中记录了虚基类相对于当前类对象的偏移量。
-
派生类对象中也会包含一个指针(通常称为 vbptr,虚基表指针),指向这张虚基类表。
虚基类表的创建时机:和虚函数表一样,虚基类表也是由编译器在编译期间分析继承关系后生成的,它是静态的,通常存放在程序的数据段中(如只读或读写数据段)。
vbptr的创建时机:在对象构造过程中(通常在构造函数执行前或构造函数初始化列表之后),编译器会插入代码,用于设置对象的 vbptr(虚基表指针),让它指向正确的虚基类表。
他俩到底谁先谁后?一般取决于编译器,vptr更常见一些,大概率先实现、且在类的布局里在前面。
【必问】虚函数表存储在内存的哪里?vptr存储在内存的哪里?
虚函数表位于只读数据段。
vptr跟随对象,对象在堆上就在堆上,对象在栈上就在栈上,对象在全局区就在全局区。
虚函数表里大致存放什么内容?
该类中所有虚函数(包括继承而来的虚函数)的函数指针(即函数的内存地址)。
具体来说:
-
对于每一个虚函数,vtable 中会有一项,该项是一个指针,指向该虚函数的具体实现(代码地址)。
-
如果子类重写了(override)基类的虚函数,那么 vtable 中对应的项就会替换为子类的实现地址。
-
如果子类没有重写某个虚函数,那么 vtable 中仍然保留基类版本的函数地址。
sizeof一个类(而非对象),会不会计入vptr的大小?
会。
sizeof(ClassName) 返回的是 该类实例化出的对象在内存中占用的字节数,也就是 对象的大小。
inline函数可以是虚函数吗?
可以,但是 inline 对 virtual 函数的“实际内联”效果通常是有限的或无效的。
-
通过对象直接调用(静态绑定)—— 可能内联
-
通过指针或引用调用(动态绑定)—— 通常不会内联
反正inline只是一个建议,具体内不内联取决于编译器。
memset函数初始化有虚函数的类对象会有什么问题?
不应该使用 memset 来初始化含有虚函数(或有虚继承等)的 C++ 类对象,这样做几乎总是会导致未定义行为(Undefined Behavior, UB),并可能引发程序崩溃或其他难以排查的错误。
当你在一个类中声明了至少一个虚函数(或者继承了虚函数),编译器会为该类自动生成一个隐藏成员 —— 虚函数表指针vptr。如果你对一个含有虚函数的类对象使用 memset(this, 0, sizeof(*this)) 或类似的操作,你实际上是在:将该对象的所有内存(包括 vptr)全部置为 0 或其它值!
后果是什么?主要问题:vptr 被破坏,导致虚函数调用失败或程序崩溃!
2.3 STL
2.3.1 函数模板
函数模板什么时候使用可以无需指定类型?
一般的函数模板必须指定类型,例如mySwap<int>(a, b);,但是如果能推导出一致的数据类型T,则可以触发自动类型推导,这时就无需指定数据类型,例如a和b都是int类型时,就可以直接使用mySwap(a, b);。
数据类型T一定要被推导出来才能省略,即使像下面的代码根本就没用到T也必须指定T,因为没办法推导出T。
template<typename T> void func(){}
函数模板有隐式类型转换吗?什么时候会触发?
有。
指定了数据类型T时,可以触发隐式类型转换。但是用自动类型推导,不可以发生隐式类型转换。
同名普通函数与函数模板的调用规则?
-
如果函数模板和普通函数都可以实现,优先调用普通函数(即使普通函数只有一个函数声明,都会调用普通函数然后报错)
-
可以通过空模板参数列表来强制调用函数模板(
Add<>(a, b);) -
函数模板也可以发生重载
-
如果函数模板可以产生更好的匹配,优先调用函数模板
2.3.2 类模板
【必问】类模板 vs 函数模板的区别?
-
类模板没有自动类型推导的使用方式
-
类模板在模板参数列表中可以有默认参数,有默认参数就可以不指定类型
类模板中成员函数创建时机和普通类的成员函数有什么区别?
-
普通类中的成员函数一开始就可以创建。
-
类模板中的成员函数在调用时才创建。
类模板可以继承吗?
可以。
当子类继承的父类是一个类模板时,子类在声明的时候,要指定出父类中T的类型。
如果不指定,除非子类也需变为类模板,不然编译器无法给子类分配内存。
既然类模板中的成员函数在调用时才创建,在分文件编写时会遇到什么问题?如何解决?
问题:
类模板中成员函数创建时机是在调用阶段,导致分文件编写时链接不到。
解决:
解决方式1:直接包含.cpp源文件。
解决方式2:将声明和实现写到同一个文件中,并更改后缀名为.hpp。
类模板的友元有什么需要注意的点?
-
普通函数作为类模板的友元:类外定义通常无法访问类模板内部成员,所以一般在类内定义。类内定义的友元函数并不是类的成员。
-
普通类作为类模板的友元。
-
另一个类模板作为类模板的友元:这是比较复杂的一种情况,涉及两个类模板之间的友元关系。需要注意:
-
友元声明需要匹配模板参数。
-
可以声明整个类模板为友元,也可以声明特定实例为友元。(在声明友元时使用<T>为友元的类型,或例如使用<int>为友元的类型)
-
-
类模板的成员函数作为另一个类模板的友元
在使用类模板的友元时,前向声明非常重要,尤其是在涉及多个模板类或模板函数时。如果顺序不对,编译器可能无法识别友元声明中的类型,导致编译错误。
模版遇到inline会发生什么?
在 C++ 中,函数模板通常定义在头文件中,而且编译器在实例化模板时,本质上是为每种类型生成一份具体的函数。这些生成的函数在头文件中定义时,通常也会被隐式地当作“inline”候选,即使你没有显式地写 inline。
所以不会发生任何事情。
类中的模板方法可以是虚函数吗?
虚函数需要虚函数表,模板可能会有无数多个模板实例,那么在运行时创建虚函数表时,无法为“潜在无限多”的模板实例预留位置。
所以会直接报错。
模板在编译期起到什么作用?
-
模板实例化(Template Instantiation)—— 编译期生成具体代码
当你写一个函数模板或类模板时,C++ 编译器并不会立即生成实际的函数或类代码,而是等到你使用具体类型来调用或实例化它时,编译器才会根据那个类型生成一份专属的代码,这个过程叫做 模板实例化(Template Instantiation)。
与运行时多态(比如通过虚函数实现)不同,模板实现的多态是编译期多态,也称为 静态多态(Static Polymorphism)。
-
类型推导(Type Deduction)—— 编译期确定类型
C++ 模板在编译期还能进行类型推导,比如函数模板会根据你传入的实参,推导出模板参数的实际类型,这个过程也是在编译期完成的。
2.3.3 容器
【必问】STL是什么?为什么需要STL?它的六大组件是什么?
STL是(Standard Template Library, 标准模板库)。
大多情况下,数据结构和算法都未能有一套标准,导致被迫从事大量重复工作。为了建立数据结构和算法的一套标准,诞生了STL。
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
【必问】STL容器大概分为哪两类?每种容器都属于哪类?
序列式容器: 强调值的排序,序列式容器中的每个元素均有固定的位置。string、vector、deque、stack、queue、list。
关联式容器: 二叉树结构,各元素之间没有严格的物理上的顺序关系。set、map、unordered_map。
string:string vs char*有什么区别?
-
char*是一个指针;string是一个类,类内部封装了char*,管理这个字符串,是一个char*型的容器。
-
string管理char*所分配的内存,不用担心复制越界和取值越界等,由类内部进行负责。
vector:vector vs 普通数组有什么区别?
不同之处在于数组是静态空间,而vector可以动态扩展。动态扩展并不是在原空间之后续接新空间,而是找更大的内存空间,然后将原数据拷贝新空间,释放原空间。
vector容器的迭代器是支持随机访问的迭代器。
vector:vector的capacity和size的区别是什么?
vector的size和capacity是两个密切相关但本质不同的概念。size表示vector当前实际存储的元素数量,也就是我们可以通过下标访问到的有效元素个数;而capacity则表示vector在内存中预先分配的存储空间大小,也就是在不重新分配内存的情况下最多能容纳的元素数量。
关键区别在于:size是已经使用的空间,capacity是已分配的空间。
vector:【必问】假设在capacity为n的时候,此时size也是n,我在插入一个数据,capacity变为多少?vector的扩展机制能详细讲讲吗?
在 C++ 中,当 std::vector 的 size 和 capacity 均为 n 时,插入一个元素会触发动态扩容。此时 capacity 的增长策略由标准库的具体实现决定,但核心机制是按比例增长,以保证插入操作的均摊时间复杂度为 O(1)。
当 size == capacity 时插入元素,新的 capacity 通常为以下两种策略之一:
-
倍增策略(例如 GCC 的
libstdc++):capacity变为2 * n。 -
1.5 倍增长策略(例如 MSVC 的
VC++):capacity变为1.5 * n。
vector:vector插入元素后重分配内存的操作顺序是什么样的?
-
分配新内存空间:std::vector 会分配一块更大的内存空间,通常是当前容量的两倍(具体倍数可能因实现而异)。
-
拷贝现有元素:std::vector 会将所有现有的元素从旧内存空间拷贝到新内存空间。这个拷贝过程是通过元素的拷贝构造函数或移动构造函数(如果可用)来完成的。
-
插入新元素:在现有元素被成功拷贝到新内存空间后,std::vector 会将新元素插入到新内存空间的末尾。
-
释放旧内存空间:最后,std::vector 会释放旧的内存空间。
vector:【必问】vector插入数据没有超过capacity大小也会整体移动空间吗?迭代器也会失效吗?
-
插入数据未超过 capacity 的情况
插入点之后的迭代器会失效:因为插入点之后的元素会被移动,所以指向这些元素的迭代器会失效。
插入点之前的迭代器仍然有效:插入点之前的元素不会被移动,所以指向这些元素的迭代器仍然有效。
-
插入数据超过 capacity 的情况
迭代器失效:所有迭代器都会失效,包括指向插入点之前和之后的元素的迭代器,因为内存地址发生了变化。
vector:如何巧妙地收缩vector的内存空间?
假设v的capacity很大但size很小,使用vector<int>(v).swap(v);其中vector<int>(v)是一个匿名对象,它用v初始化,那么它的capacity=size=v的size。再和v做交换,v的capacity就变小了,而匿名对象再用完了之后就会被直接回收。
vector:vector预留空间如何操作?为什么需要预留空间?reverse和reserve有什么不同?
reserve操作:v.reserve(100000);。
减少vector在动态扩展容量时的扩展次数。
reserve():预分配内存(容量管理);reverse:反转元素顺序(元素操作)。
vector:vector的底层实现原理是什么?
vector的底层实现是连续动态数组。它通过在堆内存中分配一块连续的空间来存储元素,所有元素紧密排列,每个元素的内存地址都是相邻的。这种实现方式带来了几个关键特性:随机访问效率极高,因为可以通过指针算术直接计算任意元素的地址;缓存友好,连续内存访问能充分利用CPU缓存预取机制。
vector:为什么vector的max_size()相对其他容器较小?
关键原因:std::vector 必须使用连续内存,而其他容器不需要!操作系统通常很难提供非常大的连续虚拟内存块!
vector:vector的shrink_to_fit是干嘛的?
shrink_to_fit() 的作用是:请求 std::vector 释放未使用的预留容量(capacity),让容器的 capacity() 减少到刚好能容纳当前元素数量(即 size()),从而减少内存占用。
vector:emplace vs insert,emplace_back vs push_back有什么区别?
核心区别:“构造时机”与“传参方式”
push_back 和 insert:
-
它们接收的是一个已经构造好的对象(或它的拷贝/移动)
-
意味着你要么:
-
传入一个临时对象(会移动,可能高效)
-
传入一个已存在的对象(会拷贝,可能低效)
-
它们做的事情是:将一个已有的对象,拷贝或移动到容器中。
emplace_back 和 emplace:
-
它们接收的是构造对象所需的参数(constructor arguments)
-
容器会直接在目标位置(比如 vector 的尾部,或你指定的位置)调用构造函数,原地构造对象
-
意味着:不需要先构造对象,再拷贝/移动,而是直接构造!
它们做的事情是:就地构造(in-place construct),避免临时对象的创建和拷贝/移动。
deque:deque是什么?deque底层实现原理是什么?
deque是双端数组,可以对头端进行插入删除操作。
deque内部有个中控器,中控器中每个节点的指针指向一个缓冲区,负责维护每段缓冲区中的内容。缓冲区是一个链表,用来存放真实数据。
中控器维护的是每个缓冲区的地址,使得使用deque时像一片连续的内存空间。
deque:deque vs vector有什么区别?
-
vector对于头部的插入删除效率低,数据量越大,效率越低。
-
deque相对而言,对头部的插入删除速度回比vector快。
-
vector访问元素时的速度会比deque快,这和两者内部实现有关。
stack:stack是什么?它具有怎样的结构?
stack是一种先进后出(First In Last Out,FlLO)的数据结构,它只有一个出口。
栈中进入数据称为——入栈push。
栈中弹出数据称为——出栈pop。
queue:queue是什么?它的底层如何实现?
Queue是一种先进先出(First In First Out,FIFO)的数据结构,它有两个出口。
队列容器允许从一端新增元素,从另一端移除元素。
队列中只有队头和队尾才可以被外界使用,因此队列不允许有遍历行为。
queue在C++标准库中的底层实现通常是基于deque(双端队列)或者list(链表)来完成的。标准库没有规定queue必须用什么容器实现,但默认情况下是用deque作为底层容器。deque是一个分段连续的内存结构,支持高效的首尾插入和删除操作,这正好符合queue先进先出(FIFO)的特性需求。
priority_queue:priority_queue是什么?它的底层如何实现?
-
一种特殊的队列,其中的每个元素都有一个优先级。
-
出队的顺序不是先进先出(FIFO),而是按照优先级来决定谁先出队。
-
通常实现为堆(heap),默认情况下是一个最大堆(Max Heap),即优先级最高的元素(最大的值)排在队首,最先出队。
-
priority_queue不提供迭代器,你无法直接访问队列中的所有元素,只能访问队首(top())或者逐个弹出。
list:【必问】list是什么?它的底层如何实现?
链表(list)是一种物理存储单元上非连续的存储结构,数据元素的逻辑顺序是通过链表中的指针链接实现的。
STL中的链表是一个双向循环链表。
List有一个重要的性质,插入操作和删除操作都不会造成原有list迭代器的失效,这在vector是不成立的。
List迭代器不支持随机访问,iterator不能+1+2+3,只支持++和--。
list:list如何优雅地使用迭代器访问元素?
使用auto iter = a.begin(); std::advance(iter, 4);获得第五个元素的迭代器。
list:C++哪些STL可以使用XX.sort,哪些可以使用sort(XX),哪些都行?哪些都不行?
-
能用
XX.sort()的:只有标准库里的std::list和std::forward_list(前者用sort()成员函数,后者也支持但一般不推荐排序),它们是链表,没有随机访问迭代器,所以不能用全局sort(XX)。 -
能用
sort(XX)的:所有支持随机访问迭代器的容器,比如std::vector、std::deque、普通数组,都可以用 全局的std::sort(XX.begin(), XX.end()),因为它们有随机访问能力。 -
两者都行的:没有,
std::list和std::forward_list没有sort(XX)这种用法,而vector、deque等没有XX.sort()成员函数。 -
两者都不行的:关联容器如
std::set、std::map,以及无序关联容器如std::unordered_set、std::unordered_map,它们内部本身就是有序(或哈希)结构,不允许直接排序,也没sort()成员或能用sort(XX)。
forward_list:为什么forward_list没法求size()?
std::forward_list 没有提供 size() 成员函数,主要是为了追求极致的性能和最小化内存开销。
std::forward_list 是一个单向链表,它每个节点只保存数据和指向下一个节点的指针,设计上非常轻量。如果要在链表中支持 O(1) 时间复杂度的 size(),就必须额外维护一个计数器,记录当前链表中有多少个元素。但这会增加每次插入、删除操作时的开销,因为每次增删都得更新这个计数器,对性能有负面影响,尤其是高频增删的场景。
相比之下,std::list(双向链表)虽然也是链表,但它提供了 size(),是因为标准委员会权衡后认为双向链表的使用场景更广泛,维护一个 size 计数器带来的便利性大于那点微小开销。
而 forward_list 更强调性能和简洁,所以标准故意没提供 size(),而是让你在需要时自己维护一个计数,或者通过遍历链表来计算元素个数(用 std::distance(begin(), end()),但这是 O(n) 操作)。
简单说,forward_list 没有 size() 是为了保持它的高效和轻量,避免维护计数带来的额外开销。
forward_list:为什么forward_list要提供before_begin()这种指向非法位置的迭代器?
虽然这个位置并不对应任何实际存储的数据节点,但它是合法的、可以用于插入操作的迭代器位置。通过这个迭代器,你可以很方便地调用 insert_after(before_begin(), value) 来在链表最前面插入一个新节点,或者用 erase_after(before_begin()) 删除第一个节点,而不需要先获取 begin() 再去操作后面的节点,避免了额外的逻辑和潜在的错误。
set:【必问】set是什么?它的底层如何实现?
所有元素都会在插入时自动被排序(从小到大)。
set/multiset属于关联式容器,底层结构是用红黑树实现。
-
set不允许容器中有重复的元素。尝试插入重复数据插了也白插。
-
multiset允许容器中有重复的元素。set插入数据的同时会返回插入结果,表示插入是否成功;multiset不会检测数据。
set:set为什么选择红黑树作为底层实现?
选择红黑树作为底层容器的主要原因在于:它既保证了元素的有序性,又提供了不慢的查找性能,非常适合需要频繁查找、插入和删除且要求元素有序的场景,比如实现排行榜、去重集合等功能。相比unordered_set哈希表实现,set虽然查找速度稍慢,但能保证元素有序排列。
set:lower_bound、upper_bound、equal_range分别是干嘛的?
对于 std::set 来说,lower_bound、upper_bound 和 equal_range 都是用来在有序集合中高效查找某个值的范围的,因为 set 本身是基于红黑树实现的,元素默认有序。
lower_bound(key) 返回一个迭代器,指向第一个不小于 key 的元素,也就是说,它找到的是集合中第一个大于或等于 key 的位置,如果 key 存在,它可能指向 key 本身,如果不存在,则指向第一个比 key 大的元素。
upper_bound(key) 返回一个迭代器,指向第一个大于 key 的元素,不管 key 是否存在,它都返回比 key 大的第一个元素的位置,所以它不会指向 key 本身。
equal_range(key) 则是更高级的封装,它一次性返回一个 pair,包含 lower_bound 和 upper_bound 的结果,也就是一个范围,这个范围里的所有元素都等于 key。在 set 里由于元素唯一,这个范围要么只有一个元素(key 存在),要么为空(key 不存在),但在 multiset 里就很有用,可以涵盖所有相等的值。
set:set使用XX.find查找有什么坑,能查到你要的值吗?find(XX)能找到吗?
XX.find:如果查找的是复杂类型,查找的索引是你自定义比较器里的方法,例如一个person类在比较的时候只按照年龄比较,那么查找的时候只要年龄一致就会返回查找结果,这时返回的结果就可能是错误的。
find(XX):在类里重载==符号,即可找到你想要的内容。
map:【必问】map是什么?它的底层如何实现?
map中所有元素都是pair。
pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)。
所有元素都会根据元素的键值自动排序。
底层结构是用红黑树实现。
map:map和set都是从小到大排序,如何改变排序规则?
利用仿函数。
map:如果在遍历一个map时,用erase删除一个元素,传入的迭代器是it++,会有问题吗,如果是vector呢?
-
erase会使当前迭代器失效 -
你传给
erase的是it++,这是“先传值,后自增” -
不同容器(如
map和vector)的迭代器失效规则不同
如果你写 myMap.erase(it++);,这种写法是安全的,也是经典用法!
如果你在遍历 vector时使用 erase(it++),虽然可能不会立即崩溃,但这是不安全、不推荐的!而且往往有严重问题!
std::vector的迭代器失效规则非常严格:
-
当你调用
erase(iterator)删除一个元素时,被删除元素处以及之后的所有迭代器都可能失效! -
包括:被删除的迭代器、以及它后面的迭代器(比如
it+1,it++后的值等)
unordered_map:【必问】unordered_map是什么?它和map从底层实现来说有什么不同?
-
哈希表实现:基于哈希表,提供平均 O(1) 复杂度的查找、插入和删除操作。
-
无序性:元素无特定顺序,遍历顺序不确定。
底层实现:
-
哈希表结构:
-
由桶(
bucket)组成的数组,每个桶存储哈希值相同的元素(通常用链表处理冲突)。 -
哈希函数:将键映射到桶索引(
hash(key) % bucket_count)。 -
冲突解决:链地址法(每个桶维护链表)、开放寻址法等(具体实现由标准库决定)。
-
-
负载因子(Load Factor):
-
定义:
load_factor = size() / bucket_count()(当前元素数 / 桶数)。 -
最大负载因子:
max_load_factor()默认约为1.0,超过时触发rehash。 -
rehash操作:增加桶数,重新分配元素(耗时操作,可能导致迭代器失效)。
-
unordered_map:unordered_map在什么场景下会触发迭代器失效?
-
插入操作:可能导致
rehash,使所有迭代器失效(若未触发rehash,仅插入的桶的迭代器可能失效)。 -
删除操作:仅影响被删除元素的迭代器。
unordered_map:unordered_map的扩容机制?
unordered_map 的扩容机制主要发生在它的负载因子(load factor)超过设定的阈值时。负载因子是当前容器中元素数量除以桶(bucket)的数量,它反映了每个桶平均存储了多少元素。
当插入新元素导致负载因子超过默认阈值(通常是 1.0,但可以手动设置)时,unordered_map 会触发重新哈希(rehash),也就是扩容。扩容过程中,容器会分配一个更大的桶数组,通常是当前桶数的两倍左右(具体倍数取决于实现,比如常见的实现会选一个接近且大于当前大小的质数),然后将所有已有的元素重新计算哈希值,并根据新的桶数量重新分配到新的桶中。
这个过程比较耗时,因为它涉及到所有元素的重新哈希和迁移,所以在元素较多时可能引起性能抖动。因此,在已知数据量较大的场景下,我们通常可以提前通过 reserve 手动指定桶的数量或者预分配空间,避免频繁的扩容操作。
简单来说,unordered_map 在元素变多、桶不够用的时候会自动扩容并重新哈希,以保证查找、插入的效率,但这个过程本身有一定开销。
unordered_map:(追问)如何使用链地址法解决哈希冲突?
C++的unordered_map使用链地址法解决冲突,每个桶维护一个链表,存储所有映射到该桶的键值对。
(1) 插入键值对
-
计算键的哈希值,确定桶索引。
-
遍历桶内链表:
-
存在相同键:更新值(若允许覆盖)。
-
无相同键:将新节点插入链表头部或尾部。
-
(2) 查找键值对
-
计算键的哈希值,定位到桶。
-
遍历链表,逐一比较键:
-
找到匹配键:返回对应值。
-
链表遍历结束未找到:返回
end()迭代器。
-
-
链表结构:通常为单链表,每个节点包含键值对和指向下一节点的指针。
-
桶动态扩展:当负载因子超过阈值时,触发
rehash,增加桶数以分散冲突。
如果unordered_map中要插入class应该重写什么函数?
-
一个哈希函数(Hash Function):用于将 key 映射成一个哈希值(通常是
size_t),以决定该键值对存储在哈希表的哪个桶(bucket)中。 -
一个相等比较函数(KeyEqual Function):用于判断两个 key 是否相等(因为哈希可能会有冲突,需要精确比较)。
array:array的底层如何实现?它和vector有什么不同?
底层实现:
静态数组封装:array 是对原生静态数组的模板类封装,本质是一个固定大小的连续内存块。
数据直接存储在对象内部(栈或全局/静态区),无动态内存分配。
内存完全连续,访问时无指针跳转。
比较:
-
array:静态数组的封装,适合固定大小、高频访问场景,性能接近原生数组。 -
vector:动态数组,尾部操作高效,但中间插入/扩容有成本。
array:std::array<int, 100> a;和std::array<int, 100> b = {};的区别?
-
当你写
std::array<int, 100> a;且 没有在全局/静态作用域 时(即在函数内部,比如 main 函数里),它执行的是默认初始化。对于std::array内部的元素(这里是 100 个int),默认初始化不会对它们进行初始化,也就是说,这些int的值是未定义的(uninitialized),它们的值是垃圾值(即内存里的随机残留值)。 -
当你使用
= {}对一个聚合类型(如std::array)进行初始化时,C++ 会对所有成员进行值初始化。对于std::array<int, 100>来说,这意味着内部的 100 个int元素都会被初始化为0。 -
因为array的默认初始化,所以他的empty永远不会返回true(除非定义size=0)。
array:为什么array的swap不是O(1)的?
它不是指针或动态分配的结构,而是一个聚合类型,直接包含所有元素在栈(或对象所在位置)上。
std::array 没有指针间接层,它直接包含了所有元素作为成员(通常是内联存储在栈上)。所以要交换两个 std::array,必须逐个元素地交换,无法“只交换指针”。因此,std::array::swap 必须交换每一个元素,时间与元素数量成正比,即 O(N)。
array:为什么array不支持clear()?
std::array 不提供 clear() 方法,是因为它是一个固定大小的、值语义的容器,它的元素是直接存储在对象内部的(通常是栈上),而不是动态分配的;并且 C++ 标准库的设计者认为:对 std::array 而言,“清空”这个概念要么没必要,要么可以通过更基础的操作实现,没有必要为它单独提供 clear() 成员函数。
bitset:bitset是什么?
-
固定大小:
std::bitset的大小在编译时确定,不能动态改变。其大小作为模板参数传入。 -
每个元素是一个 bit(位):不像
std::vector<bool>那样可能进行特殊优化,std::bitset明确地以位为单位存储数据。 -
提供丰富的位操作功能:比如按位与、或、非、异或、位移、测试某一位是否为 1、设置/重置某一位等。
-
索引顺序:
bitset的索引通常是从右向左,即:比如std::bitset<4> bs("1010")表示的是1(3) 0(2) 1(1) 0(0),bs[0]是最右边的位(最低位)。
bitset:bitset的优势在哪?为什么说平常不要使用std::vector<bool>?
std::bitset 明确、安全、语义清晰。
-
std::bitset<N>表示一个固定大小为 N 位的二进制数据,不是动态的,也不是容器语义的模拟。 -
目的明确:就是用来处理位(bit),而不是模拟布尔值的集合。
-
每一位只能是 0 或 1,不涉及复杂的代理类型或隐式转换。
至于 std::vector<bool>,关键问题:std::vector<bool> 中的每个 "bool" 其实不是一个真正的 bool 对象,而是一个代理对象(proxy object)!
-
vb[0]返回的不是bool&,而是一个临时代理对象,所以你不能取地址、不能绑定到bool&、不能用于需要真正引用的地方。 -
这破坏了 STL 容器的一致性,很多泛型代码依赖
T&,但对vector<bool>会失效。
【必问】array、vector、deque、list、set、map、unordered_map的插入删除查询的时间复杂度?
|
访问元素 |
插入元素 |
删除元素 |
查找元素 | |
|
|
O(1) |
? |
? |
O(N) |
|
|
O(1) |
尾部插入O(1)(扩容时 O(N)) 中间/头部插入O(N) |
尾部删除O(1) 中间/头部删除O(N) |
O(N) |
|
|
O(1) |
头尾插入O(1) 中间插入O(N) |
头尾删除O(1) 中间删除O(N) |
O(N) |
|
|
O(N) |
O(1) |
O(1) |
O(N) |
|
|
? |
O(log N) |
O(log N) |
O(log N) |
|
|
O(log N) |
O(log N) |
O(log N) |
O(log N) |
|
|
? |
O(1) |
O(1) |
O(1) |
|
|
O(1) |
O(1) |
O(1) |
O(1) |
2.3.4 仿函数
【必问】仿函数是什么?
重载函数调用操作符的类,其对象常称为函数对象。
函数对象使用重载的()时,行为类似函数调用,也叫仿函数。
本质:
函数对象(仿函数)是一个类,不是一个函数。
谓词是什么?
返回bool类型的仿函数称为谓词。
如果operator()接受一个参数,那么叫做一元谓词。
如果operator()接受两个参数,那么叫做二元谓词。
例如<algorithm>下的find_if();方法,find_if(v.begin(), v.end(), Pred());第三个参数就要求是一个一元谓词,在本例中Pred()是一个匿名对象。
使用函数对象,sort可以改变算法策略,变为排序规则为从大到小(或者别的规则):sort(v.begin(), v.end(), MyCompare());,MyCompare()需要是一个二元谓词。
STL有自带的仿函数吗?
有,这些仿函数所产生的对象,用法和一般函数完全相同。
使用内建函数对象,需要引入头文件#include <functional>。
2.3.5 算法
【必问】说说你知道的算法头文件、以及一些常用的算法?
算法主要是由头文件<algorithm><functional><numeric>组成。
<algorithm>是所有STL头文件中最大的一个,范围涉及到比较、 交换、查找、遍历操作、复制、修改等等。
<numeric>体积很小,只包括几个在序列上面进行简单数学运算的模板函数。
<functional>定义了一些模板类,用以声明函数对象。
<algorithm>
for_each:遍历容器,并用一个函数(仿函数)对容器的每个元素做操作。
transform:遍历容器并对其内部数据做操作后放入新容器(新容器需要提前开辟空间,不然会报错)。
find:按值查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置(如果find的类型是自定义的类,那么这个类需要重载==符号)。
find_if:按规则查找元素,找到返回指定位置迭代器,找不到返回结束迭代器位置。并用谓词限定查找规则。
binary_search:查找指定的元素,查到 返回true 否则false(序列必须是按顺序排列的)。
count:统计元素出现次数。
sort:对容器内元素进行排序,可用谓词重写排序规则。
random_shuffle:洗牌算法 指定范围内的元素随机调整次序,添加随机种子使得每次洗牌结果不同。
merge:合并两个有序的容器到一个新的有序容器。需要提前给目标容器分配空间。
reverse:反转指定范围的元素。
copy:容器内指定范围的元素拷贝到另一容器中。需要提前给目标容器分配空间。
replace:将区间内旧元素替换成新元素。
<numeric>
accumulate:计算容器元素累计总和。
fill:向容器中填充元素。
max_element、min_element:获取最大值和最小值。
对于不支持随机访问迭代器的容器,如何使用对应的算法?
容器内部会提供,所以不要用std::xxx()方法,而是用其内部的lst.xxx()。
list<int> lst; std::reverse(lst.begin(), lst.end()) // 错误 std::sort(lst.begin(), lst.end()) // 错误 lst.reverse(lst.begin(), lst.end()) // 正确 lst.sort() // 正确
2.3.6 迭代器
【必问】迭代器是什么?和指针的区别?
在C++ STL(标准模板库)中,迭代器(Iterator) 是一种抽象化的指针,用于遍历和访问容器(如 vector、list、map 等)中的元素。它是STL的核心组件之一,提供了统一的接口来操作不同容器中的元素,而无需关心底层实现细节。
迭代器类似于指针,可以指向容器中的某个元素,并支持以下操作:
-
解引用(*it):获取迭代器指向的元素。
-
递增/递减(++it、--it):移动到下一个或上一个元素。
-
比较(it1 == it2、it1 != it2):判断两个迭代器是否指向同一位置。
STL中的迭代器,++it和it++有什么区别?
-
前置递增:
++it-
含义:先递增迭代器,然后返回递增后的迭代器本身
-
语法:
++it -
特点:效率高,推荐在循环中使用
-
-
后置递增:
it++-
含义:先返回迭代器的当前值(副本),然后再递增迭代器
-
语法:
it++ -
特点:需要构造一个临时对象(副本),效率略低
-
序列式容器的迭代器都支持随机访问吗?
在 C++ 的 序列式容器(Sequence Containers) 中,并非所有容器的迭代器都支持随机访问。
std::list 和 std::forward_list(不支持随机访问)
-
std::list是双向链表,元素在内存中是非连续存储的,只能通过指针逐个遍历。它的迭代器只能进行++(前进)和--(后退)操作,不能直接计算偏移量(如it + 5是非法的)。 -
std::forward_list是单向链表,只能单向遍历(只有++操作),更不支持随机访问。
关联式容器的迭代器都不支持随机访问吗?
是的,在 C++ 标准模板库(STL)中,所有关联式容器(Associative Containers)的迭代器都不支持随机访问。它们仅支持双向迭代(Bidirectional Iteration),即只能通过 ++(前进)和 --(后退)操作遍历元素,而不能像 std::vector 或 std::deque 那样直接通过 it + n 或 it[n] 进行随机访问。
只读迭代器是什么?为什么需要只读迭代器?cbegin、cend和begin、end的区别?
只读迭代器是输入迭代器(Input Iterator)或常量迭代器(Const Iterator)的统称,具体表现为:
-
功能限制:
-
支持解引用(*it)获取元素值,但不允许通过迭代器修改元素(如 *it = new_value)。
-
-
典型实现:
-
const_iterator:容器提供的常量迭代器类型(如
vector<int>::const_iterator)。 -
const修饰的迭代器:如 const auto it = vec.begin()(C++11起需用cbegin()更明确)。
-
某些STL算法(如std::find、std::count)仅需读取数据,使用只读迭代器可避免不必要的拷贝。
2.3.7 适配器
什么是适配器?它有哪几类?
C++ STL中的适配器主要用于调整现有组件的接口,使其适应不同的需求。它分为容器适配器、迭代器适配器、函数适配器。
常见的容器适配器有哪些?
-
stack:后进先出(LIFO)结构,默认底层容器为deque,支持push()、pop()、top()等操作。 -
queue:先进先出(FIFO)结构,默认底层容器为deque,支持push()、pop()、front()等操作。 -
priority_queue:按优先级排序的队列(默认最大堆),底层容器为vector,支持push()、pop()、top(),依赖比较函数(默认std::less)。
常见的迭代器适配器有哪些?
-
反向迭代器(
reverse_iterator)。 -
插入迭代器
-
back_inserter:调用push_back(适用于vector、deque、list)。 -
front_inserter:调用push_front(适用于deque、list)。 -
inserter:在指定位置插入元素(适用于多数容器)。
-
-
流迭代器
-
istream_iterator:从输入流读取数据(如cin)。 -
ostream_iterator:向输出流写入数据(如cout)。
-
-
移动迭代器(
move_iterator)。
常见的函数适配器有哪些?
-
通用绑定器(
std::bind)-
绑定函数参数或调整参数顺序,生成新的可调用对象。
-
替代C++11之前已弃用的
bind1st和bind2nd。
-
-
函数包装器(
std::function)-
统一封装各种可调用对象(函数、Lambda、成员函数指针等)。
-
2.3.8 空间配置器
空间配置器是做什么的?
内存分配/释放:代替new/delete,管理底层内存操作。
默认实现:std::allocator<T>:标准库提供的通用分配器
自定义allocator必须实现的接口?
-
allocate、deallocate、rebind模板(用于容器内部类型转换,如list节点分配)。
说一个空间配置器的应用?
案例:自定义内存池分配器、跨容器内存共享。
-
双层级设计
-
一级配置器:
malloc_alloc直接调用malloc/free -
二级配置器:
default_alloc内存池优化(默认启用)
-
-
内存池优化
-
小块内存缓存:16~128字节区间,以8字节为间隔建立自由链表(16 free lists)
-
碎片管理:通过内存池减少频繁小块内存分配的开销
-
区块回填:释放的内存块回归链表供复用
-
更多推荐


所有评论(0)