假设有以下三个类,是继承关系:
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.对比第二行和第三行的结果,非常有意思:
将派生类指针转换为基类指针
与
将派生类对象转换为基类对象
会导致不同的虚函数调用结果。