转自:侯杰《深入浅出MFC》
虚拟函数与多态(Polymorphism)
我曾经说过,前一个例子没有办法完成这样的动作:
CShape shapes[5];
... // 令5 个shapes 各为矩形、四方形、椭圆形、圆形、三角形
for (int i=0; i<5; i++)
{
shapes[i].display;
}
可是这种所谓对象操作的一般化动作在application framework 中非常重要。作为
framework 设计者的我,总是希望能够准备一个display 函数,给我的使用者调用;不管
他根据我的这一大堆形状类别衍生出其它什么奇形怪状的类别,只要他想display,像下
面那么做就行。
CShape* pShape;
pShape->display();
Triangle
为了支持这种能力,C++ 提供了所谓的虚拟函数(virtual function)。
虚拟+ 函数?! 听起来很恐怖的样子。如果你了解汽车的离合器踩下去代表汽车空档,空档表示失去引擎本身的牵制力,你就会了解「高速行驶间煞车绝不能踩离合器」的道理并矢志遵行。好,如果你真的了解为什么需要虚拟函数以及什么情况下需要它,你就能够掌握它的灵魂与内涵,真正了解它的设计原理,并且发现认为它非常人性。并且,真正知道怎么用它。
while loop
|
Ellipse |
|
Rect |
|
|
|
Circle |
|
Circle |
|
Circle |
Square
Square
62
让我用另一个例子来展开我的说明。这个范例灵感得自Visual C++ 手册之一:Introdoction to C++。假设你的类别种类如下:
本图以Visual C++ 之「Class Info 窗口」获得程序代码实作如下:
#0001 #include <string.h>#0002
-
#0003 //---------------------------------------------------------------
-
#0004 class CEmployee // 职员
-
#0005 {
-
#0006 private:
-
#0007 char m_name[30];
#0008
-
#0009 public:
-
#0010 CEmployee();
-
#0011 CEmployee(const char* nm) { strcpy(m_name, nm); }
-
#0012 };
CEmployee
CEmployee
CManager
CManager
CWage
CWage
CSales
CSales
63
64
#0013
#0014
#0015
#0016
#0017
#0018
#0019
#0020
#0021
#0022
#0023
#0024
#0025
#0026
#0027
#0028
#0029
#0030
#0031
#0032
#0033
#0034
#0035
#0036
#0037
#0038
#0039
#0040
#0041
#0042
#0043
#0044
#0045
#0046
#0047
#0048
#0049
#0050
#0051
#0052
#0053
#0054
#0055
#0056#0057
//---------------------------------------------------------------
class CWage : public CEmployee
{
private :
float m_wage;
float m_hours;
// 时薪职员是一种职员
public :
CWage(const char* nm) : CEmployee(nm) { m_wage = 250.0; m_hours = 40.0; }void setWage(float wg) { m_wage = wg; }
void setHours(float hrs) { m_hours = hrs; }
float computePay();
};//---------------------------------------------------------------
class CSales : public CWage
{
private :
float m_comm;
float m_sale;
// 销售员是一种时薪职员
public :
CSales(const char* nm) : CWage(nm) { m_comm = m_sale = 0.0; }void setCommission(float comm) { m_comm = comm; }
void setSales(float sale) { m_sale = sale; }
float computePay();
};//---------------------------------------------------------------class CManager : public CEmployee // 经理也是一种职员
{
private :
float m_salary;
public :
CManager(const char* nm) : CEmployee(nm) { m_salary = 15000.0; }void setSalary(float salary) { m_salary = salary; }float computePay();
};//---------------------------------------------------------------void main()
{
CManager aManager("陈美静");
CSales aSales("侯俊杰");
CWage aWager("曾铭源");
}//---------------------------------------------------------------//虽然各类別的 computePay 函数都没有定义,但因为程序也没有调用之,所以无妨。
如此一来,CWage 继承了CEmployee 所有的成员(包括资料与函数),CSales 又继承了CWage 所有的成员(包括资料与函数)。在意义上, 相当于CSales 拥有资料如下:
// private data of CEmployee
char m_name[30];
// private data of CWage
float m_wage;
float m_hours;
// private data of CSales
float m_comm;
float m_sale;
以及函数如下:
void setWage(float wg);
void setHours(float hrs);
void setCommission(float comm);void setSale(float sales);void computePay();
从Visual C++ 的除错器中,我们可以看到,上例的main 执行之后,程序拥有三个对象,内容(我是指成员变量)分别为:
65
从薪水说起
虚拟函数的故事要从薪水的计算说起。根据不同职员的计薪方式,我设计computePay 函
数如下:
float CManager::computePay()
{
return m_salary; // 经理以「固定周薪」计薪。float CWage::computePay()
{
return (m_wage * m_hours); // 时薪职员以「钟点费* 每周工时」计薪。
}
float CSales::computePay()
{
// 销售员以「钟点费* 每周工时」再加上「佣金* 销售额」计薪。
return (m_wage * m_hours + m_comm * m_sale); // 语法错误。但是CSales 对象不能够直接取用CWage 的m_wage 和m_hours,因为它们是private
成员变量。所以是不是应该改为这样:
float CSales::computePay()
{
return computePay() + m_comm * m_sale;
}
这也不好,我们应该指明函数中所调用的computePay 究归谁属-- 编译器没有厉害到能
够自行判断而保证不出错。正确写法应该是:
float CSales::computePay()
{
return CWage::computePay() + m_comm * m_sale;
}
这就合乎逻辑了:销售员是一般职员的一种,他的薪水应该是以时薪职员的计薪方式作为底薪,再加上额外的销售佣金。我们看看实际情况,如果有一个销售员:
CSales aSales("侯俊杰");
}
}
66
那么侯俊杰的底薪应该是:
aSales.CWage::computePay(); // 这是销售员的底薪。注意语法。
而侯俊杰的全薪应该是:
aSales.computePay(); // 这是销售员的全薪
结论是:要调用父类别的函数,你必须使用scope resolution operator(::)明白指出。接下来我要触及对象类型的转换,这关系到指针的运用,更直接关系到为什么需要虚拟函数。了解它,对于application framework 如MFC 者的运用十分十分重要。
假设我们有两个对象:
CWage aWager;
CSales aSales("侯俊杰");销售员是时薪职员之一,因此这样做是合理的:
aWager = aSales; // 合理,销售员必定是时薪职员。这样就不合理:
aSales = aWager; // 错误,时薪职员未必是销售员。如果你一定要转换,必须使用指针,并且明显地做型别转换(cast)动作:
CWage* pWager;
CSales* pSales;
CSales aSales("侯俊杰");
pWager = &aSales; // 把一个「基础类别指针」指向衍生类别之对象,合理且自然。pSales = (CSales *)pWager; // 强迫转型。语法上可以,但不符合现实生活。
真实世界中某些时候我们会以「一种动物」来总称猫啊、狗啊、兔子猴子等等。为了某
种便利(这个便利稍后即可看到),我们也会想以「一个通用的指针」表示所有可能的
职员类型。无论如何,销售员、时薪职员、经理,都是职员,所以下面动作合情合理:
67
CEmployee* pEmployee;
CWage aWager("曾铭源");
CSales aSales("侯俊杰");
CManager aManager("陈美静");
pEmpolyee = &aWager; // 合理,因为时薪职员必是职员pEmpolyee = &aSales; // 合理,因为销售员必是职员pEmpolyee = &aManager; // 合理,因为经理必是职员
也就是说,你可以把一个「职员指针」指向任何一种职员。这带来的好处是程序设计的巨大弹性,譬如说你设计一个串行(linked list),各个元素都是职员(哪一种职员都可以),你的add 函数可能因此希望有一个「职员指针」作为参数:
add(CEmployee* pEmp); // pEmp 可以指向任何一种职员晴天霹雳
我们渐渐接触问题的核心。上述C++ 性质使真实生活经验的确在计算机语言中仿真了出来,但是万里无云的日子里却出现了一个晴天霹雳:如果你以一个「基础类别之指针」指向一个「衍生类别之对象」,那么经由此指针,你就只能够调用基础类别(而不是衍生类别)所定义的函数。因此:
CSales aSales("侯俊杰");CSales* pSales;
CWage* pWager;
pSales = &aSales;
pWager = &aSales; // 以「基础类别之指针」指向「衍生类别之对象」
pWager->setSales(800.0); // 错误(编译器会检测出来),
// 因为CWage 并没有定义setSales 函数。pSales->setSales(800.0); // 正确,调用CSales::setSales 函数。
虽然pSales 和pWager 指向同一个对象,但却因指针的原始类型而使两者之间有了差异。
延续此例,我们看另一种情况:
pWager->computePay(); // 调用CWage::computePay()pSales->computePay(); // 调用CSales::computePay()
虽然pSales 和pWager 实际上都指向CSales 对象,但是两者调用的computePay 却不
68
相同。到底调用到哪个函数,必须视指针的原始类型而定,与指针实际所指之对象无关。
三个结论
我们得到了三个结论:
1. 如果你以一个「基础类别之指针」指向「衍生类别之对象」,那么经由该指针你只能够调用基础类别所定义的函数。
CBase* pBase;
2. 如果你以一个「衍生类别之指针」指向一个「基础类别之对象」,你必须先做明显的转型动作(explicit cast)。这种作法很危险,不符合真实生活经验,在程序设计上也会带给程序员困惑。
CDerived* pDeri;
3. 如果基础类别和衍生类别都定义了「相同名称之成员函数」,那么透过对象指针调用成员函数时,到底调用到哪一个函数,必须视该指针的原始型别而定,而不是视指针实际所指之对象的型别而定。这与第1 点其实意义相通。
class CBase
BaseFunc()
虽然我们可以令pBase 实际指向CD erived 对象,却因为pBase 的类型("一个CBase* 指针")使它只能够调用BaseFunc(),不能够调用D eriFunc()。
class CDerived
|
DeriFunc() |
class CBase
CDerived *pDeri;CBase aBase("Jason");
pDeri=&aBase; //这种作法很危险,不符合真实生活经验,// 在程序设计上也会带给程序员困惑。
BaseFunc()
class CDerived
|
DeriFunc() |
69
class CBase
CBase* pBase;
CDerived* pDeri;
不论你把这两个指针指向何方,由于它们的原始类型,使它们在调用同名的Com m Func() 时有着无可改变的宿命:• pBase->CommFunc() 永远是指CBase::CommFunc
• pDeri->CommFunc() 永远是指CDerived::CommFunc
得到这些结论后,看看什么事情会困扰我们。前面我曾提到一个由职员组成的串行,如
果我想写一个printNames 函数走访串行中的每一个元素并印出职员的名字,我们可以在
CEmployee(最基础类别)中多加一个getName 函数,然后再设计一个while 循环如下:int count = 0;
CEmployee* pEmp;
...
while (pEmp = anIter.getNext())
{
count++;
cout << count << ' ' << pEmp->getName() << endl;}
你可以把anIter.getNext 想象是一个可以走访串行的函数,它传回CEmPloyee*,也因此每一次获得的指针才可以调用定义于CEmployee 中的getName。
BaseFunc()
CommFunc()
class CDerived
|
DeriFunc()
CommFunc()
|
计薪循环图
CEmployee* pEmp;
pEmp->getName();
pEmp->computePay();
while loop
|
經理 |
|
時薪職員 |
|
銷售員 |
|
時薪職員 |
|
時薪職員 |
|
銷售員 |
|
時薪職員 |
|
銷售員 |
70
但是,由于函数的调用是依赖指针的原始类型而不管它实际上指向何方(何种对象),因此如果上述while 循环中调用的是pEmp->computePay,那么while 循环所执行的将总是相同的运算,也就是CEmployee::computePay,这就糟了(销售员领到经理的薪水还不糟吗)。更糟的是,我们根本没有定义CEmployee::computePay,因为CEmployee 只是个抽象概念(一个抽象类别)。指针必须落实到具象类型上如CWage 或CManager 或CSales,才有薪资计算公式。
虚拟函数与一般化
我想你可以体会,上述的while 循环其实就是把动作「一般化」。「一般化」之所以重要,在于它可以把现在的、未来的情况统统纳入考量。将来即使有另一种名曰「顾问」的职员,上述计薪循环应该仍然能够正常运作。当然啦,「顾问」的computePay 必须设计好。「一般化」是如此重要,解决上述问题因此也就迫切起来。我们需要的是什么呢?是能够「依旧以CEmpolyee 指针代表每一种职员」,而又能够在「实际指向不同种类之职员」时,「调用到不同版本(不同类别中)之computePay」这种能力。
这种性质就是多态(polymorphism),靠虚拟函数来完成。再次看看那张计薪循环图:
■ 当pEmp 指向经理,我希望pEmp->computePay 是经理的薪水计算式,也就是CManager::computePay。
■ 当pEmp 指向销售员,我希望pEmp->computePay 是销售员的薪水计算式,也就是CSales::computePay。
■ 当pEmp 指向时薪职员,我希望pEmp->computePay 是时薪职员的薪水计算式,也就是CWage::computePay。
虚拟函数正是为了对「如果你以一个基础类别之指针指向一个衍生类别之对象,那么透
过该指针你就只能够调用基础类别所定义之成员函数」这条规则反其道而行的设计。
71
不必设计复杂的串行函数如add 或getNext 才能验证这件事,我们看看下面这个简单例子。如果我把职员一例中所有四个类别的computePay 函数前面都加上virtual 保留字,使它们成为虚拟函数,那么:
CEmployee* pEmp;
CWage aWager("曾銘源");CSales aSales("侯俊傑");CManager aManager("陳美靜");
pEmp = &aWager;
cout << pEmp->computePay(); // 调用的是 CWage::computePaypEmp = &aSales;
cout << pEmp->computePay(); // 调用的是 CSales::computePaypEmp = &aManager;
cout << pEmp->computePay(); // 调用的是 CManager::computePay
现在重新回到Shape 例子,我打算让display 成为虚拟函数:
72
#0001
#0002
#0003
#0004
#0005
#0006
#0007
#0008
#0009
#0010
#0011
#0012
#0013
#0014
#0015
#0016
#0017
#0018
#0019
#0020
#0021
#0022
#0023
#0024
#include <iostream.h>
class CShape
{
public:
virtual void display() { cout << "Shape \n"; }};
//------------------------------------------------class CEllipse : public CShape
{
public:
virtual void display() { cout << "Ellipse \n"; }};
//------------------------------------------------class CCircle : public CEllipse
{
public:
virtual void display() { cout << "Circle \n"; }};
//------------------------------------------------class CTriangle : public CShape
{
public:
virtual void display() { cout << "Triangle \n"; }};
#0025
#0026
#0027
#0028
#0029
#0030
#0031
#0032
#0033
#0034
#0035
#0036
#0037
#0038
#0039
#0040
#0041
#0042
#0043
#0044
#0045
#0046
#0047
#0048
#0049
#0050
#0051
#0052
#0053
#0054
#0055
#0056
//------------------------------------------------class CRect : public CShape
{
public:
virtual void display() { cout << "Rectangle \n"; }};
//------------------------------------------------class CSquare : public CRect
{
public:
virtual void display() { cout << "Square \n"; }};
//------------------------------------------------void main()
{
CShape aShape;
CEllipse aEllipse;CCircle aCircle;CTriangle aTriangle;
CRect aRect;
CSquare aSquare;
CShape* pShape[6] = { &aShape,
&aEllipse,
&aCircle,
&aTriangle,
&aRect,
&aSquare };
for (int i=0; i< 6; i++)
pShape[i]->display();
}//------------------------------------------------
得到的結果是:
Shape
Ellipse
Circle
Triangle
Rectangle
Square
73
如果把所有类别中的virtual 保留字拿掉,执行结果变成:
Shape
Shape
Shape
Shape
Shape
Shape
综合Employee 和Shape 两例,第一个例子是:
pEmp = &aWager;
cout << pEmp->computePay();
pEmp = &aSales;
pEmp = &aBoss;
cout << pEmp->computePay();
第二个例子是:
CShape* pShape[6];
for (int i=0; i< 6; i++)
cout << pEmp->computePay();
这三行程序码完全相同
74
pShape[i]->display(); // 此进程序代码执行了6 次。我们看到了一种奇特现象:程序代码完全一样(因为一般化了),执行结果却不相同。这
就是虚拟函数的妙用。
如果没有虚拟函数这种东西,你还是可以使用scope resolution operator(::)明白指出调用哪一个函数,但程序就不再那么优雅与弹性了。
从操作型定义来看,什么是虚拟函数呢?如果你预期衍生类别有可能重新定义某一个成员函数,那么你就在基础类别中把此函数设为virtual。MFC 有两个十分十分重要的虚拟函数:与document 有关的Serialize 函数和与view 有关的OnDraw 函数。你应该在自己的CMyDoc 和CMyView 中改写这两个虚拟函数。
多态(Polymorphism)
你看,我们以相同的指令却唤起了不同的函数,这种性质称为Polymorphism,意思是"theability to assume many forms"(多态)。编译器无法在编译时期判断pEmp->computePay到底是调用哪一个函数,必须在执行时期才能评估之,这称为后期绑定late binding 或动态绑定dynamic binding。至于C 函数或C++ 的non-virtual 函数,在编译时期就转换为一个固定地址的调用了,这称为前期绑定early binding 或静态绑定static binding。
Polymorphism 的目的,就是要让处理「基础类别之对象」的程序代码,能够完全透通地继续适当处理「衍生类别之对象」。
可以说,虚拟函数是了解多态(Polymorphism)以及动态绑定的关键。同时,它也是了解如何使用MFC 的关键。
让我再次提示你,当你设计一套类别,你并不知道使用者会衍生什么新的子类别出来。如果动物世界中出现了新品种名曰雅虎,类别使用者势必在CAnimal 之下衍生一个CYahoo。饶是如此,身为基础类别设计者的你,可以利用虚拟函数的特性,将所有动物必定会有的行为(例如哮叫roar),规划为虚拟函数,并且规划一些一般化动作(例如「让每一种动物发出一声哮叫」)。那么,虽然,你在设计基础类别以及这个一般化动作时,无法掌握使用者自行衍生的子类别,但只要他改写了roar 这个虚拟函数,你的一般化对象操作动作自然就可以调用到该函数。
再次回到前述的Shape 例子。我们说CShape 是抽象的,所以它根本不该有display 这个动作。但为了在各具象衍生类别中绘图,我们又不得不在基础类别CShape 加上display 虚拟函数。你可以定义它什么也不做(空函数):
class CShape
{
public:
virtual void display() { }
};
75
或只是给个消息:
class CShape
{
public:
virtual void display() { cout << "Shape \n"; }
};
这两种作法都不高明,因为这个函数根本就不应该被调用(CShape 是抽象的),我们根
本就不应该定义它。不定义但又必须保留一块空间(spaceholder)给它,于是C++ 提供
了所谓的纯虚拟函数:
class CShape
{
public:
virtual void display() = 0; // 注意"= 0"};
纯虚拟函数不需定义其实际动作,它的存在只是为了在衍生类别中被重新定义,只是为了提供一个多态接口。只要是拥有纯虚拟函数的类别,就是一种抽象类别,它是不能够被具象化(instantiate)的,也就是说,你不能根据它产生一个对象(你怎能说一种形状为'Shape' 的物体呢)。如果硬要强渡关山,会换来这样的编译消息:
error : illegal attempt to instantiate abstract class.
关于抽象类别,我还有一点补充。CCircle 继承了CShape 之后,如果没有改写CShape 中的纯虚拟函数,那么CCircle 本身也就成为一个拥有纯虚拟函数的类别,于是它也是一个抽象类别。
是对虚拟函数做结论的时候了:
■ 如果你期望衍生类别重新定义一个成员函数,那么你应该在基础类别中把此函
数设为virtual。
■ 以单一指令唤起不同函数,这种性质称为Polymorphism,意思是"the ability to
assume many forms",也就是多态。
■ 虚拟函数是C++ 语言的Polymorphism 性质以及动态绑定的关键。
76
-
既然抽象类别中的虚拟函数不打算被调用,我们就不应该定义它,应该把它设为纯虚拟函数(在函数声明之后加上"=0" 即可)。
-
我们可以说,拥有纯虚拟函数者为抽象类别(abstract Class),以别于所谓的具象类别(concrete class)。
-
抽象类别不能产生出对象实体,但是我们可以拥有指向抽象类别之指针,以便于操作抽象类别的各个衍生类别。
-
虚拟函数衍生下去仍为虚拟函数,而且可以省略virtual 关键词。
类别与对象大解剖
你一定很想知道虚拟函数是怎么做出来的,对不对?
如果能够了解C++ 编译器对于虚拟函数的实现方式,我们就能够知道为什么虚拟函数可以做到动态绑定。
为了达到动态绑定(后期绑定)的目的,C++ 编译器透过某个表格,在执行时期「间接」调用实际上欲绑定的函数(注意「间接」这个字眼)。这样的表格称为虚拟函数表(常被称为vtable)。每一个「内含虚拟函数的类别」,编译器都会为它做出一个虚拟函数表,表中的每一笔元素都指向一个虚拟函数的地址。此外,编译器当然也会为类别加上一项
成员变量,是一个指向该虚拟函数表的指针(常被称为vptr)。举个例:class Class1 {
public : data1; data2; memfunc(); virtual vfunc1(); virtual vfunc2(); virtual vfunc3();
};
Class1 对象实体在内存中占据这样的空间:
77
Class1 对象实体
vtable
(*vfunc1)() |
(*vfunc2)() |
(*vfunc3)() |
vptr
Class1::vfunc1()Class1::vfunc2()Class1::vfunc3()
Class1::memfunc()
class Class1
{
public :
m_data1;
78
}
m_data2;
memfunc();
virtual vfunc1();
virtual vfunc2();
virtual vfunc3();
m_data1
m_data2
C++ 类别的成员函数,你可以想象就是C 语言中的函数。它只是被编译器改过名称,并增加一个参数(this 指针),因而可以处理调用者(C++ 对象)中的成员变量。所以,你并没有在Class1 对象的内存区块中看到任何与成员函数有关的任何东西。
每一个由此类别衍生出来的对象,都有这么一个vptr。当我们透过这个对象调用虚拟函数,事实上是透过vptr 找到虚拟函数表,再找出虚拟函数的真正地址。
奥妙在于这个虚拟函数表以及这种间接调用方式。虚拟函数表的内容是依据类别中的虚
拟函数声明次序,一一填入函数指针。衍生类别会继承基础类别的虚拟函数表(以及所
有其它可以继承的成员),当我们在衍生类别中改写虚拟函数时,虚拟函数表就受了影
响:表中元素所指的函数地址将不再是基础类别的函数地址,而是衍生类别的函数地址。
看看这个例子:
class Class2 : public Class1 {
public :
data3;
memfunc();
virtual vfunc2();
};
class Class2 : public Class1{
public :
m_data3;memfunc();virtual vfunc2();
}
vtable
(*vfunc1)() |
(*vfunc2)() |
(*vfunc3)() |
vptr
Class1::vfunc1()
Class2::vfunc2()
Class1::vfunc3()Class2::memfunc()
m_data1
m_data2
m_data3
Class2 对象实体
于是,一个「指向Class1 所生对象」的指针,所调用的vfunc2 就是Class1::vfunc2,而
一个「指向Class2 所生对象」的指针,所调用的vfunc2 就是Class2::vfunc2。
动态绑定机制,在执行时期,根据虚拟函数表,做出了正确的选择。
我们解开了第二道神秘。
口说无凭,何不看点实际。观其地址,物焉C哉,下面是一个测试程序:
-
#0001 #include <iostream.h>
-
#0002 #include <stdio.h>
#0003
#0004 class ClassA
-
#0005 {
-
#0006 public:
-
#0007 int m_data1;
-
#0008 int m_data2;
-
#0009 void func1()
-
#0010 void func2()
-
#0011 virtual void
-
#0012 virtual void
-
#0013 };
#0014
-
#0015 class ClassB
-
#0016 {
-
#0017 public:
-
#0018 int m_data3;
-
#0019 void func2()
-
#0020 virtual void
-
#0021 };
#0022
-
#0023 class ClassC
-
#0024 {
{ }
{ }
vfunc1() { }vfunc2() { }
: public
{ }vfunc1()
: public
ClassA
{ }
ClassB
79
80
-
#0025 public:
-
#0026 int m_data1;
-
#0027 int m_data4;
-
#0028 void func2() { }
-
#0029 virtual void vfunc1() { }
-
#0030 };
#0031
-
#0032 void main()
-
#0033 {
-
#0034 cout << sizeof(ClassA) << endl;
-
#0035 cout << sizeof(ClassB) << endl;
-
#0036 cout << sizeof(ClassC) << endl;
#0037
-
#0038 ClassA a;
-
#0039 ClassB b;
-
#0040 ClassC c;
#0041
-
#0042 b.m_data1
-
#0043 b.m_data2
-
#0044 b.m_data3
-
#0045 c.m_data1
-
#0046 c.m_data2
-
#0047 c.m_data3
-
#0048 c.m_data4
-
#0049 c.ClassA::m_data1 = 111;
#0050
-
#0051 cout << b.m_data1 << endl;
-
#0052 cout << b.m_data2 << endl;
-
#0053 cout << b.m_data3 << endl;
-
#0054 cout << c.m_data1 << endl;
-
#0055 cout << c.m_data2 << endl;
-
#0056 cout << c.m_data3 << endl;
-
#0057 cout << c.m_data4 << endl;
-
#0058 cout << c.ClassA::m_data1 << endl;
#0059
-
#0060 cout << &b << endl;
-
#0061 cout << &(b.m_data1) << endl;
-
#0062 cout << &(b.m_data2) << endl;
-
#0063 cout << &(b.m_data3) << endl;
-
#0064 cout << &c << endl;
-
#0065 cout << &(c.m_data1) << endl;
-
#0066 cout << &(c.m_data2) << endl;
-
#0067 cout << &(c.m_data3) << endl;
-
#0068 cout << &(c.m_data4) << endl;
-
#0069 cout << &(c.ClassA::m_data1) << endl;
-
#0070 }
=1;=2;=3;= 11;= 22;= 33;= 44;
执行结果与分析如下:
执行結果 意义
12 Sizeof (ClassA)16 Sizeof (ClassB)24 Sizeof (ClassC)
-
1 b.m_data1 的內容
-
2 b.m_data2 的內容
-
3 b.m_data3 的內容
11 c.m_data1 的內容
22 c.m_data2 的內容
33 c.m_data3 的內容
44 c.m_data4 的內容
111 c.ClassA::m_data1 的內容0x0064FDCC b 对象的起始地址0x0064FDD0 b.m_data1 的地址0x0064FDD4 b.m_data2 的地址0x0064FDD8 b.m_data3 的地址0x0064FDB0 c 对象的起始地址0x0064FDC0 c.m_data1 的地址0x0064FDB8 c.m_data2 的地址0x0064FDBC c.m_data3 的地址0x0064FDC4 c.m_data4 的地址0x0064FDB4 c.ClassA::m_data1 的地址
说明
2 個 int 加上一個 vptr
继承自 ClassA,再加上 1 个 int继承自 ClassB,再加上 2 个 int
这个地址中的內容就是 vptr
这个地址中的內容就是 vptr
81
a、b、c 对象的內容图示如下:
a (ClassA 的对象)
|
m_data1 |
m_data2 |
(*vfunc1)() |
(*vfunc2)() |
ClassA::func1() |
ClassA::func2() |
|
m_data1 |
m_data2 |
m_data3 |
0x0064FDCC
0x0064FDD0
0x0064FDD4
0x0064FDD8
0x0064FDB0
0x0064FDB4
0x0064FDB8
0x0064FDBC
0x0064FDC0
0x0064FDC4
b (ClassB 的对象)
c (ClassC 的对象)
vptr vtable
vptr vtable
vptr vtable
ClassA::vfunc1()ClassA::vfunc2()
ClassB::vfunc1()ClassA::vfunc2()
ClassB::func2()
ClassC::vfunc1()ClassA::vfunc2()
ClassC::func2()
(*vfunc1)() |
(*vfunc2)() |
|
ClassA::m_data1 |
m_data2 |
m_data3 |
m_data1 |
m_data4 |
(*vfunc1)() |
(*vfunc2)() |
Object slicing 与 虚 拟 函 数我要在这里说明虚拟函数另一个极重要的行为模式。假设有三个类别,阶层关系如下:
virtual void Serialize();
void func();
virtual void Serialize();
virtual void Serialize();
CObject
CObject
CDocument
CDocument
82
CMyDoc
CMyDoc
-
#0005 public:
-
#0006 virtual void Serialize() { cout << "CObject::Serialize() \n\n"; }
-
#0007 };
#0008
-
#0009 class CDocument : public CObject
-
#0010 {
-
#0011 public:
-
#0012 int m_data1;
-
#0013 void func() { cout << "CDocument::func()" << endl;
-
#0014 Serialize();
-
#0015 }
#0016
-
#0017 virtual void Serialize() { cout << "CDocument::Serialize() \n\n"; }
-
#0018 };
#0019
-
#0020 class CMyDoc : public CDocument
-
#0021 {
-
#0022 public:
-
#0023 int m_data2;
-
#0024 virtual void Serialize() { cout << "CMyDoc::Serialize() \n\n"; }
-
#0025 };
-
#0026 //---------------------------------------------------------------
-
#0027 void main()
-
#0028 {
-
#0029 CMyDoc mydoc;
-
#0030 CMyDoc* pmydoc = new CMyDoc;
#0031
-
#0032 cout << "#1 testing" << endl;
-
#0033 mydoc.func();
#0034
-
#0035 cout << "#2 testing" << endl;
-
#0036 ((CDocument*)(&mydoc))->func();
#0037
-
#0038 cout << "#3 testing" << endl;
-
#0039 pmydoc->func();
#0040
-
#0041 cout << "#4 testing" << endl;
-
#0042 ((CDocument)mydoc).func();
-
#0043 }
第2章 C++ 的重要性質
以程序表现如下:
#0001 #include <iostream.h>#0002
-
#0003 class CObject
-
#0004 {
83
第一篇 勿在浮砂築高台