目录
一、继承的概念
- 面向对象技术强调软件的可重用性,在C++中,可重用性是通过“继承”这一机制来实现的。一个新类从已有的类那里获得其已有特性,这种现象称为继承。
- 已有的类称为基类或父类,新产生的类称为派生类或子类。
- 一个派生类只从一个基类派生,称为单继承;一个派生类从两个或多个基类派生,称为多重继承。
二、继承的方式
1.单继承
单继承的语法结构如下:
class 派生类名:[继承方式] 基类名
{
/*派生类新增加的成员*/
};
[ ]表示可选,不指定继承方式则默认为private。
①公用继承
公用继承的语法结构如下:
class 派生类名:public 基类名
{
/*派生类新增加的成员*/
};
当采用public方式继承时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能被派生类直接访问,只能通过调用基类的公有和保护成员函数来访问。
②保护继承
保护继承的语法结构如下:
class 派生类名:protected 基类名
{
/*派生类新增加的成员*/
};
当采用protected方式继承时,基类的公有和保护成员将全部成为派生类的保护成员,基类的私有成员依旧不能被派生类访问。
③私有继承
私有继承的语法结构如下:
class 派生类名:private 基类名
{
/*派生类新增加的成员*/
};
当采用private方式继承时,基类的公有和保护成员将全部成为派生类的私有成员,基类的私有成员依旧不能被派生类访问。
④多级派生
多级派生就是A类派生出B类,B类又派生出C类,依次类推。在多级派生中依旧遵守上述各一级派生的规则。其中,A类是B类的直接基类,A类是C类的间接基类。B类是A类的直接派生类,C类是A类的间接派生类。
- 从上述描述中可以看出,无论哪种继承方式,基类的私有成员都不能被访问,这体现了封装特性对成员的隐藏和保护作用,防止通过继承来恶意访问类的私有成员。
- protected关键字就是为了能让派生类可以访问,同时又不让其他类访问而存在的。
2.多重继承
多重继承就是一个类派生自多个类,其语法结构如下:
class 派生类名:[继承方式] 基类名 , ... , [继承方式] 基类名
{
/*派生类新增加的成员*/
};
各个基类之间用逗号分隔,各个基类的继承结果同单继承。
三、派生类的构造函数和析构函数
一个派生类继承了所有的基类方法,但下列情况除外:
- 基类的构造函数、析构函数和拷贝构造函数。
- 基类的友元函数。
- 基类的重载运算符。
如果用户不定义构造函数,系统会自动设置一个默认构造函数,但这是个空函数,不执行任何操作。如果想要对类中的数据进行初始化,需要自行定义构造函数。派生类不仅需要考虑自身数据的初始化,还要考虑基类数据的初始化。 这个问题的解决思路是:在执行派生类的构造函数时,调用基类的构造函数。
1.简单的派生类的构造函数
简单的派生类只有一个基类,而且只有一级派生(只有直接派生类,没有间接派生类),在派生类的数据成员中不包含基类的对象。
派生类的构造函数一般形式为:
派生类构造函数名(总参数表):基类构造函数名(参数表)
{
/*派生类中新增数据成员初始化语句*/
}
(总参数表)包含了初始化派生类所需的参数和初始化基类所需的参数;(参数表)包含了初始化基类所需的参数。
(总参数表)中的参数需要加数据类型,因为这是在定义,里面是形参。(参数表)中的参数不需要加数据类型,因为这是函数调用,里面是实参。
2.有子对象的派生类的构造函数
子对象,即对象中的对象,作为数据成员存在,类似结构体中的成员可以是结构体变量。
该类构造函数的一般形式如下:
派生类构造函数名(总参数表):基类构造函数名(参数表),子对象名(参数表)
{
/*派生类中新增数据成员初始化语句*/
}
类似的,(总参数表)中应包含所有参数,且都是带数据类型的形参;各(参数表)包含各自所需的不带数据类型的实参。总参数表中的参数的次序不要求。
3.多重继承类的构造函数
当派生类继承自多个基类,即为多重继承,并且包含多个子对象时,可以用逗号分隔,在冒号后面依次列出。这种方式类似于类的构造函数的初始化列表,可以去做相关了解。
4.多级派生类的构造函数
当遇到一个类为多个类依次派生出来的类时,其构造函数只需依照上述格式,列出其直接基类的构造函数即可。其直接基类会在其构造函数中初始化再上一级的基类,依次类推,直至第一个基类被初始化。
5.派生类的析构函数
在派生时,派生类也不能继承基类的析构函数,也需要通过派生类的析构函数去调用基类的析构函数。这里暂不做过多介绍。
四、多重继承引起的二义性问题
1.多个基类中有同名成员
当多个基类中有相同的成员变量以及函数名和参数列表都相同的成员函数时,需要使用作用域解析运算符::来说明调用时使用哪个基类中的成员,如果不说明,将编译错误。如:
class A //基类A
{
public:
int a;
void display();
};
class B //基类B
{
public:
int a;
void display();
};
class C :public A,public B //派生类C
{
public:
void Set()
{
A::a=1; //令A中的a=1
B::a=2; //令B中的a=2
A::display(); //调用A中的display()函数
B::display(); //调用B中的display()函数
}
};
C c1; //用派生类C定义一个对象
c1.A::a=1; //令A中的a=1
c1.B::a=2; //令B中的a=2
c1.A::display(); //调用A中的display()函数
c1.B::display(); //调用B中的display()函数
2.多个基类和派生类中都有同名成员
当多个基类和派生类中都有相同的成员变量以及函数名和参数列表都相同的成员函数时,需要使用作用域解析运算符::来说明调用时使用哪个基类中的成员,如果不说明,编译不会有问题,并且调用的都是派生类中的成员。如:
class A //基类A
{
public:
int a;
void display();
};
class B //基类B
{
public:
int a;
void display();
};
class C :public A,public B //派生类C
{
public:
int a;
void display();
};
C c1; //用派生类C定义一个对象
c1.A::a=1; //令A中的a=1
c1.B::a=2; //令B中的a=2
c1.A::display(); //调用A中的display()函数
c1.B::display(); //调用B中的display()函数
c1.a=1; //令C中的a=1
c1.display(); //调用C中的display()函数
//不指定作用域,将会访问派生类中的成员
许多专业人员认为,不提倡在程序中使用多重继承,只有在比较简单和不易出现二义性的情况下或实在必要时才使用多重继承,如果能使用单一继承解决的问题就不要使用多重继承。也是这个原因,有些面向对象的程序设计语言(如Java)并不支持多重继承。
五、虚基类
1.虚基类的作用
如果一个派生类D有多个直接基类B和C,而这些直接基类又有一个共同的基类A,则在最终的派生类D中会保留该间接共同基类A的成员中的多份同名成员。比如A中有一个成员变量data_A,D在继承B和C之后,D中会有两个data_A。在引用这些同名的成员时,必须在派生类D的对象名后面增加直接基类名B或C,以避免产生二义性。如d1.B::data_A(调用d1对象中的B类中的data_A)。
有时候,我们不希望这样。我们想在继承间接共同基类A时只保留一份成员,这时 ,就使用到了虚基类,这也是虚基类的作用。
声明虚基类的一般形式为:
class 派生类名:virtual 继承方式 基类名
显然,声明虚基类的方法就是在声明派生类时,指定继承的时候多加一个virtual关键字。为了使派生类D中只有一份A中的成员,可以如此使用虚基类:
class A //间接基类A
{
public:
int data_A;
}
class B :virtual public A //直接基类B,继承了A
{
}
class C :virtual public A //直接基类C,继承了A
{
}
class D :public B, public C //派生类D,继承了B、C
{
}
这时,派生类D中就只有一个int data_A成员。
特别需要注意virtual的使用地点,我们是为了防止基类A的成员被多次拷贝,所以将基类A声明为虚基类,因此对基类A的所有直接派生类(如B、C)都需要加virtual,而间接派生类(如D)是不需要加virtual的。
若没有在共同间接基类(如A)的全部直接派生类(如B、C)的声明中将基类A声明为虚基类,那么间接派生类(如D)中仍然会出现对基类A的多次继承。声明了虚基类的直接基类一起保留一份共同间接基类的成员,原理同上面;剩下的未声明虚基类的直接基类,每继承一个都将增加一份共同间接基类(如A)的成员。
2.虚基类的初始化
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生和间接派生的类)中都要通过构造函数的初始化列表来对虚基类进行初始化。
以前,在派生类的构造函数(D)中,只需负责对其直接基类(B、C)初始化,再由其直接基类(B、C)负责对间接基类(A)初始化。
但是现在,由于虚基类的使用,在派生类(D)中只有虚基类(A)的一份数据成员,所以这份数据成员应由派生类(D)直接给出。
class A //间接基类A
{
public:
A(int i) //A的构造函数
{
}
}
class B: virtual public A //直接基类B
{
public:
B(int n):A(n)
{
//B的构造函数,需对A初始化
}
}
class C: virtual public A //直接基类C
{
public:
C(int n):A(n)
{
//C的构造函数,需对A初始化
}
}
class D: public B,public C //派生类D
{
public:
D(int n):A(n),B(n),C(n)
{
//D的构造函数,需对A、B、C初始化
}
}
如果没有声明虚基类,则上述代码中的派生类D只需要对其直接基类B、C进行初始化,无需对间接基类A初始化。有无虚基类的初始化的区别仅在于此。
六、继承的优点
-
代码重用:继承使得派生类能够重用基类的代码,而无需重复编写相同的代码。
-
可扩展性:通过继承,可以轻松扩展现有类的功能而无需修改基类的代码。
-
代码维护性:维护简单,当需要修改公共的行为时,只需修改基类,减少了重复修改的工作,提高了维护效率。