-
自定义删除器怎么用?什么时候需要?
-
答:
-
1.自定义删除器就是智能指针析构时调用的函数,可以在构建智能指针时传入,支持普通函数和lambda
-
2.特殊的申请内存方式的时候,以及特殊的场景时需要
评分:6.5/10
-
不足:
-
可补充删除器的类型要求:对于
unique_ptr,删除器是类型的一部分;对于shared_ptr,删除器通过类型擦除实现,不是类型的一部分。 -
可补充函数对象(仿函数)也支持,并说明lambda是C++中最常用的方式。
-
需要列举具体场景,如:管理非内存资源(文件、套接字)、使用
malloc而非new、数组资源、共享资源的特殊释放逻辑等。 -
修正:
第一层:什么是自定义删除器
"自定义删除器是智能指针在析构时调用的可调用对象(函数、函数对象、lambda),用于释放所管理的资源。默认情况下,
unique_ptr和shared_ptr使用delete释放内存,但通过自定义删除器,我们可以定制资源的释放方式。"
第二层:如何使用自定义删除器(代码示例)
对于
unique_ptr:删除器是类型的一部分,需要在模板参数中指定。cpp
// 使用 lambda(C++11起) auto deleter = [](FILE* f) { if (f) fclose(f); }; std::unique_ptr<FILE, decltype(deleter)> ptr(fopen("test.txt", "r"), deleter); // 使用函数对象 struct FileDeleter { void operator()(FILE* f) const { if (f) fclose(f); } }; std::unique_ptr<FILE, FileDeleter> ptr(fopen("test.txt", "r"));对于
shared_ptr:删除器不是类型的一部分,可以在构造函数中传入,更灵活。cpp
// 直接传入lambda std::shared_ptr<FILE> ptr(fopen("test.txt", "r"), [](FILE* f) { fclose(f); }); // 传入函数指针 void close_file(FILE* f) { fclose(f); } std::shared_ptr<FILE> ptr(fopen("test.txt", "r"), close_file);
第三层:什么时候需要自定义删除器(具体场景)
1. 管理非内存资源
文件句柄:用
fopen打开的文件,需用fclose关闭。套接字:用
socket创建的,需用close或closesocket关闭。数据库连接:需用自定义断开连接函数释放。
2. 使用
malloc分配的内存
- 如果资源来自C语言的
malloc,不能用delete释放,需用free。cpp
auto deleter = [](void* p) { free(p); }; std::unique_ptr<void, decltype(deleter)> ptr(malloc(100), deleter);3. 管理数组资源(但更推荐用
std::vector或std::array)
- 虽然
unique_ptr<T[]>有默认删除器delete[],但如果数组来自特殊分配(如aligned_alloc),可能需要自定义。4. 需要特殊清理逻辑的资源
- 例如:需要在释放前记录日志、更新状态、或调用特定的清理API。
cpp
auto deleter = [](MyObject* obj) { std::cout << "Deleting object with ID: " << obj->id() << std::endl; delete obj; };5. 共享资源的释放
- 当多个
shared_ptr共享一个资源,但该资源不是通过new分配时(如C库返回的单例),需要确保释放函数只调用一次。6. 使用自定义内存池或分配器
- 如果对象从特定内存池分配,需调用对应的释放函数回收到池中。
第四层:注意事项(加分点)
unique_ptr的删除器类型 :因为删除器是类型的一部分,不同删除器的unique_ptr不能直接赋值或比较,除非删除器相同。
shared_ptr的删除器类型擦除 :shared_ptr的删除器通过类型擦除保存,所以不同删除器的shared_ptr可以存放在同一容器中,且赋值时不会影响类型。异常安全:自定义删除器应保证不抛出异常,否则在析构函数中抛出会导致程序终止。
空删除器优化 :对于无状态的删除器(如空lambda、无成员函数对象),
unique_ptr可以受益于空基类优化(EBO),不增加额外大小。
make_shared和直接new shared_ptr有什么区别?优势在哪?
-
答:
-
make_shared是申请一块连续的空间给管理的对象和控制块,new shared_ptr是先创建对象,然后再在构造函数里面申请空间存控制块
-
make_shared比new shared_ptr的优势
-
1.空间连续,减少内存碎片
-
2.一次操作完成,性能更好
评分:8.0/10
-
不足:
-
未提及异常安全 优势:
new和shared_ptr构造分开可能导致内存泄漏(如果new成功但shared_ptr构造前抛出异常)。 -
未提及一个重要的劣势 :如果有很多
weak_ptr长期存在,对象内存可能延迟释放(因为控制块和对象一起分配,控制块存活时对象内存无法回收)。 -
可以更结构化,区分"区别"、"优势"、"劣势",便于面试官快速理解。
-
修正:
第一层:核心区别(内存分配方式)
make_shared:一次分配一块足够大的内存,同时容纳对象本身 和控制块(包含引用计数、弱计数、删除器等)。对象和控制块在内存中连续存放。直接
new shared_ptr:分两步:
使用
new表达式为对象分配内存并构造对象。将原始指针传入
shared_ptr构造函数,此时再为控制块单独分配内存。因此,
make_shared只进行一次内存分配,而直接new需要进行两次分配。
第二层:优势(为什么通常推荐使用make_shared)
性能更高 :一次内存分配比两次分配开销小,尤其在频繁创建
shared_ptr时,减少内存分配次数和内存碎片。异常安全 :直接
new存在一个隐患:先new对象,再构造shared_ptr,如果在new之后、shared_ptr构造之前抛出异常(例如shared_ptr构造函数内存不足),则对象内存会泄漏。make_shared将这两步合并为原子操作,避免了这种风险。缓存局部性更好:对象和控制块连续存放,访问对象时可能同时将控制块加载到缓存行,减少缓存未命中,提高访问速度。
代码更简洁 :避免显式
new,符合现代C++"避免裸new"的准则,减少出错可能。
第三层:劣势(什么情况下不适合用make_shared)
内存延迟释放 :因为对象和控制块在同一块内存上,即使所有
shared_ptr都已销毁(强引用计数为0),只要还有weak_ptr指向该控制块(弱引用计数>0),整块内存就无法释放。这意味着对象内存会被延迟,直到所有weak_ptr也被销毁。如果对象很大且weak_ptr生命周期很长,可能造成不必要的内存占用。自定义删除器 :
make_shared不支持自定义删除器。如果需要自定义删除器,必须使用new方式传入删除器。控制块内存开销 :在某些实现中,
make_shared可能无法利用空基类优化(EBO)等,但通常差异很小。
第四层:选择建议
默认优先使用
make_shared,除非:
对象很大,且存在大量长期存活的
weak_ptr(需要精确控制对象释放时机)。需要自定义删除器。
构造函数是私有的(
make_shared可能需要友元)。对于数组,C++17引入了
std::make_shared<T[]>,C++20支持std::make_shared<T[N]>,也可考虑使用。
进程和线程的区别?(从资源、调度、通信、地址空间等角度)
-
答:
-
不论线程还是进程,在内核中都是以task_struct结构体的形式存在
-
进程有独立的地址空间,各个进程间相互独立互不干扰,而线程是同一个进程内的线程共享一个地址空间,全局变量,堆内存等各线程都可以访问
-
进程是操作系统资源调度的基本单位,线程是cpu资源调度的基本单位
-
进程是资源的拥有者,有独立的代码段、数据、堆栈、文件描述符表,线程是资源的使用者,不拥有资源
-
进程间通信比较复杂,需要借助管道、信号、socket、消息队列等进行,线程间通信就比较简单,可以通过全局结构体、变量的形式,但需要注意资源竞争问题
-
进程独立性高,但是开销大,线程独立性低,一个线程崩溃,同一个进程的线程都会受到影响,但轻量,开销小
评分:7.5/10
-
修正:
内核视角(共同点)
"在Linux内核中,进程和线程都用
task_struct结构体表示。线程被称为轻量级进程(LWP),它们与进程的主要区别在于是否共享某些内核资源,如内存描述符(mm_struct)、文件描述符表等。"
地址空间
进程:拥有独立的虚拟地址空间,通过页表映射到物理内存。进程间地址空间相互隔离,一个进程不能直接访问另一个进程的内存。
线程 :同一进程内的线程共享 该进程的地址空间,包括代码段、数据段、堆、共享库等。但每个线程拥有独立的栈 和线程局部存储(TLS),以及自己的程序计数器和寄存器上下文。
资源拥有与调度
进程 :是系统资源分配的基本单位。进程拥有独立的代码段、数据段、堆、栈、文件描述符表、信号处理表、当前工作目录等资源。
线程 :是CPU调度的基本单位。线程不拥有系统资源,只拥有运行必需的少量资源(如栈、寄存器、线程ID),共享所属进程的资源。
通信方式
进程间通信(IPC) :由于地址空间隔离,需要借助内核提供的机制:管道 (Pipe)、消息队列 、共享内存 (最快,但需同步)、信号 (Signal)、信号量 、套接字(Socket,可用于网络通信)等。
线程间通信 :因为共享地址空间,可以直接通过读写全局变量、堆内存、数据结构 进行通信,简单高效。但必须使用同步机制(互斥锁、条件变量、读写锁、原子操作等)来避免竞态条件和数据不一致。
健壮性与开销
健壮性:进程间相互隔离,一个进程崩溃通常不会影响其他进程(除非是父子进程依赖)。线程间共享地址空间,一个线程的非法操作(如野指针、栈溢出)可能导致整个进程崩溃。
开销:
创建销毁:进程创建需要分配独立地址空间、复制页表、初始化各种数据结构,开销较大;线程创建只需分配栈和TLS,开销小得多。
上下文切换:进程切换涉及地址空间切换(TLB刷新等),代价较高;线程切换在同一地址空间内,代价相对较低。
同步开销:线程间同步(如锁)仍有一定开销,但通常小于进程间通信。
其他补充
多线程编程的优势:资源共享、快速通信、低开销,适合I/O密集型和需要细粒度并行的任务。
多进程编程的优势:强隔离、高稳定性,适合安全性要求高、模块化强的应用(如浏览器每个标签页一个进程)。