继承性,虚拟函数与抽象基类

来源:互联网 发布:雅思备考资料 知乎 编辑:程序博客网 时间:2024/05/16 09:07

继承性,虚拟函数与抽象基类

C++语言的一个重要特性便是继承性,继承机制实现了代码的复用性。当然这是源码级别上的复用啦!C++对于继承支持两种形式:单一继承和多重继承。

单一继承

简单说就是一个类可以从另一个类中继承成员和这些成员的实现。继承的类称为派生类,被继承的类称为基类。换句话说,继承是把对许多类都通用的代码集中到一个基类中的技术,即将代码放到一个其他类可以复用它的地方。

  1. #include <iostream>
  2. using namespace std;
  3. class CBase
  4. {
  5. public:
  6.     CBase()
  7.     {
  8.     }
  9.     CBase(int i) : m_nNumber(i)
  10.     {
  11.     }
  12.     int GetNumber()
  13.     {
  14.         return m_nNumber;
  15.     }
  16.     void Print()
  17.     {
  18.         cout << m_nNumber << endl;
  19.     }
  20. private:
  21.     int m_nNumber;
  22. };
  23. class CDerived : public CBase
  24. {
  25. public:
  26.     CDerived( int i, int j ) : CBase(i), m_nNumber(j)
  27.     { 
  28.     }
  29.     //重载CBase类中的Print函数
  30.     void Print()
  31.     {
  32.         //如果派生类中的函数希望调用基类中的实现,那么可以直接在基类函数名前加基类名::
  33.         cout << CBase::GetNumber() << " ";
  34.         cout << m_nNumber << endl;
  35.     }
  36. private:
  37.     int m_nNumber;
  38. };
  39. int main()
  40. {
  41.     CBase    base(2);
  42.     CDerived derived(3, 4);
  43.     cout << "base is ";
  44.     base.Print( );                   // Print() in CBase
  45.     cout << "derived is ";
  46.     derived.Print( );                // Print() in CDerived
  47.     cout << "base part of derived is "
  48.     derived.CBase::Print( );         // Print() in CBase
  49.     CBase*  pBase;
  50.     pBase = (CBase*)new CDerived(2, 3);
  51.     pBase->Print();                  // Print() in CBase
  52.     delete pBase;
  53.     return 0;
  54. }

运行结果:

base is 2
derived is 3 4
base part of derived is 3
2

 

在CDerived构造函数中,我们看到一个奇怪的说明:

CDerived( int i, int j ) : CBase(i), m_nNumber(j)

这个说明意味着CDerived构造函数的参数i和j在构造函数运行之前便首先传给了CBase基类的构造函数和CDerived本身的m_nNumber成员变量。

 

在编程中,将各种类型的指针“浇铸”为包含公共元素的单一类型是方便的。在C++中,如同上例那样我们可以合法的将一个

CDerived指针浇铸成一个CBase指针,因为CDerived确实像CBase。

  1.     CBase*  pBase;
  2.     pBase = (CBase*)new CDerived(2, 3);
  3.     pBase->Print();      

最后的Print调用究竟是谁的Print?因为通过CBase类型的指针调用的,所以这段代码调用的是CBase::Print(),而非CDerived::Print()!

 

其实程序员总是希望能写这样一段代码:仅仅知道CBase类却能够调用派生类的成员函数!例如如上这段代码,最后那句pBase->Print(); 我们希望它调用的是CDerived::Print()!当然这里pBase是指向CDerived的物理指针,完成这个要求的是虚拟函数。

 

虚拟函数

 

为了解决上面提出的问题,我们得重新定义CBase类,使用关键字virtual来产生一个虚拟函数。如下所示:

  1. class CBase
  2. {
  3. public:
  4.         ... ...
  5.     virtual void Print()
  6.     {
  7.         cout << m_nNumber << endl;
  8.     }
  9.         ... ...
  10. };

如果实现和声明分开为两个文件,那么virtual不会出现在CBase类的实现文件中。对与CDerived类如果想重载Print那么:

  1. class CDerived : public CBase
  2. {
  3. public:
  4.     ... ...
  5.     //重载CBase类中的Print函数
  6.     virtual void Print()
  7.     {
  8.     cout << CBase::GetNumber() << " ";
  9.         cout << m_nNumber << endl;
  10.     }
  11.     ... ...
  12. };

这样之后我们在前面看到的代码运行起来会有不同的行为:

  1.     CBase*  pBase;
  2.     pBase = (CBase*)new CDerived(2, 3);
  3.     pBase->Print();                  // Print() in CDerived

运行结果如下:

base is 2
derived is 3 4
base part of derived is 3
2 3

 

知道了CBase::Print是虚拟函数后,编译器现在要负责指出pBase真正指向的是哪个类型。尽管程序本身认为它是指向CBase的指针,但在这段代码中pBase->Print仍然调用CDerived::Print.  C++编译器是通过虚拟函数表(Virtual function table)来实现这种机制的。CBase的函数表将包含指向CBase::Print的指针,如果CDerived重载了CBase中的虚拟函数,其表将包含指向CDerived::Print的指针。但是如果CDerived没有重载Print函数,那么其表包含的将是指向CBase::Print的指针。

 

指向任何对象的指针其实是一个指向对象函数表的指针的指针。当编译器需要通过对象指针调用成员函数时,它将在表中寻找合适的地址。因此如果CBase类及其基类的虚拟函数Print总是占据表中的第一个位置,则像pBase->Print这样的调用其实是对该位置上地址的调用。

 

抽象基类

 

虚拟函数还可以通过在类声明中对函数赋予0被说明为纯虚拟函数(pure virturl),如下所示:

  1. class CBase
  2. {
  3. public:
  4.         ... ...
  5.     virtual void Print()= 0;
  6.         ... ...
  7. };

纯虚拟函数意味着“没有被定义的实现”,它将CBase说明成一个抽象基类“abstract base class”,也就是说不能让CBase实例化。换句话说,纯虚拟函数并不在对象的函数表中创建入口。因此,C++不能建立一个可以通过它进行函数调用的对象,只要一个类还有一个纯虚拟成员函数,它便是一个抽象基类,便不能被实例化。

 

一个抽象基类告诉派生类:“你必须重载我的纯虚拟函数!”一个具备一般的虚拟函数的普通基类将告诉派生类:“如果你真的关心他们,那么你可以重载他们。”

 

多重继承

 

C++运行派生类从多个基类继承,因此可以从多个源中继承实现和成员。在任何情况下,如下代码的多重继承性都是很明确的:

  1. class CBase
  2. {
  3. public:
  4.     virtual FunctionA();
  5.     virtual FunctionB();
  6.     virtual FunctionC();
  7. };
  8. class CAbstractBase
  9. {
  10. public:
  11.     virtual FunctionD() = 0;
  12.     virtual FunctionE() = 0;
  13.     virtual FunctionF() = 0;
  14. };
  15. //注意多个基类间用逗号隔开
  16. class CDerived:public CBase, public CAbstractBase
  17. {
  18. public:
  19.     virtual FunctionA();
  20.     virtual FunctionB();
  21.     virtual FunctionC();
  22.     virtual FunctionD();
  23.     virtual FunctionE();
  24.     virtual FunctionF();
  25. };

一个使用多重继承的类的对象总是与多重函数表联系在一起的,一个指向派生类对象的指针指向包括所有基类的所有成员函数的表,如果该指针被定为指向其中一个派生类的指针,那么这个指针将归属与那个特定基类的表。在所有情况下,编译器总是调用该指针所引用的表中的函数。

 

 

当然使用多重继承有其局限性:主要是当多个基类都有相同名字的成员函数时,对象只能有一个在所有函数表之间被共享的一个给定的成员函数的实现。正如上图所示的,每个函数都在基类表和派生类表之间被共享一样。