C++:虚函数与多态
虚函数
虚函数是一个类的成员函数,virtual仅用于类定义中,定义格式:virtual 返回类型 函数名 (参数表);
当某一个类的一个类成员函数被定义为虚函数,则由该类派生出来的所有派生类中,该函数始终保持虚函数的特征。
当在派生类中重新定义虚函数(overriding a virtual function,亦译作超载或覆盖)时,不必加关键字virtual。但重新定义时不仅要同名,而且它的参数表和返回类型全部与基类中的虚函数一样。
同名覆盖(override):如未加关键字virtual,则是普通的派生类中的新成员函数覆盖基类同名成员函数(当然参数表必须一样,否则是重载),可称为同名覆盖函数,它不能实现运行时的多态性。
虚函数表
C++编译阶段,没办法知道一个基类的指针或引用所指对象的类型,所以没办法通过这个指针判断调用的虚函数到底是谁的,所以只能通过查找虚函数表来找到函数的入口地址。
一个类,如果有虚函数,那么编译器在编译这个类的时候就会为它添加一个虚函数表,以及指向这个虚函数表的指针。继承这个基类的子类,也会新建一个虚函数表,如果没有重载,那么这个新的虚函数表中的函数指针就被拷贝为父类该函数的地址,否则为新的函数地址。编译器会将这些函数指针在虚函数表中按照基类中该函数出现的次序排列,子类中的虚函数表也将以这种方式排列。
每个有虚函数的类都有一个虚函数表指针pv,当通过指针或引用调用一个虚函数时,先通过pv找到虚函数表,然后根据这个虚函数在虚函数表中的偏移量来找到正确的函数地址,然后再CALL之。
虚函数表是在类之外的,一个类的size不包括虚函数表的大小。而虚函数指针则包含在类中,sizeof一个类则会包含一个虚函数表的指针。
成员函数尽可能设置为虚函数,必须注意以下几条:
1.派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是重载,而不是虚函数。(基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外)。
2.只有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象(而且是公有继承)。
3.静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。
4.一个类对象的静态和动态类型是相同的,实现动态多态性时,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现动态的多态性。
5.内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。
6.析构函数可定义为虚函数,构造函数不能定义为虚函数,因为在调用构造函数时对象还没有完成实例化。在基类中及其派生类中动态分配空间时,必须把析构函数定义为虚函数,实现撤销对象的多态性。
7.函数执行速度要稍慢一些。为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。
8.如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义不包括virtual。
我们看如下代码:
class Object
{
public:
virtual int add(int a,int b){ cout<<"Object::add"<<endl;return 0;}
virtual int fun(){ cout<<"Object::fun"<<endl;return 0;}
virtual void print(){ cout<<"Object::print"<<endl;}
};
class Base : public Object
{
public:
int add(int a,int b){ cout<<"Base::add"<<endl;return 0;}
int fun(){ cout<<"Base::fun"<<endl;return 0;}
virtual void show() { cout<<"Base::show"<<endl;}
};
class Test : public Base
{
public:
int add(int a,int b){ cout<<"Test::add"<<endl;return 0;}
void print(){ cout<<"Test::print"<<endl;}
void show(){ cout<<"Test::show"<<endl;}
};
每个类都有一个虚函数表,虚函数表中存放函数的入口地址,派生类会拷贝基类虚函数表,如果派生类中有和基类同返回类型、同函数名、同参数表的函数,就会同名覆盖,派生类的虚函数地址会覆盖基类的函数地址。
Base类的对象调用时,因为add、fun函数进行了重写,show函数是自己的,所以函数指针调用这三个函数时都会调用自己的,而print函数没用重写,所以会调用父类Object类中的print函数;同理,Test类中add函数会调用自己的,fun函数没有,所以调用父类Base的,print没有调用父类的父类Object类中的,show函数进行了重写,同名覆盖,所以调用自己的。
如下图表示:
如果没有虚函数,则是同名隐藏,如下:
#include<iostream>
using namespace std;
class Object
{
public:
void fun();
};
class Base : public Object
{
public:
void fun();
};
int main()
{
Base base;
base.fun(); //调用自己的函数,同名隐藏,从基类继承的同名函数被隐藏
base.Object::fun(); //调用基类的函数
return 0;
}
多态
多态:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。
多态就是运行时的多态,即动多态,只有运行时指针会去查表。(静态的就是函数的重载)
多态必须满足三个条件:
1.具有继承关系(公有继承public);
2.派生类中有虚函数的重写;
3.基类的指针或引用指向派生类对象,调用虚函数。
如下代码就不属于多态,因为它不满足第三个条件,是直接拿对象调用,属于静态绑定,编译时确定关系,与虚表没有关系。
#include<iostream>
using namespace std;
class Object
{
public:
virtual int add(int a,int b){ cout<<"Object::add"<<endl;return 0;}
virtual int fun(){ cout<<"Object::fun"<<endl;return 0;}
virtual void print(){ cout<<"Object::print"<<endl;}
};
class Base : public Object
{
public:
int add(int a,int b){ cout<<"Base::add"<<endl;return 0;}
int fun(){ cout<<"Base::fun"<<endl;return 0;}
void show() { cout<<"Base::show"<<endl;}
};
class Test : public Base
{
public:
int add(int a,int b){ cout<<"Test::add"<<endl;return 0;}
void print(){ cout<<"Test::print"<<endl;}
void show(){ cout<<"Test::show"<<endl;}
};
int main()
{
Object obj;
Base base;
Test test;
obj.add(10,20);
obj.fun();
obj.print();
base.add(12,23);
base.fun();
base.print();
base.show();
test.add(1,2);
test.fun();
test.print();
test.show();
return 0;
}
如下才是多态:
#include<iostream>
using namespace std;
class Object
{
public:
virtual int add(int a,int b){ cout<<"Object::add"<<endl;return 0;}
virtual int fun(){ cout<<"Object::fun"<<endl;return 0;}
virtual void print(){ cout<<"Object::print"<<endl;}
};
class Base : public Object
{
public:
int add(int a,int b){ cout<<"Base::add"<<endl;return 0;}
int fun(){ cout<<"Base::fun"<<endl;return 0;}
void show() { cout<<"Base::show"<<endl;}
};
class Test : public Base
{
public:
int add(int a,int b){ cout<<"Test::add"<<endl;return 0;}
void print(){ cout<<"Test::print"<<endl;}
void show(){ cout<<"Test::show"<<endl;}
};
int main()
{
Object *pa = NULL;
Base *pb = NULL;
Test *pc = NULL;
Test test;
pa = &test; //基类指针指向派生类对象
pa->add(12,23);//该指针指向虚函数add
pa->fun();
pa->print();
return 0;
}
为什么要用多态呢?
原因:我们知道,封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);它们的目的都是为了——代码重用。而多态除了代码的复用性外,还可以解决项目中紧偶合的问题,提高程序的可扩展性.。耦合度讲的是模块模块之间,代码代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的的目的,模块模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。
多态有什么好处?
1. 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程序的可复用性。
2. 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。
上一篇: C++:纯虚函数与抽象类
下一篇: 找到第K小的元素(分治)