[C++面试] RAII资源获取即初始化(重点)

一、入门

1、什么是 RAII?

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 的核心编程范式,核心思想是 ​将资源的生命周期与对象的生命周期绑定

  • 资源获取:在对象构造函数中获取资源(如内存、文件句柄、锁等)。
  • 资源释放:在对象析构函数中自动释放资源。
    这种机制通过 C++ 的作用域规则和对象析构的确定性,确保资源始终被正确管理,避免泄漏
class FileHandler {  
public:  FileHandler(const char* filename) {  file = fopen(filename, "r");  if (!file) throw std::runtime_error("文件打开失败");  }  ~FileHandler() { if (file) fclose(file); }  
private:  FILE* file;  
};  

 当 FileHandler 对象离开作用域时,析构函数自动关闭文件,无需手动调用 fclose

// 普通代码(手动管理)
int* raw_ptr = new int(10);  // 手动
// ...(可能忘记 delete 导致内存泄漏)// RAII 代码(自动管理)
std::unique_ptr<int> smart_ptr = std::make_unique<int>(10);  // 自动

二、进阶

1、​为什么 RAII 能解决异常安全问题?

RAII 通过析构函数的确定性释放资源,即使在异常发生时也能保证资源释放,从而满足异常安全的三级标准:

  • 基本保证:程序状态合法,无资源泄漏(通过 RAII 自动释放)。
  • 强保证:操作要么完全成功,要么状态不变(通过“拷贝-交换”惯用法实现)。
class Widget {  std::vector<int> data;  
public:  Widget& operator=(const Widget& rhs) {  Widget temp(rhs);  // 可能抛出异常的拷贝操作  swap(temp);        // 无异常交换(强保证)  return *this;  }  
};  
// 若拷贝失败,原对象状态不变
  • 不抛保证:承诺不抛出异常(析构函数标记为 noexcept

2、如何用 RAII 解决 std::shared_ptr 的循环引用问题

class B;  
class A {  
public:  std::shared_ptr<B> ptrB;  
};  
class B {  
public:  std::weak_ptr<A> ptrA;  // 使用 weak_ptr 打破循环  
};  

weak_ptr 通过 lock() 获取临时 shared_ptr,确保安全访问。 

循环引用指两个对象相互持有对方的 shared_ptr,导致引用计数无法归零。解决方案是将其中一个指针改为 std::weak_ptr(弱引用,不增加计数)

三、高阶

1、RAII 如何与移动语义结合优化资源管理?

移动语义(C++11 引入)允许资源所有权的转移,避免不必要的拷贝,提升性能:

  • 移动构造函数:将资源从临时对象“窃取”到新对象。
  • 移动赋值运算符:释放当前资源并接管新资源。
class Resource {  int* data;  
public:  Resource(Resource&& other) noexcept : data(other.data) {  other.data = nullptr;  // 原对象不再拥有资源  }  ~Resource() { delete[] data; }  
};  

2、 RAII 在并发编程中的典型应用是什么?

 RAII 广泛用于管理锁,确保锁的自动释放,避免死锁。如lock_guard 在离开作用域时释放锁,即使发生异常或提前返回也能保证解锁

std::mutex mtx;  
void threadSafeFunc() {  std::lock_guard<std::mutex> lock(mtx);  // 构造时加锁  // 临界区操作  
}  // 析构时自动解锁  

3、 智能指针是 RAII 技术的典型应用场景

这个见专栏后续的智能指针博客。

四、扩展:RAII 的局限性与解决方案

​1、资源生命周期与对象作用域不一致(如全局资源)

RAII 的核心是 ​对象作用域绑定资源生命周期,但全局资源(如全局配置、日志文件、数据库连接池)可能需要在程序运行期间持续存在,无法通过局部对象作用域管理。若强行用局部对象管理全局资源,可能导致资源被提前释放或重复释放。

  • 解决方案:使用 std::shared_ptr 或单例模式管理全局资源
// 频繁开关文件的性能损耗,若多个线程同时调用 logMessage,可能导致日志内容错乱或丢失。因此设计成全局的最佳// 错误示例:局部对象管理全局资源
void logMessage(const std::string& msg) {std::ofstream logFile("app.log");  // 函数结束时文件被关闭logFile << msg << std::endl; // 若 logFile << msg 抛出异常(如磁盘满),文件可能未正确关闭
}
// 后续函数调用 logMessage() 时,文件需重新打开,效率低且可能丢失日志

解决方案 1:std::shared_ptr 管理全局资源
通过共享指针的引用计数机制,确保资源在所有使用者退出后才释放 

// 全局共享的日志文件
std::shared_ptr<std::ofstream> globalLog = std::make_shared<std::ofstream>("app.log");void logMessage(const std::string& msg) {if (globalLog && globalLog->is_open()) {*globalLog << msg << std::endl;}
}
// 程序退出前全局智能指针自动析构,文件关闭

解决方案 2:单例模式
通过单例封装全局资源,确保唯一性和可控生命周期

class Logger {
public:static Logger& getInstance() {static Logger instance;  // C++11 线程安全单例return instance;}void write(const std::string& msg) {if (logFile.is_open()) logFile << msg << std::endl;}
private:std::ofstream logFile;Logger() { logFile.open("app.log"); }  // 构造函数内打开文件~Logger() { logFile.close(); }         // 程序结束时析构单例,释放资源
};

 线程安全的核心机制:Magic Static

C++11 引入了 ​Magic Static​(魔法静态)特性,明确规定:

​“如果一个线程正在初始化局部静态变量,其他并发线程必须等待该初始化完成。”​

编译器会保证instance只被初始化一次,即使多个线程同时调用getInstance也不会重复创建。

当多个线程同时调用 getInstance() 时,​只有一个线程会执行 static Logger instance 的初始化,其他线程会被阻塞,直到初始化完成。这从根本上避免了多线程下重复创建实例的问题。

延伸:传统双检锁(Double-Checked Locking)的缺陷

static Logger* getInstance() {if (!instance) {                // 第一次检查(非线程安全)lock_guard<mutex> lock(m);  // 加锁if (!instance) {            // 第二次检查instance = new Logger(); // 可能因指令重排导致问题}}return instance;
}
  • 需要显式管理锁,代码冗余且易出错。
  • 存在 ​指令重排(Reordering)风险new 操作可能先返回指针再初始化对象,导致其他线程访问未完全初始化的实例

2、构造函数中资源申请失败的处理。

若构造函数需要申请多个资源(如内存、网络连接、文件句柄),当某一步骤失败时,已分配的资源可能无法释放,导致泄漏

class DatabaseConnection {
public:DatabaseConnection() {buffer = new char[1024];          // 步骤1:分配内存if (!connectToServer()) {         // 步骤2:连接失败?// 若此处直接返回,已分配的 buffer 内存泄漏!throw std::runtime_error("连接服务器失败");}lockFile();                       // 步骤3:加锁文件}~DatabaseConnection() {delete[] buffer;disconnectFromServer();unlockFile();}
private:char* buffer;
};
  • 解决方案:构造函数内抛出异常,确保已分配资源被释放

利用 C++ ​栈展开(Stack Unwinding)​ 机制,在抛出异常时,​已构造的子对象会自动调用析构函数,释放已分配的资源

class DatabaseConnection {
public:DatabaseConnection() {buffer = new char[1024];          // 步骤1try {if (!connectToServer()) {     // 步骤2throw std::runtime_error("连接服务器失败");}lockFile();                   // 步骤3} catch (...) {delete[] buffer;  // 显式释放已分配内存(析构函数未调用)throw;            // 重新抛出异常}}~DatabaseConnection() { /* 析构函数释放所有资源 */ }
};

优化方案:将每个资源封装为独立的 RAII 对象,依赖析构链自动释放 

class MemoryBuffer {
public:MemoryBuffer(size_t size) : ptr(new char[size]) {}~MemoryBuffer() { delete[] ptr; }  // 析构时自动释放内存
private:char* ptr;
};class DatabaseConnection {MemoryBuffer buffer;  // 成员对象,构造顺序优先于宿主类NetworkConnection conn;FileLock fileLock;
public:DatabaseConnection() : buffer(1024), conn(), fileLock() {// 若 conn 或 fileLock 构造失败,buffer 仍会通过析构函数释放}
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/73570.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity粒子系统

目录 一、界面参数介绍1.主模块2.Emission 模块3.Shape 模块4.Velocity over Lifetime 模块5.Noise 模块6.Limit Velocity Over Lifetime 模块7.Inherit Velocity 模块8.Force Over Lifetime 模块9.Color Over Lifetime 模块10.Color By Speed 模块11.Size over Lifetime 模块1…

Docker-清理容器空间prune

docker system prune -a 是一个非常有用的命令&#xff0c;用于清理 Docker 系统中未使用的资源&#xff0c;包括停止的容器、未使用的网络、卷以及未被任何容器引用的镜像&#xff08;悬空镜像和所有未使用的镜像&#xff09;。以下是关于该命令的详细说明&#xff1a; 命令格…

LabVIEW远程控制通讯接口

abVIEW提供了多种远程控制与通讯接口&#xff0c;适用于不同场景下的设备交互、数据传输和系统集成。这些接口涵盖从基础的网络协议&#xff08;如TCP/IP、UDP&#xff09;到专用技术&#xff08;如DataSocket、远程面板&#xff09;&#xff0c;以及工业标准协议&#xff08;如…

LeetCode hot 100—寻找重复数

题目 给定一个包含 n 1 个整数的数组 nums &#xff0c;其数字都在 [1, n] 范围内&#xff08;包括 1 和 n&#xff09;&#xff0c;可知至少存在一个重复的整数。 假设 nums 只有 一个重复的整数 &#xff0c;返回 这个重复的数 。 你设计的解决方案必须 不修改 数组 nums…

linux - centos7 部署 redis6.0.5

事先说明 本篇文章只解决在部署redis中出现的问题&#xff0c;并没有部署redis的全过程&#xff0c;详细部署过程可以参考Linux安装部署Redis(超级详细) - 长沙大鹏 - 博客园 执行 make 命令时报错 原因&#xff1a;是因为gcc版本太低 升级gcc版本时 出现没有可用软件包 devt…

31天Python入门——第15天:日志记录

你好&#xff0c;我是安然无虞。 文章目录 日志记录python的日志记录模块创建日志处理程序并配置输出格式将日志内容输出到控制台将日志写入到文件 logging更简单的一种使用方式 日志记录 日志记录是一种重要的应用程序开发和维护技术, 它用于记录应用程序运行时的关键信息和…

AI Agent开发大全第八课-Stable Diffusion 3的本地安装全步骤

前言 就像我们前面几课所述,本系列是一门体系化的教学,它不像网上很多个别存在的单篇博客走“吃快餐”模式,而是从扎实的基础来带领大家一步步迈向AI开发高手。所以我们的AI课程设置是相当全面的,除了有牢固的基础知识外还有外面互联网上也搜不到的生产级实战。 前面讲过…

用selenium+ChromeDriver豆瓣电影 肖申克的救赎 短评爬取(pycharm 爬虫)

一、豆瓣电影 肖申克的救赎 短评urlhttps://movie.douban.com/subject/1292052/comments 二、基本知识点讲解 1. Selenium 的基本使用 Selenium 是一个用于自动化浏览器操作的库&#xff0c;常用于网页测试和爬虫。代码中使用了以下 Selenium 的核心功能&#xff1a; webdriv…

开源在线客服系统源码-前端源码加载逻辑

客服源码是使用Golang(又称Go)开发的&#xff0c;Go是Google公司开发的一种静态强类型、编译型、并发型&#xff0c;并具有垃圾回收功能的编程语言。Go 天生支持并发。好处太多就不多说了。 全源码客服系统用户&#xff0c;想要针对自己的业务&#xff0c;进行二次开发&#xf…

Oracle数据库服务器地址变更与监听配置修改完整指南

一、前言 在企业IT运维中&#xff0c;Oracle数据库服务器地址变更是常见的运维操作。本文将详细介绍如何安全、高效地完成Oracle数据库服务器地址变更及相关的监听配置修改工作&#xff0c;确保数据库服务在迁移后能够正常运行。 二、准备工作 1. 环境检查 确认新旧服务器I…

g对象在flask中主要是用来实现什么

在Flask中&#xff0c;g对象&#xff08;全称flask.g&#xff09;是一个线程局部&#xff08;thread-local&#xff09;的临时存储对象&#xff0c;主要用于在单个请求的上下文&#xff08;request context&#xff09;中共享数据。它的核心作用是为同一请求的不同处理阶段&…

工具介绍《WireShark》

Wireshark 过滤命令中符号含义详解 一、比较运算符 Wireshark 支持两种比较运算符语法&#xff1a;英文缩写&#xff08;如 eq&#xff09;和 C语言风格符号&#xff08;如 &#xff09;&#xff0c;两者功能等价。 符号&#xff08;英文缩写&#xff09;C语言风格符号含义示…

JavaScrip-模版字符串的详解

1.模版字符串的详解 1.1 模版字符串的使用方法 在ES6之前&#xff0c;如果我们想要将字符串和一些动态的变量&#xff08;标识符&#xff09;拼接到一起&#xff0c;是非常丑陋的&#xff08;ugly) ES6允许我们使用模版字符串来嵌入变量或者表达式来进行拼接 首先&#xff0c;…

STM32C011 进入停止模式和待机模式

对于STM32C011J4M3微控制器&#xff0c;你可以使用HAL库来实现进入停止模式&#xff08;Stop Mode&#xff09;和待机模式&#xff08;Standby Mode&#xff09;。下面是进入停止模式和待机模式的示例代码&#xff1a; 进入停止模式代码示例&#xff1a; #include "stm3…

海康设备http监听接收报警事件数据

http监听接收报警事件数据 海康获取设备报警事件数据两种方式&#xff1a; 1、sdk 布防监听报警事件数据&#xff08;前面文章有示例&#xff09; 2、http监听接收报警事件数据 http监听接收报警事件数据&#xff0c;服务端可以使用netty通过端口来监听获取事件数据。 WEB 端…

FastAPI 全面指南:功能解析与应用场景实践

FastAPI 全面指南&#xff1a;功能解析与应用场景实践 FastAPI 是一个现代、快速&#xff08;高性能&#xff09;的 Python Web 框架&#xff0c;用于构建 API。它基于标准 Python 类型提示&#xff0c;使用 Starlette 和 Pydantic 构建&#xff0c;提供了极高的性能并简化了开…

【STM32】编写程序控制开发板的RGB LED灯

目录 1、原理图2、文件结构3、使用寄存器模式点亮3.1、什么是寄存器3.2、寄存器开发的本质3.3、寄存器开发步骤3.4、主要源码3.4.1、main.c3.4.2、drv_gpio.h3.4.3、drv_gpio.c3.4.4、使用BSRR和BRR影子寄存器优化drv_gpio.c3.4.5、效果演示 4、使用标准库模式点亮4.1、使用标准…

MyBatis-Plus 的加载及初始化

在 Spring Boot 启动过程中&#xff0c;MyBatis-Plus 的加载和初始化涉及多个阶段的工作。这些工作包括 MyBatis-Plus 自身的配置解析、Mapper 接口的扫描与注册、SQL 语句的动态注入以及底层 MyBatis 的初始化等。以下是对整个过程的详细分析&#xff1a; 1. Spring Boot 启动…

SpringBoot中安全的设置阿里云日志SLS的accessKey

众所周知,阿里云的服务都是基于accesskeyId和accesskeySecret来进行身份鉴权的,但唯独日志因为需要写入到.xml文件里对于accesskeyId和accesskeySecret需要进行一定程度的改进,尤其是使用了jasypt进行加密的参数传递进去logback.xml更是会遇到需要对参数进行解密的问题,而官网只…

关于解决Ubuntu终端及系统字体大小的问题

在Ubuntu中调整终端和系统字体大小可以通过以下方法&#xff08;可能不仅仅只是这几种&#xff09;实现&#xff1a; 1. 调整系统字体大小 打开终端并输入以下命令&#xff0c;安装GNOME Tweaks&#xff0c;等待安装完成&#xff1a; sudo apt install gnome-tweaks 接着进行…