前言
继承性反映的是类与类之间的层次关系,多态性则是考虑这种层次关系以及类自身特定成员函数之间的关系来解决行为的再抽象问题。多态性有两种表现形式一种是不同的对象在收到相同的消息时,产生不同的动作,主要通过虚函数来实现;另一种是同一对象收到相同的消息却产生不同的函数调用,主要通过函数重载来实现。本章将讨论多态性的主要内容:虚函数和动态联编;
一、静态联编与动态联编
多态性就是同一符号或名字在不同情况下具有不同解释的现象,既是指同一个函数的多种形态。c++可以支持两种多态性,编译时的多态性和运行时的多态性。
对一个函数的调用,要在编译时或在运行时确定将其
连接上相应的函数体的代码,这一过程称为函数联编(简称联编)。c++中两种联编形式:静态联编和动态联编。
静态联编
静态联编是指在程序编译连接阶段进行的联编。编译器根据源代码调用固定的函数标识符,然后由连接器接管这些标识符,并用物理地址代替他们。这种联编又被称为早期联编,应为这种联编工作是在程序运行之前完成的。
静态联编所支持的多态性称为编译时的多态性,当调用重载函数时,编译器可以根据调用时所使用的实参在编译时就确定下来应调用哪个函数。下面来看在层次关系中一个静态联编的例子。
#include<iostream> const double PI = 3.14; using namespace std; class Figure //定义基类; { public: Figure() {}; double area() const { return 0.0; } }; class Circle :public Figure //定义派生类,公有继承; { public: Circle(double myr) { R = myr; } double area() const { return PI * R * R; } protected: double R; }; class Rectangle :public Figure //定义派生类,公有继承; { public: Rectangle(double myl, double myw) { L = myl; W = myw; } double area() const { return L * W; } private: double L, W; }; int main() { Figure fig; //基类Figure对象; double area; area = fig.area(); cout << "Area of figure is" <<area<< endl; Circle c(3.0); //派生类Circle对象; area = c.area(); cout << "Area of Circle is" << area << endl; Rectangle rec(4.0, 5.0); area = rec.area(); cout << "Area of Rectangle is" << area << endl; return 0 ; }
上面的程序我们可以看出,在静态联编中,其实就是对重载函数的使用。定义不同的对象,通过对象来引用不同的函数从而实现我们要实现的功能。
静态联编的最大优点就是速度快,运行时的开销仅仅是传递参数,执行函数调用,清除等。不过,程序员必预测在每一种情况下所有的函数调用时,将要使用那些对象。这样,不仅有局限性,有时也是不可能实现的。
下面的程序就会说明这种情况:
#include<iostream> using namespace std; const double PI = 3.14; class Figure { public: Figure() {}; double area() const { return 0.0; } }; class Circle :public Figure //定义派生类,公有继承; { public: Circle(double myr) { R = myr; } double area()const { return PI*R*R; } protected: double R; }; class Rectangle :public Figure //定义派生类,公有继承; { public: Rectangle(double myl, double myw) { L = myl, W = myw; } double area()const { return L * W; } private: double L, W; }; void func(Figure &p) //形参为基类的引用; { cout << p.area() << endl; } int main() { Figure fig; cout << "Area of figure is"; func(fig); Circle c(3.0); //Circle派生类对象; cout << "Area of Circle is"; func(c); Rectangle rec(4.0, 5.0); cout << "Area of Rectangle is"; func(rec); return 0; }
上面的程序没有报错,但是结果正确,那是为什么?
在编译时,编译器将函数中的形参p所执行的area()操作联编到Figure类的area()上,这样访问的知识从基类继承来的成员。
那有没有什么方法来改变这种局限性喃?
动态联编
动态联编是指在程序运行时进行的联编。只有向具有多态性的函数传递一个实际对象的时候,该函数才能与多种可能的函数中的一种来联系起来。这种联编又被称为晚期联编。
动态联编所支持的多态性被称为运行时多态性。在程序代码中要知名某个成员函数具有多态性需要进行动态联编,且需要关键字virtual来标记。这种用virtual关键字标记的函数称为虚函数.
动态联编的优点 动态联编的缺点
增强了编程灵活性,问题抽象性和程序易维护性 函数调用顺序慢
虚函数
虚函数的作用
虚函数是一个成员函数,该成员函在基类内部声明并且被派生类重新定义。为了创建虚函数,应在基类中该函数生命的前面加上关键字virtual。
virtual<返回值类型><函数名>(<形式参数>)
{
《函数体》
}
如果某类中一个成员函数被说明为虚函数,这便意味着该成员函数在派生类中可能存在不同的实现方式。当继承包含虚函数的类时,派生类将重新定义该虚函数衣服和自身的需要。从本质上讲,虚函数实现了“相同界面,多种实现”的理念。而这种理念时运行时的多态性的基础,既是动态联编的基础。
动态联编需要满足的条件:
(1).类之间满足类型兼容规则;
(2).要声明虚函数;
(3).要由成员函数来调用或者是通过指针,引用来访问虚函数。
#include<iostream> using namespace std; const double PI = 3.14; class Figure { public: Figure() {}; virtual double area()const { return 0.0; } //定义为虚函数; }; class Circle :public Figure //定义派生类,公有继承; { public: Circle(double myr) { R = myr; } virtual double area()const { return PI * R * R; } protected: double R; }; class Rectangle :public Figure //定义派生类,公有继承方式; { public: Rectangle(double myl, double myw) { L = myl, W = myw; } virtual double area()const { return L * W; } private: double L , W; }; void fun(Figure& p) //形参为积基类的引用; { cout << p.area() << endl; } int main() { Figure fig; cout << "Figure of area is"; fun(fig); Circle c(3.0); cout << "Area of Circle is"; fun(c); Rectangle rec(4.0, 5.0); cout << "Area of Retangle is"; fun(rec); return 0; }
这个时候我们发现,答案正确了。看到这里,有没有一种熟悉的感觉,和我们前面学的虚基是不是相似。所以这里我们以可以理解为动态联编之所以能够不断地联编,就是因为成员函数产生了副本。
虚函数与一般重载函数的区别
乍一看,上面的程序,虚函数类似于重载函数。但它不属于重载函数,虚函数与一般的重载函数的区别:
虚函数 重载函数
虚函数不仅要求函数名相同,而且要求函数的签名,返回类型也相同。也就是说函数原型必须完全相同,而且虚函数的特性必须是体现在基类和派生类的类层次结构中 重载函数只要求函数有相同的函数名,并且重载函数 是在相同作用域定义的名字相同的不同函数
虚函数只能是非静态成员函数 重载函数可以是成员函数或友元函数
构造函数不能定义为虚函数,析构函数能定义为虚函数 构造函数可以重载,析构函数不可以
虚函数是根据对象来调用的 重载函数调用是以传递参数 序列的差别 来调用
虚函数是在运行时联编 重载函数是在编译时联编
继承虚属性
基类中说明的虚函数具有自动向下传给他的派生类的性质。不管经历多少派生类层,所有界面相同的函数都熬吃虚特性。因为派生类也是基类。
在派生类中重新定义虚函数时,要求与基类中说明的虚函数原型完全相同,这时对派生类的虚函数中virtual说明可以省略,但是为了提高程序的可读性。为了区分重载函数,而把一个派生类中重定义基类的虚函数称为覆盖。
示例:
#include<iostream> using namespace std; class Base { public: virtual int func(int x) //虚函数; { cout << "This is Base class"; return x; } }; class Subclass :public Base //派生类,公有继承; { public: int func(int x) //没有使用virtual关键字,但是依旧为虚函数; { cout << "This is Subclass class"; return x; } }; void fun(Base& x) { cout << "x=" << x.func(5) << endl; } int main() { Base bc; fun(bc); Subclass be; fun(be); return 0; }
下面我们来分析一下虚函数的错误使用
#include<iostream> using namespace std; class Base { public: virtual int func(int x) //虚函数; { cout << "This is Base class"; return x; } }; class Subclass :public Base //派生类,公有继承; { public: virtual float func(int x) //函数返回类型不同 { cout << "This is Subclass class"; return x; } }; void fun(Base& x) { cout << "x=" << x.func(5) << endl; } int main() { Base bc; fun(bc); Subclass be; fun(be); return 0; }
这里派生类中的虚函数,仅仅只是使用了不同的返回类型,但是就报错了,这是用为在c++中,只靠返回类型不同的信息,进行函数匹配是模糊的。
#include<iostream> using namespace std; class Base { public: virtual int func(int x) //虚函数; { cout << "This is Base class"; return x; } }; class Subclass :public Base //派生类,公有继承; { public: virtual int func(float x) //函数形参不同 { cout << "This is Subclass class"; return x; } }; void fun(Base& x) { cout << "x=" << x.func(5) << endl; } int main() { Base bc; fun(bc); Subclass be; fun(be); return 0; }
如果派生类与基类的虚函数仅函数名相同,其他不同,则c++认为是重定义函数,是隐藏的,丢失了虚特性。
一个类中的虚函数说明只对派生类中重定义的函数有影响,对他的基类并没有影响。
示例虚函数对他的基类的函数没有影响
#include<iostream> using namespace std; class Base { public: int func(int x) //不是虚函数; { cout << "This is Base class"; return x; } }; class Subclass :public Base { public: virtual int func(int x) //虚函数; { cout << "This is Subclass class"; return x; } }; class Subclass2 :public Subclass { public: int func(int x) //自动成为虚函数; { cout << "This is Baseclass2 class"; return x; } }; int main() { Subclass2 sc2; Base& bc = sc2; cout << "x=" << bc.func(5) << endl; Subclass& sc = sc2; cout << "x=" << sc.func(5) << endl; return 0; }
从结果看出,func()的操作贝莱你白难道Subclasses类中,显然进行的是动态联编。