某系统在某特殊情况下, 会出现bug, 经我非常保守地估计, 这个bug的定位修改费用至少3000元, 这还不包括其他的费用。 脱离具体场景, 我来抽象出一个简单的模型, 示例代码如下:
#include <iostream>
using namespace std;
class A
{
private:
static A *m_p;
public:
static A *getSingleTon()
{
if(NULL == m_p)
{
m_p = new A();
}
return m_p;
}
~A()
{
if(NULL != m_p)
{
delete m_p;
m_p = NULL;
}
}
};
A* A::m_p = NULL;
int main()
{
return 0;
}
大家可以看看上述程序有什么问题。
如果没有看出问题, 那你再运行一下如下程序:
#include <iostream>
using namespace std;
class A
{
private:
static A *m_p;
public:
static A *getSingleTon()
{
if(NULL == m_p)
{
m_p = new A();
}
return m_p;
}
~A()
{
if(NULL != m_p)
{
cout << "xxx" << endl;
delete m_p; // 递归调用析构
m_p = NULL;
cout << "yyy" << endl; // 永远也不会执行
}
}
};
A* A::m_p = NULL;
int main()
{
A *p = A::getSingleTon();
delete p;
return 0;
}
运行发现, 析构函数被多次调用了, 为什么呢?当类的使用者调用delete p;的时候, 实际上就是调用析构函数来释放单例, 但是, 现在类的提供者在析构函数中又delete这个单例, 显然又会调用析构函数, 所以形成了递归调用析构函数, 系统不异常才怪呢。
我们来反思一下, 为什么会出上述问题呢?肯定是写SingleTon的人牢牢记住了: 要在析构函数中释放资源。 但是, 他不明白, 单例应该由类的使用者来释放, 而不是类的提供者。 不要把角色搞错了。
千万不要说, 为什么出这么低级的问题! 其实, 这个问题不低级, 是个比较隐蔽的错误。 而且, 当代码多了(比如100w行), 离职的人多了, 经几次交接后, 那代码就可想而知了
下面, 我们继续看看:
#include <iostream>
using namespace std;
class A
{
private:
static A *m_p;
int x;
A()
{
x = 1;
}
public:
static A *getSingleTon()
{
if(NULL == m_p)
{
m_p = new A();
}
return m_p;
}
~A()
{
if(NULL != m_p)
{
cout << "xxx" << x << endl; // 永远是xxx1
delete m_p; // 递归调用析构
m_p = NULL;
cout << "yyy" << x << endl; // 永远也不会执行, 单例也不会被释放
}
}
};
A* A::m_p = NULL;
int main()
{
A *p = A::getSingleTon();
delete p;
return 0;
}
从结果看, x的值一直是1, 所以单例根本就没有析构掉, 也就是说, 没有执行析构函数右边的花括号处, 单例就不会被释放。
实际上, 要快速定位到析构函数的问题, 还是很不容易的, 那么多代码, 进程死掉, 咋快速定位?尤其是, 如果析构函数中没有日志打印, 根本就很难知道析构函数被多次执行了。 所以, 关于日志, 我强烈建议:
1. 所有的构造函数和析构函数都必须有日志打印。
2. 不被频繁调用的函数中, 必须有日志(很多人只喜欢在某些异常分支打日志, 甚至连异常分支都不打印日志, 确实太流氓了)。
好吧, 一个小小的bug确实让人蛋疼两三天。 这个代码是谁写的啊是不是应该在明天端午节请我吃一顿大餐呢
我再次大声疾呼, 软件质量不是一句废话。
要多反思, 多总结, 总会慢慢进步。本文先到此为止。