C++数组与多态

1420阅读 0评论2015-04-10 B_C_1024
分类:C/C++

以前在看深度探索C++对象模型的时候,对于P262页的那个例子很不是理解,再加上自己在VS2013上测试的结果,实在困扰了很长时间,今天看到这篇博客,记录一下。

  1. class Base
  2. {
  3.   public:
  4.     virtual ~B(){ cout <<"B::~B()"<<endl; }
  5. };
  6.  
  7. class Derived : public Base
  8. {
  9.   public:
  10.     virtual ~D() { cout <<"D::D~()"<<endl; }
  11. };
  12.  
  13. Base* pBase = new Derived[10];
  14. delete[] pBase;

C语言补课

我先不说这段C++的程序在什么情况下能正确调用派生类的析构函数,我还是先来说说C语言,这样我在后面说这段代码时你就明白了。

对于上面的:

  1. Base* pBase = new Derived[10];
这个语言和下面的有什么不同吗?
  1. Derived d[10];
  2.  
  3. Base* pBase = d;

一个是堆内存动态分配,一个是栈内存静态分配。只是内存的位置和类型不一样,在语法和使用上没有什么不一样的。(如果你把Base 和 Derived想成struct,把new想成malloc() ,你还觉得这和C++有什么关系吗?)

那么,你觉得pBase这个指针是指向对象的,是对象的引用,还是指向一个数组的,是数组的引用?

于是乎,你可以想像一下下面的场景:

  1. int *pInt; char* pChar;
  2.  
  3. pInt = (int*)malloc(10*sizeof(int));
  4.  
  5. pChar = (char*)pInt;

对上面的pInt和pChar指针来说,pInt[3]和pChar[3]所指向的内容是否一样呢?当然不一样,因为int是4个字节,char是1个字节,步长不一样,所以当然不一样。

那么再回到那个把Derived[]数组的指针转成Base类型的指针pBase,那么pBase[3]是否会指向正确的Derrived[3]呢?

我们来看个纯C语言的例程,下面有两个结构体,就像继承一样,我还别有用心地加了一个void *vptr,好像虚函数表一样:

  1. struct A {
  2.     void *vptr;
  3.     int i;
  4. };
  5.  
  6. struct B{
  7.     void *vptr;
  8.     int i;
  9.     char c;
  10.     int j;
  11. }b[2] ={
  12.     {(void*)0x01, 100, 'a', -1},
  13.     {(void*)0x02, 200, 'A', -2}
  14. };

注意:我用的是G++编译的,在64bits平台上编译的,其中的sizeof(void*)的值是8。

我们看一下栈上内存分配:

  1. struct A *pa1 = (struct A*)(b);
用gdb我们可以看到下面的情况:(pa1[1]的成员的值完全乱掉了)
  1. (gdb) p b
  2. $7 = {{vptr = 0x1, i = 100, c = 97 'a', j = -1}, {vptr = 0x2, i = 200, c = 65 'A', j = -2}}
  3. (gdb) p pa1[0]
  4. $8 = {vptr = 0x1, i = 100}
  5. (gdb) p pa1[1]
  6. $9 = {vptr = 0x7fffffffffff, i = 2}
我们再来看一下堆上的情况:(我们动态了struct B [2],然后转成struct A *,然后对其成员操作)
  1. struct A *pa = (struct A*)malloc(2*sizeof(struct B));
  2. struct B *pb = (struct B*)pa;
  3.  
  4. pa[0].vptr = (void*) 0x01;
  5. pa[1].vptr = (void*) 0x02;
  6.  
  7. pa[0].i = 100;
  8. pa[1].i = 200;
用gdb来查看一下变量,我们可以看到下面的情况:(pa没问题,但是pb[1]的内存乱掉了)

  1. (gdb) p pa[0]
  2. $1 = {vptr = 0x1, i = 100}
  3. (gdb) p pa[1]
  4. $2 = {vptr = 0x2, i = 200}
  5. (gdb) p pb[0]
  6. $3 = {vptr = 0x1, i = 100, c = 0 '\000', j = 2}
  7. (gdb) p pb[1]
  8. $4 = {vptr = 0xc8, i = 0, c = 0 '\000', j = 0}

可见,这完全就是C语言里乱转型造成了内存的混乱,这和C++一点关系都没有。而且,C++的任何一本书都说过,父类对象和子类对象的转型会带来严重的内存问题。

但是,如果在64bits平台下,如果把我们的structB改一下,改成如下(把struct B中的int j给注释掉):

  1. struct A {
  2.     void *vptr;
  3.     int i;
  4. };
  5.  
  6. struct B{
  7.     void *vptr;
  8.     int i;
  9.     char c;
  10.     //int j; <---注释掉int j
  11. }b[2] ={
  12.     {(void*)0x01, 100, 'a'},
  13.     {(void*)0x02, 200, 'A'}
  14. };
你就会发现,上面的内存混乱的问题都没有了,因为struct A和struct B的size是一样的:
  1. (gdb) p sizeof(struct A)
  2. $6 = 16
  3. (gdb) p sizeof(struct B)
  4. $7 = 16

注:如果不注释int j,那么sizeof(struct B)的值是24。

这就是C语言中的内存对齐,内存对齐的原因就是为了更快的存取内存(详见《》)

如果内存对齐了,而且struct A中的成员的顺序在struct B中是一样的而且在最前面话,那么就没有问题。再来看C++的程序

如果你看过我5年前写的《C++虚函数表解析》以及《C++内存对象布局 上篇下篇》,你就知道C++的标准会把虚函数表的指针放在类实例的最前面,你也就知道为什么我别有用心地在struct A和struct B前加了一个 void *vptr。C++之所以要加在最前面就是为了转型后,不会找不到虚表了。

好了,到这里,我们再来看C++,看下面的代码:

  1. #include
  2. using namespace std;
  3.  
  4. class B
  5. {
  6.   int b;
  7.   public:
  8.     virtual ~B(){ cout <<"B::~B()"<<endl; }
  9. };
  10.  
  11. class D: public B
  12. {
  13.   int i;
  14.   public:
  15.     virtual ~D() { cout <<"D::~D()"<<endl; }
  16. };
  17.  
  18. int main(void)
  19. {
  20.     cout << "sizeB:" << sizeof(B) << " sizeD:"<< sizeof(D) <<endl;
  21.     B *pb = new D[2];
  22.  
  23.     delete [] pb;
  24.  
  25.     return 0;
  26. }


上面的代码可以正确执行,包括调用子类的虚函数!因为内存对齐了。在我的64bits的CentOS上——sizeof(B):16 ,sizeof(D):16

但是,如果你在class D中再加一个int成员的问题,这个程序就Segmentation fault了。因为—— sizeof(B):16 ,sizeof(D):24。pb[1]的虚表找到了一个错误的内存上,内存乱掉了。

再注:我在Visual Studio 2010上做了一下测试,对于 struct 来说,其表现和gcc的是一样的,但对于class的代码来说,其可以“正确调用到虚函数”无论父类和子类有没有一样的size。

然而,在C++的标准中,下面这样的用法是undefined! 你可以看看StackOverflow上的相关问题讨论:《》(同样,你也可以看看《More Effective C++》中的条款三)

  1. Base* pBase = new Derived[10];
  2.  
  3. delete[] pBase;
现在,你终于知道Base* pBase = new Derived[10];这个问题是C语言的转型的问题,你也应该知道用于数组的指针是怎么回事了吧?

















上一篇:阿里巴巴2015校园招聘算法题
下一篇:几种TCP连接中出现RST的情况