多态性是实现C++面向对象性质的一个重要特征
一. 虚函数的出现
C++中为什么会出现多态这个性质呢?先看下面一个例程
#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 关键字。
#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()模型,只与基类接口通信。这样的程序是可扩展的 ,这样可以通过从公共基类继承新数据类型而增加新功能。
#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表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑的发生。
这里举一个例子,以便检查使用虚函数的类的长度,并与没有虚函数的类进行比较。
#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指令,为设置虚函数调用需要两条以上的复杂的汇编指令。这既需要代码空间,又需要执行时间。