继承的特性
继承是面向对象中引入的重要特性之一,它的一个重要的特点就是子类是父类,父类不是子类。也就是说:
1. 如果基类指针指向子类对象(pbase=&pchild),则该指针只能调用基类定义了的函数;
2. 如果子类指针指向基类对象(pchild=(child *)pbase),则会出问题;
3. 如果基类和子类定义了同名的函数,则到底调用什么函数,必须视该指针的原始类型而定,而不是视指针实际指向的对象类型而定。
4. 就算子类继承了父类的某个函数(而未改写它),该函数依然被视为父类的,该函数依然属于父类的域中,该函数中使用的普通函数(非虚函数)依然被视为父类的函数。
5. 私有变量的继承性:私有变量对子类是不可见的,即使是子类从父类继承下来了,也仍然是不可见的,它仅仅能被父类的函数(没有在子类中改写或重写过的函数)操作。
6. this指针:类的成员函数的参数中有一个隐藏的参数this指针,这保证了被继承的成员函数归属对象的正确性和无混淆性。
虚函数的实现原理
正是因为继承的这个特点,虚函数的加入似乎就是顺理成章的事情了。虚函数简化并明确了软件和各种类库的设计以及维护。
一般的函数在编译链接时就进行了绑定,这称之为早绑定。由于信息量不够,所以只能依赖于调用它的对象或指针的声明类型实现绑定。也就是侯俊杰说的,如果基类和子类定义了同名的函数,那么到底调用哪个类的函数,必须视该指针的原始类型而定,而不是视指针实际指向的对象类型而定。因为赋值的动作还没有产生。
而虚函数则不是这样。虚函数实现的机制是晚绑定,它在编译链接时并没有与某个对象绑定----这也正是虚函数能实现多态(以相同代码调用不同函数)的原因所在。当编译器对程序进行编译碰到虚函数时,将不会赋予一个地址,而是插入一段汇编代码。每个包含虚函数的类都会由编译器产生一个虚函数表和一个虚函数表指针,其中虚函数表指针放在每个类的首地址处(也许不是,不过反正地址偏移量在每个类所占内存中是固定的)。当程序执行时,碰到对虚函数的调用,则通过插入的汇编代码到当前类的地址中找到虚函数表指针,通过虚函数的序号找到需要调用的虚函数。注意,一个系列的类的虚函数表中某一个函数的序号是一样的。而且,编译器会保证在使用父类指针操作子类对象时只能在父类已有的虚函数上实现虚函数的机制。这里还有一个虚函数的默认参数的问题。虚函数是动态绑定的,而默认参数则是静态绑定的,所以在虚函数中使用默认参数可以说是不符合逻辑的。如果子类改写了父类虚函数中的默认参数,当使用多态特性时,会出现调用子类的虚函数,使用的却是父类中的对应虚函数的默认参数的情况。
虚函数适用的两种场合
1. 某个子类中调用继承下来的非虚函数中有对已改写的虚函数的调用。值得注意的是,在某个子类中调用继承下来的未改写的非虚函数中有对已改写的虚函数的调用时,调用的是当前子类中改写过的虚函数;若该非虚函数中有对已改写的非虚函数的调用时,调用的是父类的非虚函数(也是因为晚绑定)。这是MFC的惯用手法。
2. 使用向上映射(父类指针=子类指针),实现代码的重用
3. 父类中的析构函数。当一个类确信不会成为任何类的父类时,它的析构函数是不需要设置成虚函数的;当一个类肯定会成为某个类的父类时,虚析构函数是必要的。因为若是父类中的析构函数是非虚的,则当用一个父类的指向子类的指针delete子类时,这种行为在C++标准中并没有被定义,是十分危险的。
继承中的接口及其实现
经过以上分析可知,虚函数实际上就是继承中的一种接口。继承中一共有纯虚函数、非纯 虚函数和非虚函数三种接口,它们在子类中的处理如下:
1. 纯虚函数:所有子类必须强制性地改写,否则会报错。这是一种仅仅继承接口的方法。
2. 非纯虚函数:又被称为简单虚函数,可以在基类中有自己的实现(默认的动作),子类不一定要改写,这是一种继承接口及其默认实现的方法。
3. 非虚函数:子类最好不要改写,这是一种强制性地继承接口及其实现的方法,表示的是一种共性。
当在同一个类中存在同名但是参数不同的函数,叫作overloading;子类改写父类的虚函数,叫做overriding;子类改写父类的非虚函数,叫做redefining,这是不推荐的。
附:
测试代码
#include <iostream>
class father
{
public:
virtual void vp()
{
std::cout<<"I'm father's vp"<<std::endl;
}
void np()
{
std::cout<<"I'm father's np"<<std::endl;
vp();
nnp();
}
void nnp()
{
std::cout<<"I'm father's nnp"<<std::endl;
}
};
class son: public father
{
public:
/*void np()
{
std::cout<<"I'm son's np"<<std::endl;
vp();
nnp();
}*/
void vp()
{
std::cout<<"I'm son's np"<<std::endl;
}
void nnp()
{
std::cout<<"I'm son's nnp"<<std::endl;
}
};
int main ()
{
father *of = new son();
of->np();
return 0;
}