RAII惯用法:面试官问“你怎么保证资源不泄漏”,我说new/delete配对然后被教育了
2026/7/16 4:52:33 网站建设 项目流程

阶段一C++基础完结,从今天开始进入阶段二:C++进阶与实战。

先聊一个贯穿整个C++资源管理的核心思想——RAII(Resource Acquisition Is Initialization)。

面试的时候,面试官问"你怎么保证资源不泄漏?"我说"new和delete配对使用"。面试官摇摇头说"如果中间抛异常了呢?如果函数有多个return呢?"

我当时没答上来。后来才明白,RAII才是C++管理资源的正解。

RAII的核心思想

RAII的全称是"资源获取即初始化"。说白了就是:把资源的生命周期绑定到对象的生命周期上。

构造函数里获取资源,析构函数里释放资源。C++保证局部对象的析构函数一定会被调用——不管是正常退出、提前return、还是异常导致的栈展开。

// 不用RAII:容易泄漏 void processFile() { FILE* f = fopen("data.txt", "r"); if (!f) return; char* buffer = new char[1024]; if (!readData(f, buffer)) { // 忘了delete buffer就return了——泄漏! return; } delete[] buffer; fclose(f); } // 用RAII:不可能泄漏 void processFile() { std::ifstream file("data.txt"); // 构造时打开文件 if (!file) return; std::vector<char> buffer(1024); // 构造时分配内存 file.read(buffer.data(), 1024); // file和buffer的析构函数自动释放资源 // 不管函数怎么退出,都不会泄漏 }

这就是RAII的威力。你不需要手动管理资源,对象的生命周期就是资源的生命周期。

RAII不只是管理内存

很多人以为RAII只管内存。其实RAII可以管理任何资源:文件句柄、互斥锁、网络socket、数据库连接、硬件设备。

之前讲过的lock_guard就是RAII的典型应用:

std::mutex mtx; void criticalSection() { std::lock_guard<std::mutex> lock(mtx); // 构造时加锁 // ... 访问共享数据 // 析构时自动解锁,不管怎么退出 }

在机器人开发里,RAII的应用场景更多:

// 硬件设备的RAII封装 class LidarHandle { int fd_; public: LidarHandle(const char* device) { fd_ = open(device, O_RDWR); // 构造时打开设备 if (fd_ < 0) throw std::runtime_error("Cannot open lidar"); } ~LidarHandle() { if (fd_ >= 0) close(fd_); // 析构时关闭设备 } // 禁止拷贝 LidarHandle(const LidarHandle&) = delete; LidarHandle& operator=(const LidarHandle&) = delete; }; // 使用 void readLidar() { LidarHandle lidar("/dev/ttyUSB0"); // 使用lidar读取数据 // 函数退出时自动关闭设备 // 即使抛异常也不会忘记close }

自定义RAII包装器

有时候标准库没有你需要的RAII类型,需要自己写。一个通用的模式是用unique_ptr加自定义删除器:

// 用unique_ptr包装C语言的资源 auto fileDeleter = [](FILE* f) { if (f) fclose(f); }; std::unique_ptr<FILE, decltype(fileDeleter)> file( fopen("data.txt", "r"), fileDeleter); // 包装OpenCV的Mat(虽然OpenCV自己已经管理了) // 包装FFmpeg的上下文 auto ctxDeleter = [](AVFormatContext* ctx) { avformat_close_input(&ctx); }; std::unique_ptr<AVFormatContext, decltype(ctxDeleter)> fmtCtx( nullptr, ctxDeleter);

这种写法的好处是不需要定义新类,一行代码就能包装任何C语言的资源。

更正式的做法是写一个RAII类:

class ScopedTimer { std::string label_; std::chrono::steady_clock::time_point start_; public: ScopedTimer(const std::string& label) : label_(label), start_(std::chrono::steady_clock::now()) {} ~ScopedTimer() { auto end = std::chrono::steady_clock::now(); auto ms = std::chrono::duration_cast<std::chrono::milliseconds>( end - start_).count(); std::cout << label_ << " took " << ms << "ms" << endl; } }; // 使用:测量代码块的执行时间 void processFrame() { ScopedTimer timer("processFrame"); // ... 处理一帧数据 // 函数退出时自动打印耗时 }

这个ScopedTimer在机器人开发里特别实用。调试性能问题的时候,在关键函数里加一行ScopedTimer,就能知道每个模块花了多少时间。

面试中的RAII考点

"RAII的核心原理是什么?"利用C++的栈展开机制——局部对象的析构函数在离开作用域时一定会被调用。把资源释放放在析构函数里,就保证了资源一定会被释放。

"RAII和垃圾回收有什么区别?"垃圾回收(如Java的GC)是运行时自动回收不再使用的内存,但不确定性——你不知道GC什么时候运行。RAII是确定性的——对象离开作用域就立刻释放,时间点是确定的。对于硬件设备、文件句柄这种需要及时释放的资源,RAII比GC更可靠。

"如果析构函数抛出异常会怎样?"这是C++的大忌。析构函数不应该抛异常。如果析构函数在栈展开过程中抛异常,程序会直接terminate。所以RAII类的析构函数要做异常安全处理——要么catch住异常,要么标记为noexcept。

再聊一个面试中经常被追问的点:RAII在机器人硬件资源管理中的应用。除了内存,机器人开发里还有很多需要RAII管理的资源——串口连接、Socket通信、GPU显存、DMA通道等等。我之前做一个激光雷达驱动时,用RAII封装了串口资源:构造函数打开串口并配置波特率,析构函数关闭串口释放文件描述符。这样即使初始化过程中抛出异常,串口也一定会被正确关闭。面试时如果你能举出这种非内存资源的RAII应用案例,面试官会觉得你不只是理解概念,而是真正在项目中实践过。另外还有个技巧:用std::unique_ptr配合自定义删除器,可以给C语言的资源句柄加上RAII语义,比如std::unique_ptr<FILE, decltype(&fclose)>来管理文件指针。

给正在准备面试的你

RAII是C++最重要的惯用法之一,没有之一。它把资源管理和对象生命周期绑定在一起,从根本上杜绝了资源泄漏。

面试的时候,能讲清楚RAII的原理,再举出lock_guard、ifstream、unique_ptr这些标准库的例子,说明你理解了C++的设计哲学。如果能结合项目经验,自己写一个RAII包装器给面试官看,那面试官会觉得你的C++功底相当扎实。

下篇聊Rule of Three/Five/Zero——类设计的黄金法则,和RAII有着非常直接的关系。


如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。 你的支持是我持续更新的最大动力。

「机器人软件开发面试·从入门到精通」连载系列上一篇:第40篇 C++并发编程入门——thread、mutex、atomic与机器人多线程场景 下一篇预告:第42篇 Rule of Three/Five/Zero——类设计的黄金法则

有任何问题欢迎评论区留言,我会尽量回复。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询