C/C++程序设计
1
多态是面向对象理论的三大支柱之一。
多态是不能凭空独立的支柱,需要另外两个支柱构成三足鼎立之势,另外两个支柱是封装和继承。
封装是数据和算法的抽象描述,继承是关于抽象描述的演化发展,多态则是对于程序流程前瞻性的刻画。
简单的说多态是基对象的指针调用派生类的虚函数。
C/C++程序设计
2
五、名称索引的优先级六、虚函数动态绑定调用算例七、虚拟析构函数八、纯虚函数和抽象类
C/C++程序设计
3
五、名称索引的优先级在类上下继承层次中使用成员名称有两种方式:一种是有意模棱两可的 (implicit)隐约索引。即不带类域分辨符方式的索引如,Draw(x,y),Draw 是成员函数的函数名。
另一种是一清二楚 (explicit)的带类域分辨符方式的全限定索引如,CBase::Draw(x,y),严格全限定索引在避免函数的递归调用是必须的。
隐约索引的目的一方面是减少代码书写量,另一方面替虚拟函数的隐约调用铺平道路。
在成员函数中出现的名称编译器根据下面的次序搜寻:
C/C++程序设计
4
1,首先编译器在当前的内层块范围搜寻,由内向外一直到最外层的函数范围,包括函数的形参和隐约名称如 m_n或者如 Draw(x,y)。
2,编译器在当前 (最晚 )派生的类中搜寻所出现的名称。
3,如果在当前类中未搜寻到指定的隐约名称,则上溯到直接基类中搜寻直到根基类。
4,然后在全局范围内搜寻。
5,ClassX::m_f 或 this->ClassX::m_f或,:g_f的全限定名方式清晰地强制编译器搜寻指定的唯一名称。分辨符,:左边的名称是 class,struct,union或 enum引入的类型名。
6.如果名称前有关键字 class,struct和 union,强制编译器搜寻它们引入的数据类型名。
C/C++程序设计
5
对于 obj.m_n 或 pobj-> Draw(x,y)访问格式,如果派生类没有提供相关的成员,则使用从基类继承的可访问成员。
如果派生类中存在相应的成员优先采用派生类的覆盖成员。
这称为优先采用派生类成员名称的支配原则。
对于虚函数,对象指针的动态类型确定继承树层次搜寻的入口点,对于非虚函数对象指针的静态类型确定继承树层次的切入点。
如果两者的类型都是 ClassX*,则以 ClassX作为搜寻的入口点,向上搜寻指定的成员直到最快找到指定的成员为止。
C/C++程序设计
6
六,虚函数动态绑定调用算例基类的虚函数是公共的,派生类的覆盖版本可以是私有的。
这样仅允许基对象指针在上层动态调用派生类的虚函数,而不由对象直接外部访问,这保证虚函数族入口的唯一性。
C/C++程序设计
7
[例 ] 关键字 virtual的开关作用和隐约调用
#include <stdio.h>
class ClassX
{public,virtual void F();
private,long m_nx;
};
void ClassX::F()
{ printf ("ClassX=%d,",sizeof (ClassX)); }
class ClassY:public ClassX
{ private,long m_ny;
void F();
};
C/C++程序设计
8
void ClassY::F(){ printf("ClassY=%d,",sizeof(ClassY)); }
void Showv (ClassX objx) { objx.F(); }
void Show (ClassX* pBase){ pBase->F(); }
void Show (ClassX& r) { r.ClassX::F(); }
void main ()
{ ClassX* pobjx=new ClassX();
ClassY objy; int k=1;
scanf ("%d",&k);
if (k==1)
{ Show (pobjx); Show (&objy); }
else { Show (*pobjx); Show(objy); }
Showv (objy);
}
C/C++程序设计
9
说明:
在 ClassX类中成员函数 F()前带关键字 virtual且 k=1输出结果:
ClassX=8,ClassY=12,ClassX=8,当 k=2输出,
ClassX=8,ClassX=8,ClassX=8,
在 ClassX类中成员函数 F()前不带关键字 virtual即去掉
virtual时程序输出结果:
ClassX=4,ClassX=4,ClassX=4,
C/C++程序设计
10
类中函数声明中的前置关键字 virtual对于成员函数其着动静绑定的切换作用,在成员函数的调用点
[ void Show(ClassX* pBase) {pBase->F();} ]
pBase->F();
编译器扫描声明部分,如果得知 F是非虚函数则进行静态联编,即根据 pBase的静态类型进行函数的调用匹配,
pBase的静态类型就是形参列表中定义的类型即 ClassX*,
因此 [pBase->F();]的隐约调用转换为全限定名的显式调用
[pBase->ClassX::F();],这就是静态联编。
C/C++程序设计
11
不管程序运行过程中虚实结合时 pBase得到何值,调用的总是早诞生的成员函数版本。
上面多次调用中 pBase得到的对象指针动态值是不同的,但一律执行基类的版本。
如果探查到 Show是虚函数则进行动态绑定,即根据
pBase的动态类型进行函数的调用匹配。
如果虚实结合时 pBase的动态类型为 ClassY*,则调用虚函数 ClassY::F()。 pBase的动态类型为 ClassX*,则调用虚函数 ClassX::F()。
C/C++程序设计
12
[例 ] 静态联编调用非虚成员函数,派生类的覆盖版本是公共的
#include <stdio.h>
class ClassX { long m_nx; public,void F(); };
class ClassY:public ClassX
{ long m_ny; public,void F(); };
void ClassX::F()
{ printf ( "ClassX=%d,",sizeof (ClassX)); }
void ClassY::F()
{ printf ("ClassY=%d,",sizeof (ClassY)); }
enum { classx,classy };
C/C++程序设计
13
void Show (ClassX* pBase,int morph)
{ switch (morph)
{ case classx,pBase->F();break;
case classy,((ClassY*)pBase) ->F ();break;
}
}
void main()
{ Show (new ClassX(),classx);
Show (new ClassY,classy);
}
//输出,ClassX=4,ClassY=8,
C/C++程序设计
14
多态类对象内存的代价是明显的,即相应于非多态类的对象增添 4个字节的内存空间,从 [4,8]对应地增至 [8,12]。
获得的好处就是只需简单的写一条隐约调用的语句,编译器替程序员作了许多常规的分支判断选择。
C/C++程序设计
15
[例 ] 对象引用的隐含类型转换作用
# include<stdio.h>
class CIo {public:char obuffer[48];};
class CStream,public CIo
{public:CStream& operator<<(double); };
class CWithassign,public CStream {public,};
CStream& operator<<(CStream& r,int n)
{static const char* fmt = "operator<<( CStream&,%d);";
sprintf (r.obuffer,fmt,n);
printf ("%s",r.obuffer);
return r;
}
C/C++程序设计
16
CStream& CStream::operator<<(double d)
{static const char* fmt = "CStream::operator<<
(double d=%f);\n";
sprintf (obuffer,fmt,d);
printf ("\n%s",obuffer);
return *this;
}
void main ()
{ CWithassign wout; operator<<(wout,1);
wout<<2<<3; wout<<4.0;
CIo cio; operator<<((CStream&) cio,5);
(CStream&) cio<<6<<7;
}
C/C++程序设计
17
//程序运行输出结果,
operator<<(CStream&,1); operator<<( CStream&,2);
operator<<( CStream&,3);
CStream::operator<<(double d=4.0000000);
operator<<(CStream&,5); operator<<( CStream&,6);
operator<<( CStream&,7);
引用形式强制类型转换的含义是根据目标类型描述的数据结构和算法操作源对象的内存数据。
本题中对象数据由根基类中声明,派生类提供算法,算法在内存中不属于对象,因此对象引用的上下映射在内存的角度看是一致的。
C/C++程序设计
18
函数调用 operator<<(wout,1) 可以等价地写为
operator<<((CStream&)wout,1),
在接口处对应着 CStream& r=wout的继承层次上下间对象引用隐含类型转换。
简洁的调用形式 [wout<<2<<3; wout<<4.0;]也采用向上映射的默许规则。
函数调用 operator<<((CStream&) cio,5)采用向下映射形式的强制类型转换,含义为基对象借用派生类的算法操作基对象的内存数据。
隐含类型转换对于非虚成员函数不起动态绑定的作用。
C/C++程序设计
19
对于 ostream类库的运算符重载函数 operator<<以及
istream类库的 operator>>运算符重载函数而言,这一默许的类型转换机制有利于它们被派生类对象隐含地继承调用,
提供统一的接口而无须强制类型转换。
sprintf 函数是格式化转换函数,sprintf函数的目的地是一个字符缓冲区,其变换规则与 printf 函数相同。
格式为:
int sprintf (char *buffer,const char * format,
variable_list);
sprintf返回实际写入 buffer数组的字符个数。
C/C++程序设计
20
七、虚拟析构函数构造函数本身决不作为虚函数。
构造函数中可以调用虚函数,但这个虚函数不起向下的动态绑定的作用。
对于存在虚函数的类,将析构函数声明为虚拟的成员函数是至关重要的,这样可以保证内存的泄漏降至最少。
在基类的析构函数前加上关键字 virtual声明,则该析构函数就成为虚析构函数,派生类的析构函数也跟着成为虚析构函数,派生类的析构函数前可以明显地前置 virtual声明,
也可以隐含的省去不写。
C/C++程序设计
21
[例 ] 虚析构函数在动态绑定中的重要性
#include <stdio.h>
class ClassX
{ public,virtual ~ClassX(){printf("~ClassX();"); }
protected,ClassX() { printf("ClassX();"); } };
class ClassY:public ClassX
{ public,ClassY (int n=1)
{ m_n=n; m_p=new long [m_n]; printf ("ClassY();"); }
private,~ClassY()
{ delete [ ] m_p; printf ("~ClassY();"); }
long m_n; long* m_p;
};
C/C++程序设计
22
void main()
{ ClassX* pobjx= new ClassY();
delete pobjx;
}
当基类析构函数是虚函数时程序输出结果:
ClassX(); ClassY(); ~ClassY(); ~ClassX();
去掉基类析构函数前的关键字 virtual输出结果:
ClassX(); ClassY(); ~ClassX();
C/C++程序设计
23
new ClassY()导致系统先调用 new运算符函数,new运算符函数调用的结果如果成功,则继续调用构造函数
ClassY(int),派生类的构造函数诱发自基类构造函数起从上到下的连续序列调用。
delete 的调用次序与 new运算符相反,delete首先调用
new运算符诞生的 heap对象所隶属的析构函数,然后释放
new运算符动态获取的内存空间。
但此处由于派生类对象指针向上映射的结果,new
ClassY()诞生的对象是派生类的对象,但接管该对象的指针则是基对象指针,这是编译器暗许的类型转换。
C/C++程序设计
24
对于此种指针动静类型不一致的情形:
ClassX* pobjx= new ClassY(); delete pobjx;
delete pObjx激发的析构函数的调用就根据析构函数的虚拟与否进行选择:
1,析构函数若是虚拟的,则 pobjx的动态类型决定调用
ClassY类的析构函数这实际上也就是保证 delete pobjx 能够暗中正确的还原为 delete pobjy,以便和 pobjx=new ClassY()遥相呼应。
2,析构函数是非虚拟的,则 pobjx的静态类型决定调用
ClassX类的析构函数
C/C++程序设计
25
八,纯虚函数和抽象类多态是用基对象的指针调用派生类的虚函数,此种调用在基对象指针静态赋值指向距离顶层基类较远的派生类对象时,容易导致上层基类虚函数形成死码的局面。
死码是程序运行时处理器不能到达的指令。于是提出一个明显的指标,提醒基类的虚函数应该轻灵短小。
C/C++程序设计
26
最空灵的虚函数莫过于一无所有的虚函数,以一个 =0
来标志这样的虚函数如下:
class ClassA
{ public:
virtual ~ClassA ()=0 {}
virtual int vPureFunct (...)=0;
virtual int PureFunct ()=0 {return 1;}
int m_a;
};
C/C++程序设计
27
在基类的声明格式中以 =0作标志的虚函数称为纯虚函数。纯虚函数一般不带实现部分,期待派生类提供覆盖版本;纯虚函数在定义的时候也可以将空函数作为函数部分,
这在纯虚析构函数是必要的。
对于多态类析构函数通常是虚拟函数,一般不作为纯虚函数。
将至少存在一个纯虚函数的类称为抽象类。抽象类旨在为多态类提供统一的无负担的接口 (尽量避免死码的出现 ),
而接口的有份量的激活的代码由派生类覆盖版本提供。
抽象类的特点是不能用其定义对象,即
[ClassA *pObja=new ClassA();]
和 [ClassA obja;] 是非法的定义语句。
C/C++程序设计
28
以上这种抽象类无实例的概念渗透到下面几个层面:
1,无抽象类的对象即无 ClassA obja但可以有抽象类的对象指针如 ClassA* pObja;
2,无抽象类的数值形参即无 f(ClassA a)但可以有抽象类的引用形参如 f(ClassA& r);
3,不返回抽象类的对象即无 ClassA f() 到可以返回抽象类的指针如 ClassA* f();
抽象类的构造函数不宜调用纯虚函数,无论直接或间接,否则结果是不确定的。由于不存在抽象类的对象,因此抽象类基对象指针由具体派生类对象的地址初始化,抽象类的构造函数适宜处理为保护的访问控制属性,由派生类构造函数调用。
C/C++程序设计
29
抽象类常作为多态类的基类,在抽象基类派生的类如果出现了全不等于 0的虚函数的覆盖版本,称为具体类。
抽象类的直接基类可以是具体类,具体类的行为不受其后派生的抽象类的影响。抽象类的位置和其它类一样是非常灵活的。
如 MFC类中一个至关重要的纯虚函数 OnDraw就是首度在视图类 CView中亮相的,因此程序员必须在 CView之下派生类如 CUserView中提供不等于 0的 OnDraw实现。
每一次窗口重绘,应用程序框架通过多态机制调用派生类的 OnDraw函数。
C/C++程序设计
30
[例 ] 函数调用运算符成员函数 operator()作为虚函数
#include <stdio.h>
struct SB
{ virtual int operator()(int i)=0{return a[i]; }
static int a[ ]; };
struct SC:public SB{ int operator()(int i) {return a[i+1];}};
struct SD:public SC{ int operator()(int i) {return a[i+2];}};
int f (SB& r){ return r(1);}
void main()
{ SD d; SC c; //输出,3,2,1
printf ("%d,%d,%d\n",f(d),f(c),d.SB::operator ()(1)); }
int SB::a[ ]={0,1,2,3,4,5,6,7,8,9,10};
C/C++程序设计
31
[例 ] 指向成员函数的指针和虚函数指向成员函数的指针可以捕获虚函数的地址,一旦虚函数地址赋给成员函数指针,这个指针在关联虚函数地址期间,对成员函数指针的间接访问导致虚函数的动态绑定。
#include <stdio.h>
class B;
void (B::*pfm)();
class B
{ virtual void f()=0;
public:static void set () {pfm=&B::f; }
};
class D:public B{ void f(){printf ("D::f();\n");} };
C/C++程序设计
32
void main()
{ D objd;
B*pobjb=&objd;
B::set();
(pobjb->*pfm)(); //等价于 pObjb->f();
}
//输出结果,D::f();
指向成员函数的指针由对象指针 pobjb来调度,间接调用 (pObjb->*pfm)()在成员函数指针关联虚函数期间诱发多态的机制,根据 pobjb的动态类型进行函数调用的匹配。
C/C++程序设计
33