C++数组中多态问题分析

首先这是从酷壳网看到陈皓和云风两位大牛们讨论的一个问题,这里记录一下自己的理解。问题如下,先看一段代码

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base
 5 {
 6 public :
 7     Base(){}
 8     virtual ~Base(){
 9         cout<<"Base::~Base()"<<endl;
10     }
11 };
12 
13 class Derived : public Base
14 {
15 public:
16     Derived(){}
17     virtual ~Derived(){
18         cout<<"Derived::~Derived()"<<endl;
19     }
20 
21     int member;
22 };
23 
24 int main()
25 {
26     Derived *pd = new Derived[3];
27     Base *pb = new Derived[3];
28 
29     delete[] pd;
30     delete[] pb;
31 
32     return 0;
33 }

这段代码是不能正常工作的,在执行第30行时将内存出错,如下

Administrator@attention /e/Code
$ g++ -g Poly.cpp -o poly -lstdc++

Administrator@attention /e/Code
$ gdb -q poly.exe
Reading symbols from e:\Code\poly.exe...done.
(gdb) b 30
Breakpoint 1 at 0x401453: file Poly.cpp, line 30.
(gdb) r
Starting program: e:\Code\poly.exe
[New Thread 5132.0x8b0]
Derived::~Derived()
Base::~Base()
Derived::~Derived()
Base::~Base()
Derived::~Derived()
Base::~Base()

Breakpoint 1, main () at Poly.cpp:30
30          delete[] pb;
(gdb) n
Derived::~Derived()
Base::~Base()

Program received signal SIGSEGV, Segmentation fault.
0x00401477 in main () at Poly.cpp:30
30          delete[] pb;
(gdb)

可乍一看,也没什么错啊!利用基类指针操作子类对象,通常的多态不就是这样做的吗?但这里出错,不是多态的问题,而是内存管理的问题,而且还是C语言内存管理的问题。具体原因还得从C++对象实例的成员结构说起。含有虚函数的C++对象实例,是利用虚函数表来实现多态的,在继承体系中,动态的将虚函数表中的指针修改为具体类虚函数成员的地址,来实现利用基类指针调用子类不同实现方法的目的,即多态。为了达到这一目的,C++编译器将对象中虚函数表的指针放在对象的首部,之后才是各个成员变量。那么用基类指针指向子类对象时,虚函数表就可以直接相互对应上。具体请看 

(gdb) p *pd
$1 = {<Base> = {_vptr.Base = 0x452b98}, member = -17891602}
(gdb) p *pb
$2 = {_vptr.Base = 0x403180}

 好了了解这些之后,再回到刚才的问题,为什么会出现内存出错呢?仔细看一下gdb,单步跟踪30行执行n之后,是有打印一次"Derived::~Derived()"和"Base::~Base()"的,紧接着就出现SIGSEGV了,可见动态分配的数组中第一个成员的操作结果是正常的,那么显然是从第二个成员开始出错的。现在回想一下,在C/C++数组中,如何从上一个数组成员偏移到下一个数组成员呢?简单是将当前成员的地址+成员的尺寸,操作方法简单是将当指向上一个元素的指针进行++操作,注意这里我着重将计算方法的两个元素高亮来,上面代码出错也就是在这里。利用基类指针指向一个子类对象数组的起始地址之后,若直接利用基类指针做++操作,“成员的尺寸”使用的是基类的尺寸,而不是子类的,那么刚才的代码中,在操作第一个成员之后,意图进行偏移指向第二个成员时,由于基类和子类尺寸不相同,“成员的尺寸”不正确,而导致操作之后,基类指针不再正确的指向第二个元素了,那么虚函数表的指向也就是错误的,再进行析构函数调用,那么必然出错。

上面这些分析,是自己对陈皓大牛博文中分析过程的理解,叙述的可能不太准确。为此,我额外写了一段代码验证来一把

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class Base
 5 {
 6 public :
 7     Base(){}
 8     virtual ~Base(){
 9         cout<<"Base::~Base()"<<endl;
10     }
11     virtual void Method(void){
12         cout<<"Base::Mehod"<<endl;
13     }
14 };
15 
16 class Derived : public Base
17 {
18 public:
19     Derived(){}
20     virtual ~Derived(){
21         cout<<"Derived::~Derived()"<<endl;
22     }
23     virtual void Method(void){
24         cout<<"Derived::Mehod"<<endl;
25     }
26 
27     int member;
28 };
29 
30 int main(int argc, char *argv[])
31 {
32     Derived *pd = new Derived[3];
33     Base *pb = pd;
34 
35     pb++;
36     pb->Method();
37 
38     delete[] pd;
39 
40     return 0;
41 }

 按照上面的分析,该段代码将预期在36行出错,实际GDB跟踪如下

Administrator@attention /e/Code
$ g++ -g poly2.cpp -o poly2 -lstdc++

Administrator@attention /e/Code
$ ./poly2.exe

Administrator@attention /e/Code
$ gdb -q poly2.exe
Reading symbols from e:\Code\poly2.exe...done.
(gdb) b 35
Breakpoint 1 at 0x4013e2: file poly2.cpp, line 35.
(gdb) r
Starting program: e:\Code\poly2.exe
[New Thread 4212.0x13cc]

Breakpoint 1, main (argc=1, argv=0x2f2bc0) at poly2.cpp:35
warning: Source file is more recent than executable.
35          pb++;
(gdb) n
36          pb->Method();
(gdb) n

Program received signal SIGSEGV, Segmentation fault.
0x004013f0 in main (argc=1, argv=0x2f2bc0) at poly2.cpp:36
36          pb->Method();
(gdb)

结果和预期一样,证明自己的分析是符合事实的。

原文地址:https://www.cnblogs.com/lanyuliuyun/p/3052938.html