一般情况下,在编译期间就能确定一个表达式的类型,但是当存在多态时,有些表达式的类型在编译期间就无法确定了,必须等到程序运行后根据实际的环境来确定。下面的例子演示了这种情况:
- #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 资源。