对于内存的管理,在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,有几种实现方式:
- 定义一个deleter函数;
- 定义一个函数类,通过重载操作符实现;
- 通过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种:
- 自定义函数,并声明函数类型;
- 定义函数类;
- 通过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的方式,主要是为了回答这样一个问题——如果忘记释放资源了怎么办,不管是内存、网络连接、数据库连接还是文件描述符,最好的办法就是创建时就想好退路,即便最后忘记了,也提前制定了计划。
参考资料
- C++ Primer 12.1.4 - Smart pointers and Exceptions
- C++ Primer 12.1.5 - unique_ptr
- https://thispointer.com/shared_ptr-and-custom-deletor/
- https://zhuanlan.zhihu.com/p/367412477