1) 多态的重要性 – 真正的面向对象编程
C++的编程模式:
l C++代替C,面向过程的编程
l 基于对象的编程,利用了封装和继承重用代码
l 面向对象的编程,利用多态实现接口编程
2) 多态的本质
l 早捆绑
编译时刻就在函数调用处决定了函数所在的地址。没有利用多态的C/C++函数调用都是早捆绑。
l 晚捆绑
编译时刻在函数调用时插入一些代码,能根据对象的类型去调用它的函数。这是通过虚函数表VTable来实现的。
l 每个包含virtual函数的类含有一个VTable,在这个表里放置这个类的所有virtual函数的地址。每个函数地址只占2个字节,这一点比较特殊,也就是说,一个类中至多只能有65536个虚函数。
注意:每个类只有一个VTable,供所有本类对象使用。
l 每个类对象包含一个指针VPTR(一般为首地址this),指向VTable,晚捆绑时,就是通过VPTR找到VTable,将查找函数的代码插入调用处。
l 所以,虚函数会比普通函数多两到三条指令,会影响效率,且每个对象多出4个字节指针。
一个提示:为了提高效率,寻找系统中不需定义虚函数的地方。
l 一个virtual函数编译时刻的处理例子
A* p;
p->Func();
编译时,变成:
push si; //寄存器保存p
mov bx, word ptr[si]; //取出this指针放入bx
call word ptr[bx+n]; //n是0,1,2…第n个virtual函数。
l inline函数不能是virtual的,原因是没有函数地址。
3) 多态的使用
l 在函数的声明前加上virtual,没有必要在函数定义时加、也没有必要在类前加、或者每一个派生类函数前加。
l 纯虚函数
1) virtual void Func() = 0;
如果不实现,则只是告诉编译器在VTable里预留一个位置,但是不放函数地址。所以VTable是不完全的,不能创建类实例
2) 纯虚函数也是可以实现的,但是,这时VTable也是不完全的,子类必须实现它,否则VTable也是不完全的。在父类实现纯虚函数的作用在于为所有子类共享代码,仅此而已。
class A
{
public:
virtual void Func() = 0;
//不能在这实现,因为inline函数不能是
虚的。
};
void A::Func()
{
//逻辑
}
class B : public A
{
public:
void Func(){A::Func();}
//必须实现,否则B也是不能实例化的。
};
3) 一般把析构函数声明为纯虚的就可实现不让创建对象的目的。
l 抽象类
如果一个类所有函数都是纯虚函数,那么称这个类为抽象类。
l 纯虚函数/抽象类的作用
只能通过引用或指针访问抽象类的方法,接口编程的一个体现。
l 只能通过指针或引用实现多态
假设B继承于A,
A a;
B b;
a = b;
a.Func(); //这时候调用的是A的函数,因为,a = b时,调用的是A的拷贝构造函数,它会初始化VPTR为a的地址。
l 派生类实现基类的虚函数时,必须保证参数和返回值都一致
如果参数不一样,那么就是自定义的重载函数;如果参数和函数名一样,返回值不一样,则编译出错。编译器必须保证用基类的指针可以访问到子类的同一函数。
l 多态在构造函数里不起作用
1) 如果在构造函数里调用虚函数,调用的是本类的版本
所以,不要在构造函数里调用虚函数。
2) 原因
构造函数是用来构建对象的,如果调用的是后面子类的方法,这时候它还没构建呢。
l 多态在析构函数里也不起作用
1) 如果在析构函数里调用虚函数,调用的是本类的版本
所以,不要在析构函数里调用虚函数。
2) 原因
构造函数不行是因为对象没构建完,类型信息还不可用;而析构函数是为什么呢?是因为所要调用的对象方法可能已经释放了(比如,调用子类的虚方法)。
l 析构函数一般都是虚的(前提是基类有虚函数)
析构函数不像构造函数,这时候,VPTR已经构建 起来了。如果析构函数不是虚的,那么通过delete基类指针不能析构子类的对象。