第五章 继承和派生类继承 和 派生类 是面向对象程序设计的一个重要组成部分。本章要求掌握 单一继承,多重继承,
两义性,支配规则 和 虚基类 的概念;掌握派生类的 访问权限,构造函数与析构函数的调用顺序 ;理解 两义性及其支配规则,训练运用作用域分辨符,掌握赋值兼容规则 。
一、继承和派生的基本概念:
继承,基类与派生类:
继承,继承 就是创建一个具有别的类的属性和行为的新类的能力。就是从已有的对象类型出发,建立一种新的对象类型,使它继承 (具有 )原对象特点和功能。
类的派生,是一个过程,就是通过特殊化已有的类来建立新类的过程。 (我们想象水稻学家们试验出一个水稻新品种的情况,通过已有的种类,创出新的种类,
而这个新类又继承了原来品种的一些特点和属性,并有自己新的属性和特点 )
基类和派生类,如同上面的例子,原有的水稻品种就是 "基类
",新生的品种就叫做 "派生类 ",新的品种 "遗传 "了原来品种的各种基因,在这里,一个派生类自动地将基类的成员做为自己的成员,我们叫做 "继承 ",很形象地,基类和派生类又可叫做 "父类 "和 "子类 "。
性质约束和性质扩展:
类的派生 (继承 )是面向对象程序设计方法和 c++语言最重要的特征之一。
继承 常用来表示类属关系,不能将 继承 理解为构成关系,
比如我们假设昆虫是一个类,而蝴蝶也是一个类,这两个类是派生 (继承 )的关系,基类是昆虫,派生类是蝴蝶,这是一个类属的关系,蝴蝶类是属于昆虫类的,但不能理解为构成关系,蝴蝶这个类不是昆虫这个类中的成员的简单包括,它还有其他一些独特的成员,而原来的成员也被重新定义,这些都表明蝴蝶类是昆虫类的一个分支而不是构成关系。
从上面的例子来理解,从现存类中派生出新类时,可以对派生类做如下几种变化:
1.可以增加新的成员变量;
2.可以增加新的成员函数;
3.可以重新定义已有的成员函数;
4.可以改变现有成员的属性 (访问权限 )。
以上允许派生类所作的变化是很广泛的,由此我们可以用派生类对其从父类继承来的的性质进行 限制或删除 (这就是 性质约束 ),也可以对父类的性质进行 增加 (这就是 性质扩展 )。
二、单一继承
C++中有两种继承,单一继承 和 多重继承 。多重继承的派生类有多个基类单一继承 是本章的 重点,
单一继承 就是只通过 一个基类 产生派生类。这个派生类的 基类只有一个,它从基类继承所有的成员。
单一继承的一般形式为:
class 派生类名,访问控制 基类名 {
private:
成员说明列表
public:
成员说明列表
};
关于 "类的保护成员 ":
当一个 派生类 的 基类成员 是 私有成员 的时候,虽然基类的成员已被派生类继承,但 C++规定,这个 派生类 仍 不能直接访问基类的私有成员,只能通过基类的公有成员作为接口去访问,
这是符合数据封装的思想的。基类的私有成员在派生类中就是 "不可访问成员 "。
为了能够在派生类中访问基类所有成员,又使数据封装得以实现,就引入了 "保护成员 "的概念。就是在类的定义中,
用 protected来说明类成员,而不是用 "private",这样的成员就是类的保护成员。 保护成员 对于 派生类 的 成员函数 而言是 公有成员,而对于 其他函数 就仍是 私有成员 。
对于单一继承,主要要掌握的是其中的 "访问控制 (访问权限 )"
和赋值兼容规则:
当在 派生类定义 中的 访问控制 (权限 )设为 public(公有 )时,
这个类的派生就称为 "公有派生 ",它有如下特点:
1.基类的公有成员在派生类中仍然是公有的。
2.基类的保护成员在派生类中仍然是保护的。
3.基类的不可访问和私有成员在派生类中仍然是不可访问的。
因为派生是没有限制的,也就是说,派生类也可做为基类派生新的类,所以在派生类中有一种 "不可访问成员 "级别存在,它要么就是基类的不可访问成员要么就是基类的私有成员。
#include <iostream.h>
class A
{
public:
void setx(int a) { x = a; }
void sety(int b) { y = b; }
int getx() const { return x; }
int gety() const { return y; }
protected:
int x;
private:
int y;};
class B:public A
{
public:
int getsum() { return x + gety(); }
};
void main()
{
B b;
b.setx(2);
b.sety(3);
cout<<"X="<<b.getx()<<"\tY="<<b.gety()<<endl;
cout<<"X + Y = "<<b.getsum()<<endl;
}
所谓 赋值兼容原则 就是,在 公有派生 的情况下,一个派生类的对象可以作为基类的对象来使用的地方 (在 公有派生 的情况下,每一个派生类的对象都是基类的一个对象 ----它继承了基类的所有的成员并没有改变其访问权限 )。
具体的说,有 三种情况 是可以把一个派生类的对象作为基类对象来使用的,
1.派生的对象可以赋给基类的对象 。如,(约定 derived是从类
base公有派生而来的 )
derived d;
base b;
b=d;
2.派生类的对象可以初始化基类的引用 。如:
derived d ;
base &br=d;
3.派生类的对象的地址可以赋给指向基类的指针 。如:
derived d;
base *pb=&d;
在后两种情况下,通过指针或引用只能访问对象 d中所 继承的基类成员 。
关于私有派生,就是要理解,通过 私有派生,基类 的 任何成员 在派生类中都是 私有 的,这样就改变了基类中公有成员
(以及保护成员 )在派生类中的访问权限。这样一来,派生类中继承来的基类成员均成为它的私有成员,使得通过这个派生类再次派生出新类时,这些成员在新派生类中均成为不可访问成员,使派生类的使用很不方便。所以私有派生在实用中用得不多。
保护成员 上面已经提到,而 保护派生 不容易掌握,实用的意义也不大,可以略过。
总结,1、当现存类 (基类 )中派生出新类时,派生类 可以重新定义已有的成员函数;这时如要调用基类函数,必须用准确名。即使用基类名和范围分解运算符。如不加则变成本地函数的递归调用。
class X
{
int i;
enum {factor=11};
public:
X() {i=0;}
void set(int I)
{i=I;}
int read() const {return i;}
int permute() {return i=i*factor;}
};
class Y,public X
{
int i;
public:
Y() {i=0;}
int change ()
{ i=permute();
return i;}
void set (int I)
{i=I;
X::set(I);
}
};
2.可以改变现有成员的属性 (访问权限 )。
class base1
{
public:
char f() const {return 'a';}
int g() const {return 2;}
float h() const {return 3.0;}
};
class derived:base1
{
public:
base1::f;
base1::h;
};
#include "privinh.h"
main()
{
derived d;
d.f();
d.h();
}
三、多重继承多重继承 就是 一个派生类 由 多个基类 派生而来,每一个继承路径可视为一个单一继承。
多重继承的一般形式是:
class 类名 1:访问控制 类名 2,访问控制 类名 3,...访问控制 类名 n
{...//定义派生类自己的成员 };
从这个一般形式可以看出,每个基类有一个访问控制来限制其中成员在派生类中的访问权限,其规则和单一继承情况是一样的,多重继承 可以看作是 单一继承的扩展 。
#include <iostream.h>
class A
{
public:
void setx(int a){ x = a; }
void sety(int b){ y = b; }
int getx() const { return x; }
int gety() const { return y; }
protected:
int x;
private:
int y;
};
class B
{
public:
void setz(int c) { z = c; }
int getz()const { return z;}
protected:
int z;
};
class C:public A,private B
{
public:
void setCz(int c) { setz(c); }
int getCz() const { return z; }
int getsum() const { return x + gety() + z; }
};
#include "exp5_4.h"
void main()
{
C c;
c.setx(2);
c.sety(3);
c.setCz(5);
cout<<"x = "<<c.getx()<<"\tY = "<<c.gety()<<"\tZ =
"<<c.getCz()<<endl;
cout<<"X+ y +Z = "<<c.getsum()<<endl;
}
四、构造函数与析构函数调用顺序派生类与基类中 构造函数的调用顺序 是 先调用基类 的构造函数对基类成员进行初始化。然 后执行派生类 的构造函数,如果基类仍是派生类,则这个过程递归进行。
当派生类还 包括对象成员 时,则 基类 的 构造函数先被调用,
对象成员 的构造函数 次之,最后执行派生类 的 构造函数 。
在有 多个 对象成员 的情况下,这些对象成员的 调用顺序取决于 它们在派生类中 被说明 的顺序。
一般形式如下:
派生类名::派生类名 ( 参数总表 ),基类 1( 参数表 1),基类 2( 参数表
2),…,.,基类 n(参数表 n),对象成员 1(对象成员参数表 1),对象成员 2(对象成员参数表 2)……,对象成员 n(对象成员参数表 n)
{
派生类中新声明的数据成员初始化语句
}
派生类与基类中 析构函数 的调用顺序与上面的执行构造函数的顺序正好相反。即 先执行派生类 的析构函数,再调用基类的析构函数。
多重继承的构造函数与析构函数调用顺序没有作要求,但是我们还是了解一下,这种情况下,基类构造函数执行顺序按它们 被继承时说明的顺序依次调用,与它们在被始化列表中的顺序无关。
#include <iostream.h>
class A
{
public:
A();
~A();
void setx(int a){ x = a; }
void sety(int b){ y = b; }
int getx() const { return x; }
int gety() const { return y; }
protected:
int x;
private:
int y;};
class B:public A
{
public:
B();
~B();
void setz(int c) { z = c; }
int getz() { return z; }
int getsum(){ return x + gety() +z; }
private:
int z;
};
A::A():x(1),y(2)
{ cout<<"调用类 A的构造函数 "<<endl;}
A::~A()
{ cout<<"调用类 A的析构函数 "<<endl;}
B::B():z(3)
{ cout<<"调用类 B的构造函数 "<<endl;}
B::~B()
{ cout<<"调用类 B的析构函数 "<<endl;}
void main()
{
B b;
cout<<"x = "<<b.getx()<<"\tY = "<<b.gety()<<"\tZ =
"<<b.getz()<<endl;
cout<<"X + Y +Z ="<<b.getsum()<<endl;}
总结:
c++重要的性能之一是代码重用:可以用类的方法解决,
通过创建新类重用代码,使用类而不是更改已存在的代码 。
完成代码重用主要用两种方法:
1 1,简单地创建一个 包含已存在类对象的新类,称为组合
2 2,创建一个新类作为一个已存在类的类型,采取这个已存在类形式,对它 增加 代码,但不修改它,称为继承 。 其中大量的工作由 编译器 完成 。
对于组合和继承它们在 语法上 和 行为上 是类似的 。
组合语法:
1,一个对象作为公共对象嵌入到一个新类内部,访问嵌入对象 ( 称为子对象 ) 的成员函数只须再一次选择成员 。
class X
{
int i;
enum {factor=11};
public:
X() {i=0;}
void set(int I)
{i=I;}
int read() const {return i;}
int permute() {return i=i*factor;}
};
class Y
{
int i;
public:
X x;
Y() {i=0;}
void f(int I)
{i=I;}
int g() const {return i;}
};
main()
{
Y y;
y.f(47);
y.x.set(37);
}
1 2,通常嵌入的对象是私有的,这样他们就变成内部实现的一部分,而对于新类的 public接口函数,包含有对嵌入对象的使用
class X
{
int i;
enum {factor=11};
public:
X() {i=0;}
void set(int I)
{i=I;}
int read() const {return i;}
int permute() {return i=i*factor;}
};
class Y
{
int i;
X x;
public:
Y() {i=0; }
void f(int I)
{i=I; x.set(I);}
int g() const {return I*x.read();}
void permute() {x.permute();}
};
main()
{
Y y;
y.f(47);
//y.x.set(37);
y.permute();
}
五、两义性及其支配规则什么是两义性:当一个派生类是 多重派生 也就是由多个基类派生而来时,假如这些基类中的成员有成员名相同的情况,如果使用一个表达式引用了这些同名的成员,
就会造成无法确定是引用哪个基类的成员,这种对基类成员的访问就是 两义性 的 。
要避免在派生类定义及使用时出现两义性的情况,我们可以使用 成员名限定 来消除两义性,也就是在成员名前 用对象名及基类名来限定,如:
obj.A::func( );//A的 func( );
obj.B::func( );//B的 func( );
C++作用域规则,就是当基类中的成员名字在派生类中再次声明,则派生类中的名字就屏蔽掉基类中相应的名字 (也就是派生类的自定义成员与基类成员同名时,派生类的成员优先 )。那么如果要使用被屏蔽的成员呢?
这就要由作用 域分辨操作符 实现了。它的形式是类名::类标识符 。作用域分辨不仅可以用在 类 中,
而且可以用在 函数调用 时。
支配规则,类 X中的名字 N支配类中同名的名字 N,是指类 X为它的一个基类,这称为支配规则。我们可以理解为一个派生类中的名字将优先于与它的基类中相同的名字。这时二者之间不存在两义性,当选择该名字时,使用支配者 (派生类中 )的名字 。支配规则是对名字而言的。
如果一个 派生类 从 多个基类 中派生,而这些基类又有一个 共同的基类,则在这个派生类中 访问这个共同基类 中的成员时会产生 两义性 。
为了避免两义性,应使用 作用域分辨操作符 来实现。
一般只有在派生类中使用的标识符与基类中的标识符同名时,才有必要使用作用域分辨符进行存取。
六、虚基类虚基类 的定义很复杂,我们只需了解:如果一个派生类从多个基类中派生 (多重继承 ),而在它的多条继承路径上有一个 公共的基类,那么这个 公共基类 将会产生多个实例 (可理解为隐藏对象 ),要是希望这个公共基类只产生一个实例,则可以 将这个公共基类说明为 虚基类 。 这样就使得派生类的对象或成员函数在使用这个基类成员时不会产生 两义性 的问题 。因为此时虚基类只产生一个实例,
也就使得只有一个标识名字产生,而不会有同名的情况。