一般情况下,在编译期间就能确定一个表达式的类型,但是当存在多态时,有些表达式的类型在编译期间就无法确定了,必须等到程序运行后根据实际的环境来确定。下面的例子演示了这种情况:
#include <iostream>
using namespace std;
//基类
class Base{
public:
virtual void func();
protected:
int m_a;
int m_b;
};
void Base::func(){ cout<<"Base"<<endl; }
//派生类
class Derived: public Base{
public:
void func();
private:
int m_c;
};
void Derived::func(){ cout<<"Derived"<<endl; }
int main(){
Base *p;
int n;
cin>>n;
if(n <= 100){
p = new Base();
}else{
p = new Derived();
}
cout<<typeid(*p).name()<<endl;
return 0;
}
输入 45,运行结果为:
输入 130,运行结果为:
基类 Base 包含了一个虚函数,派生类 Derived 又定义了一个原型相同的函数遮蔽了它,这就构成了多态。p 是基类的指针,可以指向基类对象,也可以指向派生类对象;*p表示 p 指向的对象。
从代码中可以看出,用户输入的数字不同,*p表示的对象就不同,typeid 获取到的类型也就不同,编译器在编译期间无法预估用户的输入,所以无法确定*p的类型,必须等到程序真的运行了、用户输入完毕了才能确定*p的类型。
根据前面讲过的知识,C++ 的对象内存模型主要包含了以下几个方面的内容:
现在我们要补充的一点是,如果类包含了虚函数,那么该类的对象内存中还会额外增加类型信息,也即 type_info 对象。以上面的代码为例,Base 和 Derived 的对象内存模型如下图所示:
编译器会在虚函数表 vftable 的开头插入一个指针,指向当前类对应的 type_info 对象。当程序在运行阶段获取类型信息时,可以通过对象指针 p 找到虚函数表指针 vfptr,再通过 vfptr 找到 type_info 对象的指针,进而取得类型信息。下面的代码演示了这种转换过程:
程序运行后,不管 p 指向 Base 类对象还是指向 Derived 类对象,只要执行这条语句就可以取得 type_info 对象。
编译器在编译阶段无法确定 p 指向哪个对象,也就无法获取*p的类型信息,但是编译器可以在编译阶段做好各种准备,这样程序在运行后可以借助这些准备好的数据来获取类型信息。这些准备包括:
这样做虽然会占用更多的内存,效率也降低了,但这是没办法的事情,编译器实在是无能为力了。
这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type Identification,RTTI)。在 C++ 中,只有类中包含了虚函数时才会启用 RTTI 机制,其他所有情况都可以在编译阶段确定类型信息。
下面是 RTTI 机制的一个具体应用,可以让代码根据不同的类型进行不同的操作:
#include <iostream>
using namespace std;
//基类
class People{
public:
virtual void func(){ }
};
//派生类
class Student: public People{ };
int main(){
People *p;
int n;
cin>>n;
if(n <= 100){
p = new People();
}else{
p = new Student();
}
//根据不同的类型进行不同的操作
if(typeid(*p) == typeid(People)){
cout<<"I am human."<<endl;
}else{
cout<<"I am a student."<<endl;
}
return 0;
}
可能的运行结果:
多态(Polymorphism)是面向对象编程的一个重要特征,它极大地增加了程序的灵活性,C++、C#、Java 等“正统的”面向对象编程语言都支持多态。但是支持多态的代价也是很大的,有些信息在编译阶段无法确定下来,必须提前做好充足的准备,让程序运行后再执行一段代码获取,这会消耗更多的内存和 CPU 资源。