c++--多态性和虚函数

来源:互联网 发布:微交易源码 编辑:程序博客网 时间:2024/05/21 17:52

//虚继承解决了命名冲突的问题,只保留了一份基类成员,最终派生类要调用

虚基类的构造函数,并且虚基类是最先调用的,其他构造函数出现顺序和出现的顺序一样(一般派生类都要调用基类的构造函数)

 

1.多态的概念和用途   //虚函数实现多态

    通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。
为了消除这种尴尬,让基类指针能够访问派生类的成员函数,C++增加了虚函数Virtual Function)。

虚函数:    在函数声明前面增加 virtual 关键字。

 

使用虚函数:

基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),

指向派生类对象时就使用派生类的成员。

 

换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态

 

C++提供多态的目的是:

    可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。

 

   如果没有多态,我们只能访问成员变量。

 

借助引用也可以实现多态   

    借助引用也可以实现多态。     

从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。

 

2.引用与指针

引用不像指针灵活,指针可以随时改变指向,

 

而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说指针。

 

3.虚函数

虚函数对于多态具有决定性的作用,有虚函数才能构成多态,虚函数的注意事项:


1) 只需要在虚函数的声明处加上 virtual 关键字,函数定义处不加。

 

2) 为了方便,可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽(覆盖)关系的同名函数都将自动成为虚函数


3) 当在基类中定义了虚函数时,如果派生类没有定义新的不同参数函数来遮蔽此函数,那么将使用基类的虚函数。

 

4) 只有派生类的虚函数遮蔽基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。

 

例如基类虚函数的原型为virtual void func();

派生类虚函数的原型为virtual void func(int);

 

当基类指针 p 指向派生类对象时,

p -> func(100) 会出错,

p -> func() 将调用基类的函数。

 

5) 构造函数不能是虚函数。

对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。

也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义。


6) 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数

 

4.构成多态条件

必须存在继承关系;

继承关系中必须有同名的虚函数,并且它们是遮蔽(覆盖)关系。

存在基类的指针,通过该指针调用虚函数

 

函数解析

在基类 Base 中我们将void func()声明为虚函数,这样派生类Derived中的void func()就会自动成为虚函数。p是基类Base的指针,但是指向了派生类Derived的对象

p -> func() 调用的是派生类的虚函数,构成了多态

p -> func(10) 调用的是基类的虚函数,因为派生类中没有函数遮蔽它。

p -> func(wwww)出现编译错误

通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员

 

5.什么时候声明虚函数

1.首先看成员函数所在的类是否会作为基类。

2.看成员函数在类的继承后有无可能被更改功能

如果希望更改其功能的,一般应该将它声明为虚函数。

3.如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

 

6.C++虚析构函数的必要性

C++ 中的构造函数用于在创建对象时进行初始化工作,在执行构造函数之前对象尚未创建完成,虚函数表尚不存在,也没有指向虚函数表的指针,

所以此时无法查询虚函数表,也就不知道要调用哪一个构造函数。

 

析构函数用于在销毁对象时进行清理工作,可以声明为虚函数,而且有时候必须要声明为虚函数。

 

指针指向哪个类就调用哪个类的函数。

pa 是基类的指针,所以不管它指向基类的对象还是派生类的对象,始终都是调用基类的析构函数。

 

将基类的析构函数声明为虚函数后,派生类的析构函数也会自动成为虚函数。

这个时候编译器会忽略指针的类型,而根据指针的指向来选择函数;也就是说,指针指向哪个类的对象就调用哪个类的函数。

pbpd都指向了派生类的对象,所以会调用派生类的析构函数,继而再调用基类的析构函数。

如此一来也就解决了内存泄露的问题。

在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个基类,那么我们就必须将该析构函数声明为虚函数,否则就有内存泄露的风险。

也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。
注意,这里强调的是基类,如果一个类是最终的类,那就没必要再声明为虚函数了。


7.虚函数表 多态实现机制

通过指针访问类的成员函数时:

如果该函数是非虚函数,那么编译器会根据指针的类型找到该函数;

也就是说,指针是哪个类的类型就调用哪个类的函数。

 

如果该函数是虚函数,并且派生类有同名的函数遮蔽它,那么编译器会根据指针的指向找到该函数;

也就是说,指针指向的对象属于哪个类就调用哪个类的函数。

这就是多态。

 

 

如果一个类包含了虚函数,那么在创建该类的对象时就会额外地增加一个数组,数组中的每一个元素都是虚函数的入口地址。

不过数组和对象是分开存储的,为了将对象和数组关联起来,编译器还要在对象中安插一个指针,指向数组的起始位置。

这里的数组就是虚函数表,简写为vtable

 

 

 

左半部分是对象占用的内存,右半部分是虚函数表 vtable

在对象的开头位置有一个指针 vfptr,指向虚函数表,并且这个指针始终位于对象的开头位置。
发现基类的虚函数在 vtable 中的索引(下标)是固定的,不会随着继承层次的增加而改变,派生类新增的虚函数放在 vtable的最后。

 如果派生类有同名的虚函数遮蔽(覆盖)了基类的虚函数,那么将使用派生类的虚函数替换基类的虚函数,这样具有遮蔽关系的虚函数在 vtable中只会出现一次。

 

7.静态绑定和动态绑定

CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。

编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
将变量名和函数名统称为符号(Symbol),找到符号对应的地址的过程叫做符号绑定。

 

 

函数绑定

找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。

在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定Static binding)。

有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定dynamic binding)。

函数绑定

找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。

在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定(Static binding)。

有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定(dynamic binding)。
C++中,非虚函数都是静态绑定而虚函数却是动态绑定Statibangding.cpp

结果: 那么两次调用的行为肯定会是一样的,为:
      我来自B
      我来自B
       但是调用过程却不同。

 pB->fun();   //pB为指向B类型的指针,直接调用B中的fun()函数
pD->fun();   //pD为指向D类型的指针,编译器首先在D中查找fun()函数的声明,没有找到,然后到D的基类B中去找,找到fun(),停止查找。

 

但如果D中有自己定义的fun()函数,那执行结果就会不同:
      取消上面例子中的注释,结果为:
      我来自B
      我来自D
      过程:
      pB->fun(); //调用B::fun()
      pD->fun(); //调用D::fun()

 

原因:

1.非虚函数都是静态绑定,也就是说,由于pB被声明为指向B类型的指针,那么通过pB调用的(non-virtual)非虚函数永远是B所定义的版本,即使pB指向一个类型为“B的派生类D“的对象。

  2.virtual)虚函数却是动态绑定,那么不管是通过什么类型的指针调用的这个虚函数,都会根据指针实际指向的对象类型来决定虚函数的调用,而与指针类型无关。

如果fun()函数为虚函数,那么不管是通过pB还是pD调用fun()函数,都会调用D::fun(),因为pBpD真正指向的都是同一个类型D的对象。

 

程序中把fun函数声明为虚函数,所以不管是通过pB还是pD调用fun()函数,都会调用D::fun(),因为pBpD真正指向的都是同一个类型D的对象。程序的输出结果验证了这一点:
       我来自D
       我来自D

:C++中,绝对不要重新定义继承而来的

非虚函数,因为这样的话,函数的调用决定因素不在对象本身,而与调用函数的指针类型有关,这将给程序留下不可预知的隐患。

 

 

8.纯虚函数和抽象类

C++中,可以将虚函数声明为纯虚函数,

virtual 返回值类型 函数名 (函数参数) = 0;

1)纯虚函数没有函数体,只有函数声明,在虚函数声明的结尾加上=0,表明此函数为纯虚函数。

2) 最后的=0并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”。

3)包含纯虚函数的类称为抽象类(Abstract Class)。

之所以说它抽象,是因为它无法实例化,也就是无法创建对象。

原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。


4)抽象类通常是作为基类,让派生类去实现纯虚函数。

派生类必须实现纯虚函数才能被实例化。

 

例中定义了四个类,它们的继承关系为:

Line --> Rec --> Cuboid --> Cube

抽象基类除了约束派生类的功能,还可以实现多态。

Line *p = new Cuboid()

指针 p 的类型是 Line,但是它却可以访问派生类中的area()volume()函数,正是由于在Line类中将这两个函数定义为纯虚函数;

如果不这样做,后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。

 

 

9.关于纯虚函数的几点说明

1) 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。

2) 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

//顶层函数不能被声明为纯虚函数

void fun() = 0; //compile error

class base{

public :           //普通成员函数不能被声明为纯虚函数

 void display() = 0; //compile error

};

 

10.C++typeid的运算符

typeid 运算符用来获取一个表达式的类型信息。

类型信息对于编程语言非常重要,它描述了数据的各种属性:对于基本类型(intfloatC++内置类型)的数据,类型信息所包含的内容比较简单,主要是指数据的类型。

对于类类型的数据(也就是对象),类型信息是指对象所属的类、所包含的成员、所在的继承关系等。

类型信息是创建数据的模板,数据占用多大内存、能进行什么样的操作、该如何操作等,这些都由它的类型信息决定。

 

 

 


typeid 的操作对象既可以是表达式,也可以是数据类型,下面是它的两种使用方法:

typeid( dataType )
typeid( expression )

dataType 是数据类型,expression是表达式,这和sizeof运算符非常类似,只不过sizeof有时候可以省略括号( ),而typeid必须带上括号。

    

type_info 类的几个成员函数,它们的介绍:

name() 用来返回类型的名称。

raw_name() 用来返回名字编码(Name Mangling)算法产生的新名称。

hash_code() 用来返回当前类型对应的 hash值。

hash 值是一个可以用来标志当前类型的整数,有点类似学生的学号、公民的身份证号、银行卡号等。不过hash值有赖于编译器的实现,在不同的编译器下可能会有不同的整数,但它们都能唯一地标识某个类型。


C++ 标准规定,type_info类至少要有如下所示的4public属性的成员函数,其他的扩展函数编译器开发者可以自由发挥,不做限制。

1) 原型:const char* name() const;

返回一个能表示类型名称的字符串。

但是C++标准并没有规定这个字符串是什么形式的,

例如对于上面的objInfo.name()语句,VC/VS下返回“class Base”,但GCC下返回“4Base”。


2) 原型:bool before (const type_info& rhs) const;

判断一个类型是否位于另一个类型的前面,rhs 参数是一个 type_info 对象的引用。

 

但是C++标准并没有规定类型的排列顺序,不同的编译器有不同的排列规则,程序员也可以自定义。

要特别注意的是,这个排列顺序和继承顺序没有关系,基类并不一定位于派生类的前面。

 

3) 原型:bool operator== (const type_info& rhs) const;

重载运算符==”,判断两个类型是否相同,rhs参数是一个type_info对象的引用。

 

4) 原型:bool operator!= (const type_info& rhs) const;

重载运算符!=”,判断两个类型是否不同,rhs参数是一个type_info对象的引用。

 

 

 

 

判断类型是否相等

typeid 运算符经常被用来判断两个类型是否相等。

1) 内置类型的比较

例如有下面的定义:

char *str;

int a = 2;

int b = 10;

  float f;

 

 

typeid 返回 type_info对象的引用,而表达式typeid(a) == typeid(b)的结果为true,可以说明,

一个类型不管使用了多少次,编译器都只为它创建一个对象,所有 typeid都返回这个对象的引用。

 

2) 类的比较

例如有下面的定义:

class Base{};

class Derived: public Base{};

 

Base obj1; Base *p1;

Derived obj2;

Derived *p2 = new Derived;

  p1 = p2;

 

 

 

type_info 类的声明

最后我们再来看一下 type_info 类的声明,以进一步了解它所包含的成员函数以及这些函数的访问权限。type_info类位于typeinfo头文件,声明形式类似于:

 

 

这种在程序运行后确定对象的类型信息的机制称为运行时类型识别(Run-Time Type IdentificationRTTI)。在C++中,只有类中包含了虚函数时才会启用RTTI机制,其他所有情况都可以在编译阶段确定类型信息。

 

11.RTTI机制下的内存模型

C++ 中,除了 typeid 运算符,dynamic_cast运算符和异常处理也依赖于RTTI机制,并且要能够通过派生类获取基类的信息,或者说要能够判断一个类是否是另一个类的基类,这样上节讲到的内存模型就不够用了,我们必须要在基类和派生类之间再增加一条绳索,把它们连接起来,形成一条通路,让程序在各个对象之间游走。在面向对象的编程语言中,我们称此为继承链(Inheritance Chain)。

 

 

静态绑定:指在编译过程中就能确定所调用的是哪一个函数。静态多态性为编译时多态性,通过函数重载和运算符重载实现;

 

动态多态性是指在程序运行的过程中才能动态的确定哪一个函数。运行时多态。通过继承和虚函数实现。

 

原创粉丝点击