第三章-继承与派生
1.类的继承
类的继承,是新的类从已有类那里得到已有的特性;从已有类产生新类的过程就是类的派生
- 原有的类叫做基类或者父类,产生的新类叫做派生类或者子类
- 一个派生类可以有多个基类,即多继承,只有一个基类的叫做单继承
- 如果不显式地给出继承方式,默认为私有继承
派生类的语法定义:
class 派生类名:继承方式 基类名1,继承方式 基类名2,······,继承方式 基类名n
{
派生类成员声明;
};
- 派生类包含了它的全部基类中除了构造函数和析构函数之外的所有成员
- 派生类成员是指除了从基类继承来的所有成员之外,新增加的数据和函数成员
示例:
//基类
class A
{
public:
int a;
void printA(){
printf("A");
}
};
//派生类,公有继承
class B:public A
{
public:
int b;
void printB(){
printf("B");
}
};
基类与派生类继承规则(访问权限)
派生类访问权限 | 公有类型 | 保护类型 | 私有类型 | 不可访问类型 |
---|---|---|---|---|
公有继承 | 公有 | 保护 | 不可访问 | 不可访问 |
保护继承 | 保护 | 保护 | 不可访问 | 不可访问 |
私有继承 | 私有 | 私有 | 不可访问 | 不可访问 |
2.类型兼容规则
类型兼容规则是指需要基类对象的地方,都可以使用公有派生类的对象来替代
- 派生类的对象可以隐含地转化为基类对象
- 派生类的对象可以初始化基类的引用
- 派生类的指针可以隐含地转化为基类指针
在替代之后,派生类对象可以作为基类对象使用,但只能使用从基类继承的成员
class B{···}
class D:public B{···}
B b1,*pb1;
D d1;
(1)b1=d1; //派生类对象转化为基类对象
(2)B &rb=d1; //d1为rb的一个引用,修改它成员变量的值,rb里面也会改变
(3)pb1=&d1; //派生类对象的地址也可以转化为基类指针,即基类指针指向派生类对象
class A{
public:
void display(){
cout<<"A::display()"<<endl;
}
};
class B:public A{
public:
void display(){
cout<<"B::display()"<<endl;
}
};
class C:public B{
public:
void display(){
cout<<"C::display()"<<endl;
}
};
void fun(A* ptr){
ptr->display();
}
int main()
{
A a;
B b;
C c;
fun(&a); //用 A对象的指针调用fun函数
fun(&b);
fun(&c);
}
/*
输出结果均为 A::display()
*/
3.派生类构造函数
构造派生类的对象时,就要对基类的成员对象和新增成员对象进行初始化
派生类构造函数的一般语法形式为:
派生类名 :: 派生类名(参数表):基类名n(参数表),成员对象名n(参数表)
{
其他初始化操作;
}
- 如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数,提供一个将参数传递给基类构造函数的途径
- 如果不需要调用基类的带参数的构造函数,也不需要调用新增的成员变量的带参数的构造函数,派生类也可以不声明构造函数,全部采用默认的构造函数
派生类构造函数执行的一般次序如下:
- 调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左到右)
- 对派生类新增的成员对象初始化,调用顺序按照它们在类中声明的顺序
- 执行派生类的构造函数体中的内容
class A{
public:
A(int i){
cout<<"A "<<i<<endl;
}
};
class B{
public:
B(int j){
cout<<"B "<<j<<endl;
}
};
class C{
public:
C(){
cout<<"C *"<<endl;
}
};
class D:public B,public A,public C{
public:
D(int x,int y,int z,int w):A(x),mem2(w),mem1(z),B(y)
{}
private:
A mem1;
B mem2;
C mem3;
};
int main()
{
D obj(1,2,3,4);
return 0;
}
/*
程序的运行结果为:
B 2
A 1
C *
A 3
B 4
C *
*/
如果为派生类编写复制构造函数,一般需要为基类相应的复制构造函数传递参数(这里可以使用替代兼容规则)
class A{
A(A & n){
}
};
class B:public A{
B(B & m):A(m){ //用派生类对象初始化基类的引用
}
};
4.派生类析构函数
派生类中编写析构函数,只需要在函数体中把派生类新增的非对象成员清理工作做好就行了,系统会自己调用基类及对象成员的析构函数来对基类及对象成员进行清理。
- 执行次序与构造函数完全相反,首先执行析构函数的函数体,然后对派生类新增的类型的成员对象进行清理,最后对所有从基类继承而来的成员清理。
- 没有显式声明析构函数的话,编译系统会自动生成默认的析构函数
class A{
public:
A(int i){cout<<"A "<<i<<endl;}
~A(){cout<<"A is destructed"<<endl;}
};
class B{
public:
B(int j){cout<<"B "<<j<<endl;}
~B(){cout<<"B is destructed"<<endl;}
};
class C{
public:
C(){cout<<"C *"<<endl;}
~C(){cout<<"C is destructed"<<endl;}
};
class D:public B,public A,public C{
public:
D(int x,int y,int z,int w):A(x),mem2(w),mem1(z),B(y)
{}
private:
A mem1;
B mem2;
C mem3;
};
int main()
{
D obj(1,2,3,4);
return 0;
}
/*
程序的运行结果为:
B 2
A 1
C *
A 3
B 4
C *
C is destructed
B is destructed
A is destructed
C is destructed
A is destructed
B is destructed
*/
5.同名隐藏
如果派生类中声明了一个和基类成员同名的新成员,派生类的新成员就隐藏了外层同名成员,这叫做同名隐藏
这时使用“对象名 . 成员名”只能访问到派生类新增成员,如果想要调用基类的成员,必须用基类名和作用域符进行限定
class A{
A(){
x=12;
}
int x;
};
class B:public A{
B(){
x=23;
}
void print(){
cout<< A::x <<endl;
cout<< x <<endl; //B类中的x
}
int x;
};
int main(){
B obj;
cout<< obj.A::x <<endl;
cout<< obj.B::x <<endl;//为了避免二义性,必须用基类名和作用域符
}
为了不产生二义性,也可以使用using关键字加以澄清
将using用于基类中的函数名,派生类中定义同名但参数不同的函数,基类的函数就不会被隐藏,两个重载的函数将会并存在于派生类作用域中
class A{
public:
void fun(){···}
};
class B:public A{
public:
using A :: fun;
void fun(int i){···}
};
//使用 B的对象,既可以直接调用无参fun函数,也可以直接调用有参fun函数
6.虚基类
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员就拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要用作用域符来唯一标识,而且必须用直接基类进行限定
class A{
int x;
};
class A1:public A{···};
class A2:public A{···};
class B:public A1,public A2{···};
int main(){
B b;
cout<< b.A1::x <<endl;
}
可以将共同基类设置为虚基类,这样从不同路径继承而来的同名数据成员在内存中只有一个副本,同一函数名也只有一个映射
虚基类的声明是在派生类定义过程中进行的,语法为:
class 派生类名:virtual 继承方式 基类名
在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对其后的基类起作用。声明了虚基类之后,虚基类的成员在进一步的派生过程中和派生类一起维护同一个内存数据副本
class A{
public:
A(){
x=10;
}
int x;
};
class B1:virtual public A{
public:
void fun1(){
x=23;
}
};
class B2:virtual public A{
public:
void fun2(){
x=45;
}
};
class C:public B1,public B2{
public:
void fun3(){
x=67;
}
};
int main()
{
C c;
cout<<c.A::x<<","<<c.x<<endl;
c.fun1();
cout<<c.A::x<<","<<c.x<<endl;
c.fun2();
cout<<c.A::x<<","<<c.x<<endl;
c.fun3();
cout<<c.A::x<<","<<c.x<<endl;
return 0;
}
/*
程序的运行结果为:
10,10
23,23
45,45
67,67
*/
- 如果虚基类没有声明构造函数,那么所有相关类使用的都是默认构造函数
- 如果虚基类声明有带形参的构造函数,并且没有声明默认模式的构造函数,那么在整个继承关系中,直接或间接继承虚基类的所有派生类,都必须在构造函数的成员初始化列表中列出对虚基类的初始化
建立对象时所指定的类称为最远派生类。
在上一个例子中,对于虚基类A而言,C为最远派生类
建立一个对象时,如果对象中含有从虚基类继承来的成员,虚基类的成员由最远派生类的构造函数通过调用虚基类的构造函数进行初始化。
并且只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类(如B1、B2)对虚基类构造函数的调用会自动忽略。
Tips
构造一个类的对象的一般顺序:
- 如果该类有直接或者间接的虚基类,则先执行虚基类的构造函数。
- 如果该类有其他基类,则按照它们在继承声明的列表中出现的次序,分别执行它们的构造函数,但构造过程中,不在执行它们基类的构造函数。
- 按照在类定义中出现的顺序,对派生类中新增的成员对象进行初始化。对于类类型的成员对象,如果出现在构造函数初始化列表中,则以其中指定的参数执行构造函数。如果未出现,则执行默认构造函数;对于基本数据类型的成员对象,如果出现在构造函数的初始化列表中,则使用其中指定的值为其赋初值,否则什么也不做。
- 执行构造函数的函数体中的内容。