虚函数探究与接口

本文探讨了C++中的虚函数与接口多态性的概念,解析了虚函数的实现机制,包括虚拟表和虚拟指针的工作原理。同时介绍了纯虚函数及抽象类的概念,并通过实例展示了如何利用纯虚函数实现接口。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

虚函数探究与接口
多态性是面向对象语言中除数据抽象,继承外的第三个基本特征。多态性提供了接口与具体实现的分离,使得代码有了更强的可扩展性。在c++中,多态性就是通过虚函数来实现的。
问题的提出:
例如下面一段代码:
#include
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
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
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
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一章.,文中错误多谢指出,批评。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值