Effective C++ 《资源管理》 — RAII思想

以对象管理资源

  • 为防止资源泄漏,使用RAII思想构建资源管理类,在构造函数中获取资源,在析构函数中释放资源
  • 可以使用智能指针管理资源

手动管理资源容易造成泄漏

struct Payment {
    Payment() {
        printf("Construct Payment\n");
    }

    ~Payment() {
        printf("Deconstruct Payment\n");
    }
};

static Payment *getPayment() {
    static Payment *instance = new Payment;
    return instance;
}

int main() {
    Payment *p1 = getPayment();
    Payment *p2 = getPayment();
    cout << p1 << " " << p2 << endl;
}
/*
Construct Payment
0x1b40b081910 0x1b40b081910
*/

上述通过工厂函数获取到对象指针,但是存在问题

  • 容易忘记delete
  • 容易重复delete同一块地址(可见,这里delete了p1,那么p2呢?)

通过智能指针等RAII类来管理资源

智能指针对heap-based对象有奇效

  • 获得资源后立即放进管理对象内。即RAII:Resource Acquisition Is Initialization(资源获取即初始化)。
  • 管理对象使用析构函数来释放资源。即离开作用域后就释放资源
int main() {
    shared_ptr<Payment> ptr(getPayment());
    {
        auto ptr2 = ptr;
        cout << ptr.use_count() << endl;
    }
    cout << ptr.use_count() << endl;
}
/*
Construct Payment
2
1
Deconstruct Payment
*/

但是要注意避免循环引用,造成资源泄漏

class B;

struct A {
    shared_ptr<B> ptr;

    ~A() {
        printf("~A\n");
    }
};

struct B {
    shared_ptr<A> ptr;

    ~B() {
        printf("~B\n");
    }
};

int main() {
    shared_ptr<A> pA(new A);
    shared_ptr<B> pB(new B);
    pA->ptr = pB;
    pB->ptr = pA;
}
// 没有析构函数被调用

解决办法: 将类成员的shared_ptr改用为弱引用指针weak_ptr即可

注意智能指针的默认析构方法是delete

智能指针默认的析构方法是delete,所以以下函数可以正确编译,但是不能正确释放。
shared_ptr<int> p(new int[3]);
解决办法: 自定义释放器

struct A {
    static int v;
    int selfV;

    A() : selfV(v++) {}

    ~A() {
        printf("~A %d\n", selfV);
    }
};

int A::v = 0;

void deleter(A *a) {
    printf("call deleter B\n");
    delete[]a;

}

int main() {
    A *a = new A[2];
    shared_ptr<A> ptr(a, [](A *toD) {
        printf("call deleter A\n");
        delete[]toD;
    });

    shared_ptr<A> p2(new A[2], deleter);
}
/*
call deleter B
~A 3
~A 2
call deleter A
~A 1
~A 0
*/

在资源管理类中小心copy行为

  • 拷贝RAII对象必须考虑其管理的资源,针对其资源做出拷贝行为的实现
  • 常见的RAII对象拷贝行为:拒绝拷贝、引用计数法、深拷贝、资源所有权转移

并非所有资源都是基于堆的(heap-based),对于这种对象不能直接使用智能指针,需要自定义其资源管理类。
例子:锁
为了说明锁的资源管理行为,我们这里给定义一个锁,来替代C++里的锁

struct MyMutex {
    MyMutex() {
        printf("Construct MyMutex\n");
    }

    ~MyMutex() {
        printf("Deconstruct MyMutex\n");
    }
};

其上锁解锁行为:

void lock(MyMutex *) {
    printf("lock\n");
}

void unlock(MyMutex *) {
    printf("unlock\n");
}

锁的资源管理类,在构造函数获取资源(加锁),在析构函数释放资源(解锁):

struct Lock {
private:
    MyMutex *myMutex;
public:
    explicit Lock(MyMutex *mutex) : myMutex(mutex) {
        lock(myMutex);
    }

    ~Lock() {
        unlock(myMutex);
    }
};

使用:

int main() {
    MyMutex myMutex;
    {
        printf("---------\n");
        Lock lk(&myMutex);
        printf("---------\n");
        // 离开代码块将自动析构局部对象,因此会释放锁
    }
}
/*
Construct MyMutex
---------
lock
---------
unlock
Deconstruct MyMutex
*/

潜在风险,如果发生了拷贝行为:

Lock l1(&mutex);
Lock l2(l1);

那么将立即死锁(Linux里一般是非递归锁,重复加锁会造成死锁)

禁止复制

继承nocopyable,或者将拷贝相关函数设置为delete

对底层资源使用引用计数法

思想:维护一个计数器,当最后一个使用者被销毁时,才真正释放资源

  • 维护计数器
  • 使用智能指针管理资源
    注意:智能指针释放资源的默认行为是delete,但是这里我们是想要解锁即可,不必将锁释放,这个时候就可以用到智能指针的自定义删除器Deleter
struct Lock {
private:
    shared_ptr<MyMutex> mutexPtr;
public:
	// 将unlock函数设置为删除器
    explicit Lock(MyMutex *mutex) : mutexPtr(mutex, unlock) {
        lock(mutexPtr.get());
    }
    // 不必声明析构函数,因为mutexPtr是栈上对象,所以会被默认释放,那么智能指针就会调用其释放器unlock
};

复制底部资源(深拷贝)或者转移资源管理权(移动语义)

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

  • API常需要要求访问原始资源,所以RAII资源管理类应该提供访问原始资源的接口
  • 对原始资源可以由显示转换或者隐式转换获得.其在安全性和方便性上各有取舍.

智能指针提供了get接口来访问原始资源

在其中要注意,不可以get一个智能指针去初始化另一个智能指针,否则会发生重复释放

int main() {
    shared_ptr<MyMutex> p1 = make_shared<MyMutex>();
    {
        shared_ptr<MyMutex> p2(p1.get());
        cout << p1.use_count() << " " << p2.use_count() << endl;
//        1 1
//        p2离开代码块,释放其管理的资源,p1指针指向被释放的内存
    }
}

程序将异常退出

显式/隐式资源转换

struct Resource {
    string str;

    Resource(string s) : str(s) {}
};

struct Manager {
private:
    Resource *resource;
public:
    Manager(Resource *r) : resource(r) {}

    ~Manager() {
        delete resource;
    }
	
	// 显式转换,获得其资源
    Resource *getResource() {
        return resource;
    }
	// 隐式转换,获得其资源
    operator Resource *() {
        return resource;
    }
};

void useResource(Resource *resource) {
    cout << resource->str << endl;
}

int main() {
    Manager manager(new Resource("Hello"));
    useResource(manager.getResource());
    Manager manager1(new Resource("World"));
    useResource(manager1);
}
/*
Hello
World
*/

一般推荐使用显式转换(get),避免潜在错误

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

  • 如果在new中使用了[],那么同样需要delete []来释放。反之亦然

在使用new时,有两件事发生:

  • 调用operator new函数来分配内存空间
  • 针对此内存调用一个或多个构造函数
    在使用delete时,发生:
  • 针对此内存调用一个或多个析构函数
  • 调用operator delete函数来释放空间

如何知道delete []需要释放多少内存
通过new []构造的内存布局一般如下(不同编译器的实现方式可能不同):
n − O b j e c t 1 − O b j e c t 2 − . . . − O b j e c t n n - Object_1 - Object_2 - ... - Object_n nObject1Object2...Objectn,其中n是数组的长度。

对new调用delete[]或对new []调用delete都是未定义行为。

所以在不同的构造函数中,对于对应的指针应采用相同的new方式,不然的话怎么决定析构函数如何析构该资源呢?
特别是使用了自定义类型,容易造成错误释放

int main() {
    typedef Base Base_Array_4[4];
    Base *base = new Base_Array_4;
    // 正确
    delete[] base;
}

这里初始化的是一个数组,所以需要delete []

以独立语句将new出来的对象放入智能指针(对象管理类)

  • 以独立语句将newed对象置入到智能指针去,否则异常不安全,存在潜在的资源泄露可能性
  • 或者使用make_shared来初始化智能指针
func(shared_ptr<Resource>(new Resource), f());

是异常不安全的,因为f()的执行次序不确定。
new Resource一定先于智能指针的构造函数之前被调用。但是f()的调用次序可能会在两者之前、之间、之后。
如果f()在两者之间调用,然后发生了异常抛出后,new Resource的资源就没有被接管,可能会造成资源泄露。
改写为:

shared_ptr<Resource> pr(new Resource);
func(pr, f());

或者

func(make_shared<Resource>(构造函数参数), f());
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值