1、类的继承与派生
类的继承,是新的类从已有类那里得到已有的特性。从已有类产生新类的过程就是类的派生。
原有的类称为基类或父类,产生的新类称为派生类或子类。
(1)派生类的定义
class 派生类名:继承方式 基类名1,继承方式 基类名2,继承方式 ...
{
派生类成员声明;
};
例如: Base1 和 Base2 是已经定义的类,下面是定义一个 名为 Derived 的派生类:
class Derived:public Base1,private Base2
{
public:
Derived();
~Derived();
};
一个派生类,可以同时有多个基类,这种情况称为多继承,只有一个直接基类的情况称为单继承。
继承方式规定了如何访问从基类继承的成员。默认缺省为私有继承(private)。
派生类成员是指除了从基类继承来的所有成员之外,新增加的数据和函数成员。
(2)派生类生成过程
a、吸收基类成员
这是继承的第一步,是将基类的成员全盘接收
这样派生类实际包含了它的基类中除构造函数和析构函数之外的所有成员
b、改造基类成员
对基类成员的改造包括两个方面,一是基类成员的访问控制问题,主要依靠派生类定义时的继承方式来控制
另一个是对基类数据或函数成员的覆盖或隐藏,而隐藏就是简单地在派生类中声明一个和基类数据或函数同
名的成员。如果派生类声明了一个和某基类成员同名的新成员(如果是成员函数,则参数表也要相同,参数不同的情况属
于重载),派生的新成员就隐藏了外层同名成员(称作同名隐藏)。
c、添加新的成员
2、访问控制
(1)公有继承
当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员不可直接访问
(2)私有继承
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成员身份出现在派生类中,
而基类的私有成员在派生类中不可直接访问。
(3)保护继承
保护继承中,基类的公有成员和保护成员都以保护成员身份出现在派生类中,而基类的私有成员不可直接访问。
3、类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代:
(1)派生类的对象可以隐含转换为基类的对象
(2)派生类的对象可以初始化基类的引用
(3)派生类的指针可以隐含转换为基类的指针
在替代之后,派生类对象就可以作为基类的对象使用,但只能使用从基类继承的成员,实例:
#include<iostream>
using namespace std;
class Base1 //基类 Base1 定义
{
public:
void display() const
{cout<<"Base1::display()"<<endl;}
};
class Base2:public Base1 //公有派生类 Base2 定义
{
public:
void display() const
{cout<<"Base2::display()"<<endl;}
};
class Derived:public Base2 //公有派生类 Derived 定义
{
public:
void display() const
{cout<<"Derived::displat()"<<endl;}
};
void fun(Base1 * ptr) //参数为指向基类对象的指针
{
ptr->display(); //对象指针->成员名
}
int main()
{
Base1 base1;
Base2 base2;
Derived derived;
fun(&base1);
fun(&base2);
fun(&derived);
return 0;
}
运行结果:
通过这个例子可以看到,根据类型兼容规则,可以在基类对象出现的场合使用派生类对象进行替代,但是替代
之后派生类仅仅发挥出基类的作用,发挥更多作用还需以后对多态的设计方法学习。
4、派生类的构造和析构函数
由于基类的构造函数和析构函数不能被继承,在派生类中,如果对派生类新增的成员进行初始化,就必须为派生类
添加新的构造函数。但是派生类的构造函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来的成员,其
初始化工作还是由基类的构造函数完成。同样,对派生类对象的扫尾、清理工作也需要加入新的析构函数。
(1)构造函数
构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化。一般语法格式为:
派生类名::派生类名(参数表):基类名1(基类1初始化参数表),... ,基类名n(基类名n初始化参数表),
成员对象名1(成员对象1初始化参数表),... ,成员对象名m(成员对象m初始化参数表)
{
派生类构造函数的其他初始化操作;
}
这里,派生类的构造函数名与类名相同。
如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数。
派生类构造函数执行的一般次序如下:
a、调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
b、对派生类新增的成员对象初始化,调用顺序按照它们在类中声明的顺序
c、执行派生类的构造函数体中的内容
实例:派生类构造函数实例(多继承,含有内嵌对象)
#include<iostream>
using namespace std;
class Base1 //基类 Base1,构造函数有参数
{
public:
Base1(int i)
{cout<<"Constructing Base1 "<<i<<endl;}
};
class Base2 //基类 Base2,构造函数有参数
{
public:
Base2(int j)
{cout<<"Constructing Base2 "<<j<<endl;}
};
class Base3 //基类 Base3,构造函数无参数
{
public:
Base3()
{cout<<"Constructing Base3 * "<<endl;}
};
//派生新类 Derived,注意基类名的顺序
class Derived:public Base2,public Base1,public Base3
{
public:
Derived(int a,int b,int c,int d):Base1(a),
member2(d),member1(c),Base2(b){}
//注意基类名的个数与顺序,注意成员对象的个数与顺序
private:
Base1 member1;
Base2 member2;
Base3 member3;
};
int main()
{
Derived obj(1,2,3,4);
return 0;
}
运行结果:
分析:基类构造函数的调用顺序是按照派生类定义时的顺序,为 Base2,Base1,最后Base3
而内嵌对象的构造函数调用顺序应该是按照成员在类中的声明顺序:Base1,Base2,Base3。
(2)拷贝构造函数
一般缺省时系统会在必要时自动生成一个隐含的拷贝构造函数,这个拷贝构造函数会自动调用基类的拷贝构造
函数,然后对派生类新增的成员对象一一执行拷贝。
如果要为派生类编写拷贝构造函数,一般需要为基类相应的拷贝构造函数传递参数。
(3)析构函数
派生类析构函数的声明方法与没有继承关系的类中析构函数的声明方法完全相同,只要在函数体中负责把派生类
新增的非对象成员的清理工作做好就够了。它的执行次序和构造函数正好完全相反。
5、派生类成员的标识与访问
(1)作用域分辨符
作用域分辨符,就是 "::" ,它可以用来限定要访问的成员所在的类的名称。
对于在不同的作用域声明的标识符,可见性原则是:外层声明一个标识符,内层如果未声明,则外层在内层仍然可见,若内层声明
了同名标识符,则隐藏外层同名标识符(称隐藏规则)。
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。
(2)虚基类
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。
在引用这些同名的成员时,必须在派生类对象名后增加直接基类名,以避免产生二义性,使其惟一地标识一个成员,如: obj.A::display( )。
在一个类中保留间接共同基类的多份同名成员是不好的,C++提供虚基类(virtual base class )的方法,使得在继承间接共同基类时只保留一份成员。
语法形式: class 派生类名:virtual 继承方式 基类名
实例:
#include<iostream>
using namespace std;
class Base0 //定义基类 Base0
{
public:
int var0;
void fun0()
{cout<<"Member of Base0 "<<endl;}
};
class Base1:virtual public Base0 //定义派生类 Base1
{
public:
int var1;
};
class Base2:virtual public Base0 //定义派生类 Base2
{
public:
int var2;
};
//派生新类 Derived
class Derived:public Base1,public Base2
{
public:
int var;
void fun()
{cout<<"Member of Derived"<<endl;}
};
int main()
{
Derived d;
d.var0=2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}
运行结果:
(3)虚基类及其派生类构造函数
如果虚基类声明有非默认形式的(即带形参的)构造函数,并且没有声明默认形式的构造函数,事情就比较麻烦。
实例:若基类声明了带参数的构造函数,则对上面程序的修改如下:
#include<iostream>
using namespace std;
class Base0 //定义基类 Base0
{
public:
Base0(int var):var0(var){}
int var0;
void fun0()
{cout<<"Member of Base0 "<<endl;}
};
class Base1:virtual public Base0 //定义派生类 Base1
{
public:
Base1(int var):Base0(var){}
int var1;
};
class Base2:virtual public Base0 //定义派生类 Base2
{
public:
Base2(int var):Base0(var){}
int var2;
};
//派生新类 Derived
class Derived:public Base1,public Base2
{
public:
Derived(int var):Base0(var),Base1(var),Base2(var){}
int var;
void fun()
{cout<<"Member of Derived"<<endl;}
};
int main()
{
Derived d(1);
d.var0=2; //直接访问虚基类的数据成员
d.fun0(); //直接访问虚基类的函数成员
return 0;
}