由虚函数展开的一些小实验

 
假设有以下三个类,是继承关系:
 1class A()
 2{
 3public:
 4    int a;        //用于测试类的大小
 5public:
 6    A()            //用于测试构造函数的调用顺序及default
 7                //constructor的必要性
 8    {
 9        cout<<"A Constructor!"
10    }
;
11
12    f()            //用于测试虚函数的特性,此处并不是虚函数。注意!
13    {
14        cout<<"f of A"
15    }
;
16}
;
17
18class B() : public A
19{
20public:
21    int B;        //用于测试类的大小
22public:
23    B()            //用于测试构造函数的调用顺序及default
24                //constructor的必要性
25    {
26        cout<<"B Constructor!"
27    }
;
28
29    virtual f()            //用于测试虚函数的特性,这里是虚函数
30    {
31        cout<<"f of B"
32    }
;
33}
;
34
35class C()
36{
37public:
38    int c;        //用于测试类的大小
39public:
40    C()            //用于测试构造函数的调用顺序及default
41                //constructor的必要性
42    {
43        cout<<"C Constructor!"
44    }
;
45
46    virtual f()            //用于测试虚函数的特性
47    {
48        cout<<"f of C"
49    }
;
50}
;
51

几点结论:
 1.构造函数调用:基类必需有无参的默认构造函数。否则派生类的构造函数无法能过编译。
   提示:no appropriate default consturctor.
 2.当new一个派生类时,如C,会依次调用所有父类的默认拷贝构造函数。
  测试代码:
   
    A* a = new c;
  输出结果为:依次调用三个构造函数。
 3.用基类指针指向派生类时,基类指针所指向对象的大小,变化为基类的大小。即由指针类型来决定的。
  测试代码1:
   C objectC;
   A* pA;
   B* pB;
   C* pC;
   pA = &objectC;
   pB = &objectC;
   pC = &objectC;
   cout<<"sizeof(*pA)"<<endl;
   cout<<"sizeof(*pB)"<<endl;
   cout<<"sizeof(*pC)"<<endl;
   这时输出的结果为:
   4  //一个int成员,大小为4字节
   12 //二个int,另外加上一个虚函数表指针,大小为12字节
   16 //三个int,另外加上一个虚函数表指针,大小为16字节
  测试代码2:
   A* pA = new C;
   B* pB = new C;
   C* pB = new C;
   cout<<"sizeof(*pA)"<<endl;
   cout<<"sizeof(*pB)"<<endl;
   cout<<"sizeof(*pC)"<<endl;
   输出结果同上。
 4.将基类指针指向派生类对象的操作,是安全的,也是常见的,如用CShape指针指向CRect,CCircle,COval,CTriangle对象。
 5.将派生类指针转化为基类指针,如:
   pC = new C;
   A* pA = pC;
   B* pB = pC;
   或者:
   A* pA = new C;
   B* pB = new C;
   同样是安全的,也是常见的。
  但是,在本文的三个类中,由于类A并没有虚函数,所以情况有一些特殊:
   1.pA->f(); //输出结果为:f of A.
   这仅仅是因为A类中恰好有同名函数f,若将该函数改名为fa(),
   则编译会出现错误:
    f() is not a member function of classs A...
   这说明,转换后,由于本身类A无虚函数,所以编译器并没有为其构造虚函数表,也没有为其加上虚函数表指针。所以
   不能寻找到函数f。即,该类没有多态能力。
   
   2.pB->f(); //输出结果为:f of C
   因为B本身也是拥有虚函数的类,所以,编译器也为其构造了虚函数表,并在类中加上虚函数表指针变量。而虚函数表中
   的内容,同类C中的虚函数表是一致的。所以,可以正确找到要调用的函数。
  需要注意的是,因为第三条的缘故,此时若
   cout<<sizeof(*pA); 
   cout<<sizeof(*pB); 
  得到的结果依然是4,12。
  这从另一个角度说明了,类的函数在内存中有单独的位置,与C语言中的函数没有差别。——并不占用类的内存空间,也不受类
  的影响。
 6.将派生类对象转换为基类对象,则会发生一些潜在的危险。这里只从虚函数的调用来说明:
    测试代码:
    C testC;
    ((A*)(&testC))->f(); //输出结果为:f of A
    ((B*)(&testC))->f(); //输出结果为:f of C
    ((B)(testC)).f();  //输出结果为:f of B
   1.第一行的结果,同上一条的分析是一致的:将派生类的指针转换成了基类指针。由于A类并没有虚函数,因此没有虚函数表
   和虚函数表指针,所以只能调用自己所拥有的、“名为f”的、函数。
   2.第二行的结果,同上一条的分析是一致的:将派生类的指针转成了基类指针。但B类有虚函数,因此有虚函数表和虚函数表
   指针,所以可以正确调用到对象testC,即类C中的f()函数。
   3.第三行的结果,是因为将一个C类的testC对象转换成一个B类的对象,产生了称之为object
   slicing
 的操作。
   这基于一个通常来说正确的结论:一个派生类的内存空间要比基类的内存空间要大
   (因为派生类通常会拥有基类没有的数据。如在这里,类C显然要比类A大。一个16,一个4。)
   因此强制转换为将testC“割”掉一部分,然后编译器会自动调用的类B的拷贝构造函数
   来给testC此时的各项赋值。在这个过程中,虚函数表也被换成了类B的虚函数表。
   所以,此时再调用f(),显然要调用B的f()函数。
   4.对比第二行和第三行的结果,非常有意思:
     将派生类指针转换为基类指针
       与
     将派生类对象转换为基类对象
    会导致不同的虚函数调用结果。
   

 

posted on 2011-05-26 22:52 lateCpp 阅读(134) 评论(0)  编辑 收藏 引用 所属分类: C/C++ 基础&算法


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿

随笔分类

文章分类

文章档案

搜索

最新评论