2.3 继承和派生
继承是面向对象语言的一个重要机制,通过继承可以在一个一般类的基础上建立新类。被继承的类称为基类(base class),在基类上建立的新类称为派生类(derived class)。如果一个类只有一个基类则称为单继承,否则称为多继承。通过类继承,可以使派生类有条件地具有基类的属性,这个条件就是继承方式。
2.3.1 单继承
从一个基类定义一个派生类可按下列格式:
class <派生类名> : [<继承方式>] <基类名> { [<派生类的成员>] };
其中,继承方式有3种:public(公有)、private(私有)及protected(保护),若继承方式没有指定,则被指定为默认的public方式。继承方式决定了派生类的继承基类属性的使用权限,下面分别说明。
1.公有继承(public)
公有继承的特点是基类的公有成员和保护成员作为派生类的成员时,它们都保持原有的状态,而基类的私有成员仍然是私有的。例如:
class CStick : public CMeter { int m_nStickNum; // 声明一个私有数据成员 public: void DispStick(); // 声明一个公有成员函数 }; // 注意分号不能省略 void CStick:: DispStick() { m_nStickNum=GetPos(); // 调用基类CMeter的成员函数 cout<<m_nStickNum<<’ ’; }
这时,从基类CMeter派生的CStick类除具有CMeter所有公有成员和保护成员外,还有自身的私有数据成员m_nStickNum和公有成员函数DispStick。
【例Ex_PublicDerived】 派生类的公有继承示例
#include <iostream.h> class CMeter { public: CMeter(int nPos=10) { m_nPos=nPos; } ~CMeter() { } void StepIt() {m_nPos++;} int GetPos() {return m_nPos;} protected: void SetPos(int nPos) { m_nPos = nPos; } private: int m_nPos; }; class CStick:public CMeter // 从CMeter派生,公有继承 { int m_nStickNum; // 声明一个私有数据成员 public: void DispStick(); // 声明一个公有成员函数 void SetStick(int nPos) { SetPos(nPos); // 类中调用基类的保护成员 } }; void CStick:: DispStick() { m_nStickNum=GetPos(); // 调用基类CMeter的成员函数 cout<<m_nStickNum<<' '; } int main() { CMeter oMeter(20); CStick oStick; cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oMeter.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oStick.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetPos()<<endl; oStick.DispStick(); oStick.StepIt(); oStick.DispStick(); return 0; }
程序运行结果如下:
CMeter:20,CStick:10
CMeter:21,CStick:10
CMeter:21,CStick:11
11 12
需要注意的是:派生类中或派生类的对象可以使用基类的公有成员(包括保护成员),例如CStick的成员函数DispStick中调用了基类CMeter的GetPos函数,oStick对象调用了基类的StepIt成员函数;但基类或基类的对象却不可以使用派生类的成员。
2.私有继承(private)
私有继承的特点是基类的公有成员和保护成员都作为派生类的私有成员,并且不能被这个派生类的子类所访问。
【例Ex_PrivateDerived】 派生类的私有继承示例
#include <iostream.h> class CMeter { public: CMeter(int nPos=10) { m_nPos=nPos; } ~CMeter(){ } void StepIt(){ m_nPos++; } int GetPos(){ return m_nPos;} protected: void SetPos(int nPos) { m_nPos = nPos; } private: int m_nPos; }; class CStick:private CMeter // 从CMeter派生,私有继承 { int m_nStickNum; // 声明一个私有数据成员 public: void DispStick(); // 声明一个公有成员函数 void SetStick(int nPos) { SetPos(nPos); // 调用基类的保护成员 } int GetStick() { return GetPos(); // 调用基类的公有成员 } }; void CStick::DispStick() { m_nStickNum = GetPos(); // 调用基类CMeter的成员函数 cout<<m_nStickNum<<' '; } int main() { CMeter oMeter(20); CStick oStick; cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetStick()<<endl; oMeter.StepIt(); cout<<"CMeter:"<<oMeter.GetPos()<<",CStick:"<<oStick.GetStick()<<endl; oStick.DispStick(); return 0; }
程序运行结果如下:
由于私有继承的派生类对象不能访问基类的所有成员,因此oStick不能调用基类的GetPos函数,但在派生类中是可以访问的。注意CStick的GetStick函数实现,并与上例相比较,看看有什么不同。
CMeter:20,CStick:10
CMeter:21,CStick:10
10
3.保护继承(protected)
保护继承的特点是基类的所有公有成员和保护成员都成为派生类的保护成员,并且只能被它的派生类成员函数或友元访问,基类的私有成员仍然是私有的。表2.1列出了三种不同的继承方式的基类成员和其在派生类中的特性。
表2.1 不同继承方式的基类成员和其在派生类中的特性
需要注意的是,一定要区分清楚派生类的对象和派生类中的成员函数对基类的访问是不同的。例如,在公有继承时,派生类的对象可以访问基类中的公有成员,派生类的成员函数可以访问基类中的公有成员和保护成员。在私有继承和保护继承时,基类的所有成员不能被派生类的对象访问,而派生类的成员函数可以访问基类中的公有成员和保护成员。
2.3.2 派生类的构造函数和析构函数
由于基类的构造函数和析构函数不能被派生类继承,因此,若有:
CMeter oA(3);
是可以的,因为CMeter类有与之相对应的构造函数。而
CStick oB(3);
是错误的,因为CStick类没有对应的构造函数。但
CStick oC;
是可以的,因为CStick类有一个隐含的不带参数的默认构造函数。
当派生类的构造函数和析构函数被执行时,基类相应的构造函数和析构函数也会被执行。因而,在前面两个例子中,CStick对象oStick在建立时还调用了基类的构造函数,使得oStick.GetPos返回的值为10。
需要注意的是,派生类对象在建立时,先执行基类的构造函数,然后执行派生类的构造函数。但对于析构函数来说,其顺序刚好相反,先执行派生类的析构函数,而后执行基类的析构函数。
【例Ex_ClassDerived】 派生类的构造函数和析构函数的示例
#include <iostream.h> #include <string.h> class CAnimal { public: CAnimal(char *pName = "noname"); ~CAnimal(); void setName(char *pName) { strncpy(name, pName, sizeof(name)); } char*getName(void) {return name;} private: char name[20]; }; CAnimal::CAnimal(char *pName) { setName(pName); cout<<"调用CAnimal的构造函数!"<<endl; } CAnimal::~CAnimal() { cout<<"调用CAnimal的析构函数!"<<endl; } class CCat : public CAnimal { public: CCat() { cout<<"调用CCat的构造函数!"<<endl; } ~CCat() { cout<<"调用CCat的析构函数!"<<endl; } void DispName() { cout<<"猫的名字是:"<<getName()<<endl; } }; int main() { CCat cat; cat.DispName(); cat.setName("Snoopy"); cat.DispName(); return 0; }
程序运行结果如下:
调用CAnimal的构造函数!
调用CCat的构造函数!
猫的名字是:noname
猫的名字是:Snoopy
调用CCat的析构函数!
调用CAnimal的析构函数!
需要注意的是,在对派生类进行初始化时,如果需要对其基类设置初值,则可按下列格式进行:
<派生类名>(总参表):<基类1>(参数表1), <基类2>(参数表2),…, <基类n>(参数表n), 对象成员1(对象成员参数表1), 对象成员2(对象成员参数表2),…, 对象成员n(对象成员参数表n) { … }
其中,构造函数总参表后面给出的是需要用参数初始化的基类名、对象成员名及各自对应的参数表,基类名和对象成员名之间的顺序可以是任意的,且对于使用默认构造函数的基类和对象成员,可以不列出基类名和对象成员名。这里所说的对象成员是指在派生类中新声明的数据成员,它属于另外一个类的对象。对象成员必须在初始化列表中进行初始化。
例如,在【例Ex_PublicDerived】中,CStick的构造函数可这样定义:
class CStick : public CMeter { int m_nStickNum; public: CStick():CMeter(30) { } void DispStick(); void SetStick(int nPos) { SetPos(nPos); } };
此时再重新运行程序,结果就会变为:
CMeter:20,CStick:30
CMeter:21,CStick:30
CMeter:21,CStick:31
31 32
2.3.3 多继承
前面所讨论的是单继承的基类和派生类之间的关系,实际在类的继承中,还允许一个派生类继承多个基类,这种多继承的方式可使派生类具有多个基类的特性,因而不仅使程序结构清晰,且大大提高了程序代码的可重用性。
多继承下派生类的定义按下面的格式:
class <派生类名> : [<继承方式1>] <基类名1>,[<继承方式2>] <基类名2>,… { [<派生类的成员>] };
其中的继承方式还是前面提到的3种:public、private和protected。例如:
class A { //… }; class B { //… }; class C : public A,private B { //…
};
由于派生类C继承了基类A和B,具有多继承性,因此派生类C的成员包含了基类A中成员和B中成员及该类本身的成员。
除了类的多继承性以外,C++还允许一个基类有多个派生类(称为多重派生),以及从一个基类的派生类中再进行多个层次的派生。总之,掌握了基类和派生类之间的关系,类的多种形式的继承也就清楚了。
2.3.4 虚基类
一般说来,在派生类中对基类成员的访问应该是唯一的。但是,由于在多继承情况下可能造成对基类中某成员的访问出现不唯一的情况,这种情况称为基类成员调用的二义性。
【例Ex_Conflict】 基类成员调用的二义性
#include <iostream.h> class A { public: int x; A(int a = 0) { x = a; } }; class B1 : public A { public: int y1; B1( int a = 0, int b = 0) : A(b) { y1 = a; } }; class B2 : public A { public: int y2; B2( int a = 0, int b = 0) : A(b) { y2 = a; } }; class C : public B1, public B2 { public: int z; C(int a, int b, int d, int e, int m) : B1(a,b), B2(d,e) { z = m; } void print() { cout<<"x="<<x<<endl; // 编译出错的地方 cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; } }; int main() { C c1(100,200,300,400,500); c1.print(); return 0; }
程序中,派生类B1和B2都从基类A继承,这时在派生类中就有两个基类A的拷贝。当编译器编译到“cout<<"x = "<<x<<endl;”语句时,因无法确定成员x是从类B1中继承来的,还是从类B2继承来的产生了二义性,从而出现编译错误。
解决这个问题的方法之一是使用域作用运算符“ ::”来消除二义性,例如若将print函数实现代码变为:
void print() { cout<<"B1::x = "<<B1::x<<endl; cout<<"B2::x = "<<B2::x<<endl; cout<<"y1 = "<<y1<<", y2 = "<<y2<<endl; cout<<"z = "<<z<<endl; }
重新运行,结果为:
B1::x = 200
B2::x = 400
y1 = 100, y2 = 300
z = 500
实际上,还有另一种更好的方法,即使用虚基类(或称为虚继承)。使用虚基类的目的是在多重派生的过程中,使公有的基类在派生类中只有一个拷贝,从而解决上述这种二义性问题。
【例Ex_VirtualBase】 基类成员调用的二义性
#include <iostream.h> class A { public: int x; A(int a = 0) { x = a; } }; class B1:virtual public A // 声明虚继承 { public: int y1; B1( int a = 0, int b = 0):A(b) { y1=a; } void print(void) { cout<<"B1:x="<<x<<",y1="<<y1<<endl; } }; class B2:virtual public A // 声明虚继承 { public: int y2; B2( int a = 0, int b = 0) : A(b) { y2 = a; } void print(void) { cout<<"B2:x="<<x<<",y2="<<y2<<endl; } }; class C : public B1, public B2 { public: int z; C(int a, int b, int d, int e, int m) : B1(a,b), B2(d,e) { z = m; } void print() { B1::print(); B2::print(); cout<<"z = "<<z<<endl; } }; int main() { C c1(100,200,300,400,500); c1.print(); c1.x = 400; c1.print(); return 0; }
程序运行结果如下:
B1: x = 0, y1 = 100
B2: x = 0, y2 = 300
z = 500
B1: x = 400, y1 = 100
B2: x = 400, y2 = 300
z = 500
从程序中可以看出:
(1)声明一个虚基类的格式如下:
virtual <继承方式><基类名>
其中,virtual是声明虚基类的关键字。声明虚基类与声明派生类一道进行,写在派生类名的后面。
(2)在派生类B1和B2中只有基类A的一个拷贝,当改变成员x的值时,由基类B1和B2中的成员函数输出的成员x的值是相同的。
(3)虚基类的构造函数的调用方法与一般基类的构造函数的调用方法是不同的。C++规定,由虚基类经过一次或多次派生出来的派生类,在其每一个派生类的构造函数的成员初始化列表中必须给出对虚基类的构造函数的调用,如果未列出,则调用虚基类的默认构造函数。在这种情况下,虚基类的定义中必须要有默认的构造函数。程序中,类C的构造函数尽管分别调用了其基类B1和B2的构造函数,但由于虚基类A在类C中只有一个拷贝,所以编译器无法确定应该由类B1的构造函数还是由类B2的构造函数来调用基类A的构造函数。在这种情况下,C++规定,执行类B1和B2的构造函数都不调用虚基类A的构造函数,而是在类C的构造函数中直接调用虚基类A的默认构造函数。这样也就说明了运行结果中成员x的初始值为什么为“0”了。若将A的构造函数改为:
A(int a = 100) { x = a; }
则成员x的初始值为100。当然,不能仅变成:
A(int a) { x = a; }
因为类A中没有定义默认构造函数,因此会出现编译错误。与“A(int a = 100) { x = a; }”构造函数等价的是:
A(int a) { x = a; } A():x(100){} // 添加默认构造函数的定义