您的位置:首页 > 编程语言 > C语言/C++

C++基础--基类和派生类

2008-09-02 14:42 176 查看
C++基础--基类和派生类 (1)
继承的定义及工作方式
继承的一般形式如下:
  class 派生类:访问权限 基类
  {
   …
  }
  如果需要,派生类可以从多个基类继承,也就是多重继承,这将在后面章节中讲解。通过继承,派生类自动得到了除基类私有成员以外的其它所有数据成员和成员函数,在派生类中可以直接访问,从而实现了代码的复用。
  从概念上讲,基类和派生类的关系与类和对象的关系有着根本的不同。比如说,我们定义了猫科动物类:
  class felid
  {
   //类中定义了猫科动物的基本特征
  };
  现在由于某种需要,我们想对猫这个动物进行定义。猫是猫科动物的一种,很自然,我们就想到从猫科动物这个类里面派生:
  class cat: public felid
  {
   char name[20] //名字
   char color[80] //毛色
   cat();
   cat(char* name, char* color);
  }
  在类cat中,示意性地定义了两个变量。我们知道,对象是类的一个实例,而在这里,cat还是一个类,一个更加具体的类,不是felid类的对象,这一点要分辨清楚。我们可以定义猫的一个对象(实例):
  cat mycat("Jack","bronze");
mycat才是一只实实在在的猫对象。
  派生类对象生成时,要调用构造函数进行初始化。编译器的调用过程是先调用基类的构造函数,对派生类中的基类数据进行初始化,然后再调用派生类自己的构造函数,对派生类的数据进行初始化工作。当然,在派生类中也可以更改基类的数据,只要它有访问权限。
  基类数据的初始化要通过基类的构造函数,而且,它要在派生类数据之前初始化,所以基类构造函数在派生类构造函数的初始化表中调用:
  派生类名 (参数表1):基类名(参数表2)
  其中"参数表1"是派生类构造函数的参数,"参数表2"是基类构造函数的参数。通常情况下,参数表2中的参数是参数表1的一部分。也就是说,用户应该提供给派生类所有需要的参数,包括派生类和基类的。事实上也是这样,派生类继承了基类的成员变量,就相当于是自己的一部分,当然有责任对基类的变量进行初始化,只不过对于基类成员的初始化要借助于基类的构造函数而已。如果派生类构造函数没有显式调用基类的构造函数,编译器也会先调用基类的缺省参数的构造函数,对基类数据进行初始化。如果派生类自己也没有显式定义构造函数,那么编译器会为派生类定义一个缺省的构造函数,在生成派生类对象时,仍然先调用基类的构造函数。所以,派生类没有定义构造函数的话,必须保证基类有缺省参数的构造函数。

2 类的继承访问特性


基类的访问特性
类的继承特性子类的访问特性
Public
Protected
Private
Public
Public
Protected
No access
Public
Protected
Private
Protected
Protected
Protected
No access
Public
Protected
Private
Private
Private
Private
No access
C++基础--基类和派生类 (2)
3 函数的隐藏与覆盖
  我们已经知道了函数的重载是怎么回事,重载的函数名字相同,但它们的参数个数和类型不同。函数的隐藏和覆盖,与函数的重载不同,它们只是在继承的情况下才存在。
  如果在派生类中定义了一个与基类同名的函数,也就是说为基类的成员函数提供了一个新的定义,有两种情况:
  ◇ 在派生类中的定义与在基类中的定义有完全相同的信号(signature)(即参数个数与类型均相同)和返回类型,对于普通成员函数,这便称之为重定义;而对于虚成员函数(在本章的后面介绍),则称之为覆盖。
  ◇ 在派生类中,改变了成员函数参数表与返回类型。此时会出现什么情况?

在派生类中重定义基类的成员函数,会隐藏基类的该成员函数,例如:


例11-7
class A
{
public:
 int a(int i) {return i;}
 float a(float i) {return i;}
};
class B: public A
{
public:
 void a(char* str) {}
};

void main()
{
 B b;
 b.a(1); //error;
 b.a(1.0); //error
 b.a("hehe");
}

  上面程序中,类A的成员函数a被重载了两次,但是由于类B中隐藏了基类A的成员函数a,所以B的对象无法直接看到基类的成员函数a,当然用户可以通过域运算符显式调用。
  b.a::a(1);
  b.a::a(1.0);

4 不合适的继承
  继承和派生有一个基本的原则,就是基类对象能够使用的地方,也能用同样的方法使用派生类的对象。
  下面是一个破坏了这个原则的实例,BoundedStack时从Stack派生的,Stack类支持基本的栈操作,即压入一个元素到栈中和从栈中弹出一个元素,如例11-8。
  在该类中,隐含push成员函数总能压入一个新的元素到栈中。有时,我们可能需要只能压入固定元素个数的栈。为了复用Statck类的实现,从Statck类派生了一个BoundedStack类,如例11-9。
  但是,这破坏了上面提到的基本的原则:基类对象能够使用的地方,派生类对象也能够被使用。例如,假定BoundedStack的capacity成员设置为5,即栈中最多只能有5个元素,则压栈次数超过5,则会出错。
  之所以从Statck派生BoundedStack,只是为了复用Statck类的代码,其实,我们也可以用组合的方法复用代码,所谓组合是指用一个类的对象作为另一个类的数据成员,如例11-9。
  现在我们对这个原则可能还理解不深,在学习了多态的概念后,对这个原则会有更深的理解。

例11-8
class Stack
{
private:
 ...
public:
 // 未给出构造函数
 int size(); // 返回当前栈元素的个数
 int pop();
 void push(int item);
};



例11-9
class BoundedStack: public Stack
{
private:
 int capacity;
public:
 // 许多成员函数未给出
 void push(int item)
 {
  if (size()< capacity) Stack::push(item);
  else <error!!>
 }
};



例11-10
class BoundedStack
{
private:
 Stack _myStack;
 int capacity;
public:
 // 省略了许多成员函数
 void push(int item)
 {
  if (_myStack.size()<capacity) _myStack.push(item);
  else <error!!!>
 }
}
承和派生有一个基本的原则,就是基类对象能够使用的地方,也能用同样的方法使用派生类的对象。
  当某一个类A希望拥有另一个类B的性能,但又不希望类B的接口成为类A的接口,或者类B是类A的一个组成部分,就使用组合,而不要使用继承。例如,一台电脑可以由一个主机,一个显示器和一个键盘组成。用类描述可以是:



程序段11-5
class mainframe
{
 ……
};

class monitor
{
 ……
};

class keyboard
{
 ……
};

class computer
{
public:
 mainframe mf;
 monitor con;
 keyboard kb;
 ……
};

C++基础--基类和派生类 (3)

5 多继承
  我们前面介绍的内容中,一个派生类继承一个基类,我们称之为单继承。C++也支持多继承,即一个派生类继承多个基类,参见图11-1。

图11-1 基类和派生类



多继承与单继承很类似。

例11
class X
{
public:
 X(int n);
 ~X();
 //…
};
class Y
{
public:
 Y(double d);
 ~Y();
 //…
};
class Z : public X, public Y {
public:
 Z(int n, double d);
 ~Z();
 //…
};

派生类Z继承了两个基类X和Y所有的公有和保护成员,由于两个基类有带参的构造函数,派生类构造函数应该在成员初始化表中调用它们:
  Z ::Z(int n, double d):X(n), Y(d)
  {
   …
  }
  基类构造函数的调用顺序与在定义派生类时指定的顺序相同,与它们在成员初始化表中出现的顺序无关。本例中,X的构造函数在Y的构造函数之前调用,即使我们把Z的构造函数改成下面的形式,也仍然如此:
  Z ::Z(int n, double d): Y(d), X(n)
  {
   …
  }
  析构函数的调用顺序与构造函数的调用顺序相反,类X、Y、Z析构函数的调用顺序为:~Z、~Y、~X。
  派生类对象包含每一个基类对象的成员,图11-2表示了Z对象与它的基类对象之间的关系:
  在多继承时,C++并没有限制基类的个数,但不能有相同的基类,例如11-12:

图11-2 基类与派生类对象




例11-12
class Z : public X, public Y, public X // 非法: X出现两次
{
 //...
};
多继承类成员的引用比单继承复杂。例如11-13,假定Z的基类X和Y均有成员函数H。
  我们也可以在Z类中定义H成员,并调用基类的H成员,例如11-14。

例11-13
class X
{
public:
 //…
 void H (int part);
};
class Y
{
public:
 //…
 void H (int part);
};

  派生类Z将继承这些成员函数,使得下面的调用发生歧义:
  Z zObj;
  ZObj. H (0);
  编译器并不知道是要调用X::H,还是Y::H,我们可以通过显式调用来解决这个问题:
  ZObj. Y::H (0);



例11-14
class Z: public X, public Y
{
public:
 //…
 void H (int part);
};
void Z::H (int part)
{
 X::H (part);
 Y::H (part);
}
 当继承基类时,派生类对象就包含了每一个基类对象的成员。假定以类X和类Y为基类派生出类Z,类Z就会同时包含类X的和类Y的数据成员,如图11-2表示。
  如果类X和类Y都是从相同的基类A派生的,那么从类层次上看,就成了一个菱形的结构,如图11-3表示。

图11-2 基类与派生类对象




图11-3

有菱形的情况,多继承并没有什么麻烦。一旦出现菱形的情况,事情就变得复杂起来。首先,对于A类的数据成员来说,它在Z中是重叠的,即在Z中它有两个副本。这不仅增加了存储空间,更严重的是产生了二义性。
  类X和类Y都实现了一个func1()函数,当Z从X和Y继承时,就会导致一个冲突:当我们使用基类指针调用虚函数func1()时,编译器就不知道它调用的是类X还是类Y的成员,这样编译器就会给出一个错误信息:"Z::func1' is ambiguous"。正确的作法是必须在类Z中重定义func1()函数,以消除二义性。
  如果继承出现菱形的情况,还有很多地方会出现类似的问题。所以我们建议尽量不使用多重继承。

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: