一、C++ 资源管理
写代码时经常会碰到这种情况:在函数开始时申请了buffer、或打开了某文件、打开某so,需要在函数返回前做对应的释放或关闭操作。不使用任何语言机制的情况下,写出来的代码是这样的:
void before() {
uint8_t *a = new uint8_t[10];
double *b = new double(1.2);
if (xxx) {
delete[] a;
delete b;
return;
}
if (xxx) {
delete[] a;
delete b;
return;
}
delete[] a;
delete b;
}
在任意一个函数出口,都要进行资源释放,代码丑陋、可读性差,而且还容易出现某代码块抛异常直接退出但资源没有释放的情况。
在C++中已经内置了管理内存的RAII类,即智能指针,直接用即可。需要注意两点:
- 如果资源不需要被共享,出函数之前就需要释放,则优先用unique_ptr
- 如果是申请数组,unique_ptr提供了对数组的特化,但shared_ptr直到c++17才支持对数组的特化。没有条件用较新编译器的时候需要自己写deleter
- c++11/14的写法1:shared_ptr<int> a(new int[10], default_delete<int[]>());
- c++11/14的写法2:shared_ptr<int> a(new int[10], [](int *p) {delete[] p;});
- c++17的写法:shared_ptr<int[]> a(new int[10]);
void after() {
unique_ptr<uint8_t[]> a(new uint8_t[10]);
unique_ptr<double> b(new double(1.2));
if (xxx) {
return;
}
if (xxx) {
return;
}
}
二、利用栈变量的析构函数
除了资源管理外,如果需要在函数返回前执行某个操作,也可以使用RAII,将对应的操作放到栈变量的析构函数中去,由编译器自动为我们做这件事。
举例,需要在函数结束前将函数执行的结果错误码通过一个notify函数传递出去:
void before() {
int ret = 0;
if (xxx) {
ret = -1;
notify(ret);
return;
}
if (xxx) {
ret = -2;
notify(ret);
return;
}
notify(ret);
}
改造后:
void after1() {
int ret = 0;
class RAIIHelper {
public:
explicit RAIIHelper(int &r) : mRet(r) {}
~RAIIHelper() {
notify(mRet);
}
private:
int &mRet; //传引用
} helper(ret);
if (xxx) {
ret = -1;
return;
}
if (xxx) {
ret = -2;
return;
}
}
但是这样写可扩展性不好,这个RAIIHelper类只能在当前函数中用,在其他情况下不一定适用。
go语言中有一个特性叫defer——延迟执行,虽然C++中没有这玩意,但是我们可以用lambda表达式和function造一个出来。很简单,把需要执行的操作直接传进来,在析构函数中执行。
class Defer {
public:
explicit Defer(function<void()> f) : m_func(move(f)) {}
~Defer() { m_func(); }
private:
function<void()> m_func;
};
再次改造后,十分清爽:
void after2() {
int ret = 0;
Defer defer([&ret]() { notify(ret); }); //引用捕获
if (xxx) {
ret = -1;
return;
}
if (xxx) {
ret = -2;
return;
}
}
三、goto
C语言举例:
void before() {
uint8_t *a = malloc(10);
uint8_t *b = malloc(5);
if (xxx) {
free(a);
free(b);
return;
}
if (xxx) {
free(a);
free(b);
return;
}
free(a);
free(b);
}
在C中由于没有类这个概念,没法在析构函数中写入一些操作,可以用goto方式来搞:
void after1() {
uint8_t *a = malloc(10);
uint8_t *b = malloc(5);
if (xxx) {
goto End;
}
if (xxx) {
goto End;
}
End:
free(a);
free(b);
}
四、do while(0)大法
相对goto来说我更喜欢用do while(0)大法。在每个出口处加上break,然后在最后执行一次必要的资源释放等操作:
void after2() {
uint8_t *a = malloc(10);
uint8_t *b = malloc(5);
do {
if (xxx) {
break;
}
if (xxx) {
break;
}
} while(0);
free(a);
free(b);
}