类的继承与派生
类的继承就是新类由已经存在的类获得已有特性,类的派生是由已经存在的类产生新类的过程。已有类叫做基类,产生的新类叫做派生类。
派生类的声明�
class Clild: public Parent1,pravite Parent2
{
public:
Child();
~Child();
}
一个派生类可以有多个基类,叫做多继承;否则为单继承。直接派生出某个类的基类叫做这个类的直接基类,基类的基类或更高层的基类叫做派生类的间接基类。
• 派生类从基类继承的过程可以分为三个步骤:吸收基类成员,修改基类成员和添加新成员。
• 继承方式限定了派生类访问从基类继承来的成员的方式,指出了派生类成员或类外的对象对基类继承来的成员的访问权限。�继承方式有:公有继承、保护继承和私有继承。其中公有继承最常用。
○ 共有继承时,派生类对基类中的公有成员和保护成员的访问属性都不变,而对基类的私有成员则不能访问。(类的对象也属于类外的,不能访问保护成员)
○ 保护继承时,基类的公有成员和保护成员被派生类继承后变成派生类的保护成员,而基类的私有成员在派生类红不能访问。
○ 私有继承时,基类的公有成员和保护成员被派生类继承后变成派生类的私有成员,而基类的私有成员在派生类中不能访问。
#include <iostream>
using namespace std;
class Base // 基类Base的声明
{
public: // 公有成员函数
void SetTwo(int a, int b) { x=a; y=b; }
int GetX() { return x; }
int GetY() { return y; }
private: // 私有数据成员
int x;
int y;
};
class Child : private Base // 派生类的声明,继承方式为私有继承
{
public: // 新增公有成员函数
void SetThree(int a, int b, int c) { SetTwo(a, b); z=c; }
int GetX() { return Base::GetX(); }
int GetY() { return Base::GetY(); }
int GetZ() { return z; }
private: // 新增私有数据成员
int z;
};
int main()
{
Child child; // 声明Child类的对象
child.SetThree(1, 2, 3); // 设置派生类的数据
cout << "The data of child:"<<endl;
cout << child.GetX() << "," << child.GetY() << "," << child.GetZ() << endl;
return 0;
}
派生类的构造函数
基类的构造函数和析构函数派生类是不能继承的,如果派生类需要对新成员初始化或者进行特定的清理工作,就需要就需要自己定义构造函数和析构函数了。从基类继承的成员的初始化仍可通过基类的构造函数来完成。
派生类的数据成员包括从基类继承来的数据成员和派生类新增的数据成员,还可能包括其他类的对象作为其数据成员,包括其他类的对象时实际上还间接包括了这些对象的数据成员。那么我们对派生类初始化时就需要对基类的数据成员、派生类新增数据成员和内嵌的其他类对象的数据成员进行初始化。由于不能继承基类的构造函数,派生类就必须增加自己的构造函数。派生类的构造函数需要做的工作有,使用传递给派生类的参数,调用基类的构造函数和内嵌对象成员的构造函数来初始化它们的数据成员,再添加新语句初始化派生类新成员。派生类构造函数的语法形式为:
派生类名::派生类名(参数表):基类名1(参数表1),...基类名m(参数名m),
内嵌对象名(内嵌对象参数表1),...,内嵌对象名n(内嵌对象参数表n)
{
初始化派生类新成员的语句;
}
基类的构造函数若有参数,则派生类必须定义构造函数,将传入的参数再传递给基类的构造函数,对基类进行初始化。若基类没有定义构造函数,则派生类也可以不定义构造函数,都使用默认构造函数,对于派生类的新增数据成员可以通过其他的公有函数成员来初始化。而如果基类同时定义了默认构造函数和带参数的构造函数,那么在派生类的构造函数中可以给出基类名及其参数表,也可以不显式给出。
构造派生类的对象调用构造函数时的处理顺序是:1.首先调用基类的构造函数,若有多个基类,调用顺序按照它们在派生类声明时从左到右出现的顺序;2.如果有内嵌对象成员,则调用内嵌对象成员的构造函数,若为多个内嵌对象,则按照它们在派生类中声明的顺序调用,如果无内嵌对象则跳过这一步;3.调用派生类构造函数中的语句。
这里需要说明的是,基类和内嵌对象成员的构造函数的调用顺序和它们在派生类构造函数中出现的顺序无关。
#include <iostream>
using namespace std;
class Base1 // 基类Base1,只有默认构造函数
{
public:
Base1() { cout<<"Base1 construct"<<endl; }
};
class Base2 // 基类Base2,只有带参数的构造函数
{
public:
Base2(int x) { cout<<"Base2 construct "<<x<<endl; }
};
class Base3 // 基类Base3,只有带参数的构造函数
{
public:
Base3(int y) { cout<<"Base3 construct "<<y<<endl; }
};
class Child : public Base2, public Base1, public Base3 // 派生类Child
{
public:
Child(int i,int j,int k,int m):Base2(i),b3(j),b2(k),Base3(m) { }
private: // 派生类的内嵌对象成员
Base1 b1;
Base2 b2;
Base3 b3;
};
int main()
{
Child child(3,4,5,6);
return 0;
}
程序运行结果为:
Base2 construct 3
Base1 construct
Base3 construct 6
Base1 construct
Base2 construct 5
Base3 construct 4
派生类的析构函数
派生类的析构函数一般只需要在其函数体中清理新增成员就可以了,对于继承的基类成员和派生类内嵌对象成员的清理,则一般由系统自动调用基类和对象成员的析构函数来完成。这个执行过程的顺序正好和派生类构造函数相反:1.执行析构函数语句清理派生类的新增成员;2.调用内嵌对象成员所属类的析构函数清理派生类内嵌对象成员,各个对象成员的清理顺序与其在构造函数中的构造顺序相反;3.调用基类的析构函数清理继承的基类成员,如果是多继承则各个基类的清理顺序也与其在构造函数中的构造顺序相反。总起来一句话,析构函数执行时所有成员或对象的清理顺序与构造函数的构造顺序刚好完全相反。
作用域分辨符
我们可以通过基类名和作用域分辨符来访问基类中的同名成员。作用域分辨符就是“::”,在派生类内部访问基类同名成员的语法形式是:
基类名::数据成员名; // 数据成员
基类名::函数成员名(参数表); // 函数成员
如果是在派生类外通过派生类对象访问的话,前面还要加上“派生类对象名.”:
派生类对象名.基类名::数据成员名; // 数据成员
派生类对象名.基类名::函数成员名(参数表); // 函数成员
这里的基类名就限定了后面的成员属于哪个类。
赋值兼容规则
赋值兼容规则就是指在基类对象可以使用的地方都可以用公有派生类对象来代替。
那么根据赋值兼容规则,可以使用类Base对象的地方都可以使用类Child的对象来代替。这里的代替有三种:
a. 派生类的对象可以赋值给基类的对象。也就是将派生类对象从基类继承的成员的值分别赋值给基类对象相应的成员。例如:� base = child;
b. 派生类对象的地址可以赋值给基类类型的指针。例如:� pBase = &child;
c. 派生类对象可以用来初始化基类的引用。例如:� Base &b = child;
公有派生类对象可以代替基类对象使用,但是我们只能使用它从基类继承的成员,而无法使用它的新添成员。
#include <iostream>
using namespace std;
class Base // 基类Base的声明
{
public:
void show() { cout << "Base::show()" << endl; } // 公有成员函数show
};
class Child0 : public Base // 类Base的公有派生类Child0的声明
{
public:
void show() { cout << "Child0::show()" << endl; } // 公有成员函数show
};
class Child1 : public Child0 // 类Child0的公有派生类Child1的声明
{
public:
void show() { cout << "Child1::show()" << endl; } // 公有成员函数show
};
void CallShow(Base *pBase) // 一般函数,参数为基类指针
{
pBase->show();
}
int main()
{
Base base; // 声明Base类的对象
Base *pBase; // 声明Base类的指针
Child0 ch0; // 声明Child0类的对象
Child1 ch1; // 声明Child1类的对象
pBase = &base; // 将Base类对象base的地址赋值给Base类指针pBase
CallShow(pBase);
pBase = &ch0; // 将Child0类对象ch0的地址赋值给Base类指针pBase
CallShow(pBase);
pBase = &ch1; // 将Child1类对象ch1的地址赋值给Base类指针pBase
CallShow(pBase);
return 0;
}
虚函数
虚函数是非静态的成员函数,一定不能是静态(static)的成员函数。
一般的虚函数声明形式为:
virtual 函数类型 函数名(形参表)
{
函数体
}
虚函数就是在类的声明中用关键字virtual限定的成员函数。以上声明形式是成员函数的实现也在类的声明中的情况。如果成员函数的实现在类的声明外给出时,则虚函数的声明只能出现在类的成员函数声明中,而不能在成员函数实现时出现,简而言之,只能在此成员函数的声明前加virtual修饰,而不能在它的实现前加。
析构函数用于在类的对象消亡时做一些清理工作,我们在基类中将析构函数声明为虚函数后,其所有派生类的析构函数也都是虚函数,使用指针引用时可以动态绑定,实现运行时多态,通过基类类型的指针就可以调用派生类的析构函数对派生类的对象做清理工作。
前面讲过,析构函数没有返回值类型,没有参数表,所以虚析构函数的声明也比较简单,形式如下:
virtual ~类名();
纯虚函数
即使有的虚函数在基类中不需要做任何工作,我们也要写出一个空的函数体,这时这个函数体没有什么意义,重要的是此虚函数的原型声明。C++为我们提供了纯虚函数,让我们在这种情况下不用写函数实现,只给出函数原型作为整个类族的统一接口就可以了,函数的实现可以在派生类中给出。
纯虚函数是在基类中声明的,声明形式为:
virtual 函数类型 函数名(参数表) = 0;
大家可以看到,纯虚函数的声明形式与一般虚函数类似,只是最后加了个“=0”。纯虚函数这样声明以后,在基类中就不再给出它的实现了,各个派生类可以根据自己的功能需要定义其实现。