文章目录
- 1、Terms13:Use objects to manage resources
- 1.1、普通指针进行资源管理存在的问题
- 1.2、RALL
- 1.3、没有针对“动态分配数组”而设计的智能指针
- 2、面试相关
- 2.1 解释RAII原则,并给出它在资源管理中的重要性。举出一个可以编译的例子
- 2.2 描述一个你曾经使用RAII原则解决的实际问题。
- 2.3 描述一个可能导致资源泄漏的场景,并解释如何使用RAII原则来避免它
- 3、总结
- 4、参考
1、Terms13:Use objects to manage resources
所谓资源就是,一旦使用了它,将来必须还给系统,如果不这样,糟糕的事情就会发送。
1.1、普通指针进行资源管理存在的问题
例如我们使用一个用来塑模投资行为(例如股票,债券等等)的程序库,其中各式各样的投资类型继承于一个基类Investment:
class Investment { ... };
进一步假设,这个程序库通过一个工厂函数(条款7)供应我们获得某特定的Investment对象:
Investment* createInvestment(); //返回一个Investment继承体系中的动态分配对象
如果我们在某作用域内调用这个函数返回的对象,那么使用完这个对象之后要复制删除这个对象。
void f()
{Investment* pInv = createInvestment();//...delete pInv;
}
这种程序设计的缺陷主要在于我们可能无法释放获取的pInv对象:
(1)如果在delete之前有return语句导致函数执行结束,那么对象就无法释放;
(2)如果在delete之前程序抛出异常,那么也无法释放对象;
(3)如果这段代码在之后软件开发维护过程中被修改,那么后人可能无法知道要释放这个pInv对象,因为单纯的靠函数f中的delete语句来释放对象是行不通的。
1.2、RALL
那么为确保资源被释放,我们应该:把资源放进对象中,我们可依赖 C++ 的“析构函数自动调用机制”确保资源被释放。
以对象管理资源的思想:
(1)获得资源后立刻放进管理对象内:获得资源之后将其封装到类中,例如shared_ptr等智能指针。实际上“以对象管理资源”的观念常被称为“资源获得时机便是初始化时机”(Resource Acquisition Is Initialization,RAII)
(2)管理对象运用析构函数确保资源被释放:当离开作用域之后,对象可以调用析构函数自动的释放资源,而无须我们手动释放。但是如果析构函数抛出异常,可能需要自己手动处理(析构函数异常处理可以参阅条款8)
C++程序库提供了两种类,更加安全的管理自己的资源,分别是:shared_ptr和auto_ptr现在它已经被unique_ptr替代。
auto_ptr:
auto_ptr是个智能指针,其析构函数自动对其所指对象调用delete
如:我们修改函数f,并利用auto_ptr获得对象,在函数作用域结束之后,资源自动释放
void f()
{//调用函数获得对象std::auto_ptr<Investment> pInv(createInvestment());...
}//函数执行完之后,auot_ptr的析构函数自动删除pInv
以对象管理资源的想法:
- 获取资源后立刻放进管理对象。
- 管理对象运用析构函数确保资源被释放。
auto_ptr对象的唯一性:auto_ptr只保存自己管理对象的一份副本,因此当auto_ptr被赋值或复制时,就会将自己的资源管理权转交给它人,从而是自己变为null
//获得一个对象,并管理该对象
std::auto_ptr<Investment> pInv1(createInvestment());//拷贝pInv1,此时pInv1被设为null,现在pInv2管理这个资源
std::auto_ptr<Investment> pInv2(pInv1);//赋值操作,此时pInv2变为null,现在pInv1管理这个资源
pInv1 = pInv2;
shared_ptr:
shared_ptr也是智能指针,但是其与autp_ptr不同,其是“引用计数型智能指针”(RCSP),也就是多个shared_ptr可以同时指向一个管理对象。
1.3、没有针对“动态分配数组”而设计的智能指针
注意:auto_ptr和shared_ptr析构函数中做释放资源使用的是delete而不是delete [],因此将动态数组绑定于智能指针对象上是不行的,但是数组是可以与智能指针使用的。
//都是错误的
std::auto_ptr<std::string> aps(new std::string[10]);
std::shared_ptr<int> aps(new int[1024]);
C++没有为动态分配数组而设计的智能指针类,是因为C++已经有了vector和string这样的管理类,这些类已经够我们使用了,因此提供动态分配数组管理的类是多余的
但是Boost库中的boost::scoped_array和boost::shared_array类是接近于为数组而设计的类,因此你们想要了解的话,可以参阅这两个类。
2、面试相关
2.1 解释RAII原则,并给出它在资源管理中的重要性。举出一个可以编译的例子
RAII(Resource Acquisition Is Initialization)原则是一种编程技术,用于在C++程序中管理资源。这个原则强调资源的获取(Acquisition)应该与对象的初始化(Initialization)紧密绑定,以确保资源能够在使用完毕后被正确地释放。简单来说,当一个对象被创建(即初始化)时,它会获取必要的资源,并在其生命周期结束时(即析构时)自动释放这些资源。
RAII原则在资源管理中的重要性主要体现在以下几个方面:
-
自动资源管理:通过将对象的生命周期与资源的生命周期绑定,可以确保资源在不再需要时得到自动释放,从而防止资源泄漏。
-
异常安全性:在复杂的程序中,异常处理是一个重要的部分。RAII确保即使在发生异常时,资源也能被正确释放,因为对象的析构函数总是会被调用。
-
代码简洁性:通过使用RAII,程序员无需显式地管理资源的释放,这使得代码更加简洁,更易于阅读和维护。
下面是一个简单的C++例子,展示了如何使用RAII原则管理动态分配的内存:
#include <iostream>// 自定义一个简单的动态数组类,用于演示RAII
class DynamicArray {
private:int* data;size_t size;public:// 构造函数:获取资源(分配内存)DynamicArray(size_t sz) : size(sz) {data = new int[size];std::cout << "Memory allocated for " << size << " integers." << std::endl;}// 析构函数:释放资源(释放内存)~DynamicArray() {delete[] data;std::cout << "Memory released." << std::endl;}// 禁止拷贝构造和拷贝赋值,以避免资源管理的复杂性DynamicArray(const DynamicArray&) = delete;DynamicArray& operator=(const DynamicArray&) = delete;// 一个简单的函数来设置数组中的值void setValue(size_t index, int value) {if (index < size) {data[index] = value;}}// 打印数组内容void printArray() const {for (size_t i = 0; i < size; ++i) {std::cout << data[i] << ' ';}std::cout << std::endl;}
};int main() {// 创建DynamicArray对象,自动分配内存{DynamicArray arr(5);// 使用资源for (size_t i = 0; i < 5; ++i) {arr.setValue(i, static_cast<int>(i));}arr.printArray();// 离开作用域时,DynamicArray的析构函数会被自动调用,从而释放内存}// 在此处,内存已经被自动释放std::cout << "After the block, memory has been released." << std::endl;return 0;
}
在上面的例子中,DynamicArray
类在构造时分配了一块内存来存储整数,并在析构时释放了这块内存。这就确保了无论何时DynamicArray
对象离开其作用域,分配的内存都会被正确释放。这遵循了RAII原则,将资源的生命周期与DynamicArray
对象的生命周期绑定在了一起。
2.2 描述一个你曾经使用RAII原则解决的实际问题。
在过去的一个项目中,我负责开发一个网络通讯模块,该模块需要与远程服务器建立连接,并进行数据的收发。在这个项目中,我遇到了一个问题:如何确保网络连接在不再需要时被正确关闭,以避免资源泄漏和其他潜在的网络问题。
为了解决这个问题,我使用了RAII原则。我创建了一个名为NetworkConnection
的类,它负责管理与远程服务器的连接。在NetworkConnection
的构造函数中,我建立了与远程服务器的连接,并在析构函数中关闭了该连接。这样,当NetworkConnection
对象离开其作用域时,连接会自动关闭。
下面是一个简化的示例代码,展示了如何使用RAII原则管理网络连接:
#include <iostream>class NetworkConnection {
private:// 假设有一个用于表示网络连接的句柄或指针void* connection_handle;public:// 构造函数:建立网络连接NetworkConnection(const std::string& server_address) {std::cout << "Connecting to server at " << server_address << std::endl;// 假设这里进行了网络连接的建立操作connection_handle = /* 建立连接的代码 */;}// 析构函数:关闭网络连接~NetworkConnection() {std::cout << "Closing network connection..." << std::endl;// 假设这里进行了网络连接的关闭操作/* 关闭连接的代码 */;}// 其他成员函数,用于数据的收发等操作void sendData(const std::string& data) {// 发送数据的代码}std::string receiveData() {// 接收数据的代码return "received data";}
};int main() {// 使用RAII原则管理网络连接{NetworkConnection conn("127.0.0.1:8080");conn.sendData("Hello, server!");std::string response = conn.receiveData();std::cout << "Received: " << response << std::endl;// 当conn对象离开作用域时,其析构函数会被自动调用,从而关闭网络连接}// 在此处,网络连接已经被自动关闭std::cout << "Network connection has been closed." << std::endl;return 0;
}
通过使用RAII原则,我确保了网络连接在不再需要时能够被正确关闭,从而避免了资源泄漏和潜在的网络问题。这种方法使得代码更加简洁、可读,并且减少了出错的可能性。同时,它也使得资源管理的责任更加明确,提高了代码的可维护性。在实际项目中,这种基于RAII的资源管理方法被广泛应用于文件句柄、数据库连接、锁等各种资源的管理中。
2.3 描述一个可能导致资源泄漏的场景,并解释如何使用RAII原则来避免它
一个常见的可能导致资源泄漏的场景是文件操作。当我们在程序中打开文件进行读写操作时,如果忘记在操作完成后关闭文件,就会导致文件句柄一直被占用,造成资源泄漏。这种情况在复杂的程序中尤其容易发生,特别是当文件操作分散在多个函数或类中时。
为了避免这种情况,我们可以使用RAII原则来管理文件资源。具体做法是创建一个封装了文件操作的类,将文件句柄的打开和关闭与该类的构造函数和析构函数绑定。这样,当创建该类的对象时,文件会自动打开,当对象离开作用域或被销毁时,文件会自动关闭。
以下是一个简单的示例,展示了如何使用RAII原则来管理文件资源:
#include <fstream>
#include <iostream>
#include <stdexcept>class FileManager {
private:std::fstream fileStream;std::string fileName;public:// 构造函数:打开文件FileManager(const std::string& name) : fileName(name) {fileStream.open(fileName, std::fstream::in | std::fstream::out);if (!fileStream.is_open()) {throw std::runtime_error("Unable to open file: " + fileName);}std::cout << "File " << fileName << " opened." << std::endl;}// 析构函数:关闭文件~FileManager() {fileStream.close();std::cout << "File " << fileName << " closed." << std::endl;}// 其他成员函数,用于文件的读写等操作void writeToFile(const std::string& data) {fileStream << data;}std::string readFromFile() {std::string data;std::getline(fileStream, data);return data;}
};int main() {try {// 使用RAII原则管理文件资源{FileManager file("example.txt");file.writeToFile("Hello, World!");std::string content = file.readFromFile();std::cout << "Read from file: " << content << std::endl;// 当file对象离开作用域时,其析构函数会被自动调用,从而关闭文件}// 在此处,文件已经被自动关闭} catch (const std::exception& e) {std::cerr << "Exception: " << e.what() << std::endl;}return 0;
}
在这个示例中,我们创建了一个FileManager
类,它在构造函数中打开文件,在析构函数中关闭文件。这样,无论何时FileManager
对象离开其作用域或被销毁,文件都会被自动关闭,从而避免了资源泄漏。这种方法不仅简化了资源管理,还提高了代码的健壮性和可读性。
3、总结
天堂有路你不走,地狱无门你自来。
4、参考
4.1 《Effective C++》
4.2 Effective C++条款13:以对象管理资源(Use objects to manage resources)