概述
首先考虑下面这道题:
A* pr = new B();
delete pr;
a) 运行正常,没有错误
b) 运行正常,但出现内存泄漏
c) 出现保护错,程序退出
注: 很多C++教科书都强调在析构函数不是虚函数的情况下,不要以基类指针去
delete一个对象,因此这两条语句是很不标准的作法. 我的意图是想弄明白这
样作到底会产生什么后果,以便更牢固地记住这条规则.
《-----------------------------------------------》
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()啥也不作,这段内存就泄漏了.
符合如下两个条件下这么作是完全可以使用 基类指针去 delete一个对象:
(1)派生类的析构函数中没有释放资源语句
(2)基类和派生类要么都不包含虚函数,要么都包含虚函数
再看下面一道题:
#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", 抑或其它值?
分析:
1)基类和派生类的内存映象
(2)虚函数的实现原理--虚函数表
可当你象我一样遇到上面这样莫名其妙的保护错时,肯定也想探个究竟. 很多C++书籍对这两点有很精彩的描述(例如<<Inside the C++ Object Model>>)
(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一样.
最后
以上就是含蓄哑铃为你收集整理的c++的中的指向类的指针和引用的比较的全部内容,希望文章能够帮你解决c++的中的指向类的指针和引用的比较所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复