当前位置: 首页 > news >正文

智能指针之设计模式5

这次介绍一下如何使用适配器模式来对智能指针对象进行接口适配转换,让它可以为外界提供新的接口形式,以二维指针类型的形式来访问指针资源。

5、适配器模式

在前面的文章中介绍过,在构造智能指针对象时,并没有在构造函数中同时创建资源对象,而是在外部把一个指向资源对象的裸指针作为参数,传递给构造函数,例如:

int *ptr = new int(42);
shared_ptr<int> sptr(ptr);

这样就为资源对象的初始化带来了灵活性,除了使用new等操作符之外,还可以使用别的初始化方式,比如下面的方式是先通过fopen()函数创建了FILE指针类型的资源,然后再放入unique_ptr对象中进行管理:

FILE *file = fopen("/tmp/tmp.txt", "r"); // 分配FILE资源
unique_ptr<FILE, void(*)(FILE *)> fup(file, [](FILE *file) {fclose(file);
});

同时智能指针对象也可以使用reset()成员函数来重置资源对象,例如:

int *ptr1 = new int(42);
unique_ptr<int> uptr(ptr1);
...
int *ptr2 = new int(24);
uptr.reset(ptr2); // 重新设置新的资源

从这些例子中,我们可以发现,智能指针管理的裸指针都是一维指针类型,但是有一些场合需要使用二维指针,比如在C语言中,因为没有引用类型,一个函数在对指针做出修改时,往往需要使用二维指针作为它的参数类型,在函数内部来初始化、修改或者销毁指针参数指向的内存资源,此时,就无法直接使用智能指针进行管理了。我们看下面的例子:

void print_file(const char *path)  {char *line = nullptr;size_t len = 0;FILE *file = fopen(path, "r");// getline()可能改变line指向的内存地址 while (getline(&line, &len, file) != -1) {...// 业务处理,可能会抛出异常}free(line); // 需要手动释放内存fclose(file); // 手动关闭文件
}

这段代码是打开一个文本文件并逐行获取它的内容。看一下getline()函数,它返回文件中的一行文本内容,我们知道文件中的一行内容长度在调用前是不知道的,因此getline()函数在返回时,如果要返回的文本长度大于len,说明line指向的内存资源空间太小,getline()会先要free掉line,再使用realloc重新分配内存资源,当最后读完文件内容之后还要free这块内存资源。

代码中file指向的文件资源和line指向的内存资源都是手动释放的,为了防止在循环时发生了异常导致没有释放这些资源,可以使用智能指针来管理它们。那我们试着改成下面的代码:

    FILE *file = fopen(path, "r");unique_ptr<FILE, void(*)(FILE *)> fup(file, [](FILE *file) {fclose(file);});char *line = nullptr;unique_ptr<char, void(*)(char*)> up(line, [](char *ptr) {free(ptr);});while (getline(&line, &len, file) != -1) {...}

file可以被unique_ptr管理,但是如果getline()重新为line分配内存资源时,修改的是局部变量line,并不是智能指针对象up里面的裸指针数据,这个方案是行不通的。如果不使用局部变量,改成下面的方式,则无法编译:

    char *line = nullptr;unique_ptr<char, void(*)(char*)> up(line, [](char *ptr) {free(ptr);});while (getline(&(up.get()), &len, file) != -1) {...}

这里的问题是内存资源是使用二维指针来初始化和分配的,但是智能指针只能管理一维指针,getline()的参数需要二维指针,而unique_ptr::get()返回的是一维指针,无法对它进行取地址操作&。getline()是C标准库中的函数,不可能修改它的函数原型,而unique_ptr是C++库的类,也不能修改它的代码来提供一个能够返回二维指针的get()成员函数。因此,如果要使用unique_ptr来管理getline()输出的二维指针,只能手动进行处理,比如使用下面的方案:

void print_file(const char *path) {...unique_ptr<char, void(*)(char*)> up(nullptr, [](char *ptr) {free(ptr);});size_t len = 0;char *line = nullptr;while (getline(&line, &len, file) != -1) {up.release();up.reset(line);...}
}

在每次while循环中,都要使用reset()把line传入up中进行管理,因为getline()有可能随时会释放掉line指向的内存资源,并且为它重新分配内存资源,因此up必须先release()再reset(),否则会在reset()中释放掉内存资源,后来可能在getline()中又释放了一次,导致重复释放。

虽然能解决问题,但是方案有点啰嗦,有没有更好的方案呢?

计算机科学中的所有问题都可以通过增加一个额外的间接层来解决”,既然unique_ptr类和函数getline()都无法修改来适配对方,那就使用一个中间层来转换接口类型,把它们适配起来,这个中间层就是适配器,也就是适配器模式。适配器模式的意图是将一个模块的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而不能一起工作的那些模块可以一起工作。因此,定义一个适配器,它能够提供二维指针的操作以满足getline()的需求,同时通过它来转换unique_ptr的相关接口,把它返回的一维指针转换为二维指针,如下:

// 目标接口,即提供一个访问二维指针的接口
struct target {virtual operator char**() = 0;
};// 适配器类,继承target,同时把unique_ptr的一维指针转换为二维指针
struct adaptor : target {char *raw = nullptr;unique_ptr<char, void(*)(char*)> &up; // adaptee adaptor(unique_ptr<char, void(*)(char*)> &up) : up(up) {}virtual operator char**() override {// 取出裸指针,此处不释放内存,由外面的调用者getline()释放 raw = up.release();// 返回二维指针,由外面的调用者getline()更新它return &raw;}~adaptor() {// 放入unique_ptr进行管理if (raw != nullptr) {up.reset(raw);}}
};

当然上面的target类是一个概念上的类,用来定义一个适配接口:返回二维指针,因为在C++中有类型转换操作符,可以不用专门定义一个显式的基类来定义适配接口,在适配器adapter类中直接实现operator char**()操作符成员函数就可以了。方案修改为下面的方式,显然unique_ptr用起来就轻省多了:

void print_file(const char *path)  {FILE *file = fopen(path, "r");unique_ptr<FILE, void(*)(FILE *)> fup(file, [](FILE *file) {fclose(file);});unique_ptr<char, void(*)(char*)> up(nullptr, [](char *ptr) {free(ptr);});size_t len = 0;while (getline(adaptor(up), &len, file) != -1) {...}
}

既然getline()需要一个二维指针的参数类型,那就把unique_ptr对象转换为二维指针类型,即使用适配器模式为unique_ptr提供一个适配器,让它把unique_ptr对象转换为能够以二维指针类型的方式来访问它所管理资源的接口。这样,adaptor类就为unique_ptr间接地提供了类型转换。

为了能够管理通过二维指针分配内存资源,在C++23中也对智能指针进行了功能扩展,当然并没有直接修改std::unique_ptr和shared_ptr类,而是提供了它们的适配类std::out_ptr_t和std::inout_ptr_t,通过它们可以把智能指针对象转换为二维指针的形式,显然这是适配器模式的典型应用。因为std::out_ptr_t类和std::inout_ptr_t类都是在标准库中实现的,它比上面的adapter类实现的更简单,是直接对智能指针管理的裸指针进行的转换。

同时还提供了工厂方法:out_ptr()和inout_ptr(),分别用于创建std::out_ptr_t和std::inout_ptr_t类型的适配器对象。比如上面的例子,在C++23中可以改成下面的实现形式:

void print_file(const char *path)  {...unique_ptr<char, void(*)(char*)> up(nullptr, [](char *ptr) {free(ptr);});size_t len = 0;while (getline(inout_ptr(up), &len, file) != -1) {...}
}

相关文章:

  • Xray-安全评估工具
  • 19.TVS特性与使用注意事项
  • 学习spark-streaming收获
  • 7.14 GitHub命令行工具测试实战:从参数解析到异常处理的全链路测试方案
  • 深入Java JVM常见问题及解决方案
  • web基础+HTTP+HTML+apache
  • 回顾|Apache Cloudberry™ (Incubating) Meetup·2025 杭州站
  • 蓝桥杯Java全攻略:从零到一掌握竞赛与企业开发实战
  • 腾讯 Kuikly 正式开源,了解一下这个基于 Kotlin 的全平台框架
  • 基于esp32实现键值对存储读写c程序例程
  • MongoDB 操作全解析:从部署到安全控制的详细指南(含 emoji 趣味总结)
  • 测试基础笔记第十四天
  • 从回溯到记忆化搜索再到递推
  • 树莓派学习专题<11>:使用V4L2驱动获取摄像头数据--启动/停止数据流,数据捕获,缓存释放
  • Web前渗透
  • Win11 配置 Git 绑定 Github 账号的方法与问题汇总
  • flask返回文件的同时返回其他参数
  • 【密码学——基础理论与应用】李子臣编著 第七章 公钥密码 课后习题
  • ubuntu扩展逻辑卷并调整文件系统大小步骤
  • “RS232转Profinet,开启“变频器工业版绝绝子!”
  • 国家发改委答澎湃:力争6月底前下达2025年两重建设和中央预算内投资全部项目清单
  • 高璞任中国第一汽车集团有限公司党委常委、副总经理
  • 泽连斯基与特朗普进行简短会谈
  • 中青报:“猿辅导员工猝死”事件上热搜,是对健康职场环境的共同关切
  • 美称中美贸易谈判仍在进行中,外交部:美方不要混淆视听
  • 记录发生真相,南沙岛礁生态调查纪实片《归巢》发布