C++数组看似支持多态

(我的观点:精通C++的未必精通C,反之亦然。我只是在C++的世界里苦苦追寻,C的世界依然陌生。) html

 

注:如下内容拷贝自 http://news.cnblogs.com/n/176728/  ,根据这篇文章的内容, 能够看到,在跨平台的时候,某平台编译器ok,未必其它编译器ok,须要事实说话。 程序员

       1) 一个 Base*[]的指针数组中,存放了一堆派生类的指针,这样,你 delete [] pBase; 只是把指针数组给删除了,并无删除指针所指向的对象。这个是最基础的C的问题。你先得 for 这个指针数组,把数据里的对象都 delete 掉,而后再删除数组。很明显,这和 C++ 没有什么关系。

  2)第二种多是:Base *pBase = new Derived[n] 这样的状况。这种状况下,delete[] pBase 明显不会调用虚析构函数(固然,这并不必定,我后面会说) ,这就是上面云风回的微博。对此,我以为若是是这个样子,这个程序员彻底没有搞懂C语言中的指针和数组是怎么一回事,也没有搞清楚, 什么是对象,什么是对象的指针和引用,这彻底就是C语言没有学好。 shell

  后来,在看到了 @GeniusVczh 的原文 《如何设计一门语言(一)——什么是坑(a)》最后时,才知道了说的是第二种状况。也就是下面的这个示例(我加了虚的析构函数这样方便编译): 编程


class Base { public: virtual ~B(){ cout <<"B::~B()"<<endl; } }; class Derived : public Base { public: virtual ~D() { cout <<"D::D~()"<<endl; } }; Base* pBase = new Derived[10]; delete[] pBase;

  C语言补课 数组

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

  对于上面的: 布局


Base* pBase = new Derived[10];

  这个语言和下面的有什么不一样吗? 测试


Derived d[10]; Base* pBase = d;

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

  那么,你以为 pBase 这个指针是指向对象的,是对象的引用,仍是指向一个数组的,是数组的引用? .net

  因而乎,你能够想像一下下面的场景:


int *pInt; char* pChar; pInt = (int*) malloc (10*sizeof(int)); pChar = (char*) pInt;

  对上面的 pInt 和 pChar 指针来讲,pInt[3]和 pChar[3]所指向的内容是否同样呢?固然不同,由于 int 是 4 个字节,char 是 1 个字节,步长不同,因此固然不同。

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

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


struct A { void *vptr; int i; }; struct B{ void *vptr; int i; char c; int j; }b[2] ={ {(void*)0x01, 100, 'a', -1}, {(void*)0x02, 200, 'A', -2} };

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

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


struct A *pa1 = (struct A*)(b);

  用 gdb 咱们能够看到下面的状况:(pa1[1]的成员的值彻底乱掉了)


(gdb) p b $7 = {{vptr = 0x1, i = 100, c = 97 'a', j = -1}, {vptr = 0x2, i = 200, c = 65 'A', j = -2}} (gdb) p pa1[0] $8 = {vptr = 0x1, i = 100} (gdb) p pa1[1] $9 = {vptr = 0x7fffffffffff, i = 2}

  咱们再来看一下堆上的状况:(咱们动态了 struct B [2],而后转成 struct A *,而后对其成员操做)


struct A *pa = (struct A*) malloc (2*sizeof(struct B)); struct B *pb = (struct B*) pa; pa[0].vptr = (void*) 0x01; pa[1].vptr = (void*) 0x02; pa[0].i = 100; pa[1].i = 200;

  用 gdb 来查看一下变量,咱们能够看到下面的状况:(pa 没问题,可是 pb[1]的内存乱掉了)


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

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

  可是,若是在 64bits 平台下,若是把咱们的 structB 改一下,改为以下(把 struct B 中的 int j 给注释掉):


struct A { void *vptr; int i; }; struct B{ void *vptr; int i; char c; //int j; <---注释掉 int j }b[2] ={ {(void*)0x01, 100, 'a'}, {(void*)0x02, 200, 'A'} };

  你就会发现,上面的内存混乱的问题都没有了,由于 struct A 和 struct B 的 size 是同样的:


(gdb) p sizeof(struct A) $6 = 16 (gdb) p sizeof(struct B) $7 = 16

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

  这就是C语言中的内存对齐,内存对齐的缘由就是为了更快的存取内存(详见《深刻理解C语言》)

  若是内存对齐了,并且 struct A 中的成员的顺序在 struct B 中是同样的并且在最前面话,那么就没有问题。

  再来看 C++ 的程序

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

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


#include using namespace std; class B { int b; public: virtual ~B(){ cout <<"B::~B()"<<endl; } }; class D: public B { int i; public: virtual ~D() { cout <<"D::~D()"<<endl; } }; int main (void) { cout << "sizeB:" << sizeof(B) << " sizeD:"<< sizeof(D) <<endl; B *pb = new D[2]; delete [] pb; return 0; }

  上面的代码能够正确执行,包括调用子类的虚函数!由于内存对齐了。在个人 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 上的相关问题讨论:《Why is it undefined behavior to delete[] an array of derived objects via a base pointer?》(一样,你也能够看看《More Effective C++》中的条款三)


Base* pBase = new Derived[10]; delete[] pBase;

  因此,微软 C++ 编程译器 define 这个事让我很是不解,对微软的 C++ 编译器再度失望,看似默默地把其编译对了很漂亮,实则误导了好多人把这种 undefined 的东西当成 defined 来用,还赞赏作得好,真是使人无语。就像微博上的这个贴同样,说 VC 多么牛,还说这是 OO 的特性。我勒个去!

相关文章
相关标签/搜索