10.4 虚基类在多重继承过程中,可能发生在最终的派生类中存在某一个基类的多个副本的现象。比如,“大学生”类和“职工”类均可以从“人”类派生出来,“职工大学生”又可以通过从“大学生”类和“职工”类多重继承而来。但这个最终类中存在两个
“人”类的副本。如下图所示。
Person Person
Student Worker
Work_Stu
Person
Person
Student
Worker
pName
uAge
uSex
pName
uAge
uSex
pDepartment
pDepartment
pSpeciality
pJob
从上图可见,类中存在某一基类的多个副本不仅浪费了存储空间,而且导致二义性。更为严重的是为了保持多个副本中数据成员值的同步,将使操作变得极为复杂。
若欲保证派生类中仅有所有基类的一个副本,则应将其所有基类说明为虚基类。说明虚基类的一般形式为:
class derivative,virtual <access> base<,...>
{
//…
};
其中,关键字 virtual 既可以放在访问控制字之前,也可以放在访问控制字之后。
一个类无论是否存在虚基类,其对象的使用方式完全一样。
例如,可以用以下的形式定义类 Student,Worker 和 Work_stu:
class Student,virtual public Person {
private:
char *pDepartment; // 系部
char *pSpeciality; // 专业
// …
};
class Worker,virtual public Person {
private:
char *pDepartment; // 工作部门
char *pJob; // 工种
// …
};
class Work_Stu,public Student,public Work {
// …
};
这样,类 Work_Stu 的派生关系就如下图所示(注意:该类的对象中,数据成员 pDepartment 存在二义性)。
Person
Student
Work_Stu
Worker
pName
uAge
uSex
pDepartment
pDepartment
pSpeciality
pJob
第 11章 类的其它特性
11.1 友元函数
11.1.1 友元函数的说明和定义友元函数也叫友元,是在类中说明的、用关键字 friend 修饰的 普通函数 。友元虽然不是类中的成员函数,但却拥有访问类中任何成员的特权。说明友元的一般形式为:
friend type func_name(arg_list);
注意:友元的函数原型必须在类中说明,且不受访问权限的制约。说明它的类授予它有访问类中所有成员的权利。
#include <iostream.h>
class X {
private:
int x;
friend long Power(X&,int);
public:
X(int a = 0),x(a) {}
void Set(int a) { x = a; }
int Get() { return x; }
};
// 待续由于友元不是类中的成员函数而只是一个普通函数,所以习惯上总是在类外定义,即使是内联函数也是如此。对于本例而言,上述的“待续”由以下的内容替换:
long Power(X& rx,int exp)
{
long power = 1;
while(exp > 0) {
power *= rx.x;
exp --;
}
return power;
}
// 待续注意:由于友元 Power() 不是类 X 的成员函数,所以 this 指针与它无关。为了能够访问类中的成员,就只有利用参数将
X 类的对象传给它以使其有访问对象中成员的机会。
友元至少应当有一个说明它的类类型的参数 。
11.1.2 使用友元函数友元是一个普通函数,因此它与所有非成员函数的用法完全一样。例,用以下内容替换上述“待续”:
void main()
{
X aX(5);
cout << Power(aX,3) << endl; // 输出 125
}
11.1.3 成员函数用作友元从理论上讲,只要类给予授权,任何一个函数都可以成为友元。
自然,一个类中的成员函数可以作为另一个类的友元。例:
class Y; // 超前说明
class X {
int x;
public:
X(int a = 0),x(a) {}
void Set(Y& ry) { x = ry.y; }
int Get() { return x; }
};
class Y {
int y;
public:
Y(int a = 0),y(a) {}
void Set(int a) { y = a; }
int Get() { return y; }
friend void X,,Set(Y&);
};
#include <iostream.h>
void main()
{
Y aY(5);
X aX;
aX.Set(aY);
cout << aX.Get() << '\t' << aY.Get() << endl;
}
在实用中,上述用法极少使用。由于类的封装性使得其外部难以破坏其内部数据,所以,若需要将一个类中的某个成员函数说明为另一个类的友元,特别是需要将多个成员函数说明为另一个类的友元时,常常直接将整个类说明成另一个类的友元。
将一个类说明成另一个类的友元,实际上是将类中所有的成员函数说明成另一个类的友元。例如,上例可以将类 Y 中的友元说明改为:
friend class X;
这样就将类 X 中所有成员函数都 说明成类 Y 的友元。
应当说明的是:虽然使用友元可以提高程序的运行效率,但友元就像在类的封装上打了一些洞。这样的洞越多类的封装就被破坏的越严重。因此在实用中要仔细地权衡效率和安全之间的矛盾,以决定是否使用以及如何使用友元。另外,当类 A 被授权为类 B 的友元后,只有类 A 中包含有类 B 参数的成员函数才有条件访问类 B 中的所有成员。
11.2 虚函数
11.2.1 虚函数虚函数是类中的一个用关键字 virtual 修饰的成员函数。
virtual type func_name(<arg_List>)
{
func_body;
}
一个函数一经说明为虚函数,则无论说明它的类被继承了多少层,在每一层派生类中该函数将永远保持其 virtual 特性。
定义虚函数的目的是为了让派生类 覆盖 ( Overriding)它。覆盖不同于重载,它要求重新定义的函数在参数和返回值方面与原函数完全相同。否则将属于重载(参数不同)或导致一个编译错误(返回值类型不同)。与函数重载相同,虚函数也体现了 OOP 技术的多态性。
class X {
//…
public:
virtual void vFunc() { cout << "X" << endl; }
//…
};
class Y,public X {
public:
//…
};
class Z,public Y {
//…
public:
virtual void vFunc() { cout << "Z" << endl; }
//…
};
X aX;
Y aY;
Z aZ;
aX.vFunc(); // 输出 X
aY.vFunc(); // 输出 X
aZ.vFunc(); // 输出 Z
这种用法在实用中并不常用,因为这样做仅体现了编译时间多态性(与函数重载相同)。
利用指针或引用来调用不同类中的虚函数则体现了运行时间多态性,因为这时具体调用哪一层中所定义的虚函数只有在程序运行期间才能确定。
X aX,*p;
Y aY;
Z aZ;
p = &aX;
p->vFunc(); // 输出 X
p = &aY;
p->vFunc(); // 输出 X
p = &aZ;
p->vFunc(); // A 输出 Z
注意:虚函数必须是类中的一个非静态的成员函数,不得为友元;另外,构造函数不得是虚函数,但析构函数可以是虚函数。一般讲,若类中定义有虚函数则析构函数常常也需要说明成虚的。特别是涉及到动态内存分配的类,其析构函数甚至必须说明成虚函数。
在 10.3.4 中曾介绍过:若将派生类对象的地址赋给指向基类的指针,则用该指针仅能访问派生类中从基类继承来的公有成员,除非采用强制类型转换。那么上述 A 行并没有进行强制转换,为什么调用的却是派生类中的成员函数呢?
这就是虚函数所导致的多态性。可以从两个方面来理解这一现象:其一,派生类中的虚函数复盖了其类中的同名虚函数;其二,类体系中的虚函数采用的是“就近解决”的策略 —— 若本类中存在虚函数的新定义,则调用本类中的同名虚函数,否则逐级向基类中查找。
利用成员名限定可以调用基类中的虚函数。例:
p = &aZ;
p->X,,vFunc(); // 输出 X
11.2.2 纯虚函数虚函数为一个类体系中所有类提供了一个统一的接口。然而在有些情况下,定义基类时虽然知道其子孙类应当具有某一接口,
但其自身由于某种原因却无法实现该接口。这里就应将该接口说明成一个纯虚函数,其具体操作由各子孙类来定义,而 包含纯虚函数的类为抽象类 。说明纯虚函数的一般形式为:
virtual type func_name(<arg_list>) = 0;
例:
class Position {
int x,y;
public:
//…
virtual void Show() = 0;
};
更为一般地,多数商品化类库中大都定义有一个 Object 类,
该类用作绝大多数类的“最基类”(再无父类的类)。下面是一 个简化了的 Object 类:
// OBJECT.H
#if !defined _OBJECT_H_
#define _OBJECT_H_
class Object {
public:
Object() {}
virtual ~Object() {}
virtual int IsEqual(Object&) = 0;
virtual void Show() = 0;
};
#endif
// ARRAY.H
#include "object.h"
class Array {
Object **pArr;
int nSize,nElemNum;
public:
Array(int = 100);
~Array();
Object* Get(int);
int ElemNumber();
int Append(Object*);
int Insert(Object*,int);
Object* Delete(int);
int Lookup(Object*);
virtual void Show();
};
// ARRAY.CPP
#include "array.h"
Array,,Array(int n)
{
nSize = n;
nElemNum = 0;
if(n == 0)
pArr = 0;
else
pArr = new Object*[n];
}
Array,,~Array()
{
for(int i = 0; i < nElemNum; i ++)
delete pArr[i];
delete []pArr;
}
Object* Array,,Get(int Index)
{
if(Index >= nElemNum || Index < 0)
return 0;
return pArr[Index];
}
int Array,,ElemNumber()
{
return nElemNum;
}
int Array,,Append(Object* pObj)
{
if(nElemNum == nSize)
return 0;
pArr[nElemNum] = pObj;
nElemNum ++;
return 1;
}
int Array,,Insert(Object* pObj,int iPos)
{
if(nElemNum == nSize)
return 0;
if(iPos > nElemNum) {
pArr[nElemNum ++] = pObj;
return -1;
}
for(int i = nElemNum; i > iPos; i --)
pArr[i] = pArr[i - 1];
pArr[iPos] = pObj;
nElemNum ++;
return 1;
}
Object* Array,,Delete(int Index)
{
if(nElemNum == 0 || Index < 0 || Index >= nElemNum)
return 0;
Object* pObj = pArr[Index];
for(int i = Index; i < nElemNum; i ++)
pArr[i] = pArr[i + 1];
nElemNum --;
return pObj;
}
void Array,,Show()
{
for(int i = 0; i < nElemNum; i ++)
pArr[i]->Show();
}
intArray,,Lookup(Object* pObj)
{
for(int i = 0; i < nElemNum; i ++)
if(pArr[i]->IsEqual(*pObj))
return i;
return -1;
}
// TEST.H
#include "object.h"
class X,public Object {
private:
int x;
public:
X(int a = 0),x(a) {}
void SetX(int a) { x = a; }
int GetX() { return x; }
virtual int IsEqual(Object&);
virtual void Show();
};
class Y,public Object {
private:
int y;
int z;
public:
Y(int a = 0,int b = 0),y(a),z(b) {}
void SetY(int a) { y = a; }
void SetZ(int a) { z = a; }
int GetY() { return y; }
int GetZ() { return z; }
virtual int IsEqual(Object&);
virtual void Show();
};
// TEST.CPP
#include <iostream.h>
#include "test.h"
int X,,IsEqual(Object& rObj)
{
X &aX = (X&)rObj;
return(aX.x == x);
}
void X,,Show()
{
cout << "X = " << x << endl;
}
int Y,,IsEqual(Object& rObj)
{
Y &aY = (Y&)rObj;
return(aY.y == y && aY.z == z);
}
void Y,,Show()
{
cout << "Y = " << y << '\t' << "Z = " << z << endl;
}
// TESTARR.CPP
#include "array.h"
#include "test.h"
void main()
{
Array aArr(10);
X *px;
Y *py;
for(int i = 0; i < 4; i ++) {
px = new X(i + 1);
py = new Y((i + 1) * 10,(i + 1) * 20);
aArr.Append(px);
aArr.Append(py);
}
aArr.Show();
输出:
X = 1
Y = 10 Z = 20
X = 2
Y = 20 Z = 40
X = 3
Y = 30 Z = 60
X = 4
Y = 40 Z = 80
px = new X(100);
aArr.Insert(px,5);
aArr.Show();
py = (Y*)aArr.Delete(6);
aArr.Show();
py->Show();
delete py;
}
输出:
X = 1
Y = 10 Z = 20
X = 2
Y = 20 Z = 40
X = 3
X = 100
Y = 30 Z = 60
X = 4
Y = 40 Z = 80
输出:
X = 4
Y = 40 Z = 80
习题:
25,26
“人”类的副本。如下图所示。
Person Person
Student Worker
Work_Stu
Person
Person
Student
Worker
pName
uAge
uSex
pName
uAge
uSex
pDepartment
pDepartment
pSpeciality
pJob
从上图可见,类中存在某一基类的多个副本不仅浪费了存储空间,而且导致二义性。更为严重的是为了保持多个副本中数据成员值的同步,将使操作变得极为复杂。
若欲保证派生类中仅有所有基类的一个副本,则应将其所有基类说明为虚基类。说明虚基类的一般形式为:
class derivative,virtual <access> base<,...>
{
//…
};
其中,关键字 virtual 既可以放在访问控制字之前,也可以放在访问控制字之后。
一个类无论是否存在虚基类,其对象的使用方式完全一样。
例如,可以用以下的形式定义类 Student,Worker 和 Work_stu:
class Student,virtual public Person {
private:
char *pDepartment; // 系部
char *pSpeciality; // 专业
// …
};
class Worker,virtual public Person {
private:
char *pDepartment; // 工作部门
char *pJob; // 工种
// …
};
class Work_Stu,public Student,public Work {
// …
};
这样,类 Work_Stu 的派生关系就如下图所示(注意:该类的对象中,数据成员 pDepartment 存在二义性)。
Person
Student
Work_Stu
Worker
pName
uAge
uSex
pDepartment
pDepartment
pSpeciality
pJob
第 11章 类的其它特性
11.1 友元函数
11.1.1 友元函数的说明和定义友元函数也叫友元,是在类中说明的、用关键字 friend 修饰的 普通函数 。友元虽然不是类中的成员函数,但却拥有访问类中任何成员的特权。说明友元的一般形式为:
friend type func_name(arg_list);
注意:友元的函数原型必须在类中说明,且不受访问权限的制约。说明它的类授予它有访问类中所有成员的权利。
#include <iostream.h>
class X {
private:
int x;
friend long Power(X&,int);
public:
X(int a = 0),x(a) {}
void Set(int a) { x = a; }
int Get() { return x; }
};
// 待续由于友元不是类中的成员函数而只是一个普通函数,所以习惯上总是在类外定义,即使是内联函数也是如此。对于本例而言,上述的“待续”由以下的内容替换:
long Power(X& rx,int exp)
{
long power = 1;
while(exp > 0) {
power *= rx.x;
exp --;
}
return power;
}
// 待续注意:由于友元 Power() 不是类 X 的成员函数,所以 this 指针与它无关。为了能够访问类中的成员,就只有利用参数将
X 类的对象传给它以使其有访问对象中成员的机会。
友元至少应当有一个说明它的类类型的参数 。
11.1.2 使用友元函数友元是一个普通函数,因此它与所有非成员函数的用法完全一样。例,用以下内容替换上述“待续”:
void main()
{
X aX(5);
cout << Power(aX,3) << endl; // 输出 125
}
11.1.3 成员函数用作友元从理论上讲,只要类给予授权,任何一个函数都可以成为友元。
自然,一个类中的成员函数可以作为另一个类的友元。例:
class Y; // 超前说明
class X {
int x;
public:
X(int a = 0),x(a) {}
void Set(Y& ry) { x = ry.y; }
int Get() { return x; }
};
class Y {
int y;
public:
Y(int a = 0),y(a) {}
void Set(int a) { y = a; }
int Get() { return y; }
friend void X,,Set(Y&);
};
#include <iostream.h>
void main()
{
Y aY(5);
X aX;
aX.Set(aY);
cout << aX.Get() << '\t' << aY.Get() << endl;
}
在实用中,上述用法极少使用。由于类的封装性使得其外部难以破坏其内部数据,所以,若需要将一个类中的某个成员函数说明为另一个类的友元,特别是需要将多个成员函数说明为另一个类的友元时,常常直接将整个类说明成另一个类的友元。
将一个类说明成另一个类的友元,实际上是将类中所有的成员函数说明成另一个类的友元。例如,上例可以将类 Y 中的友元说明改为:
friend class X;
这样就将类 X 中所有成员函数都 说明成类 Y 的友元。
应当说明的是:虽然使用友元可以提高程序的运行效率,但友元就像在类的封装上打了一些洞。这样的洞越多类的封装就被破坏的越严重。因此在实用中要仔细地权衡效率和安全之间的矛盾,以决定是否使用以及如何使用友元。另外,当类 A 被授权为类 B 的友元后,只有类 A 中包含有类 B 参数的成员函数才有条件访问类 B 中的所有成员。
11.2 虚函数
11.2.1 虚函数虚函数是类中的一个用关键字 virtual 修饰的成员函数。
virtual type func_name(<arg_List>)
{
func_body;
}
一个函数一经说明为虚函数,则无论说明它的类被继承了多少层,在每一层派生类中该函数将永远保持其 virtual 特性。
定义虚函数的目的是为了让派生类 覆盖 ( Overriding)它。覆盖不同于重载,它要求重新定义的函数在参数和返回值方面与原函数完全相同。否则将属于重载(参数不同)或导致一个编译错误(返回值类型不同)。与函数重载相同,虚函数也体现了 OOP 技术的多态性。
class X {
//…
public:
virtual void vFunc() { cout << "X" << endl; }
//…
};
class Y,public X {
public:
//…
};
class Z,public Y {
//…
public:
virtual void vFunc() { cout << "Z" << endl; }
//…
};
X aX;
Y aY;
Z aZ;
aX.vFunc(); // 输出 X
aY.vFunc(); // 输出 X
aZ.vFunc(); // 输出 Z
这种用法在实用中并不常用,因为这样做仅体现了编译时间多态性(与函数重载相同)。
利用指针或引用来调用不同类中的虚函数则体现了运行时间多态性,因为这时具体调用哪一层中所定义的虚函数只有在程序运行期间才能确定。
X aX,*p;
Y aY;
Z aZ;
p = &aX;
p->vFunc(); // 输出 X
p = &aY;
p->vFunc(); // 输出 X
p = &aZ;
p->vFunc(); // A 输出 Z
注意:虚函数必须是类中的一个非静态的成员函数,不得为友元;另外,构造函数不得是虚函数,但析构函数可以是虚函数。一般讲,若类中定义有虚函数则析构函数常常也需要说明成虚的。特别是涉及到动态内存分配的类,其析构函数甚至必须说明成虚函数。
在 10.3.4 中曾介绍过:若将派生类对象的地址赋给指向基类的指针,则用该指针仅能访问派生类中从基类继承来的公有成员,除非采用强制类型转换。那么上述 A 行并没有进行强制转换,为什么调用的却是派生类中的成员函数呢?
这就是虚函数所导致的多态性。可以从两个方面来理解这一现象:其一,派生类中的虚函数复盖了其类中的同名虚函数;其二,类体系中的虚函数采用的是“就近解决”的策略 —— 若本类中存在虚函数的新定义,则调用本类中的同名虚函数,否则逐级向基类中查找。
利用成员名限定可以调用基类中的虚函数。例:
p = &aZ;
p->X,,vFunc(); // 输出 X
11.2.2 纯虚函数虚函数为一个类体系中所有类提供了一个统一的接口。然而在有些情况下,定义基类时虽然知道其子孙类应当具有某一接口,
但其自身由于某种原因却无法实现该接口。这里就应将该接口说明成一个纯虚函数,其具体操作由各子孙类来定义,而 包含纯虚函数的类为抽象类 。说明纯虚函数的一般形式为:
virtual type func_name(<arg_list>) = 0;
例:
class Position {
int x,y;
public:
//…
virtual void Show() = 0;
};
更为一般地,多数商品化类库中大都定义有一个 Object 类,
该类用作绝大多数类的“最基类”(再无父类的类)。下面是一 个简化了的 Object 类:
// OBJECT.H
#if !defined _OBJECT_H_
#define _OBJECT_H_
class Object {
public:
Object() {}
virtual ~Object() {}
virtual int IsEqual(Object&) = 0;
virtual void Show() = 0;
};
#endif
// ARRAY.H
#include "object.h"
class Array {
Object **pArr;
int nSize,nElemNum;
public:
Array(int = 100);
~Array();
Object* Get(int);
int ElemNumber();
int Append(Object*);
int Insert(Object*,int);
Object* Delete(int);
int Lookup(Object*);
virtual void Show();
};
// ARRAY.CPP
#include "array.h"
Array,,Array(int n)
{
nSize = n;
nElemNum = 0;
if(n == 0)
pArr = 0;
else
pArr = new Object*[n];
}
Array,,~Array()
{
for(int i = 0; i < nElemNum; i ++)
delete pArr[i];
delete []pArr;
}
Object* Array,,Get(int Index)
{
if(Index >= nElemNum || Index < 0)
return 0;
return pArr[Index];
}
int Array,,ElemNumber()
{
return nElemNum;
}
int Array,,Append(Object* pObj)
{
if(nElemNum == nSize)
return 0;
pArr[nElemNum] = pObj;
nElemNum ++;
return 1;
}
int Array,,Insert(Object* pObj,int iPos)
{
if(nElemNum == nSize)
return 0;
if(iPos > nElemNum) {
pArr[nElemNum ++] = pObj;
return -1;
}
for(int i = nElemNum; i > iPos; i --)
pArr[i] = pArr[i - 1];
pArr[iPos] = pObj;
nElemNum ++;
return 1;
}
Object* Array,,Delete(int Index)
{
if(nElemNum == 0 || Index < 0 || Index >= nElemNum)
return 0;
Object* pObj = pArr[Index];
for(int i = Index; i < nElemNum; i ++)
pArr[i] = pArr[i + 1];
nElemNum --;
return pObj;
}
void Array,,Show()
{
for(int i = 0; i < nElemNum; i ++)
pArr[i]->Show();
}
intArray,,Lookup(Object* pObj)
{
for(int i = 0; i < nElemNum; i ++)
if(pArr[i]->IsEqual(*pObj))
return i;
return -1;
}
// TEST.H
#include "object.h"
class X,public Object {
private:
int x;
public:
X(int a = 0),x(a) {}
void SetX(int a) { x = a; }
int GetX() { return x; }
virtual int IsEqual(Object&);
virtual void Show();
};
class Y,public Object {
private:
int y;
int z;
public:
Y(int a = 0,int b = 0),y(a),z(b) {}
void SetY(int a) { y = a; }
void SetZ(int a) { z = a; }
int GetY() { return y; }
int GetZ() { return z; }
virtual int IsEqual(Object&);
virtual void Show();
};
// TEST.CPP
#include <iostream.h>
#include "test.h"
int X,,IsEqual(Object& rObj)
{
X &aX = (X&)rObj;
return(aX.x == x);
}
void X,,Show()
{
cout << "X = " << x << endl;
}
int Y,,IsEqual(Object& rObj)
{
Y &aY = (Y&)rObj;
return(aY.y == y && aY.z == z);
}
void Y,,Show()
{
cout << "Y = " << y << '\t' << "Z = " << z << endl;
}
// TESTARR.CPP
#include "array.h"
#include "test.h"
void main()
{
Array aArr(10);
X *px;
Y *py;
for(int i = 0; i < 4; i ++) {
px = new X(i + 1);
py = new Y((i + 1) * 10,(i + 1) * 20);
aArr.Append(px);
aArr.Append(py);
}
aArr.Show();
输出:
X = 1
Y = 10 Z = 20
X = 2
Y = 20 Z = 40
X = 3
Y = 30 Z = 60
X = 4
Y = 40 Z = 80
px = new X(100);
aArr.Insert(px,5);
aArr.Show();
py = (Y*)aArr.Delete(6);
aArr.Show();
py->Show();
delete py;
}
输出:
X = 1
Y = 10 Z = 20
X = 2
Y = 20 Z = 40
X = 3
X = 100
Y = 30 Z = 60
X = 4
Y = 40 Z = 80
输出:
X = 4
Y = 40 Z = 80
习题:
25,26