一、多态的概念
多态就是多种形态,C++的多态分为静态多态与动态多态。
静态多态就是重载,因为在编译期决议确定,所以称为静态多态。在编译时就可以确定函数地址。
动态多态就是通过继承重写基类的虚函数实现的多态,因为实在运行时决议确定,所以称为动态多态。运行时在虚函数表中寻找调用函数的地址。
在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。
如果对象类型是子类,就调用子类的函数;如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。
例:
class Person
{
public :
virtual void BuyTickets()
{
cout<<" 买票"<< endl;
}
protected :
string _name ; // 姓名
};
class Student : public Person
{
public :
virtual void BuyTickets()
{
cout<<" 买票-半价 "<<endl ;
}
protected :
int _num ; //学号
};
//void Fun(Person* p)
void Fun (Person& p)
{
p.BuyTickets ();
}
void Test ()
{
Person p ;
Student s ;
Fun(p );
Fun(s );
}
二、多态的实现原理
一个接口,多种方法
1. 用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数。
2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
3. 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
4. 虚函数表是一个存储类成员函数指针的数据结构。
5. 虚函数表是由编译器自动生成与维护的。
6. virtual成员函数会被编译器放入虚函数表中。
7. 当存在虚函数时,每个对象中都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当进行test(parent *base)函数的时候,C++编译器不需要区分子类或者父类对象,只需要再base指针中,找到vptr指针即可)。
8. vptr一般作为类对象的第一个成员。
三、探索虚表
虚表是通过一块连续的内存来存储虚函数的地址。这张表解决了继承、虚函数(重写)的问题。在有虚函数的对象实例中都存在这样一张虚函数表,它就像一张地图,指向了实际调用的虚函数。
例:
class Base
{
public :
virtual void func1()
{}
virtual void func2()
{}
private :
int a ;
};
void Test1 ()
{
Base b1;
}
单继承对象模型
class Base
{
public :
virtual void func1()
{
cout<<"Base::func1" <<endl;
}
virtual void func2()
{
cout<<"Base::func2" <<endl;
}
private :
int a ;
};
class Derive :public Base
{
public :
virtual void func1()
{
cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
cout<<"Derive::func3" <<endl;
}
virtual void func4()
{
cout<<"Derive::func4" <<endl;
}
private :
int b ;
};
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
{
cout<<" 虚表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}
void Test1 ()
{
Base b1 ;
Derive d1 ;
int* VTable1 = (int*)(*( int*)&b1 );
int* VTable2 = (int*)(*( int*)&d1 );
PrintVTable(VTable1 );
PrintVTable(VTable2 );
}
可以看到派生类Derive::func1重写基类Base::func1,覆盖了相应虚表位置上的函数。 ps:可以看到这里没有看到派生类Derive中的func3和func4,这两个函数就放在func2的后面,这里没有显示是VS的问题(bug)。
多继承对象模型
class Base1
{
public :
virtual void func1()
{
cout<<"Base1::func1" <<endl;
}
virtual void func2()
{
cout<<"Base1::func2" <<endl;
}
private :
int b1 ;
};
class Base2
{
public :
virtual void func1()
{
cout<<"Base2::func1" <<endl;
}
virtual void func2()
{
cout<<"Base2::func2" <<endl;
}
private :
int b2 ;
};
class Derive : public Base1, public Base2
{
public :
virtual void func1()
{
cout<<"Derive::func1" <<endl;
}
virtual void func3()
{
cout<<"Derive::func3" <<endl;
}
private :
int d1 ;
};
typedef void (* FUNC) ();
void PrintVTable (int* VTable)
{
cout<<" 虚表地址>"<< VTable<<endl ;
for (int i = 0; VTable[i ] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i , VTable[i ]);
FUNC f = (FUNC) VTable[i ];
f();
}
cout<<endl ;
}
void Test1 ()
{
Derive d1 ;
int* VTable = (int*)(*( int*)&d1 );
PrintVTable(VTable );
// Base2虚函数表在对象Base1后面
VTable = (int *)(*((int*)&d1 + sizeof (Base1)/4));
PrintVTable(VTable );
}
四、一些考题
为什么调用普通函数比调用虚函数的效率高?
因为普通函数是静态联编的,而调用虚函数是动态联编的。
联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。
静态联编 :在编译的时候就确定了函数的地址,然后call就调用了。
动态联编 : 首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址后,再加上偏移量才能找到要调的虚函数,然后call调用。
明显动态联编要比静态联编做的操作多,肯定就费时间。
为什么要用虚函数表(存函数指针的数组)?
实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数。
同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表。
为什么要把基类的析构函数定义为虚函数?
在用基类操作派生类时,为了防止执行基类的析构函数,不执行派生类的析构函数。因为这样的删除只能够删除基类对象, 而不能删除子类对象, 形成了删除一半形象, 会造成内存泄漏.如下代码:
#include<iostream>
using namespace std;
class Base
{
public:
Base() {};
~Base()
{
cout << "delete Base" << endl;
};
};
class Derived : public Base
{
public:
Derived() {};
~Derived()
{
cout << "delete Derived" << endl;
};
};
int main()
{
//操作1
Base* p1 = new Derived;
delete p1;
//因为这里子类的析构函数重写了父类的析构函数,虽然子类和父类的析构函数名不一样,
//但是编译器对析构函数做了特殊的处理,在内部子类和父类的析构函数名是一样的。
//所以如果不把父类的析构函数定义成虚函数,就不构成多态,由于父类的析构函数隐藏了子类
//的析构函数,所以只能调到父类的析构函数。
//但是若把父类的析构函数定义成虚函数,那么调用时就会直接调用子类的析构函数,
//由于子类析构先要去析构父类,在析构子类,这样就把子类和继承的父类都析构了
system("pause");
}
为什么子类和父类的函数名不一样,还可以构成重写呢?
因为编译器对析构函数的名字做了特殊处理,在内部函数名是一样的。