《Effective C++》《资源管理——13、以对象管理资源》

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原则在资源管理中的重要性主要体现在以下几个方面:

  1. 自动资源管理:通过将对象的生命周期与资源的生命周期绑定,可以确保资源在不再需要时得到自动释放,从而防止资源泄漏。

  2. 异常安全性:在复杂的程序中,异常处理是一个重要的部分。RAII确保即使在发生异常时,资源也能被正确释放,因为对象的析构函数总是会被调用。

  3. 代码简洁性:通过使用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)

  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值