C++ --继承和派生

来源:互联网 发布:淘宝小二电话 编辑:程序博客网 时间:2024/06/09 15:34

1.面向对象程序设计有3个主要特点:封装、继承和多态性。

我们已经讲解了类和对象,了解了面向对象程序设计的个重要特征一数据封装,已经能够设计出基于对象的程序,这是面向对象程序设计的基础。
    要较好地进行面向对象程序设计,还必须了解面向对象程序设计另外两个重要特 ——继承性和多态性。

2.继承的概念

继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。

例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

被继承的类称为父类或基类,继承的类称为子类或派生类。

派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能

 

以下是两种典型的使用继承的场景:
1) 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。


2) 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。

 

3.class Student: public People

这就是声明派生类的语法。class 后面的“Student”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“People”之前有一关键宇public,用来表示是公有继承

 

由此总结出继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};  

 继承方式包括 public(公有的)、private(私有的)和protected(受保护的),此项是可选的,如果不写,那么默认为private

 

4.继承权限和方式

继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和protected(受保护的)。此项是可选项,如果不写,默认为private

publicprotectedprivate修饰类的成员

类成员的访问权限由高到低依次为:

 public --> protected --> private

public 成员可以通过对象来访问,private成员不能通过对象访问

 

现在再来补充一下 protectedprotected成员 private成员类似,也不能通过对象访问。

但是当存在继承关系时,protected private 就不一样了:

基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用

 

publicprotectedprivate指定继承方式

不同的继承方式会影响基类成员在派生类中的访问权限。
1) public继承方式基类中所有 public成员在派生类中为public属性;

基类中所有 protected 成员在派生类中为 protected 属性;基类中所有private成员在派生类中不能使用。

2) protected继承方式基类中的所有 public成员在派生类中为protected属性;

基类中的所有 protected 成员在派生类中为 protected 属性;

基类中的所有 private 成员在派生类中不能使用。

3) private继承方式基类中的所有 public成员在派生类中均为private属性

基类中的所有 protected 成员在派生类中均为 private 属性;

基类中的所有 private 成员在派生类中不能使用。

 

 

在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private成员函数,如果基类没有非 private成员函数,那么该成员在派生类中将无法访问(除非使用下面讲到的using关键字)。

 

变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将public改为private、将private改为public

 

5.C++继承时名字的遮拦

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。

 

所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

 

基类成员函数和派生类成员函数不构成重载

基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。

 

换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

Base 是基类,Derived是派生类,那么它们的作用域的嵌套关系如下图所示:

 

B 继承自 AC继承自B,它们作用域的嵌套关系如下图所示

 

6.C++继承对象内存模型

继承时的内存模型

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。

 

有成员变量遮蔽时的内存分布

当基类 AB的成员变量被遮蔽时,仍然会留在派生类对象obj_c的内存中,C类新增的成员变量始终排在基类AB的后面。例:4.052.cpp
总结:在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

 



7.C++派生类的构造函数

类的构造函数不能被继承。

构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。

在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

            *****在派生类的构造函数中调用基类的构造函数

 

7.1 Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }

 

People(name, age)就是调用基类的构造函数,并将 nameage作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。

7.2也可以将基类构造函数的调用放在参数初始化表后面:

Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }

 

但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码)

 

总体上看和下面的形式类似:
Student::Student(char *name, int age, float score){

People(name, age);

m_score = score;

}

当然这段代码只是为了方便大家理解,实际上这样写是错误的,因为基类构造函数不会被继承,不能当做普通的成员函数来调用。换句话说,只能将基类构造函数的调用放在函数头部,不能放在函数体中。

 

函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等,例如:

Student::Student(char *name, int age, float score): People("小明", 16), m_score(score){}

 

构造函数的调用顺序

从上面的分析中可以看出,基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:A --> B --> C

那么创建 C 类对象时构造函数的执行顺序为:A类构造函数--> B类构造函数--> C类构造函数

 

构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

 

基类构造函数调用规则

事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;

如果不指明,就调用基类的默认构造函数(不带参数的构造函数);

如果没有默认构造函数,那么编译失败。

 

创建对象 stu1 时,执行派生类的构造函数Student::Student(),它并没有指明要调用基类的哪一个构造函数,从运行结果可以很明显地看出来,系统默认调用了不带参数的构造函数,也就是People::People()

创建对象 stu2 时,执行派生类的构造函数Student::Student(char *name, int age, float score),它指明了基类的构造函数。

 

在第 27 行代码中,如果将People(name, age)去掉,也会调用默认构造函数,第37行的输出结果将变为:
xxx的年龄是0,成绩是90.5

如果将基类 People 中不带参数的构造函数删除,那么会发生编译错误,因为创建对象stu1时需要调用People类的默认构造函数, People类中已经显式定义了构造函数,编译器不会再生成默认的构造函数。

 

8.C++派生类析构函数

和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

 

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。

 

而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。

 

9.C++的多继承类

多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类Dclass D: public A, private B, protected C{
    //D新增加的成员
}

 

D 是多继承形式的派生类,它以公有的方式继承 A类,以私有的方式继承B类,以保护的方式继承C类。D根据不同的继承方式获取 ABC中的成员,确定它们在派生类中的访问权限。

 

多继承下的构造函数

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。

以上面的 ABCD类为例,D类构造函数的写法为:D(形参列表): A(实参列表), B(实参列表), C(实参列表){
    //其他操作
}

 

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 ABCD类为例,即使将D类构造函数写作下面的形式:D(形参列表): B(实参列表), C(实参列表), A(实参列表){
    //其他操作
}

那么也是先调用 A 类的构造函数,再调用B类构造函数,最后调用C类构造函数。

 

 

10.C++多继承的内存模型

AB 是基类,C 是派生类,假设 obj_c的起始地址是0X1000,那么obj_c的内存分布如下图所示:

 

 

C++ 不允许通过对象来访问 privateprotected属性的成员变量,

 

如果通过 p 指针访问 m_aint a = p -> m_a;

那么将被转换为下面的形式:int a = * (int*) ( (int)p + 0 );

经过简化以后为:int a = *(int*)p;

 

11.C++虚函数和虚基类

多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,

 

 

A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类D中变成了两份,一份来自A-->B-->D这条路径,另一份来自A-->C-->D这条路径。

 

在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:

因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。

假如类 A 有一个成员变量a,那么在类D中直接访问a就会产生歧义,编译器不知道它究竟来自A -->B-->D这条路径,还是来自A-->C-->D这条路径。

 

这段代码实现了上图所示的菱形继承,第 25 行代码试图直接访问成员变量 m_a,结果发生了错误,因为类B和类C 中都有成员变量m_a(从A类继承而来),编译器不知道选用哪一个,所以产生了歧义。

为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:void seta(int a){ B::m_a = a; }

这样表示使用 B 类的 m_a。当然也可以使用C类的:void seta(int a){ C::m_a = a; }

 

虚继承 //解决命名冲突

在继承方式前面加上 virtual 关键字就是虚继承

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的A就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。


观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:

必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和C类不是从A类虚派生得到的,那么D类还是会保留A类的两份成员。

换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

 

在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。

 

对最终的派生类来说,虚基类是间接基类,而不是直接基类。

这跟普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的

 

 

 

D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }

 

在最终派生类 D 的构造函数中,除了调用BC 的构造函数,还调用了A的构造函数,这说明 D不但要负责初始化直接基类BC,还要负责初始化间接基类A

而在以往的普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。

 

现在采用了虚继承,虚基类 A 在最终派生类 D 中只保留了一份成员变量m_a,如果由BC 初始化 m_a,那么B C 在调用 A 的构造函数时很有可能给出不同的实参,这个时候编译器就会犯迷糊,不知道使用哪个实参初始化m_a

 

为了避免出现这种矛盾的情况,C++ 干脆规定必须由最终的派生类 D 来初始化虚基类A,直接派生类BC A 的构造函数的调用是无效的。

 

在代码中,调用 B 的构造函数时试图将m_a初始化为90,调用C的构造函数时试图将m_a初始化为100,但是输出结果有力地证明了这些都是无效的,m_a最终被初始化为50,这正是在D中直接调用A的构造函数的结果。

 

另外需要关注的是构造函数的执行顺序。

虚继承时构造函数的执行顺序与普通继承时不同:

在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;

而对于普通继承,就是按照构造函数出现的顺序依次调用的。

 

 

修改代码,改变构造函数出现的顺序:

D::D(int a, int b, int c, int d): B(90, b), C(100, c), A(a), m_d(d){ }

 

虽然我们将 A() 放在了最后,但是编译器仍然会先调用A(),然后再调用B()C(),因为A()是虚基类的构造函数,比其他构造函数优先级高。

如果没有使用虚继承的话,那么编译器将按照出现的顺序依次调用 B()C()A()