[C/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 ;

[CSDN网友pengzhixi提供delete标准说明]
delete-expression:
::opt delete cast-expression
::opt delete [ ] cast-expression

In the first alternative (delete object), if the static type of the operand is different from its dynamic type, the static type shall be a base class of the operand’s dynamic type and the static type shall have a virtual destructor or the behavior is undefined. In the second alternative (delete array) if the dynamic type of the object to be deleted differs from its static type, the behavior is undefined.
翻译如下:
对于delete object;这种形式,如果delete操作的对象的静态类型不同于动态类型,那么该静态类型必须是动态类型的基类,而且该静态类型必须有一个虚析构函数,否则行为未定义。而对于delete[]这种形式,如果静态类型和动态类型不一致,那么行为未定义(即使你静态类型包含虚析构函数,因为对数组用多态就是一个错误)

[问题总结]
如果你特意去做delete object并且属于行为未定义的,那么有没有内存泄漏还是得跟着开发编译器厂商屁股走,他们说有那就是有,他们说没有那就是没有.但是有一点是,我们必须要做到的:在编写C++代码时,不要做不符合标准的事情.至于那些未定义的行为,让编译器开发商去解决或者操心吧.


[结尾]
写这个文章花费了我1个小时,但在写之前,花费了我2个小时去翻阅4本C++书籍重新去消化这个经典问题.

[查阅资料]
1> C++ Primer Plus 里面的 第13章 类继承
2> C++编程思想 2卷合订本 新版 里面的 第13章 动态对象创建 第14章 继承和组合 第15章 多态性和虚函数

你可能感兴趣的:(delete)