`

虚函数探究与接口

阅读更多

虚函数探究与接口
多态性是面向对象语言中除数据抽象,继承外的第三个基本特征。多态性提供了接口与具体实现的分离,使得代码有了更强的可扩展性。在c++中,多态性就是通过虚函数来实现的。
问题的提出:
例如下面一段代码:
#include <iostream.h></iostream.h>
class A{
public:
void fun1(){}
void fun2(){
cout<<"base class's function call"<}
};
class B:public A{
public:
void fun1(){}
void fun2(){
cout<<"inherit class's function call"<}
};
main(){
B b;
A *a=&b;
a->fun2();
}
他们的关系如图:

A 类中有一个fun2()函数,B类继承于A.在B类中又定义了fun2();函数;b为对象,a为A的基类型指针并指向b,那么当我们用此指针访问成员函数fun2();时,调用的实际类中的函数还是子类中的函数呢?通过检验,结果显示:base class’s function call,即调用了积累的函数,为什么不调用B类的函数呢,我明明定义了它自己的fun2();函数呀?
分析:在c语言中,函数是采用早期绑定的,即在程序运行之前,编译器就要决定调用那个函数,这个过程叫做静态联编。这在c语言中工作的很好,c++沿用了这个方法,但是由于类关系的复杂使得这个方法产生了缺陷,比如上例,我想通过基类的指针访问子类的成员函数,编译器和连接器在程序没运行前,还没有能力去判断准确,它能做的只是为所有这种调用决定一个唯一的函数地址,这个地址应该是用于所有情形。无论指针所指的是A类自身的对象还是它的子类B的对象都应该得到调用,所以编译器没有别的选择,只能调用基类的那个函数,因为它有普遍的适应性,能保证所有的子类可以使用。
可是又是我们不想得到这种迁就的结果,怎么办呢,症结就在早期绑定上,更改的措施就是应用晚期绑定,即在程序运行的时候才临时决定调用哪一个函数,这个过程也叫动态联编。然而这项功能在c++中默认是关闭的。开启某个函数的晚期绑定的方式是在此函数的类型前加入virtual关键字,这种采用晚期绑定的函数就称为虚函数。
有人会问,为什么c++设计得这么罗嗦,为什么不设计成晚期绑定默认开启呢?c++是效率优先的语言,采用晚期绑定需要临时查找函数入口地址,还要占用额外的空间,这都迫使程序员向电脑妥协了。在c++语言发明的年代效率是多么的重要,在今日就不一样了,好像在.net中编译器已经把工作做得很好了。
那么虚函数的晚期绑定是怎样实现的呢?这是本文研究的重点.
为了获取足够的证据,我们先做一个实验:
#include "stdafx.h"
#include <iostream.h></iostream.h>
class A{
public:
int x;
void f(){};
};

class B{
public:
int x;
virtual void f(){};
};
main(){
A a;
B b;
cout<<"The None Virtual Funciton is "<cout<<" and the virtual function is "<}
A类和B类除了f()函数之外都一样,执行的结果是The None Virtual Funciton is 2 and the virtual function is 4,多了两个字节,(在vc++中显示为4和8,因为它用的是32位,而用tc编译器产生的是十六位) 正好是一个指针的长度,那是什么呢,我们很有可能想到是指针,这是我们推断到晚期绑定很可能在类中添加了函数的地址信息,依照这个推断,我们在做以下的分析:
在本文的开头一例中,我们对关键性的一条语句a->fun2();进行反汇编,逻辑上可以看到下面的代码:
push s
mov bx,worf ptr[si]
call word ptr [bx+2]
add sp,4
实验分析:
寄存器SI 存放i的地址,因为它是被选中的手地址,所以被压栈,这个首地址正是关键字this的值,正因为调用每个成员函数时this都必须作为参数压栈,所以成员函数能过去它的所属对象信息。接下类就是虚函数的实现,首先是寻找一个指针,对于这个编译器(大多数编译器都是这样),这个指针在对象的开头,因而第二行取出si所指的字,即类对象中的第一个字长的内容,就是存在开头的那个指针,接着对指针加上偏移量作为字,然后用这个字长的变量调用了一个函数,可见,这个字也是一个指针,这个指针正时函数的入口地址。我们可以分析出,偏移量的添加证明说有连续的函数入口地址组成一个表,用偏移的变法从这个表中选择恰当的函数地址,从而完成调用。而在类对象的开头有一个指针指向了这个表的首地址,因而可以通过对象找到这个表。最后一句话是将栈指针移回,已清楚在调用之前压入栈中的参数。
我们总结一下,晚期绑定的原理是这样:
编译器对每个包含虚函数的泪创建一个virtual table表,简称VTABLE表,在VTABLE表中,编译器放置特定类对象的虚函数入口地址。在每个带有虚函数的类中,编译器秘密的放置了一个指针叫virtual pointer,简称VPTR,指向这个对象的VTABLE表,同一函数在各个对象中VTABLE表中函数的地址顺序都是一样的。这保证了编译器知道到底偏移多少。当通过基类指针作虚函数的调用的时候,编译器静态的插入一段代码,这段代码完成两件事:<1>取得VPTR,<2>在VTABLE中寻找所需的地址。这样就正确的调用了虚函数。有点像授之鱼与授之以渔的区别。
如果用一张图表示他们之间的调用关系的话,可以画成这样:


然而,并不是说只要看到virtual关键字就进行晚期绑定,当编译器能名曲业的判定到底该调用那个函数的时候,它会自动用早期绑定,尤其是通过子类对象的名称(而不是地址)进行该对象成员的访问的时候,很可能是早期绑定的。
继续推想一下:既然我们能在基类中声明一个函数,在各自类中有不同的实现,那么在基类中给出一个抽象的没有具体功能的虚函数,具体的实现由子类的虚函数来完成,这不就能大大增强程序的可扩展性和组织性吗?对,这就是接口的雏形。
纯虚函数与接口:
C++的接口通过纯虚函数来实现,纯虚函数的语法是在巴苏函数的实现体{}去掉,换成=0,如:
virtual void f(int i)=0;
如果类中至少有一个函数为纯虚函数,那么此类被称为抽象类,如果某认识着生成一个抽象类的对象,不会通过编译,它的子类继承之后,也必须实现所有的纯虚函数,否则它也是抽象类。
抽象类的目的是为所有从它派生的泪创建公共接口,他指出作为子类,必须完成什么功能。
下面是一例经典的接口与实现:(引自《Thinking in C++》)
//: C15:Instrument5.cpp
// Pure abstract base classes
#include <iostream></iostream>
using namespace std;
enum note { middleC, Csharp, Cflat }; // Etc.

class Instrument {
public:
// Pure virtual functions:
virtual void play(note) const = 0;
virtual char* what() const = 0;
// Assume this will modify the object:
virtual void adjust(int) = 0;
};
// Rest of the file is the same ...

class Wind : public Instrument {
public:
void play(note) const {
cout << "Wind::play" << endl;
}
char* what() const { return "Wind"; }
void adjust(int) {}
};

class Percussion : public Instrument {
public:
void play(note) const {
cout << "Percussion::play" << endl;
}
char* what() const { return "Percussion"; }
void adjust(int) {}
};

class Stringed : public Instrument {
public:
void play(note) const {
cout << "Stringed::play" << endl;
}
char* what() const { return "Stringed"; }
void adjust(int) {}
};

class Brass : public Wind {
public:
void play(note) const {
cout << "Brass::play" << endl;
}
char* what() const { return "Brass"; }
};

class Woodwind : public Wind {
public:
void play(note) const {
cout << "Woodwind::play" << endl;
}
char* what() const { return "Woodwind"; }
};

// Identical function from before:
void tune(Instrument& i) {
// ...
i.play(middleC);
}

// New function:
void f(Instrument& i) { i.adjust(1); }

int main() {
Wind flute;
Percussion drum;
Stringed violin;
Brass flugelhorn;
Woodwind recorder;
tune(flute);
tune(drum);
tune(violin);
tune(flugelhorn);
tune(recorder);
f(flugelhorn);
} ///:~
然而,我们有时候仅仅是象使这个类禁止产生对象,但需要对函数进行定义,以便于子类向上类型转换时能够使用,怎么办?那就给纯虚函数加定义:
#include "stdafx.h"
#include<iostream.h></iostream.h>
class A{
public:
virtual void f()=0;
};
void A::f(){
cout<<"hello,I can do sth."<};
class B:public A{
public:
void f(){A::f();};
};
int main(){
B b;
b.f();
}此例中A的f()函数是纯虚函数,然而它有定义。这使得它的派生类B可以访问它。这应是.NET中mustinheri关键字的由来吧。
还有一些话题,诸如对象切片,构造函数与虚函数关系等等,与了解这些内容,请参阅《thinking in c++》的polynorphism and virtual functions一章.,文中错误多谢指出,批评。

分享到:
评论

相关推荐

    C#中虚函数,抽象,接口的简单说明

    利用C#语言对面向对象中的虚函数,抽象,接口的简单说明

    c++虚函数与虚函数表

    学习 C++ 的同志不知道有没有和我一样遇到过这样的困惑:C++中的虚函数到底怎么实现的?在各种继承关系中,虚函数表的结构到底是什么样的?曾经我是很想当然,可是后来在使用ATL的过程中,我发现并不是我想的那样。...

    构造函数不能声明为虚函数的原因及分析

    1. 从存储空间角度,虚函数对应一个指向vtable虚函数表的指针,这大家都知道,可是这个指向vtable的指针其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有...

    C++ 多态 虚函数 虚函数表 最是详细

    高质量的C++多态讲解,详细讲解虚函数,虚函数表,虚函数继承,虚函数继承下的内存分配等

    构造函数不能声明为虚函数,析构函数可以声明为虚函数

    构造函数不能声明为虚函数,析构函数可以声明为虚函数。

    C++虚函数实现原理

    虚函数表中虚函数的分布情况;其中包括发生继承的情况下虚函数表中虚函数的分布情况;

    c++虚函数使用

    c++虚函数.C++中的虚函数的作用主要是实现了多态的机制。

    虚基类、虚函数与纯虚函数

    网上看到的一篇比较好的对c++虚类,虚基类,全虚函数的解释

    虚函数表工作原理

    虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容...

    C++虚函数表测试源码

    C++虚函数表的测试代码,用于学习C++虚函数的调用关系。

    C++虚函数及虚函数表解析

    C++虚函数及虚函数表解析,内容详细,分析清晰,推荐给大家。

    c++多态性与虚函数练习题

    2、熟练掌握虚函数的作用及其使用方法。 3、掌握静态关联和动态关联的概念和用法。 4、理解纯虚函数和抽象类的概念和用法。 (二)实验内容 1、定义一个类A,在A中有两个私有的整型变量a和b,定义构造函数对a和b进行...

    C++继承多接口,调用虚函数跳转到错误接口的虚函数的奇怪问题demo

    C++继承多接口,调用虚函数跳转到错误接口的虚函数的奇怪问题,一个示例的dmeo

    C++实验六 多态性和虚函数的应用 课程 实验报告

    C++实验六 多态性和虚函数的应用 课程 实验报告 作业参考的良品!

    用C++实现虚函数

    用C++简单编码实现虚函数,展现虚函数的用法,以及虚析函数的用法 和 判断类的大小(在类中有虚函数的时候,无虚函数的时候)

    虚函数虚表的详解,大家看看!

    个类如果有虚函数,不管是几个虚函数,都会为这个类声明一个虚函数表,这个虚表是一个含有虚函数的类的,不是说是类对象的。一个含有虚函数的类,不管有多少个数据成员,每个数据成员都有一个虚指针,在内存中,存放...

    C++虚函数表解析

    C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际 子类的成员函数。

    c++虚函数表解析(彻底攻克继承和虚函数)

    彻底搞清楚继承是个什么东西 彻底搞清楚虚函数和虚函数表是个什么东西

    虚基类与虚函数

    虚基类与虚函数 虚基类的概念 在C++语言中,一个类不能被多次说明为一个派生类的直接基类,但可以不止一次地成为间接基类。这就导致了一些问题。为了方便 说明,先介绍多继承的“类格”表示法。

    指针高级应用_虚函数

    V6.0验证源码 ,分析虚函数的实现过程 ,强制转换的实质 ,多种指针用法,如果完全看懂了相信你的C++,指针会有一个更深刻的认识。希望对大家有用

Global site tag (gtag.js) - Google Analytics