[C++][经典探讨]类继承中,通过基类指针delete释放,是否会造成内存泄漏

我不是代码教父 2011-07-31 12:04:44
[序言]
很久不写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章 多态性和虚函数

...全文
1342 61 打赏 收藏 转发到动态 举报
写回复
用AI写文章
61 条回复
切换为时间正序
请发表友善的回复…
发表回复
smilenot 2011-08-06
  • 打赏
  • 举报
回复
看的云里雾里
  • 打赏
  • 举报
回复
嚯嚯, 我是打酱油的,mark
yby4769250 2011-08-04
  • 打赏
  • 举报
回复
我感兴趣的是,你所说的“链式调用”的原理,我的问题的意思是:当用父类指针去析构子类对象时,是如何通过你说的“链式调用“来调用父类的析构函数的?这个原理的实现机制是什么?
我的问题可以简单的概括为:子类对象中到底保存了一个什么数据,使得它能通过这个数据来寻址到父类的虚析构函数,进而调用父类析构函数?
非常希望得到答案,我对这个问题困惑已久,这是我发过的一个帖子http://topic.csdn.net/u/20110721/10/8c187923-e4f5-41bc-ade0-942792424b50.html

再补充我的问题:我们知道虚函数表只是一个一维数组,数组的每一个元素都是该类的一个虚函数的地址,且元素之间的关系都是独立的,没有关系,虚析构函数的地址也存放在虚函数表中,子类同时会继承每个父类的虚函数表,我猜测”链式调用“的实现机制难道是说,如果判断到析构函数是虚函数,当析构时,则会通过访问从父类继承来的虚表指针(调用每一个从父类继承过来的虚表指针,遍历),进而调用父类的析构函数来完成对父类的析构,我仅仅是猜测,但是我一直找不到答案,期待大牛!
hzy694358 2011-08-03
  • 打赏
  • 举报
回复
嚯嚯, 我是打酱油的,mark
机智的呆呆 2011-08-03
  • 打赏
  • 举报
回复
ls和lss说的太好了,学习了。
2011-08-03
  • 打赏
  • 举报
回复
不过楼主说的这个情况或许可以从其他方面找到定义。

delete 表达式首先调用析构函数,然后调用 deallocation function。
non-placement delete 对应的 deallocation function 原型是 void (void*);,就是说负责内存释放的函数不需要知道对象的实际类型也能正确地回收内存。因此 delete 一个没有虚析构函数的基类指针时,可能因为析构函数的问题引起未定义行为,但不会因为对象占用内存回收的问题引起未定义行为。
2011-08-03
  • 打赏
  • 举报
回复
[Quote=引用 23 楼 frais 的回复:]
针对 undefined
如果有人觉得该意思是 c++不知道怎么做,我觉得就太荒唐了

如果是,编译阶段就应该报错,而且这就会和其他知识严重冲突

编译不报错就说明 已经知道如何操作
[/Quote]
int arr[5];
int *p = arr - 1; // 未定义行为,报错?
p = arr + 5;
*p; // 未定义行为,报错?

“undefined”不是说 C++ 不知道该怎么做,而是 C++ 根本不在乎这种情况下应该怎么做。C/C++ 从来都假设程序员知道自己在做什么,不是么?
“未定义行为”存在的意义,就是编译器设计的时候可以假定程序中永远不会出现此类行为,从而简化编译器的设计。
C/C++ 语言标准从来都是倾向于只规定正确的行为,“未定义行为”几乎可以等同于不需要检查、不需要报错的错误行为。
如果某个编译器对某类未定义行为有明确定义,那么就应当将这个行为的定义写入编译器文档,否则这个行为只能理解为编译器的偶然行为。
如果某个未定义行为的明确定义能够带来程序设计上的便利,那么讨论它的定义或许还有必要;如果定义某个未定义行为的意义只是编译器或者程序在遇到这种行为时能保证报错,那么还不如留着这个未定义行为。

以下摘自 GCC 的 Bug Zilla(http://gcc.gnu.org/bugzilla/show_bug.cgi?id=11751#c29):
Andrew Pinski 2005-02-23 20:41:16 UTC
(In reply to comment #28)
The code is undefined, which means we should be able to do system("rm -Rf /");, note we don't.
mumuliang 2011-08-03
  • 打赏
  • 举报
回复

我理解这里的undefine实际上是指,因为delete基类指针的时候,执行的是基类的析构,并非实际类型的析构,而这两个析构之间的区别只有在真实代码中才存在,也就是说编译器和标准制定者都对将来发生的事一无所知,这个未知的行为差异是undefine的。

也就是说,这种undefine的差异存粹是因为两个析构之间的差别造成的。如果在实际类型的析构里并没有比基类的析构多做一些内存回收的动作(例如实际类型中有新建堆上的内存),那么是不会泄露的。

总结一下这个undefine就是“程序猿知道但编译器和标准都不知道的未知”。

换句话说,我怎么知道你的main里面是在抹桌子还是扫地呢。
RLib 2011-08-03
  • 打赏
  • 举报
回复

int arr[5];
int *p = arr - 1; // 这叫未定义行为?
p = arr + 5;
*p; // 同上?
wjlazio 2011-08-02
  • 打赏
  • 举报
回复
看看,学习了。。。。
w20011025 2011-08-02
  • 打赏
  • 举报
回复
[Quote=引用 50 楼 kevin_qing 的回复:]
再补充下,这个是多态的实现方法,完全不明白你们为啥认为这代码是未定义行为
[/Quote]study
Lucky_6Q 2011-08-02
  • 打赏
  • 举报
回复
[Quote=引用 49 楼 kevin_qing 的回复:]
如果符合以下2点,就可以确认没有泄露。
1.基类析构函数是虚
2.子类没有重载new/delete

解释下。

1.保证了析构函数正确调用,只要你析构函数没写错,这里就不会泄露。
2.子类没有重载new/delete op那么调用的必然是基类或者global的new/delete,如果子类重载了的话基本上可以肯定会泄露或者造成内存分配错误。
delete()是不用指定大小的,那些……
[/Quote]支持
Kevin_qing 2011-08-02
  • 打赏
  • 举报
回复
再补充下,这个是多态的实现方法,完全不明白你们为啥认为这代码是未定义行为
Kevin_qing 2011-08-02
  • 打赏
  • 举报
回复
如果符合以下2点,就可以确认没有泄露。
1.基类析构函数是虚
2.子类没有重载new/delete

解释下。

1.保证了析构函数正确调用,只要你析构函数没写错,这里就不会泄露。
2.子类没有重载new/delete op那么调用的必然是基类或者global的new/delete,如果子类重载了的话基本上可以肯定会泄露或者造成内存分配错误。
delete()是不用指定大小的,那些说什么大小不对会泄露的都是瞎扯。
dinko321 2011-08-01
  • 打赏
  • 举报
回复
mark
月中蓝 2011-07-31
  • 打赏
  • 举报
回复
这个问题值得讨论
  • 打赏
  • 举报
回复
论坛不支持高亮,如果要看高亮版本的,可以到我博客去看。
http://blog.csdn.net/code_godfather/article/details/6648033#reply
taodm 2011-07-31
  • 打赏
  • 举报
回复
楼主怎么消失了?
等待升级 2011-07-31
  • 打赏
  • 举报
回复
[Quote=引用 44 楼 pengzhixi 的回复:]
引用 41 楼 frais 的回复:
引用 39 楼 demon__hunter 的回复:
类似的问题最近讨论过。知道是未定义行为即可,平时杜绝写这样的代码。
许多人还是仅仅通过编译器的运行结果来分析题,要知道面对着c+++标准明确规定的未定义的行为,即使既存的所有编译器都有相同的行为,也说明不了啥问题,这是一个典型的以偏概全的错误。


愤怒啊,迷信老外
标准为什么可以修订,学习是……
[/Quote]

你的逻辑能力实在太不敢让人恭维了
如有可能,我仅想与您代码交流
pengzhixi 2011-07-31
  • 打赏
  • 举报
回复
[Quote=引用 41 楼 frais 的回复:]
引用 39 楼 demon__hunter 的回复:
类似的问题最近讨论过。知道是未定义行为即可,平时杜绝写这样的代码。
许多人还是仅仅通过编译器的运行结果来分析题,要知道面对着c+++标准明确规定的未定义的行为,即使既存的所有编译器都有相同的行为,也说明不了啥问题,这是一个典型的以偏概全的错误。


愤怒啊,迷信老外
标准为什么可以修订,学习是一个过程。
要知道面对着c+++标准明……
[/Quote]
幸亏 标准不是一个人订的,否则又被你说一通了。我只知道编写编译器的人参考的就是这个东西。
加载更多回复(41)

64,636

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧