在《基类和派生类》中讲述了单继续的基本概念,这节着重讲述继续的具体应用。
在单继续中,每个类可以有多个派生类,但是每个派生类只能有一个基类,从而形成树形结构。
成员访问权限的控制
在《基类和派生类》一讲中,我们讲述了派生类和派生类的对象对基类成员的访问权限的若干规定,这里通过一个实例进一步讨论访问权限的具体控制,然后得出在使用三种继续方式时的调用方法。
//继续性的public继续方式的访问权限的例子
#include
file://定义基类A
class A
{
public:
A() { cout<<"类A的构造函数!"<
A(int a) { Aa = a, aa = a, aaa = a; }
void Aprint() { cout<<"类A打印自己的private成员aa:"< int Aa;
private:
int aa;
protected:
int aaa;
};
file://定义由基类A派生的类B
class B : public A
{
public:
B() { cout<<"类B的构造函数!"<
B(int i, int j, int k);
void Bprint() { cout<<"类B打印自己的private成员bb和protected成员bbb:"<
void B_Aprint() { cout<<"类B的public函数访问类A的public数据成员Aa:"< cout<<"类B的public函数访问类A的protected数据成员aaa:"< GetAaaa();
GetAaaa1();}
private:
int bb;
void GetAaaa() { cout<<"类B的private函数访问类A的public数据成员Aa:"< cout<<"类B的private函数访问类A的protected数据成员aaa:"< protected:
int bbb;
void GetAaaa1() { cout<<"类B的protected函数访问类A的public数据成员Aa:"< cout<<"类B的protected函数访问类A的protected数据成员aaa:"< };
file://基类B的构造函数,需负责对基类A的构造函数的初始化
B::B(int i, int j, int k):A(i), bb(j), bbb(k) {}
file://程序主函数
void main()
{
B b1(100, 200, 300); file://定义类B的一个对象b1,并初始化构造函数和基类构造函数
b1.B_Aprint(); file://类B调用自己的成员函数B_Aprint函数
b1.Bprint(); file://类B对象b1访问自己的private和protected成员
b1.Aprint(); file://通过类B的对象b1调用类A的public成员函数
}
该程序的输出结果为:
类B的public函数访问类A的public数据成员Aa:100
类B的public函数访问类A的protected数据成员aaa:100
类B的private函数访问类A的public数据成员Aa:100
类B的private函数访问类A的protected数据成员aaa:100
类B的protected函数访问类A的public数据成员Aa:100
类B的protected函数访问类A的protected数据成员aaa:100
类B打印自己的private成员bb和protected成员bbb:200,300
类A打印自己的private成员aa:100
上述是属public继续方式,我们可以得出以下结论:
在公有继续(public)时,派生类的public、private、protected型的成员函数可以访问基类中的公有成员和保护成员;派生类的对象仅可访问基类中的公有成员。
让我们把继续方式public改为private,编译结果出现1处如下错误:
'Aprint' : cannot Access public member declared in class 'A'
出错语句在于:b1.Aprint();,因此,我们可以得出以下结论:
在公有继续(private)时,派生类的public、private、protected型的成员函数可以访问基类中的公有成员和保护成员;但派生类的对象不可访问基类中的任何成员。另,使用class要害字定义类时,缺省的继续方式是private,也就是说,当继续方式为私有继续时,可以省略private。
让我们把继续方式public改为protected,可以看出,结果和private继续方式一样。
构造函数和析构函数
派生类的构造函数和析构函数的构造是讨论的主要问题,读者要把握它。
1. 构造函数
我们已知道,派生类的对象的数据结构是由基类中说明的数据成员和派生类中说明的数据成员共同构成。将派生类的对象中由基类中说明的数据成员和操作所构成的封装体称为基类子对象,它由基类中的构造函数进行初始化。
构造函数不能够被继续,因此,派生类的构造函数必须通过调用基类的构造函数来初始化基类子对象。所以,在定义派生类的构造函数时除了对自己的数据成员进行初始化外,还必须负责调用基类构造函数使基类数据成员得以初始化。假如派生类中还有子对象时,还应包含对子对象初始化的构造函数。
派生类构造函数的一般格式如下:
<派生类名>(<派生类构造函数总参数表>):<基类构造函数>(参数表1),<子对象名>(<参数表2>)
{
<派生类中数据成员初始化>
};
派生类构造函数的调用顺序如下:
· 基类的构造函数
· 子对象类的构造函数(假如有的话)
· 派生类构造函数
在前面的例子中,B::B(int i, int j, int k):A(i), bb(j), bbb(k)就是派生类构造函数的定义,下面再举一个构造派生类构造函数的例子。
#include
class A
{
public:
A() { a=0; cout<<"类A的缺省构造函数.\n"; }
A(int i) { a=i; cout<<"类A的构造函数.\n"; }
~A() { cout<<"类A的析构函数.\n"; }
void Print() const { cout< int Geta() { reutrn a; }
private:
int a;
}
class B : public A
{
public:
B() { b=0; cout<<"类B的缺省构造函数.\n"; }
B(int i, int j, int k);
~B() { cout<<"类B的析构函数.\n"; }
void Print();
private:
int b;
A aa;
}
B::B(int i, int j, int k):A(i), aa(j)
{
b=k;
cout<<"类B的构造函数.\n";
}
void B::Print()
{
A::Print();
cout<
void main()
{
B bb[2];
bb[0] = B(1, 2, 5);
bb[1] = B(3, 4, 7);
for(int i=0; i<2; i++)
bb[i].Print();
}
2. 构造函数
当对象被删除时,派生类的析构函数被执行。由于析构函数也不能被继续,因此在执行派生类的析构函数时,基类的析构函数也将被调用。执行顺序是先执行派生类的构造函数,再执行基类的析构函数,其顺序与执行构造函数时的顺序正好相反。这一点从前面讲过的例子可以看出,请读者自行分析。
3. 派生类构造函数使用中应注重的问题
(1) 派生类构造函数的定义中可以省略对基类构造函数的调用,其条件是在基类中必须有缺省的构造函数或者根本没有定义构造函数。当然,基类中没有定义构造函数,派生类根本不必负责调用基类的析构函数。
(2) 当基类的构造函数使用一个或多个参数时,则派生类必须定义构造函数,提供将参数传递给基类构造函数途径。在有的情况下,派生类构造函数的函数体可能为空,仅起到参数传递作用。如本讲第一个例子就属此种情况。
子类型化和类型适应
1. 子类型化
子类型的概念涉及到行为共享,它与继续有着密切关系。
有一个特定的类型S,当且仅当它至少提供了类型T的行为,由称类型S是类型T的子类型。子类型是类型之间的一般和非凡的关系。
在继续中,公有继续可以实现子类型。例如:
class A
{
public:
void Print() const { cout<<"A::print() called.\n"; }
};
class B : public A
{
public:
void f() {}
};
类B继续了类A,并且是公有继续方式。因此,可以说类B是类A的一个子类型。类A还可以有其他的子类型。类B是类A的子类型,类B具备类A中的操作,或者说类A中的操作可被用于操作类B的对象。
子类型关系是不可逆的。这就是说,已知B是A的子类型,而认为A也是B的子类型是错误的,或者说,子类型关系是不对称不。
因此,可以说公有继续可以实现子类型化。
2. 类型适应
类型适应是指两种类型之间的关系。例如,B类型适应A类型是指B类型的对象能够用于A类型的对象所能使用的场合。
前面讲过的派生类的对象可以用于基类对象所能使用的场合,我们说派生类适应于基类。
同样道理,派生类对象的指针和引用也适应于基类对象的指针和引用。
子类型化与类型适应是致的。A类型是B类型的子类型,那么A类型必将适应于B类型。
子类型的重要性就在于减轻程序人员编写程序代码的负担。因为一个函数可以用于某类型的对象,则它也可以用于该类型的各个子类型的对象,这样就不必为处理这些子类型的对象去重载该函数。