C++内存管理:智能指针、异常与自定义deleter的关系


对于内存的管理,在C++中是一个重要的主题,而C++中异常的出现又带来了新的困惑,异常发生时,会对内存的管理有何影响?

本节从3个方面阐述:

  • 对于传统显式管理方法new/delete的影响;
  • 对于智能指针的影响;
  • 泛化思考:如果对于资源(不仅是内存),忘记处理了怎么办?

异常对于new/delete的影响

先说结论,对于采用这种方式管理的内存,极有出现内存泄露的问题。简单来讲,

  • 在一个block scope中,例如函数,如果delete调用之前,异常没有捕获,则会出现内存泄露;
  • 如果在delete之前捕获了,就不会出现内存泄露;

如下例所示:

class Foo{
public:
    Foo(){ cout << "call Foo constructor" << endl; }
    ~Foo(){ cout << "call Foo destructor" << endl; }
};

void test()
{
    Foo *foo = new Foo();
    try{
        throw std::invalid_argument("invalid_argument");
    }
    catch (const logic_error & e) {
        cout << e.what() << endl;
    }
    delete foo;
}

int main()
{
    test();
    return 0;
}
/**output
call Foo constructor
invalid_argument
call Foo destructor
*/

说明此时,当异常捕获后,析构函数被调用成功,因此资源被释放了。

void test()
{
    Foo *foo = new Foo();
    throw std::invalid_argument("invalid_argument");
    delete foo;
}

int main()
{
    test();
    return 0;
}
/**
call Foo constructor
libc++abi.dylib: terminating with uncaught exception of type std::invalid_argument: invalid_argument
*/

当异常直接被抛出,没有捕获时,就会发现析构函数没有调用成功,因此出现了内存泄露。

异常对于smart pointers的影响

对于smart pointers来说,不需要手动释放内存资源,而是交给其隐式管理,能够保证资源及时被释放,即便因各种问题,导致函数提前结束(如出现异常)。

class Foo
{
public:
    Foo() { cout << "call Foo constructor" << endl; }
    ~Foo() { cout << "call Foo destructor" << endl; }
};

void test()
{
    shared_ptr<Foo> sp(new Foo());
    throw std::invalid_argument("invalid_argument");
}

int main()
{
    try
    {
        test();
    }
    catch (const logic_error &e)
    {
        cout << e.what() << endl;
    }
    return 0;
}
/** output
call Foo constructor
call Foo destructor
invalid_argument
*/

说明即便test函数中的异常没有捕获,shared_ptr指向的内存空间也被释放了,因为成功调用了析构函数。

更不用说,在函数test中成功捕获到异常时,也会成功调用析构函数。

void test()
{
    shared_ptr<Foo> sp(new Foo());
    try
    {
        throw std::invalid_argument("invalid_argument");
    }
    catch (const logic_error &e)
    {
        cout << e.what() << endl;
    }
}

int main()
{
    test();
    return 0;
}
/** output
call Foo constructor
invalid_argument
call Foo destructor
*/

这里有个注意事项,当main函数中的异常没有被捕获时,会出现程序直接终止,此时内存泄露问题也就无意义了,因为整个进程都被销毁了。但是在此时不管是new/delete管理的,还是smart pointers管理的,都不会调用析构函数,因为在异常抛出的那个时间点,程序就结束了。

int main()
{
    shared_ptr<Foo> sp(new Foo());
    throw std::invalid_argument("invalid_argument");
    return 0;
}
/** output
call Foo constructor
libc++abi.dylib: terminating with uncaught exception of type std::invalid_argument: invalid_argument
*/

General:如何避免忘记释放资源

从上述的例子中可以发现:

  • new/delete的操作方法,如果忘记delete,或者因为导致无法delete时,就会有泄漏;

  • 相比指向,smart pointers利用了超出block scope后,局部变量自动销毁的特点,可以实现自动地释放资源。这种idea也可以应用到其他形式资源的管理上。

shared_ptr隐式进行内存资源的管理,在其内部也是通过delete完成内存的释放的,当reference count变成0之后,shared_ptr被销毁,其指向的内存也被销毁,此时默认情况下,通过delete完成。

如果说对于内存资源,可以通过delete完成,但是对于其他资源,也需要进行释放,比如:文件操作符、网络socket、数据库连接等等,但是这是就不能使用shared_ptr中默认的delete了,因为不同的资源提供的接口不同,比如文件操作符是close等等。

为了实现这种技术,一般有两条路:

  • 定义一个良好的析构函数,当smart pointers被销毁后,自动调用该析构函数;

  • 如果一个外部类中,没有定义良好的析构函数,可以在使用smart pointers时,传入一个deleter函数。

这里,析构函数我们足够熟悉,因此主要阐述deleter的使用方法。

shared_ptr中定义deleter

在shared_ptr中自定义deleter,有几种实现方式:

  1. 定义一个deleter函数;
  2. 定义一个函数类,通过重载操作符实现;
  3. 通过lambda表达式实现;

首先定义一个类如下:

class Foo {
public:
    int m_a;
    Foo(int a) : m_a(a)
    {
        cout << "call constructor" << endl;
    }

    ~Foo()
    {
        cout << "call destructor" << endl;
    }
};

定义一个deleter函数

在下边的代码中,我们定义了两个deleter,第一个与shared_ptr默认执行的deleter的操作行为是一致的。另外,C++也提供了一个函数来完成默认的操作default_delete

第二个则是自定义的deleter,作为一个额外的函数,其参数为传入shared_ptr的通过new得到的指针对应的类型。从下面的代码中,可以发现代码的执行顺序为:

  • 首先,构造函数先被执行;
  • 之后是自定义的deleter被执行;
  • 最后是foo的析构函数被执行,说明即便有自定义的deleter,析构函数仍然会被执行
void default_deleter(Foo *foo)
{
    delete foo;
}

void custom_deleter(Foo *foo)
{
    cout << "call custom deleter" << endl;
    delete foo;
    // close file
    // close connection
    // close socket
}

int main()
{
    int a = 100;
    // the three deleters do the same things by default.
    shared_ptr<Foo> foo(new Foo(a));
    shared_ptr<Foo> foo(new Foo(a), std::default_delete<Foo>());
    shared_ptr<Foo> foo(new Foo(a), default_deleter);
    // define customized deleter
    // shared_ptr<Foo> foo(new Foo(a), custom_deleter);
    return 0;
}
/** for custom_deleter, the output:
call constructor
call custom deleter
call destructor
*/

定义函数对象类

  • 一个类将()重载为成员函数,这个类就称为函数对象类,该类的对象就是函数对象。

  • 函数对象是一个对象,但是使用的形式看起来像函数调用,实际上也执行了函数调用。

class FuncObject{
public:
    void operator()(Foo *foo){
        cout << "call func object" << endl;
        delete foo;
    }

};

// main
int a = 100;
shared_ptr<Foo> foo(new Foo(a), FuncObject());
/** output
call constructor
call func object
call destructor
*/

lambda表达式

lambda表达式类似函数类,作为一个匿名函数,可以直接传入到shared_ptr的构造函数中,作为自定义的deleter。

int a = 100;
shared_ptr<Foo> foo(new Foo(a), [](Foo *f){
    cout << "call lambda expression" << endl;
    delete f;
});
/** output
call constructor
call lambda expression
call destructor
*/

unique_ptr中定义deleter

在unique_ptr中自定义deleter有点繁琐,不同于shared_ptr,需要指定deleter的类型。

// p points to an object of type objT and uses an object of type delT to free that object
// it will call an object named fcn of type delT
unique_ptr<objT, delT> p (new objT, fcn);

其中,fcn是delT类型的实例,即可以是普通类的对象,也可以是函数指针指向的一个函数。

那么如何自定义deleter呢?有几种方法,这里主要介绍3种:

  1. 自定义函数,并声明函数类型;
  2. 定义函数类;
  3. 通过function函数;

自定义函数

在这里,用到了函数指针的概念。对于unique_ptr初始化时,提供了自定义的deleter函数custom_deleter,其对应的类型为void (*)(Foo)。这里使用了decltype进行类型推导,注意:不要忘记后面那个*,因为decltype(custom_deleter)返回的是函数类型,这里必须要用函数指针类型,才能与后面的custom_deleter对应,因此要加*

void custom_deleter(Foo *foo)
{
    cout << "call custom deleter" << endl;
    delete foo;
}

int main()
{
    int a = 100;
    cout << typeid(void (Foo)).name() << endl;
    cout << typeid(void (*)(Foo)).name() << endl;
    cout << typeid(decltype(custom_deleter)).name() << endl;
    cout << typeid(decltype(custom_deleter) *).name() << endl;
    unique_ptr<Foo, decltype(custom_deleter) *> up(new Foo(a), custom_deleter);
    
    return 0;
}
/** output
Fv3FooE
PFv3FooE
FvP3FooE
PFvP3FooE
call constructor
call custom deleter
call destructor
*/

函数对象

这里使用函数对象定义deleter,其类型为FuncObject,对应的对象为FuncObject()

class FuncObject{
public:
    void operator()(Foo *foo){
        cout << "call func object" << endl;
        delete foo;
    }
};

int main()
{
    int a = 100;
    unique_ptr<Foo, FuncObject> up(new Foo(a), FuncObject());
    return 0;
}
/**
call constructor
call func object
call destructor
*/

lambda表达式

使用lambda表达式的方式,与在shared_ptr中的方式类似,更为简洁。

int a = 100;
auto custom_deleter = [](Foo *f)
{
    cout << "call lambda expression" << endl;
    delete f;
};
unique_ptr<Foo, decltype(custom_deleter)> up(new Foo(a), custom_deleter);

/** output
call constructor
call lambda expression
call destructor
*/

总结

上述介绍了各种自定义deleter的方式,主要是为了回答这样一个问题——如果忘记释放资源了怎么办,不管是内存、网络连接、数据库连接还是文件描述符,最好的办法就是创建时就想好退路,即便最后忘记了,也提前制定了计划。

参考资料

  1. C++ Primer 12.1.4 - Smart pointers and Exceptions
  2. C++ Primer 12.1.5 - unique_ptr
  3. https://thispointer.com/shared_ptr-and-custom-deletor/
  4. https://zhuanlan.zhihu.com/p/367412477

文章作者: alex Li
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 alex Li !
  目录