1. 多态
多态是人类思维方式的一种直接模拟,多态性是指不同对象接收到相同的消息时,根据对象类的不同而产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数。就是同一个函数名称,作用在不同的对象上将产生不同的行为。
多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,而后者是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象过程就是联编。联编是指计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程。在面向对象上来讲,就是把一条消息和一个对象的方法结合起来的过程。按照联编进行阶段的不同,分为:静态联编和动态联编,它们分别对应着多态的两种实现方式。
1.1 多态的引入
#include<iostream>
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++中,可以用虚函数来实现运行时的多态。虚函数是实现运行时多态的一个重要方式,是重载的另一种形式,实现的是动态重载,即函数调用与函数体之间的联系是在运行时建立的,也就是动态联编。
#include<iostream>
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)在派生类中重新定义虚函数时,关键字virtual可以写可以不写,但是在容易引起混乱的地方,应该写上关键字;
(3)动态联编只能通过成员函数来调用或通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。
在派生类中重新定义基类中的虚函数,是函数重载的另一种形式,但是它与函数重载有如下区别:
(1)一般的函数重载,要求其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同;
(2)重载一个虚函数时,要求函数名、返回类型、参数个数、参数类型和参数的顺序必须与基类中的虚函数的原型完全相同;
(3)如果仅返回类型不同,其余相同,则系统会给出错误的信息;
(4)如果函数名相同,而参数个数、参数的类型或者参数顺序不同,系统认为是普通的函数重载,虚函数的特性将被丢失。
3.1 多级继承和虚函数
多级继承可以看作是多个单继承的组合,多级继承的虚函数与单继承的虚函数的调用相同,一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。
#include<iostream>
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运算符的操作对象是指向派生类对象的基类指针,系统会调用相应类的析构函数;否则系统会只执行基类的析构函数,而不执行派生类的析构函数,从而可能导致异常发生。
所以专业的编程人员一般习惯声明虚析构函数,即使基类不需要虚析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态存储空间时能得到正确的处理。
构造函数不能声明为虚函数,这是因为在执行构造函数时类对象还未完全建立过程,当然谈不上函数与类对象的关联。
文章评论(0条评论)
登录后参与讨论