原创 C++多态(1)

2018-2-11 16:24 2294 30 2 分类: 软件与OS 文集: C++学习记录(1)

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++的多态性混淆,fhFish类的对象,应该调用Fishbreath(),但是结果却不是这样的。

这是因为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)允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象;
  • (3)声明为指向基类对象的指针,当其指向它的公有派生类对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问,可以将基类指针用显式类型转换方式转换为派生类指针。

虚函数可以很好地实现多态,使用时要注意以下的问题:

(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条评论)

登录后参与讨论
我要评论
0
30
关闭 站长推荐上一条 /2 下一条