1. 多态 多态是人类思维方式的一种直接模拟,多态性是指不同对象接收到相同的消息时,根据对象类的不同而产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数。就是同一个函数名称,作用在不同的对象上将产生不同的行为。 多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,而后者是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象过程就是联编。联编是指计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程。在面向对象上来讲,就是把一条消息和一个对象的方法结合起来的过程。按照联编进行阶段的不同,分为:静态联编和动态联编,它们分别对应着多态的两种实现方式。 1.1 多态的引入 #includeiostream using namespace std; class Animal { public: void sleep() { cout "Animal sleep...." endl; } void breath() { cout "Animal breath..." endl; } }; class Fish:public Animal { void breath() { cout "Fish breath...." endl; } }; int main() { Fish fh; Animal *an = fh; an-breath(); system("pause"); return 0; } 在上述代码中,主函数定义了一个 Fish 类的对象 fh, 接着定义了一个指向 Animal 类的指针变量 an ,将 fh 的地址赋给了指针变量 an ,然后利用该变量调用 breath() 。这种情况很容易和 C++ 的多态性混淆 ,fh 是 Fish 类的对象,应该调用 Fish 的 breath() ,但是结果却不是这样的。 这是因为 C++ 编译器在编译的时候,要确定每个对象调用的函数地址,这称为早期绑定。当 Fish 类的对象 fh 的地址赋给 an 时, C++ 编译器进行了类型转换,编译器认为变量 an 保存的就是 Animal 对象的地址。所以在主函数调用的是 Animal 对象的 breath() 函数。此时, Fish 创建的对象 fh 在内存中的存储如下图所示。 在构造 Fish 类的对象时,系统首先要调用 Animal 的构造函数去构造 Animal 类的对象,然后才调用 Fish 类的构造函数完成自身部分的构造。因此,当将 Fish 类的对象转换为 Animal 类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中“ Animal 的对象所占的内存”。当利用类型转换完成后的对象指针去调用它的方法时,是调用它所在的内存中的方法。 2 .函数重载 由静态联编支持的多态性称为编译时的多态性或静态多态性,也就是说,确定同名操作的具体对象的过程是在编译过程中完成的。可以用函数重载和运算符重载来实现编译时的多态性。 函数重载也称为多态函数,是实现编译时的多态性的形式之一。函数重载时,函数名相同,但是函数所带的参数个数或数据类型不同。函数重载分为两种情况: 参数个数或类型有所差别的重载; 函数的参数完全相同但属于不同的类; 当函数的参数完全相同但属于不同的类时,为了让编译器正确区分调用哪个类的同名函数,可以采用以下两种方法: 用对象名区别:在函数名前面加上对象名来限制; 用类名和作用域运算符加以区别; 3. 虚函数 由动态联编支持的多态性称为运行时的多态性或者动态多态性,也就是说同名操作的具体操作对象的过程是在运行过程中完成的。在 C++ 中,可以用虚函数来实现运行时的多态。虚函数是实现运行时多态的一个重要方式,是重载的另一种形式,实现的是动态重载,即函数调用与函数体之间的联系是在运行时建立的,也就是动态联编。 #includeiostream using namespace std; class Animal { public: void sleep() { cout "Animal sleep...." endl; } virtual void breath() { cout "Animal breath..." endl; } }; class Fish:public Animal { void breath() { cout "Fish breath...." endl; } }; int main() { Fish fh; Animal *an = fh; an-breath(); system("pause"); return 0; } 实事上,当将基类中的成员函数 breath() 声明为虚函数时,编译器在编译的时候发现 Animal 类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放每个虚函数的地址。上述代码中 Animal 类和 Fish 类都包含了一个虚函数 breath() ,因此编译器会为这两个类分别建立一个虚表。 当 Fish 类的 fh 对象构造完成后,其内部的虚表指针也就被初始化为指向 Fish 类的虚表。在类型转换后,调用 an-breath() ,由于 an 实际上指向的是 Fish 类的对象,该对象内部的虚表指针指向的是 Fish 类的虚表,因此最终调用的是 Fish 类的 breath() 函数。 下边说明使用派生类对象指针时应注意的问题: (1)声明为指向基类对象的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象; (2)允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象; (3)声明为指向基类对象的指针,当其指向它的公有派生类对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问,可以将基类指针用显式类型转换方式转换为派生类指针。 虚函数可以很好地实现多态,使用时要注意以下的问题: (1)虚函数的声明只能出现在类函数原型的声明中,不能出现在函数体实现的时候,而且基类中只有保护成员或公有成员才能被声明为虚函数; (2)在派生类中重新定义虚函数时,关键字 virtual 可以写可以不写,但是在容易引起混乱的地方,应该写上关键字; (3)动态联编只能通过成员函数来调用或通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。 在派生类中重新定义基类中的虚函数,是函数重载的另一种形式,但是它与函数重载有如下区别: (1)一般的函数重载,要求其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同; (2)重载一个虚函数时,要求函数名、返回类型、参数个数、参数类型和参数的顺序必须与基类中的虚函数的原型完全相同; (3)如果仅返回类型不同,其余相同,则系统会给出错误的信息; (4)如果函数名相同,而参数个数、参数的类型或者参数顺序不同,系统认为是普通的函数重载,虚函数的特性将被丢失。 3.1 多级继承和虚函数 多级继承可以看作是多个单继承的组合,多级继承的虚函数与单继承的虚函数的调用相同,一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。 #includeiostream using namespace std; class Base { public: virtual ~Base() {}; virtual void func() { cout "Base func..." endl; } }; class Derived1 :public Base { public: void func() { cout "Derived1 func..." endl; } }; class Derived2 :public Derived1 { public: void func() { cout "Derived2 func..." endl; } }; void test(Base b) { b.func(); } int main() { Base bobj; Derived1 d1Obj; Derived2 d2Obj; test(bobj); test(d1Obj); test(d2Obj); system("pause"); return 0; } 在上述代码中,定义了一个多级继承,在基类中定义了虚函数 func() ,在主函数调用该函数时,不同类创建的对象其调用的函数是不同的,即实现了多态“一个接口,多种实现”的功能。上述代码中在析构函数前加上关键字 virtual 进行说明,则该析构函数就称为虚析构函数。 使用虚析构函数时,要注意以下两点: (1)只要基类的析构函数被声明为虚析构函数,则派生类的析构函数无论是否使用 virtual 关键字进行声明,都自动成为虚函数; (2)如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数; 一般来说,在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数 。这样,如果程序中用 delete 运算符准备删除一个对象,而 delete 运算符的操作对象是指向派生类对象的基类指针,系统会调用相应类的析构函数;否则系统会只执行基类的析构函数,而不执行派生类的析构函数,从而可能导致异常发生。 所以专业的编程人员一般习惯声明虚析构函数,即使基类不需要虚析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态存储空间时能得到正确的处理。 构造函数不能声明为虚函数,这是因为在执行构造函数时类对象还未完全建立过程,当然谈不上函数与类对象的关联。