C++中,继承就是在一个已存在的类的基础上建立一个新的类。已存在的类称为基类,又称父类;新建立类称为派生类,又称为子类。
基类是对派生类的抽象,派生类是对基类的具体化。
(一)派生类的定义与构成
1)派生类的定义
class 派生类名:类派生列表{
成员列表
};
类派生列表指定了一个或多个基类,形式如下:
访问权限标号 基类名1,访问权限标号 基类名2,...
2)派生类的构成
派生类由两部分组成:第一部分是从基类继承得到的,另一部分是自己定义的新成员,这些成员仍然分为三种访问属性。
注意,友元关系是不能继承的:一方面,基类的友元对派生类成员没有特殊的访问权限;另一方面,如果基类被授予了友元关系,则只有基类有特殊访问权限,该基类的派生类不能访问授予友元关系的类。
实际编程中,设计一个派生类包括三个方面工作:
a)从基类接收成员。
除了构造函数与析构函数之外,派生类会把基类的全部成员继承过来。
b)调整基类成员的访问。
程序员可以对接收的成员指定访问策略。
c)在定义派生类时增加新的成员。另外还应该自己定义派生类的构造函数和析构函数,因为它们不能从基类继承过来。
(二)派生类成员的访问
不同的继承方式决定了基类成员在派生类中的访问属性
1)公有继承
基类的公有成员和保护成员在派生类中保持原有访问属性,私有成员仍为基类所有。
2)私有继承
基类的所有成员在继承类中为私有成员。
3)保护继承
一般地,保护继承与私有继承在实际编程中极少使用,它们只在技术理论上有意义。
在公有继承中,派生类成员可以访问继承的基类的public部分与protected部分,但是不能访问private部分。只有基类成员以及基类的友元可以访问private部分。
无论采用何种继承方式得到的派生类,派生类成员及其友元都不能访问基类的私有成员。
(三)赋值兼容性规则
赋值兼容规则是指在需要基类对象的任何地方,都可以使用共有派生类的对象来替代。通过公有继承,派生类得到了基类中除了构造函数与析构函数之外的所有成员。这样共有派生类实际就具有了基类的所有功能,凡是基类能解决的问题,公有派生类都可以解决。
class Base{}; //基类
class Derive:public Base{}; //公有派生类
Base b,*pb; //定义基类对象,指针
Derive d; //定义派生类对象
赋值兼容性规则中所指的替代包括如下情况。
(1)派生类的对象可以赋值给基类对象;
b=d; //派生类对象赋值给基类,复制基类继承部分
(2)派生类的对象可以初始化基类的引用;
Base &rb=d; //基类引用到派生类对象
(3)派生类对象的地址可以赋值给指向基类的指针。
pb=&d; //基类指针指向派生类对象
(四)派生类的构造和析构函数
1)构造函数
派生类的构造函数定义形式如下:
派生类名站(形式参数列表):基类名(基类构造函数实参列表),派生类初始化列表
例子:
class Point{int x,y;
public:Point(int a,int b):x(a),y(b){} //基类构造函数
};
class Rect:public Point {int h,w;
public :Rect(int a,int b,int c,int d):Point(a,b),h(c),w(d){} //派生类构造函数
如果基类有默认构造函数,或者参数全部都是默认参数的构造函数,那么派生类的构造函数中可以不显式初始化子对象。编译器会调用基类的构造函数进行初始化。
2)组合关系的派生类的构造函数
包括多个子对象的派生类的构造函数定义为:
派生类名(形式参数列表):基类名(基类构造函数实参列表),
子对象名1(子对象1所属类的构造函数实参列表)
...
派生类初始化列表
{
派生类初始化函数体
};
下面是一段实例代码:
#include<iostream> using namespace std; class A{ public: A():a1(1),a2(2){} A(int a,int b):a1(a),a2(b){} //private: int a1,a2; }; class B{ public: B(int a,int b):b1(a),b2(b){} //private: int b1,b2; A A1; }; class C:public B{ public: C(int a,int b):B(3,4), A2(5,6), c1(a),c2(b){} //private: int c1,c2; A A2; }; int main() { C C1(7,8); cout<<C1.c2<<endl<<C1.c1<<endl; cout<<C1.A2.a2<<endl<<C1.A2.a1<<endl; cout<<C1.b2<<endl<<C1.b1<<endl; cout<<C1.A1.a2<<endl<<C1.A1.a1<<endl; return 0; }
利用gcc编译器测试:
lzb@lzb:~/classic_lib/C++_learning$ g++ 417.cpp
lzb@lzb:~/classic_lib/C++_learning$ ./a.out
8
7
6
5
4
3
2
1
3)析构函数
在执行派生类的析构函数时候,系统会自动调用基类的析构函数与子对象的析构函数,对基类和子对象进行处理。
(五)多重继承
C++支持一个派生类同时继承多个基类。
多重继承派生类的定义:
class 派生类名:访问标号1 基类名1,访问标号2 基类名2,...{
成员列表
};
多重继承构造函数:
多重继承派生类的构造函数形式与单一继承时的构造函数形式基本相同,只是在派生类的构造函数初始化列表中调用调用多个基类构造函数。一般形式为:
派生类名(形参列表):基类名1(基类1构造函数实参列表)
基类名2(基类2构造函数实参列表)
...
子对象名1(子对象1所属类的构造函数实参列表)
...
派生类初始化列表
{
派生类初始化列表
};
a)二义性问题
class A{
public:
void fun (){cout<<"a";}
};
class B{
public:
void fun(){cout<<"b";}
void gun(){cout<<"c";}
};
class C:public A,public B{
public:
void gun(){cout<<"d";}
void hun(){fun();}
};
C c,*c1;
c1=&c;
多重继承时,多个基类可能会出现同名的成员。在派生类中对这种基类对象的直接访问会有二义性。C++要求派生类对基类成员的访问必须是无二义性的。这时我们需要使用成员名限定消除二义性:
基类名::成员名 //在派生类内部使用基类成员,可以在fun()前面加上::,得到A::fun()
对象名.基类名::成员名 //派生类对象使用基类成员 ,c.A::fun(); c.B::fun();
对象指针名->基类名::成员名 //派生类指针使用基类成员, c1->A::fun(); c1->B::fun();
b)名字支配规则
在类的派生层次结构中,基类的成员与派生类新增的成员都具有类作用域,二者的作用域是不同的。它们是相互包含的两个层,基类在外边,派生类在里面。配生类如果声明了一个和基类成员同名的成员,这时直接使用成员名只能访问到派生类的成员。如果派生类中声明了与基类成员函数同名的函数,即使函数形式参数不同,直接使用成员名也只能访问到派生类的新函数。这时需要作用域运算符和基类名来限定。
c.gun; //c.C::gun();
c.B::gun(); //使用B::gun
(六)虚基类
如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员。
在一个类中保留间接共同基类的多份同名成员,虽然有时候是必要的,可以在不同的数据成员中分别存放不同的数据,也可以通过构造函数分别对他们进行初始化。但是大多数情况下,这种情况是人们不希望出现的。因为保留多份数据成员的副本,不仅占用较多的存储空间,还增加了访问这些成员时的困难。
C++提供虚基类机制,使得在继承间接共同基类时只保留一份成员。
1)虚基类定义
虚基类是在派生类定义时指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。声明虚基类的一般形式为:
class 派生类名:virtual 访问权限标号 虚基类名,...{
成员列表
};
经过这样的声明之后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是基类成员只保留一次。
注意:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。
2)虚基类的初始化
如果在虚基类中定义了带参数的构造函数,而且没有定义