康复训练 5

  • 自定义删除器怎么用?什么时候需要?

  • 答:

  • 1.自定义删除器就是智能指针析构时调用的函数,可以在构建智能指针时传入,支持普通函数和lambda

  • 2.特殊的申请内存方式的时候,以及特殊的场景时需要

评分:6.5/10

  • 不足:

  • 可补充删除器的类型要求:对于unique_ptr,删除器是类型的一部分;对于shared_ptr,删除器通过类型擦除实现,不是类型的一部分。

  • 可补充函数对象(仿函数)也支持,并说明lambda是C++中最常用的方式。

  • 需要列举具体场景,如:管理非内存资源(文件、套接字)、使用malloc而非new、数组资源、共享资源的特殊释放逻辑等。

  • 修正:

第一层:什么是自定义删除器

"自定义删除器是智能指针在析构时调用的可调用对象(函数、函数对象、lambda),用于释放所管理的资源。默认情况下,unique_ptrshared_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创建的,需用closeclosesocket关闭。

  • 数据库连接:需用自定义断开连接函数释放。

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::vectorstd::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

  • 不足:

  • 未提及异常安全 优势:newshared_ptr构造分开可能导致内存泄漏(如果new成功但shared_ptr构造前抛出异常)。

  • 未提及一个重要的劣势 :如果有很多weak_ptr长期存在,对象内存可能延迟释放(因为控制块和对象一起分配,控制块存活时对象内存无法回收)。

  • 可以更结构化,区分"区别"、"优势"、"劣势",便于面试官快速理解。

  • 修正:

第一层:核心区别(内存分配方式)

make_shared :一次分配一块足够大的内存,同时容纳对象本身控制块(包含引用计数、弱计数、删除器等)。对象和控制块在内存中连续存放。

直接new shared_ptr:分两步:

  1. 使用new表达式为对象分配内存并构造对象。

  2. 将原始指针传入shared_ptr构造函数,此时再为控制块单独分配内存。

因此,make_shared只进行一次内存分配,而直接new需要进行两次分配。

第二层:优势(为什么通常推荐使用make_shared
  1. 性能更高 :一次内存分配比两次分配开销小,尤其在频繁创建shared_ptr时,减少内存分配次数和内存碎片。

  2. 异常安全 :直接new存在一个隐患:先new对象,再构造shared_ptr,如果在new之后、shared_ptr构造之前抛出异常(例如shared_ptr构造函数内存不足),则对象内存会泄漏。make_shared将这两步合并为原子操作,避免了这种风险。

  3. 缓存局部性更好:对象和控制块连续存放,访问对象时可能同时将控制块加载到缓存行,减少缓存未命中,提高访问速度。

  4. 代码更简洁 :避免显式new,符合现代C++"避免裸new"的准则,减少出错可能。

第三层:劣势(什么情况下不适合用make_shared
  1. 内存延迟释放 :因为对象和控制块在同一块内存上,即使所有shared_ptr都已销毁(强引用计数为0),只要还有weak_ptr指向该控制块(弱引用计数>0),整块内存就无法释放。这意味着对象内存会被延迟,直到所有weak_ptr也被销毁。如果对象很大且weak_ptr生命周期很长,可能造成不必要的内存占用。

  2. 自定义删除器make_shared不支持自定义删除器。如果需要自定义删除器,必须使用new方式传入删除器。

  3. 控制块内存开销 :在某些实现中,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密集型和需要细粒度并行的任务。

  • 多进程编程的优势:强隔离、高稳定性,适合安全性要求高、模块化强的应用(如浏览器每个标签页一个进程)。

相关推荐
0 0 02 小时前
CCF-CSP 38-4 月票发行【C++】考点:动态规划DP+矩阵快速幂
c++·算法·动态规划·矩阵快速幂
OxyTheCrack2 小时前
【C++】详细拆解std::mutex的底层原理
linux·开发语言·c++·笔记
sa100272 小时前
淘宝商品详情 API 接口开发实战:item_detail 调用、参数与 Python 示例
linux·数据库·python
sbjdhjd2 小时前
RHCE | Web 服务器与 Nginx 全栈详解
linux·nginx·http·云原生·oracle·架构·web
敲代码还房贷2 小时前
FSL6.0.7安装教程
linux·ubuntu·医学生·fsl
小云数据库服务专线2 小时前
linux awk使用
linux·运维·服务器
LuDvei3 小时前
linux TCP/UDP
linux·tcp/ip·udp
j_xxx404_3 小时前
力扣困难算法精解:串联所有单词的子串与最小覆盖子串
java·开发语言·c++·算法·leetcode·哈希算法
杰克崔3 小时前
preempt_count()、in_interrupt()等上下文判断常用函数及宏介绍
linux·运维·服务器·车载系统