C++编程思想笔记——多态和虚函数

函数调用绑定

把函数体与函数调用相联系成为绑定。当绑定在程序运行之前(由编。译器和连接器)完成时,成为早绑定。C编译只有一种函数调用,就是遭绑定。绑定在运行发生时,成为晚绑定。当一个语言实现晚绑定时,必须有一种机制在运行时确定对象的类型和合适的调用函数。这就是,编译器还不知道实际的对象类型,但它插入能找到和调用正确函数体的代码。晚绑定机制因语言而异,但可以想象,一些种类的类型信息必须装在对象自身中。

虚函数

对于特定函数,为了引起晚绑定,C++要求在基类中声明这个函数时使用virtual关键字。晚绑定只对virtual起作用,而且只发生在我们使用一个基类的地址时,并且这个基类中有virtual函数,尽管它们也可以在更早的基类中定义。如果一个函数在基类中被声明为virtual,那么再所欲的派生类中它都是virtual的,在派生尅中的virtual函数的重定义通常称为越位。

C++如何实现晚绑定

关键字virtual告诉编译器它不应当完成早绑定,相反,它应当自动安装实现晚绑定所必须的所有机制。

为了完成这件事,编译器为每个包含虚函数的类创建一个表(称为VTABLE)。在VTABLE中,编译器放置特定类的虚函数地址。在每个带有虚函数的类中,编译器秘密地置一指针,称为vpointer(缩写为VPTR),指向这个对象的VTABLE。通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入取得这个VPTR,并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数使晚绑定发生。

为每个类设置VTABLE,初始化VPTR,为虚函数调用插入代码,所有这些都是自动发生的,所以我们不必担心这些。利用虚函数,这个对象的合适函数就能被调用,哪怕在编译器还不知道这个对象的特定类型的情况下。

一个类中如果没有数据成员,C++编译器会轻质这个类的对象是非零长度,因为每个对象必须有一个互相区别的地址。如果我们想要在一个零长度对象的数组中索引,我们就能理解这一点。一个“哑”成员被插入到对象中,否则这个对象就有零长度。当virtual关键字插入类型信息时,这个“哑”成员的地址就被占用。

对虚函数作图



每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个VTABLE。如果这个派生类中没有对在基类中声明为virtual的函数进行重定义,编译器就使用基类的这个虚函数地址。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE(这在构造函数中发生)

一旦VPTR被初始化为指向相应的VTABLE,对象就“知道”它自己是什么类型。但只用当虚函数被调用时这种自我只是才有用。

通过基类调用一个虚函数时(这时编译器没有能完成早绑定的足够信息),要特殊处理。它不是事项典型的函数调用,对特定地址的简单的汇编语言CALL,而是编译器为完成这个函数调用产生不同的代码。编译器从基类指针开始,这个指针指向这个对象的起始地址。所有的基类对象和派生对象都有他们的VPTR,它在对象的相同位置(常常在对象的开头),所以编译器能够去除这个对象的VPTR。VPTR指向VTABLE的开始地址。所有的VTABLE有相同的顺序。

安装vpointer

在VPTR适当初始化前,不能调用虚函数,初始化的地点在构造函数中

对象是不同的

向上映射(up-cast)仅处理地址。

为什么需要虚函数

虚函数有格外的函数调用开销,虚函数必须有地址存放在VTABLE中,所以内联函数不行。

有证据表明,进入C++的规模和速度改进是在C的闺蜜和速度的10%之内,能够得到更小的规模和更够爱速度的原因是因为C++可以有比用C更快的方法设计程序。

抽象类和纯虚函数


纯虚函数告诉编译器在VTABLE中为函数保留一个间隔,但在这个特定间隔中不放地址。只要一个函数在类中被声明为纯虚函数,则VTABLE就是不完全的,当试图创建这个类的对象时,编译器就会发出一个出错信息。

继承和VTABLE

当现实继承和定义一些虚函数时,编译器对新类创建一个新VTABLE表,并插入新函数的地址,对于没有重定义的的虚函数使用基类函数的地址。无论如何,在VTABLE中,总会有全体函数的地址

编译器防止对值在派生类中存在的函数做虚函数调用。

对象切片

当多态地处理函数时,传地址与传值有明显的不同,如果使用对象二笔是使用地址或引用进行向上映射,这个对象被切片。

虚函数和构造函数

当创建一个包含有虚函数的对象时,必须初始化它的VPTR以指向相应的VTABLE。这必须在有关虚函数的调用前完成。编译器在构造函数的开头部分秘密地插入能初始化VPTR的代码。

内联函数的理由是对小函数减少调用代价。预处理器没有通道或类的概念,因此不能用于创建成员函数宏,另外,有了由编译器插入隐藏代码的构造函数,预处理宏根本不能正常工作。

当寻找效率漏洞时,我们必须明白,编译器赈灾歘如隐藏代码到我们的构造函数中,这些隐藏代码不仅不许初始化VPTR,还必须检查this的值(万一operator new返回0)和调用基类构造函数。如果做大量构造函数的调用,我们的代码长度就会增长,而在速度上没有任何好处。

构造函数调用次序

所有基类构造函数总是在继承类构造函数中被调用。,因为构造函数有意向专门的工作:确保对象被正确的建立。派生类值访问它自己的成员,而不访问基类的成员,只有基类构造函数能恰当地初始化它自己的成员。因此,确保构造函数被调用是很关键的。如果不在构造函数初始化列表中显示地调用基类构造函数,它就调用缺省构造函数。

构造函数调用顺序是重要的,基类构造函数应首先被调用。只要可能,应当在构造函数初始化列表中初始化所有的成员对象。

虚函数在构造函数中的行为

在构造函数中调用一个虚函数,被调用的只是这个函数的本地版本,也就是说,虚机制在构造函数中不工作。

析构函数和虚析构函数

构造函数不能是虚的。但析构函数能够且常常必须是虚的。

当delete在堆中应将用new创建的类的对象指针时,如果这个指针是指向基类的,编译器只能知道在delete期间调用这个析构函数的基类版本,虚析构函数可以解决这个问题。如如果创建一个纯析构函数,我们就必须提供函数体,因为(不像普通函数)在类层析中所有的析构函数都总是被调用的。

在析构函数中的虚机制

如果在一个普通成员函数中调用一个虚函数,则这个函数被使用晚绑定机制调用。对于析构函数,这样不行,不论是虚的还是非虚的。在虚构函数中,只有成员函数的本地版本被调用,虚机制被忽略。

以为可能会在当前析构函数调用继承层次更外(更完派生的)的函数,这样被调用的函数就有可能操作已被删除的对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值