多态性(一)

本文介绍了C++中多态性的概念,重点讨论了虚函数的作用和晚捆绑(动态绑定)机制。通过示例说明了在基类中使用virtual关键字声明函数以实现运行时绑定,并探讨了虚函数对类大小的影响以及效率问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

        多态性是实现C++面向对象性质的一个重要特征

一. 虚函数的出现

          C++中为什么会出现多态这个性质呢?先看下面一个例程

//Inheritance & upcasting
#include <iostream>
using namespace std;
enum note { middleC,Csharp,Eflat };    //Etc.

class Instrument
{
public:
       
void play(note) const
       {
                  cout
<<"Instrument::play"<<endl;
       }
};

//Wind objects are Instruments
//because they have the same interface:
class Wind:public Instrument
{
public:
        
//Redefine interface function:
        void play(note) const
        {
                 cout
<<"Wind::play"<<endl;
         }
};

void tune(Instrument& i)
{
           i.play(middleC);
}

int main()
{
          Wind flute;
          tune(flute);
}

          在上面的程序中可以看出函数 tune() 接受一个Instrument 对象,而我们却将Wind对象传给了它,由于Wind是从Instrument中派生出来,所以这是完全没有问题的。然而运行程序的的输出是 Instrument::play,而不是我们所希望的 Wind::play 。

        这里需要了解一个称为捆绑(binding) 的概念。 把函数体与函数调用相联系称为捆绑。当捆绑在程序运行之前(由编译器和连接器)完成时,这称为早捆绑(early binding) 。当捆绑发生在运行时,这就称为晚捆绑,又叫动态捆绑(dynamic binding) ,或运行时捆绑(runtime binding)。 当一个语言实现晚捆绑时,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。不同的编译器实现晚捆绑的方式也不尽相同。

        在C++中,为了实现晚捆绑,要求在基类中声明函数时使用virtual 关键字。

//Late binding with the virtual keyword
#include <iostream>
using namespace std;
enum note { middleC,CSharp,Cflat };    //Etc.

class Instrument
{
public:
       
virtual void play(note) const
       {
                cout
<<"Instrument::play"<<endl;
       }
};

//Wind objects are Instruments
//because they have the same interface:
class Wind : public Instrument
{
public:
         
//override interface function:
          void play(note) const
         {
                     cout
<<"Wind::play"<<endl;
         }
};

void tune(Instrument &i)
{
         i.play(middleC);
}

int main()
{
        Wind flute;
         tune(flute);
}

        这样就可以得到我们期望的结果了。 在派生类中virtual 函数的重定义通常称为重写(overriding)。

        通过将play()在基类中定义为virtual,不用改变tune() 函数就可以在系统中随意增加新函数。在一个设计风格良好的OOP程序中,大多数函数都是沿用tune()模型,只与基类接口通信。这样的程序是可扩展的 ,这样可以通过从公共基类继承新数据类型而增加新功能。

//Extensibility in OOP
#include <iostream>
using namespace std;
enum note { middleC,Csharp,Cflat };     //Etc.

class Instrument
{
public:
       
virtual void play(note) const
       {
             cout
<<"Instrument::play"<<endl;
       }
       
virtual char* what() const
       {
              
return "Instrument";
        }
        
//Assume this will modify the object:
        virtual void adjust(int) {}
};

class Wind : public Instrument
{
public:
         
void play(note) const
         {
                cout
<<"Wind::play"<<endl;
         }
         
char* what() const { return "Wind"; }
         
void adjust(int) {}
};

class Percussion: public Instrument
{
public:
          
void play(note) const
         {
                 cout
<<"Percussion::play"<<endl;
          }
          
char* what() const { return "Percussion"; }
          
void adjust(int) {}
};

class Stringed : public Instrument
{
public:
          
void play(note) const
          {
                 cout
<<"Stringed::play"<<endl;
           }
           
char* what() const { return "Stringed"; }
           
void adjust(int) {}
};

class Brass : public Wind
{
public:
           
void play(note) const 
           {
                  cout
<<"Brass::play"<<endl;
           }
           
char* what() const { return "Brass"; }
};

class Woodwind : public Wind
{
public:
            
void play(note) const
            {
                     cout
<<"Woodwind::play"<<endl;
            }
            
char* what() const { return "Woodwind"; }
};

//Identical function from before:
void tune(Instrument& i)
{
           i.play(middleC);
}

//New function
void f(Instrument& i) { i.adjust(1); }

//Upcasting during array initialization:
Instrument* A[] = {
           
new Wind,
           
new Percussion,
           
new Stringed,
           
new Brass
};
    
int main()
{
             Wind flute;
             Percussion drum;
             Stringed violin;
             Brass flugelhorn;
             Woodwind recorder;
             tune(flute);
             tune(drum);
             tune(violin);
             tune(flugelhorn);
             tune(recorder);
             f(flugelhorn);
}

二. C++的虚机制

        晚捆绑如何发生?所有的工作都由编译器在幕后完成。当告诉编译器要晚捆绑时,编译器安装必要的晚捆绑机制。

        为了达到这个目的,典型的编译器对每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数的地址。在每个带有虚函数的类中,编译器秘密的放置一个指针,称为vpointer(VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时,编译器静态的插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑的发生。

       这里举一个例子,以便检查使用虚函数的类的长度,并与没有虚函数的类进行比较。

//Object sizes with/without virtual functions
#include <iostream>
using namespace std;

class NoVirtual
{
        
int a;
public:
        
void x() const {}
        
int i() const { return 1; }
};

class OneVirtual
{
        
int a;
public:
        
virtual void x() const {}
        
int i() const { return 1; }
};

class TwoVirtuals
{
         
int a;
public:
         
virtual void x() const {}
         
virtual int i() const { return 1; }
};

int main()
{
         cout
<<"int: "<<sizeof(int)<<endl;
         cout
<<"NoVirtual: "<<sizeof(NoVirtual)<<endl;
         cout
<<"void* : "<<sizeof(void*)<<endl;
         cout
<<"OneVirtual: "<<sizeof(OneVirtual)<<endl;
         cout
<<"TwoVirtuals: "<<sizeof(TwoVirtuals)<<endl;
}

        不带虚函数,对象的长度恰好就是所期望的长度: 单个int 的长度。而带有单个虚函数的类,长度是 int 长度加上一个void 指针的长度。它反应出,如果有一个或多个虚函数,编译器都只在这个结构中插入一个单个指针(VPTR) 。这个指针就是指向自己类的VTABLE。如果在派生类中没有对在基类中声明为virtual 的函数进行重新定义,编译器就使用基类的这个虚函数地址。

三. 虚函数的效率

       “如果这个技术如此重要,并且使得任何时候都能调用正确的函数,那么为什么它是可选的呢?”

        这关系到C++的基本哲学: " 因为它不是相当高效的" 。 虚函数的调用不是对于绝对地址的一个简单的CALL指令,为设置虚函数调用需要两条以上的复杂的汇编指令。这既需要代码空间,又需要执行时间。    

       

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值