cpp资源管理--智能指针、引用计数、weak_ptr



如需评论,请移步至我的github blog

前言

本文面向c++初学者,讲述现代c++语言中的资源管理,包含但不限于:资源获取即初始化(RAII), 智能指针,引用计数,weak_ptr的由来,垃圾回收机制。帮助c++初学者了解c++与c在资源管理上的不同,理解c++中的struct与c中的不同之处。并与具有完备的垃圾回收机制的语言进行对比。

c++中的struct/class

正确且全面的掌握class是理解和使用c++的基础。本文只关注class提供的构造函数、析构函数,其他内容请参阅 Effective C++。

c语言提供了struct,但c++中的struct和它不同。c++中的struct是所有成员默认都是public的class. c++中的对象在被构造时,相应的构造函数会被调用,对象被释放时,相应的析构函数会被调用。

struct Foo {
    ... 
};

int main()
{
    {
        Foo foo;
    }

    // 当运行到这里时:
    // 在c++中,Foo的析构函数会被调用。
    // 在c中,当前作用域中无foo。

    return 0;
}

上面的代码在c和c++中都不会有内存泄漏。但c++提供了更多的控制能力。对于任意一个对象(存储在堆中或栈中),该对象在被释放时(显示的使用delete或局部变量隐式的释放),其析构函数必然会被调用。这意味着我们可以利用局部变量的隐式释放来管理资源。具体来说,可以在析构函数中添加一些资源管理的代码,这些代码会在对象析构时被调用。同时,如果将资源获取的过程放在对象的构造函数中,使得实例化一个对象就是获取了一定的资源。这样的资源管理方式就是资源获取即初始化(Resource Acquisition Is Initialization - RAII)。

运用RAII可以更好的管理资源,减少代码重复,提高可读性,预防bug的出现。下面给出实际的一个例子。


int syncFromFile(const string& filepath)
{
    // 获取资源
    FILE* fp = fopen(filepath.c_str(), "r");
    if(!fp){
        ...
    }
    
    if(handle(file.fp) == FAIL){
        fclose(fp);
        return -1;
    }
    if(handle_second_step(file.fp) == FAIL){
        fclose(fp);
        return -2;
    }
    if(handle_third_step(file.fp) == FAIL){
        return -3; // 忘记添加释放资源的代码了。当资源更多,函数更复杂,团队多人协作时,这种情况很可能发生。
    }
    fclose(fp);
    return 0;
}

下面是运用RAII的代码。RAII不仅仅可以用来管理内存,还可以用来管理锁、文件描述符、socket连接等等。这也是为什么本文的标题是资源管理而不是内存管理的原因。

struct FileHandler 
{
    FILE* fp;

    FileHandler(const string& filepath)
    {
        fp = fopen(filepath.c_str(), "r"); 
        if(fp == NULL){
            ...
        }
    }
    ~FileHandler()
    {
        if(fp) fclose(fp);
    }
};

// 代码非常简洁明了
int syncFromFile(const string& filepath)
{
    FileHandler file(filepath);

    if(handle(file.fp) == FAIL)
        return -1;
    if(handle_second_step(file.fp) == FAIL)
        return -2;
    if(handle_third_step(file.fp) == FAIL)
        return -3;
    return 0;
}

c++智能指针

auto_ptr

首先,我想说的是智能指针并不智能,不知道这是怎么取名的 :(。我们先来看看早期c++提供的智能指针 auto_ptr。早期c++只有这一个智能指针,功能是在被构造时传入一个指针,并且在析构时自动delete掉这个指针。下面是一个使用例子。

#include 
#include  // auto_ptr needs this
using namespace std;

struct Foo {
    int val;
    Foo(int val) : val(val) {}
};

void func(Foo* raw_pointer) { ... }

int main()
{
    auto_ptr fooh(new Foo(123));
    cout << fooh->val << endl; // 就像使用一个原生指针一样: foop->val
    fooh->val = 321;
    cout << fooh->val << endl;

    Foo* foop = fooh.get(); // 获得raw pointer
    func(foop);
    return 0;
}

auto_ptr现在已被抛弃,不推荐使用,请使用它的替代品unique_ptr。本文不深入介绍c++智能指针的所有方面,请参考cplusplus website和cppreference website。

shared_ptr

auto_ptr和unique_ptr在复制拷贝时,资源的所有权会变更,复制之后原先的就不能使用了。但如果我想让多个智能指针同时引用同一个指针时,又该怎么办呢?shared_ptr就是为解决这个问题而生的。如其所名(shared),大家共享一个指针,共同管理一个资源。但此时问题来了,大家都引用一个指针,那怎么知道什么时候该释放这个指针呢?c++使用一个简单的机制来解决这个问题————引用计数。每当一个新的shared_ptr指向一个指针时,就增加1,当一个shared_ptr析构时,就减1,当计数的值为0时就释放这个指针。

示例代码:(代码来自笑对人生的blog, 有更改)

shared_ptr sp(new int(10));        // 一个指向整数的shared_ptr
sp.unique();                            // unique() return true,现在shared_ptr是指针的唯一持有者
shared_ptr sp2 = sp;               // 第二个shared_ptr,拷贝构造函数
sp == sp2 && sp.use_count() == 2;       // 此表达式为真,两个shared_ptr相等,指向同一个对象,引用计数为2
*sp2 = 100;                             // 使用解引用操作符修改被指对象
if(*sp == 100)
    cout << "sp也同时被修改";
sp.reset();                             // 停止sp对被指对象的引用
if(!sp && sp2.use_count() == 1)
    cout << "sp不再持有任何指针(空指针)";

shared_ptr的特性使得它可以代替原生的指针,我们可以方便的传递指针,并且不用担心资源管理的细节。在恰当的时候,基于引用计数的机制会帮我们释放指针。一切都很令人开心。就像下面例子中的使用方式。将下面代码中的所有shared_ptr更换为Foo*,你会发现一切都正常。

struct Boo {
    string str;
    shared_ptr foo;

    Boo(const string& str, shared_ptr foo)
        : str(str), foo(foo) {}
};

shared_ptr createFoo(int val);
void doSomethingWithFoo(shared_ptr foo);

shared_ptr func()
{
    auto foo = createFoo(123);
    doSomethingWithFoo(foo);
    return shared_ptr(new Boo("str", foo));
}

weak_ptr

先让我们了解引用计数更多一点,你可能会想既然引用计数很完美,为什么java,javascript,lua需要复杂的垃圾回收机制?原因是引用计数存在循环引用的问题,考虑下面这个情景:

cpp资源管理--智能指针、引用计数、weak_ptr_第1张图片

这种情况下,foo1的引用计数是2,一旦fooListHead不再引用foo1,此时foo1的引用计数将变为1,并且造成右边环形的三个对象永远不会被释放的问题。想要在每次解除引用时侦测是否有环形成是非常麻烦的事,所以提出了weak_ptr。weak_ptr在引用对象时,不增加引用计数值(所以叫虚引用)。既然不增加计数值,那么在引用之后,被指对象可能被释放了,所以不能直接读取weak_ptr中引用的对象。在使用对象之前必须先检查所引用的对象是否已经被释放,如果没有,则需要将引用计数值加1,然后访问此对象,访问完之后,再将引用计数值减1。由此可见,使用shared_ptr需要在必要的地方用weak_ptr代替shared_ptr。这项工作需要由程序员来手工管理,所以shared_ptr只是提供了有限的垃圾回收功能。

垃圾回收(c++, java, javascript, lua)

java, javascript, lua都使用复杂的垃圾回收机制来管理object,使得程序员从繁重的内存管理中解放出来。这些语言中的对象在没有地方引用它们时,在一定的时候就会被垃圾回收机制处理掉。如何跟踪这些unreachable objects,是垃圾回收的主要任务,了解相关的算法。

Garbage Collection in c++

设想一下,一个c++库提供垃圾回收的能力,它实现了一个object wrapper。这个object wrapper是一个类似shared_ptr的智能指针,我们暂且叫它smart_ptr。smart_ptr不使用引用计数来管理它引用的对象,它使用一个真正的垃圾回收算法。如果我们约定不在代码中传递指针和new运算符,而总是传递smart_ptr和使用new_smart_ptr函数来代替new运算符,并且new_smart_ptr总是返回一个smart_ptr,在不考虑性能的情况下,那么我们可以获得和java, javascript等语言一样的内存自动管理能力。我们永远不需要手动地编写代码来释放内存。

template
struct smart_ptr
{
    ...
};

template
smart_ptr new_smart_ptr(Args &&...args) { ... }


smart_ptr example()
{
    {
        smart_ptr foo = new_smart_ptr(123);
    }
    smart_ptr foo = new_smart_ptr(321);
    str = smart_ptr_null; // 使用smart_ptr_null代替nullptr
                          // 之前分配的对象将被回收掉
}

现在联系前面的一节 c++中的struct/class 中关于c与c++的对比,考虑一下,使用c语言能够实现上面的能力吗?

额外链接

  • memorymanagement.org - Resource for programmers and computer scientists interested in memory management and garbage collection.
  • cplusplus.com - Memory page in cplusplus.
  • cppreference.com - Memory page in cppreference.

转载于:https://www.cnblogs.com/morrow1nd/p/6405180.html

你可能感兴趣的:(java,内存管理,javascript)