EffectiveC++(3)(13~17)

13:以对象管理资源

以对象管理资源的两个关键想法:

  1. 获得资源后立刻放进资源管理对象内
  2. 管理对象应用析构函数确保资源被释放

考虑:

class test{
    public:
    test* createTets();     //创建一个指向对象的指针
    void f();   //用于删除使用完毕之后的对象
};

void test::f()
{
    test* newTett = createTets();
    //...
    delete newTett;
}

这样的做法看似稳妥,但是假如在...部分有return语句使函数提前结束,则资源无法释放,久而久之造成内存泄漏。

有时候也会有无我们个人的原因,忘记析构某个对象之类的,为此我们的解决方案是利用对象管理资源

如使用 shared_ptrunique_ptr.

但仍要注意,在使用shared_ptr时可能会造成的错误。

如:

#include <memory>
#include <iostream>
int main()
{
    int a = 52;
    {
        std::shared_ptr<int> test(&a);
    }
    std::cout << a;
}

在test退出程序块时会释放掉内存,因此之前定义的变量a就不能使用了,需时刻注意。

注意:shared_ptr在析构时调用的是delete而不是delete[ ],若想改变这一状况可自行传入删除器

请记住:

  1. 为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源
  2. 两个常用的RAII类为shared_ptr与unique_ptr,前者通常为较佳选择,因为其复制行为比较直观,若选择unique_ptr,复制动作会报错,但是可以通过右值来转移所有权(即std::move)

14:在资源管理类中小心copying行为

当一个RAII对象被复制,可以的选择是:

  • 禁止复制

    有些RAII对象的复制并不合理,可以使用=delete来禁止这类对象的辅助。

  • 对底层资源使用"引用计数"

    在希望保有资源,知道其最后一个使用者被销毁。这种情况下使该资源的"引用计数"递增。

    使用shared_ptr可以为一个资源管理类方便地添加一个引用计数功能,当shared_ptr析构时所做的操作不是你所想要的时候,可以为其提供一个删除其来提供合理的删除操作。

  • 复制底部资源

    使用"资源管理类"的唯一理由时确保在不再需要时被释放,此时复制资源管理对象,也应复制其所包覆的资源,即"深拷贝"。

  • 转移底部资源的拥有权

    自己编写拷贝函数,以免编译器合成的默认函数造成不符合预期的效果。

15:在资源管理类中提供对原始资源的访问

在一些API中直接指涉资源,除非永远不会使用这样的API,否则只得绕过资源管理对象而直接访问原始资源。

类shared_ptr与unique_ptr都提供给了一个函数get(),用于返回智能指针的原始指针。

而shared_ptr与unique_ptr都重载了"->"与".",它们允许隐式转换至底部原始指针。

class Investment
{
    public:
    bool isTaxFree() const;
};

Investment* createInvestment();     // factory函数
std::shared_ptr<Investment> pi1(createInvestment());
bool taxablel1 = pi1->isTaxFree();       // operator ->
std::unique_ptr<Investment> pi2(createInvestment());
bool taxablel2 = (*pi2).isTaxFree();    // 右operator*访问资源

由于有时候还是需要取得RAII对象内部的原始资源,为此可以设置一个显示get,例如:

class ResourceManage
{
public:
    int *get()
    {
        return a;
    }

private:
    int *a;
};
// 调用
ResourceManage a;
invoke(a.get());

这可能会造成大量的get调用。

或为类提供隐式转换函数

class NeedType
{};

class ResourceManage
{
public:
    operator NeedType() const
    {
        return a;
    }
private:
    NeedType a;
};
// 调用
ResourceManage a;
invoke(a);

但这可能增加出错几率,可能会造成在赋值时调用隐式转换而造成预期之外的结果。

请记住

  1. API往往要求访问原始资源,所以每一个RAII类都有提供一个获取其所管理资源的方法。
  2. 对原始资源的访问可能经由显示转换和隐式转换,一般而言显示转换比较安全,而隐式转换比较方便。

16:成对使用new和delete时要采用相同的形式

当使用new生成对象时,会:

  1. 分配内存
  2. 在此内存上调用构造函数

而当使用delete删除对象时,会:

  1. 在此内存上调用析构函数
  2. 释放此内存

然而如果对数组使用delete而非delete [ ],则会造成未定义行为。

这常常意味着只删除了单一对象,数组内的其余对象未被删除。

同样的,对单一对象调用delete [ ],也会造成未定义行为。

因此如调用new时使用了[ ],则调用delete时也需要使用[ ],如果调用new时未使用[ ],则调用delete时也不该使用[ ]

此外,考虑

typedef std::string AddressLines[4];
std::string* pal = new AddressLines;

则删除pal时必须使用[ ]:

delete pal;	//未定义
delete [] pal; // 好

因此,最好不要对数组形式使用typedef。

17:以独立语句将newed对象置入智能指针

考虑:

int priority();
void a(sharded_ptr<Obj> pw, int p);
a(new Obj, priority());		// 无法通过编译,因为shared_ptr需要指针的构造函数是一个explicit函数
a(shared_ptr<obj>(new obj), priority());	// 虽然通过编译,但是可能造成资源泄漏

为什么第二种方式可能会资源泄漏呢?

在以第二种形式调用时,需要做以下三件事:

  • 调用priority
  • 执行"new obj"
  • 调用shared_ptr构造函数

但是由于C++编译器的自由性,编译器优化后可能会产生这样的顺序:

  • 执行"new obj"
  • 调用priority
  • 调用shared_ptr构造函数

假如在调用priority时抛出异常,则new obj返回的指针不会被置入shared_ptr中,从而造成资源泄漏。

解决该问题的办法就是将new语句分开来写。

shared_ptr<obj> pw(new obj);
a(pw, priority());

请记住

  • 以独立语句将newed对象存储入智能指针内,如果不这样做,一旦一场被抛出,则有可能产生难以察觉的资源泄漏。
posted @ 2022-06-02 17:35  帝皇の惊  阅读(31)  评论(0)    收藏  举报