类的继承与派生
类的继承,是新的类从已有类那里得到已有的特性。从已有类产生新类的过程就是类的派生。
派生类的定义:
class 派生类名:继承方式 基类名1, 继承方式 基类名2, ……, 继承方式 基类名n
{
派生类成员声明;
};
一个派生类,可以同时有多个基类,这种情况称为多继承。这时的派生类同时得到了多个已有类的特性。一个派生类只有一个直接基类的情况,称为单继承。在派生类的定义中,除了要指定基类外,还需要指定继承方式。继承方式规定了如何访问从基类继承的成员。在派生类的定义语句中,每一个“继承方式”,仅限定紧随其后的基类。如果不显示的给出继承方式关键字,系统默认值就认为是私有继承(private)。派生类成员是指除了从基类继承来的所有成员之外,新增加的数据和函数成员。
派生类的生成过程
①吸收基类成员
吸收基类中除了构造函数和析构函数之外的所有非静态成员。
②改造基类成员
改造包括两方面:类成员的访问控制问题、对基类数据和函数成员的覆盖或隐藏。
如果派生类声明了一个和基类成员同名的新成员,派生的新成员就隐藏了外层(基类)同名成员。
③添加新的成员
访问控制
①公有继承
当类的继承方式为公有继承时,基类的公有成员和保护成员的访问属性在派生类中不变,而基类的私有成员在派生类中不可直接访问。
②私有继承
当类的继承方式为私有继承时,基类的公有成员和保护成员都以私有成员的身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。
③保护继承
保护继承中,基类的公有成员和保护成员都以保护成员的身份出现在派生类中,而基类的私有成员在派生类中不可直接访问。
类型兼容规则
类型兼容规则是指在需要基类对象的任何地方,都可以使用公有派生类的对象来替代。
派生类的对象可以隐含的转换为基类对象。
派生类的对象可以初始化基类的引用。
派生类的指针可以隐含的转换为基类的指针。
在替换之后,派生类的对象可以作为基类的对象使用,但只能使用从基类继承的成员。
派生类的构造函数和析构函数
派生类的构函数只负责对派生类新增的成员进行初始化,对所有从基类继承下来的成员,其初始化工作还是由基类的构造函数完成。同样,对派生类的扫尾、清理工作也需要加入新的析构函数。
构造函数
派生类对于基类的很多成员对象是不能直接访问的,因此要完成对基类成员对象的初始化工作,需要调用基类的构造函数。派生类的构造函数需要以合适的初值作为参数,其中一些参数要传递给基类的构造函数,用来初始化相应的成员,另一些参数要用于对派生类新增的成员对象进行初始化。
如果对基类初始化时,需要调用基类的带有形参表的构造函数时,派生类就必须声明构造函数。
派生类执行构造函数的一般次序:
①调用基类构造函数,调用顺序按照它们被继承时声明的顺序(从左向右)
②对派生类新增的成员对象初始化,调用顺序按照它们在类中的声明顺序。
③执行派生类的构造函数体中的内容。
细节:构造函数初始化列表中基类名、对象名之间的次序无关紧要,它们各自出现的顺序可以是任意的,无论它们的顺序怎样安排,基类构造函数的调用和各成员对象的初始化顺序都是确定的。
复制构造函数
如果程序员没有编写复制构造函数,编译系统会在必要时自动生成一个隐含的复制构造函数,这个隐含的复制构造函数会自动调用基类的复制构造函数,然后对派生类新增的成员对象一一执行复制。
如:类B是类A的派生类,继承方式为public
B::B(constB &ref):A(ref)
{
……
}
这里运用了类型兼容规则,可以用派生类的对象去初始化基类的引用。但是继承方式一定要是public。
析构函数
只要在函数体中负责把派生类新增的非对象成员的清理工作做好就够了,系统会自动调用基类及对象成员的析构函数来对基类及对象成员进行清理。这些清理工作分别是执行派生类析构函数体、调用类类型的派生类对象成员所在类的析构函数和调用基类的析构函数。对这些析构函数的调用次序,与对构造函数的调用次序刚好完全相反。
派生类成员的标识和访问
在派生类中,成员可以按访问属性划分为以下4种:
不可访问的成员、私有成员、保护成员和公有成员。
作用域分辨符
在类的派生层次结构中,基类的成员和派生类新增的成员都具有类作用域。二者作用范围不同,是相互包含的两个层,派生类在内层。
如果派生类中声明了与基类成员函数同名的新函数,即使函数的参数表不同,从基类继承的同名函数的所有重载形式也都会被隐藏。如果要访问被隐藏的成员,就需要使用作用域分辨符和基类名来限定。
细节:如果子类中定义的函数与父类的函数同名但具有不同的参数数量或参数类型,不属于函数重载,这时子类中的函数将使父类中的函数隐藏,调用父类中的函数必须使用父类名称来限定。只有在相同的作用域中定义的函数才可能重载。
using的一般功能是将一个作用域中的名字引入到另一个作用域中,它还有一个非常有用的用法:将using用于基类中的函数名,这样派生类中如果定义同名但参数表不同的函数,基类的函数不会被隐藏,两个重载的函数将会并存在派生类的作用域中。
如:
usingBase::function;//在子类定义中使用
如果某个派生类的部分或全部直接基类是从另一个共同的基类派生而来,在这些直接基类中,从上一级基类继承来的成员拥有相同的名称,因此派生类中也就会产生同名现象,对这种类型的同名成员也要使用作用域分辨符来唯一标识,而且必须用直接基类来进行限定。
struct的继承规则及void *
struct默认的继承方式为public。
例如:
struct A
{
A(int x):x(x){}
int x; //默认权限为公有的
};
struct B
{
B(int y):y(y){}
int y; //默认权限为公有的
};
structC:A, B //采用默认的继承方式
{
C(int x = 1, int y = 2):A(x), B(y){}
};
C c;
c.x = 8; //OK 派生类的对象可以访问基类的公有成员
cout<< c.x << endl; //OK
cout<< c.y << endl; //OK
C *c =new C;
c->x= 6;
c->y= 8;
void *pv= c;//任何类型的指针都可以隐式的转换为void*
B *b =static_cast<B *>(pv);//用void*转换时,只是更改了类型信息而已,值不变
cout<< "c:" << c << endl; //006706D0
cout<< "pv:" << pv << endl; //006706D0
cout<< "b:" << b << endl; //006706D0
//因为在B类中,y的偏移量为0,所有通过指针b访问y时,直接输出地址006706D0处的值,即6
cout<< c->y << " " << b->y << endl; //8 6
b =static_cast<B *>(c); //用C*转换时,不仅更改了类型信息,指针值也加了偏移量
cout<< "b:" << b << endl; //006706D4(C类中B类数据成员的首地址)
//因为在B类中,y的偏移量为0,所有通过指针b访问y时,直接输出地址006706D4处的值,而006706D4正是C类中B类数据成员的首地址,所有正确访问到y
cout<< c->y << " " << b->y << endl; //8 8
deletec;//释放空间
注意:用void*接收了什么类型的指针,最后都将其转换为对应类型的指针,void*起一个中间转接的作用,不能将void*接收的指针转换为其他类型的指针,即使是将接收的派生类的指针转给基类指针也不行,那样会缺少偏移量,导致访问数据时出错(见上例)。
虚基类
将共享基类设置为虚基类,这时从不同的路劲继承来的同名数据成员在内存中就只有一个副本,同一个函数名也只有一个映射。这样就解决了同名成员的唯一标识问题。在多继承情况下,虚基类关键字的作用范围和继承方式关键字一样,只对紧跟其后的基类起作用。
虚基类及其派生类构造函数
建立一个对象时,如果这个对象中含有从虚基类继承来的成员,则虚基类的成员是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化。而且,只有最远派生类的构造函数会调用虚基类的构造函数,该派生类的其他基类对虚基类构造函数的调用都自动被忽略。
如:
class Base0
{
public:
Base0(int var = 0):var0(var){}
int var0; //间接基类的数据成员
void fun0() //间接基类的成员函数
{
cout <<"Base0" << var0 << endl;
}
};
//以Base0为虚基类,派生Base1
classBase1:virtual public Base0
{
public:
Base1(int var = 1):Base0(var),var1(var){}
int var1; //直接基类的数据成员
void fun1() //直接基类的函数成员
{
cout << "base1" << endl;
}
void fun(int a) //直接基类的函数成员
{
cout << a <<endl;
}
};
//以Base0为虚基类,派生Base2
classBase2:virtual public Base0
{
public:
Base2(int var = 2):Base0(var),var1(var){}
int var1; //直接基类的数据成员
void fun1() //直接基类的函数成员
{
cout <<"Base2" << endl;
}
};
//最远派生类
classBase3:public Base1, public Base2
{
public:
Base3(int var = 8):Base0(var),Base1(var), Base2(var), var(var){}
Base3(const Base3 &ref) //:Base1(ref), Base2(ref),Base0(ref)
{
var = ref.var;
cout <<"&Base3" << endl;
}
using Base1::fun; //使用直接基类Base1中的fun函数。通过Base3类的对象调用fun函数时,先在Base3中寻找,如果没有匹配的就从Base1作用域中寻找
int var; //最远派生类新增的数据成员
void fun() //最远派生类新增的成员函数
{
cout <<"Base3" << endl;
}
};
int main()
{
Base3 b(4); //实例化一个最远派生类的对象
Base2 c(6); //实例化一个直接基类的对象,此时Base2也是最远派生类。可以当做Base3不存在。
b.fun0(); //输出4
c.fun0(); //输出6
b.fun(); //调用Base3中定义的无参fun函数(匹配到了就用Base3类中的函数)
b.fun(3); //调用Base1中定义的有参fun函数(没有匹配到了就用Base1类中的函数)
b.var0 = 9; //直接给间接基类Base0的数据成员赋值
b.fun0(); //直接调用间接基类Base0的成员函数,输出9
c.fun0(); //输出6
b.Base1::fun0();//通过直接基类调用fun0函数,实际是映射到间接基类(映射和副本只有一份),输出9
b.Base2::fun0();//通过直接基类调用fun0函数,实际是映射到间接基类(映射和副本只有一份)
return 0;
}
在派生类的复制构造函数的初始化列表中对基类的临时对象显示的初始化时,如果基类写了复制构造函数,将调用基类的复制构造函数,否则将调用基类中默认的复制构造函数。
细节:构造一个类的对象的一般顺序
①如该类有直接或间接的虚基类,则先执行虚基类的构造函数。
②如果该类有其他基类,则按照它们在继承声明列表中出现的次序,分别执行它们的构造函数,但构造过程中,不再执行它们的虚基类的构造函数。
③按照在类定义中的顺序,对派生类新增的成员对象进行初始化。
④执行构造函数体中的内容。