出道题给大家玩玩

deng2000 2007-03-18 07:04:30
闲来无聊,出道题给大家玩一玩.只博一哂,呵呵

#include <stdio.h>

class A
{
public:
int m_a;
};

class B : public A
{
public:
int m_b;
void virtual fun() {}
};

int main()
{
B b;
B* pb = &b;
A* pa = pb;
void *pb2 = (void *)pb;
void *pa2 = (void *)pa;

const char *str1 = (pa==pb) ? "yes" : "no";
const char *str2 = (pa2==pb2) ? "yes" : "no";

printf("%s %s\n", str1, str2);
return 0;
}

你能预测程序的结果吗? 是 "yes yes" 还是 "yes no", 抑或其它值?
有些牛人可能会说结果依赖于编译器, 那让我问得更具体些:
用Visual C++编译后结果如何, 在C++ Builder中又怎样呢?
...全文
852 20 打赏 收藏 转发到动态 举报
写回复
用AI写文章
20 条回复
切换为时间正序
请发表友善的回复…
发表回复
sufei1727 2007-03-27
  • 打赏
  • 举报
回复
分析的很好,顶....
Hemee 2007-03-24
  • 打赏
  • 举报
回复
mark
maplewasp 2007-03-23
  • 打赏
  • 举报
回复
偶用gcc编译器验证的结果是: yes, yes
bladeIII 2007-03-23
  • 打赏
  • 举报
回复
在这里一些常规的想当然行不通了,编译器作了太多的手脚。
看看汇编代码就知道是怎么回事了。
femalelover 2007-03-23
  • 打赏
  • 举报
回复
pa, pb, pa2, pb2是四块四字节的内存, 里面记录着同一个0X.....的地址, 就是对象b的首地址. 因此我觉得pa, pb, pa2, pb2四个都是相等的.
至于为什么答案是yes, no, 就不伤脑筋了.
pubb1986 2007-03-23
  • 打赏
  • 举报
回复
挖 一个指针cast成另一种类型指针后,可能已指向不同的内存地址! 明白明白。。。

zhouzhouboa 2007-03-23
  • 打赏
  • 举报
回复
菜鸟一个,左右还是没看懂,回去补补在来
CQZE 2007-03-19
  • 打赏
  • 举报
回复
试了.你用VC得到错误是因为你用的是Debug模式

事实上.
A* pr = new B();
delete pr;
这段代码确实是正常的(准确地说是代码是错误的,预留了BUG。但是程序确实不会是错误的)。因为A的析构是trivial的。

不信请你在Release模式下尝试下面代码
A* ptr = (A*)0x50;
delete [] ptr;
错误地把0x50这个地址传递给ptr.更错误地delete[] 一个这样的A对象.
但是程序没有一点异常的反应。

最后还是说。这样写代码是绝对禁止的。
todototry 2007-03-19
  • 打赏
  • 举报
回复
mark
CQZE 2007-03-19
  • 打赏
  • 举报
回复
b) 运行正常,但出现内存泄漏
对对.应该是这个.嘿嘿

zbw8080 2007-03-19
  • 打赏
  • 举报
回复
有意思
deng2000 2007-03-19
  • 打赏
  • 举报
回复
该兑现之前的承诺解释一下来龙去脉了. 有两点需要先弄清楚:
(1)基类和派生类的内存映象
(2)虚函数的实现原理--虚函数表
"Come on", 我听到抗议声,"作为C++ Coder, 有面向对象思想,知道虚函数概念就可以了,用得着这样费劲吗?" 是的,绝大多数情况下用不着, 可当你象我一样遇到上面这样莫名其妙的保护错时,肯定也想探个究竟. 很多C++书籍对这两点有很精彩的描述(例如<<Inside the C++ Object Model>>),CSDN上也是高手如云,我再多讲就纯属班门弄斧了,只粗略地列举两个事实以便讨论:
(1)在内存布局中,基类与派生类是"头对齐"的.派生类的前半部分就是基类.(声明: 不考虑多重继承)
在我们的例子中(没有虚函数的情况下), b对象的内存映象大概是这样的
   -----------------------
   | A  |
   -----------------------
   |   B   |
   -----------------------
   | m_a | m_b |

因此,指向派生类的指针(B*)同时又是指向基类的指针(A*)

(2)有虚函数的对象在最前面有4字节是指向虚函数表的指针, 因此B类型对象在内存中实际应该是这样的:
   -----------------------
   | vptr | m_a | m_b  |

高手们可能会觉得可笑,有一天我想到例子中的这种情况时突然纳闷不已: 这两点根本就是相互矛盾的! 因为A没有vptr而B有. 怎么可能作到"头对齐"而且B的开头是vptr? 我兴冲冲地用VC跟了一遍看看它怎么处理的.结果让我颇为惊叹. VC的处理方法是照顾虚函数表而牺牲"头对齐"原则, 真正的内存映象是这样的:
   --------------------------
        | A  |
   --------------------------
   |      B      |
   --------------------------
   | vptr | m_a | m_b |

在我们的例子中, "B* pb = &b;" 使得pb指向B的开头,也就是vptr的位置. 接下来"A* pa = pb;"就很有意思了.编译器让pa指向哪呢? 总不能也指向vptr的位置吧? 结果你可能猜到了,编译器把pa向后挪了4字节,使其依然指向A(也就是m_a)的位置. "等等",反应快的同学会问了,"这样一来pa与pb指的就不是同一个位置了.那(pa==pb)为何还为true呢?". 呵呵,这就是编译器的聪明之处,它分析这两个指针指向的是基类/派生类的关系,就会考虑进去这4字节偏差而返回true. 而当我们把二者cast成void *类型(pa2与pb2)后,编译器就啥也不知道了.对(pa2==pb2)老老实实地返回false. 第二题的保护错也是同样道理,我在上面已解释过.总之,这件事给我最深刻的教训就是:一个指针cast成另一种类型指针后,可能已指向不同的内存地址!
btw,我还没说C++Builder中的情况.与VC不同,在BCB中程序的结果是"yes yes". 这是因为BCB不管你类中有没有虚函数,上来就给一个虚函数表指针,以便往其中塞一些运行时类型识别之类的私活.因此就不存在例子程序的这种问题了.真个是一劳永逸,呵呵(代价是所有对象都无缘无故增大了4字节).我手头没有gcc环境,但我想gcc的结果应该与VC一样.
deng2000 2007-03-19
  • 打赏
  • 举报
回复
Really? 楼上的是在什么环境下测试的? 我用VC6.0编译运行的结果总是保护错,无论是debug版还是release版. 不过你说的"delete (A*)0x50"这种语句在Release版中倒确实没有出错,挺有意思,以后有时间可以瞅瞅.
回到我们的问题. 在C++中"delete pr"这条语句大致作了两件事:
1 调用pr所指对象的析构函数(再调用基类的析构函数,etc)
2 释放pr所指对象本身所在的内存.(用operator delete, 最终好象是调用free()来完成)

在我们的程序中, 因为pr的类型是A*(虽然它所指的对象实际上是B类型), 第1步只调用A的析构函数,B的析构函数没有被执行. 不过正如楼上所说,在我们的例子中这不会有什么问题,因为~B()啥也没干.
麻烦出在第2步. 由于虚函数表的存在, 在 "A* pr = new B()" 后, pr实际上没有指向所生成的B对象的开始,而是在其后4字节(这是所有问题的关键,我以后会解释.记得我们第一题中pa2!=pb2吗? 事实上pa2==pb2+4). 因此free()在释放内存时,它的指针参数并不是程序之前申请的空间的开始位置, 而是其后4字节! 你说free()会如何是好? 可以预料只可能有两种结果: 或者立即出保护错, 或者, 如果free()啥也不作,这段内存就泄漏了.
因此楼上的即使在release版下没有出错,结果也不是(a),而是(b), 我可以肯定有内存泄漏. 换句话说,如果
A* pr = new B();
delete pr;
运行正常的话(对此我怀疑),那如下的荒诞语句
A* pr = new B();
delete pr;
pr->m_a = 5;
也会正常运行.

诚然,在实际中我们只要遵守"不以基类指针delete对象"原则就不会有这么多麻烦.但C++的魅力之一之处就在于你可以探究其所以然. 至少通过这些分析,我们可以断言,符合如下两个条件下这么作是完全可以的:
(1)派生类的析构函数中没有释放资源语句
(2)基类和派生类要么都不包含虚函数,要么都包含虚函数
(当然,不可能基类中有虚函数而派生类中没有:) )
afgkidy 2007-03-19
  • 打赏
  • 举报
回复
有点难哦,头混了!
deng2000 2007-03-18
  • 打赏
  • 举报
回复
a) 运行正常,没有错误

因为B的析构是trivial的
===============================================

CQZE同学,你用VC测试过吗,结果恐怕出乎你的意料
CQZE 2007-03-18
  • 打赏
  • 举报
回复
a) 运行正常,没有错误

因为B的析构是trivial的
deng2000 2007-03-18
  • 打赏
  • 举报
回复
用VC++是yes no;
LZ能讲一下原因吗?

=======================================================

我随后会解释(CQZE说得对,trick就在虚函数表). 但在这之前容我再提一
个更有实际意义的问题:
在VC中,以上的class A和class B的定义下,如下语句会导致什么结果:

A* pr = new B();
delete pr;

a) 运行正常,没有错误
b) 运行正常,但出现内存泄漏
c) 出现保护错,程序退出

注: 很多C++教科书都强调在析构函数不是虚函数的情况下,不要以基类指针去
delete一个对象,因此这两条语句是很不标准的作法. 我的意图是想弄明白这
样作到底会产生什么后果,以便更牢固地记住这条规则.
albertMn 2007-03-18
  • 打赏
  • 举报
回复
用VC++是yes no;
LZ能讲一下原因吗?
CQZE 2007-03-18
  • 打赏
  • 举报
回复
当然是yes no,或者 yes yes.莫非还有其他情况?
只要看编译器把vptr是放在B对象的前面还是后面了
littlegang 2007-03-18
  • 打赏
  • 举报
回复
感觉在VC++下像是 yes yes

64,636

社区成员

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

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