通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏函数中保存了一个指向函数地址数组的指针,这个地址数组是虚函数表,虚函数表中保存了类中声明的虚函数的地址。
无论类中包含的虚函数是一个还是10个,都只在对象中添加一个指针,只是指向的地址表的大小不同而已。基类中会有一个指针指向一个虚函数表,派生类中也包含一个指针。
1,若派生类,没有重新定义虚函数,则派生类的地址表将包含基类虚函数表的内容。
2,若提供了虚函数的重新定义,则派生类的虚函数表将保存新的地址内容。
3,若派生类添加了新的虚函数,则新的函数地址内容将添加到派生类的虚函数表中。
虚函数表是用数组实现的,调用虚函数时,程序查看对象中指针指向的虚函数表地址,如果调用类声明中的第n个虚函数,则使用虚函数表----地址数组中的第n个元素。
使用虚函数增加了内存和速度成本,
1,每个对象都增大,曾大量为一个指针的大小
2,对每个类,编译器都创建一个虚函数表(数组实现)
3,每次虚函数调用,都要去表中查找地址
虚函数要点
1,在基类方法的声明中使用virtual关键字可使该方法在基类、子类以及子子类中都是虚函数
2,如果使用指向对象的引用和指针来调用虚方法,程序将根据指向的对象类型来调用,若是调用非虚方法,则根据指针或者引用的类型来调用方法
3,如果定义的类要用作基类,则要将在派生类中重新定义的方法声明为虚拟的。
一些注意点:
1,构造函数
构造函数不能是虚函数,因为派生类对象建立时调用的是自己的构造函数,并不是基类的构造函数,然后在派生类的构造函数中使用父类的构造函数,这不是继承,派生类并没有继承父类的构造函数,将构造函数声明为虚拟的是没有意义的。
2,析构函数
析构函数必须定义为virtual的派生类中继承了父类的内容,如果父类指针指向一个子类对象,当执行析构函数时,如果析构函数不是虚拟的,则调用父类的析构函数,子类新增的内容却无法释放。如果将析构函数定义为虚拟的,则先调用子类析构函数释放内容,再调用父类析构函数释放父类内容。
3,友元函数
友元函数不能是虚函数,因为它不是类成员,只有成员才能使虚函数
4,没有重新定义
如果派生类没有重新定义,则使用基本版本
5,重新定义隐藏方法
假设创建了以下代码
class base
{
public:virtual void fun(int a);
};
class derive:public base
{
public:virtual void fun();
};
新定义的fun()为一个不接受任何参数的函数,重新定义不会生成函数个两个重载版本,而是隐藏了基类中接受一个参数的版本,简而言之,重新定义继承的方法并不是重载,如果在派生类中重新定义函数,而不是使用相同的函数特性来覆盖基类声明,则是隐藏同名的基类方法
所以有两条经验
a 如果要重新定义继承的方法,应确保与原来的原型完全相同 但是如果返回类型是基类指针或引用,则可以修改为指向派生类的引用和指针,这种特性是返回类型协变,因为允许返回类型随类型的变化而变化
class base
{
public:virtual base & fun(int a);
};
class derive:public base
{
public:virtual derive & fun(int a);
};
b 如果基类声明被重载了,则应该在派生类中重新定义所有的版本。
class base
{
public:virtual void fun(int a);
virtual void fun(double x);
virtual base & fun();
};
class derive:public base
{
public:virtual void fun(int a);
virtual void fun(double x);
virtual derive & fun();
};