[C++][经典探讨]类继承中,通过基类指针delete释放,是否会造成内存泄漏
[序言]
很久不写C/C++技术贴了,算一下自己用C++也有7~8年了,虽然现在用Delphi比较多,但是对C++还是有一份热情.前段时间在CSDN看到一个帖子, 很多人都没有引用权威文献来针对这个问题进行讨论,如果没有全文文献的引用,那么讨论将会是一个持久战.要结束这种情况,还是以书为准。如果大家都喜欢探讨技术,可以加入我的QQ:643439947一起学习
[建议]
C++是一门非常重要语言且博大精深.没有10年的使用时间和大量C++的书籍阅读,最好不要轻易去探讨C++某些特性,不然真的是那着石头砸自己的脚.就因为这些原因本人也很少在CSDN解答C++的问题,因为C++实在太多细节要注意了,知道得越多,越觉得自己是C++菜鸟.我很害怕的回答是错误的.
[感谢]
曾半仙, 简约而不简单 这些热心网友提出建议性
[适用范围]
本问题所涉及的知识点太多和范围太广,我特定归类为windows桌面系统. 如果突然有人牵扯到嵌入式系统以及嵌入式编译器,那就真的没完没了.下面是一个牛人看了文件给的思路和范围,可想而知太多不可预测的因素了."你考虑一下嵌入开发环境, 虽然语法上支持, 但是库并没有实现new和delete, 这样就引发了不确定因素, 特别是程序员喜欢模版, 喜欢优化, 想使用内存池的情况 "
[原则]
本人是中立人士,不针对任何人,只针对问题.在分析这个问题我又复习了一边C++.这个问题牵涉到 析构函数 虚函数 构造函数 派生 new/delete 5个主要问题.本着学术认真的态度,我翻阅了如下C++书籍
1> C++ Primer Plus
2> C++编程思想 2卷合订本 新版
3> Effective C++
4> Imperfect C++
[引发问题的CSDN链接]
http://topic.csdn.net/u/20110715/15/7ca1e66b-8a04-4c90-80f0-6265ff0269af.html?91968
[还原问题]
class A
{
public:
A(){} ;
~A() {} ; // Ooops must use virtual ~A()
} ;
class B : public A
{
public:
B(){} ;
~B() {} ;
} ;
int main()
{
A *pclass_A = new B ; // 创建一个B对象指针 隐性转换为 A*
// 这里我们需要注意这个转换涉及到一个概念叫: Upcast 中文翻译叫:向上类型转换
delete pclass_A ;
pclass_A= NULL ;
return 0;
}
[分析结论]
就这段代码本身而言我看了4本书也没有很明确的说到这样的写法就会有泄漏.但可以确定这样的写法是一个隐性错误,已违反C++的继承规则和违背继承的实现原理机制.
详细请看:http://www.parashift.com/c++-faq-lite/virtual-functions.html#faq-20.7
"....不把析构函数设为虚函数是一个隐性的错误,因为它常常不会对程序有直接影响。但要注意它不知不觉得引入存储器泄漏(关闭程序是内存为释放)。同样,这样的析构操作还有可能掩盖发生的问题...."(摘自: C++编程思想 2卷合订本 第387页)。这句话虽然很短,但是解答了我们很多疑问.
1> “如果你不使用虚析构函数,不会对程序有直接影响”.这里的“不会对程序有直接影响”,我们可以认为delete一个基类指针(基类是没有析构函数),不会照成内存泄漏(仅针对上面的代码而言,如果在派生类中有分配堆,那么肯定会有内存泄漏).
这里为什么我们可以认为delete一个基类指针(基类是没有析构函数),不会照成内存泄漏呢?这就是C++的new 和 delete 的特有机制和职责了.下面看这句话:
"....当在堆栈里主动创建对象时,对象的大小和它们的声明周期被准确地内置在生成的代码里,这是因为编译器知道确切的类型,数量和范围....."(摘自: C++编程思想 2卷合订本 第318页的)这里非常明确的告诉我们,会知道确切的"类型,数量和范围",注意这里有"范围",因此可以推断通过基类指针进行delete,是不会对“不会对程序有直接影响”(备注:请谅解,我没敢直接说不会有内存泄漏,因为我没有能跟编译器厂商求证,但我认为是"应该"不会造成内存泄漏).
2>"但要注意它不知不觉地引入存储器泄漏"这句话又针对前句话做了补充,特别强调了"不知不觉地"+"引入"+"存储器泄漏".很明显的说明了,如果会发生泄漏,那就是外部人为造成的,比如的B类内部中使用了new操作,比如申请10个字节char *char_A = new char[10],那么根据“C++的继承规则和继承的实现原理机制”如果你不把基类的析构函数声明并定义为virtual,那么B类在释放的时候,没法做尾场清理的.比如前面的 new char[10]不能被释放.
额外讨论: 在类继承机制中,构造函数和析构函数具有一种特别机制叫 “层链式调用通知”,这个机制原理是建立在 “vpointer” “VPTR” “VTABLE”这种东西(摘自: C++编程思想 2卷合订本 第369页)(备注:层链式调用通知是我个人理解并总结的词汇.大家可以通过阅读 C++编程思想 2卷合订本 第385页).
流程是这样:在构造一个有类继承机制的类,比如上面的类B,那么会先调用A类的构造,A构造完成之后在调用B类的构造函数,达到"由里向外"通知调用的效果.那么释放一个有类继承机制的类,那么会调用B类的析构函数, 再调用A类的析构函数,达到"由外向里"通知通知的效果,那么为了达到这个这种“层链式调用通知”的效果,C++标准规定:基类的析构函数必须声明为virtual, 如果你不声明,那么"层链式调用通知"这样的机制是没法构建起来.从而就导致了基类的析构函数被调用了,而派生类的析构函数没有调用这个问题发生.但这里要特别注意:这种特殊情况下派生类的析构函数没有被调用,有2中情况发生:
1>如果你的派生类内部没有分配任何堆,而只是单一的局部变量,那么根据局部变量和类的生命周期理论,他们是会被释放的,“不会对程序有直接影响”(备注:请谅解,我没敢直接说不会有内存泄漏,因为我没有能跟编译器厂商求证,但我因为是"应该"不会造成内存泄漏),比如本文顶部列举的代码片段.
2>如果你的派生类内部有分配堆,那么派生类就没法通过自身的析构函数进行尾场清理了,比如 delete []a ;
[结尾]
写这个文章花费了我1个小时,但在写之前,花费了我2个小时去翻阅4本C++书籍重新去消化这个经典问题.
[查阅资料]
1> C++ Primer Plus 里面的 第13章 类继承
2> C++编程思想 2卷合订本 新版 里面的 第13章 动态对象创建 第14章 继承和组合 第15章 多态性和虚函数