C++: RAII编程实战
对于很多人来说,提到C/C++第一印象就是指针,而对于指针的印象,很多人提到最最多的就是内存泄漏,即申请了内存而忘记了释放。针对这一点,自C++11起引入了智能指针,那么智能指针是如何保证或者避免内存泄漏的呢?
我们以经常用到的std::shared_ptr为例,在面试候选人的时候,我也会问相关问题,很多人都会提到引用计数为0的时候会自动释放该指针所关联的内存资源:
int main() {
std::shared_ptr<int> p = std::make_shared<int>();
}也就是说在智能指针p离开main函数作用域的时候会自动释放资源,这种实现技术,就是我们本文要讲的RAII。
概念
RAII即Resource Acquisition Is Initialization的缩写,如果用中午来翻译则是资源获取即初始化。现代 C++ 编程中的一个非常重要的概念,它确保了资源管理既安全又高效,同时也很直观,其核心思想是将必须显式获取和释放的资源的生命周期与对象的生命周期绑定在一起。这个概念在编写稳健的 C++ 应用程序时特别重要,尤其是当你需要处理像内存、文件句柄、网络连接这些需要手动释放的资源时。
当一个对象被创建时,它会获取某些资源(比如打开文件、分配内存等);而当对象超出作用域时,它的析构函数会自动被调用,从而释放这些资源。这种方法有助于防止资源泄漏,确保程序的异常安全,同时使代码更加易读和易维护。
应用
在C++中,RAII 使用构造函数和析构函数实现。构造函数获取资源,相应的析构函数释放资源。
下面,我们从几个例子来聊聊RAII的用法。
未使用RAII
让我们考虑一个类似的例子,其中一个资源是动态分配的,但是函数中有几个路径可能会导致资源无法正确释放,从而导致内存泄漏。
struct Resource {
int x_;
int y_;
Resource(int x, int y) : x_(x), y_(y) {}
}
void fun(bool flag) {
auto &&ptr = newResource(1, 2);
if (!flag) {
return;
}
// do sth
delete ptr;
}在这个代码中:
- 通过new方式创建一个Resource指针,并将其赋值给ptr
- 该函数参数为一个bool类型
- 如果用户输入参数为false,则直接返回
- 如果输入参数为true,则继续后面的逻辑,到最后通过delete方式释放资源
这段代码,很明显存在内存泄漏,如果想要避免的,可以使用我们前面提到的std::shared_ptr:
struct Resource {
int x_;
int y_;
Resource(int x, int y) : x_(x), y_(y) {}
}
void fun(bool flag) {
auto &&ptr = std::make_shared<Resource>(1, 2);
if (!flag) {
return;
}
// do sth
}原理也是比较简单,不再赘述~
从这个例子可以看出,如果 不使用RAII,而单纯人为去释放资源,难免会有遗漏之处,如果此函数调用频繁,泄漏会很严重~
内存管理
下面是一个使用了RAII(基于对象本身)的例子:
class MyArray {
public :
MyArray ( size_t size ) : data ( new int [size] ) {}
~ MyArray () { delete [] data_; }
private :
int * data_;
}这个例子,算是一个非常非常常用的基于RAII的例子,在这个例子中,我们在构造函数中申请资源,在析构函数中释放资源。当MyArray对象超出其作用域时候,会自动调用析构函数,进而释放资源。
socket
除了管理我们通常所说的内存外,也可以用来socket。
class Socket {
public:
Socket(int domain, int type, int protocol) {
sockfd = socket(domain, type, protocol);
if (sockfd == -1) {
std::cerr << "Socket creation failed!" << std::endl;
} else {
std::cout << "Socket created successfully!" << std::endl;
}
}
~Socket() {
if (sockfd != -1) {
close(sockfd); // 关闭 socket
std::cout << "Socket closed!" << std::endl;
}
}
private:
int sockfd = -1; // socket 文件描述符
};在这个示例中,构造函数创建一个socket,在析构函数中关闭此描述符。当Socket对象退出 作用域时候,自动调用析构函数,进而关闭描述符。
文件管理
下面是一个创建临时文件的类:
class TempFile {
public:
TempFile(const std::string& name) : filename(name) {
std::ofstream temp(filename_);
temp << "Temporary content";
}
~TempFile() {
std::remove(filename_.c_str());
}
private:
std::string filename_;
};在这个例子中,我们使用 RAII(资源获取即初始化)来管理一个临时文件。TempFile 类会在其构造函数中创建一个临时文件,并向其中写入一些内容;当 TempFile 对象的生命周期结束时,它的析构函数会自动删除该文件。这样,即使程序结束或者发生异常,临时文件也会被正确地删除,不会留下任何多余的文件。
结语
RAII(资源获取即初始化)是 C++ 中一个强大的概念,它简化了资源管理,防止内存泄漏,并确保程序的异常安全。通过将资源管理与对象的生命周期绑定,RAII 使得代码更加直观且不容易出错。作为一种最佳实践,我们应当在设计中充分利用 RAII,借助语言的构造函数和析构函数来高效地管理资源。