解析C++中的虚拟函数及其静态类型和动态类型

所属分类: 软件编程 / C 语言 阅读数: 75
收藏 0 赞 0 分享

虚拟函数是C++语言引入的一个很重要的特性,它提供了“动态绑定”机制,正是这一机制使得继承的语义变得相对明晰。
(1)基类抽象了通用的数据及操作,就数据而言,如果该数据成员在各派生类中都需要用到,那么就需要将其声明在基类中;就操作而言,如果该操作对各派生类都有意义,无论其语义是否会被修改或扩展,那么就需要将其声明在基类中。
(2)有些操作,如果对于各个派生类而言,语义保持完全一致,而无需修改或扩展,那么这些操作声明为基类的非虚拟成员函数。各派生类在声明为基类的派生类时,默认继承了这些非虚拟成员函数的声明/实现,如同默认继承基类的数据成员一样,而不必另外做任何声明,这就是继承带来的代码重用的优点。
(3)另外还有一些操作,虽然对于各派生类而言都有意义,但是其语义并不相同。这时,这些操作应该声明为基类的虚拟成员函数。各派生类虽然也默认继承了这些虚拟成员函数的声明/实现,但是语义上它们应该对这些虚拟成员函数的实现进行修改或者扩展。另外在实现这些修改或扩展过程中,需要用到额外的该派生类独有的数据时,将这些数据声明为此派生类自己的数据成员。
再考虑更大背景下的继承体系,当更高层次的程序框架(继承体系的使用者)使用此继承体系时,它处理的是一个抽象层次的对象集合(即基类)。虽然这个对象集合的成员实质上可能是各种派生类对象,但在处理这个对象集合中的对象时,它用的是抽象层次的操作。并不区分在这些操作中,哪些操作对各派生类来说是保持不变的,而哪些操作对各派生类来说有所不同。这是因为,当运行时实际执行到各操作时,运行时系统能够识别哪些操作需要用到“动态绑定”,从而找到对应此派生类的修改或扩展的该操作版本。
也就是说,即只需关心它自己问题域的业务逻辑,只要保证正确,其任务就算完成了
。即使继承体系内部增加了某种派生类,或者删除了某种派生类,或者某某派生类的某个虚拟函数的实现发生了改变,它的代码不必任何修改。这也意味着,程序的模块化程度得到了极大的提高。而模块化的提高也就意味着可扩展性、可维护性,以及代码的可读性的提高,这也是“面向对象”编程的一个很大的优点。

虚拟函数的静态类型和动态类型
先来看一个问题,如果一个子类重载的虚拟函数为privete,那么通过父类的指针可以访问到它吗?

#include <IOSTREAM> 
class B 
{ 
public: 
  virtual void fun()  
  { 
    std::cout << "base fun called"; 
  }; 
}; 
class D : public B  
{ 
private: 
  virtual void fun()  
  { 
    std::cout << "driver fun called"; 
  }; 
}; 
int main(int argc, char* argv[]) 
{   
  B* p = new D(); 
  p->fun(); 
  return 0; 
} 

运行时会输出

driver fun called

从这个实验,可以更深入的了解虚拟函数编译时的一些特征:

在编译虚拟函数调用的时候,例如p->fun(); 只是按其静态类型来处理的, 在这里p的类型就是B,不会考虑其实际指向的类型(动态类型)。

也就是说,碰到p->fun();编译器就当作调用B的fun来进行相应的检查和处理。

因为在B里fun是public的,所以这里在“访问控制检查”这一关就完全可以通过了。然后就会转换成(*p->vptr[1])(p)这样的方式处理, p实际指向的动态类型是D,所以p作为参数传给fun后(类的非静态成员函数都会编译加一个指针参数,指向调用该函数的对象,我们平常用的this就是该指针的值), 实际运行时p->vptr[1]则获取到的是D::fun()的地址,也就调用了该函数, 这也就是动态运行的机理。

为了进一步的实验,可以将B里的fun改为private的,D里的改为public的,则编译就会出错。

C++的注意条款中有一条" 绝不重新定义继承而来的缺省参数值" (Effective C++ Item37, never redefine a function's inherited default parameter value) 也是同样的道理。
可以再做个实验

class B 
{ 
public: 
  virtual void fun(int i = 1)  
  { 
    std::cout << "base fun called, " << i; 
  }; 
}; 
class D : public B  
{ 
private: 
  virtual void fun(int i = 2)  
  { 
    std::cout << "driver fun called, " << i; 
  }; 
}; 

则运行会输出

driver fun called, 1

关于这一点,Effective上讲的很清楚“virtual 函数系动态绑定, 而缺省参数却是静态绑定”,也就是说在编译的时候已经按照p的静态类型处理其默认参数了,转换成了(*p->vptr[1])(p, 1)这样的方式。

更多精彩内容其他人还在看

用标准c++实现string与各种类型之间的转换

这个类在头文件中定义, < sstream>库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本
收藏 0 赞 0 分享

C++如何通过ostringstream实现任意类型转string

再使用整型转string的时候感觉有点棘手,因为itoa不是标准C里面的,而且即便是有itoa,其他类型转string不是很方便。后来去网上找了一下,发现有一个好方法
收藏 0 赞 0 分享

C/C++指针小结

要搞清一个指针需要搞清指针的四方面的内容:指针的类型,指针所指向的类型,指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区
收藏 0 赞 0 分享

C++ 类的静态成员深入解析

在C++中类的静态成员变量和静态成员函数是个容易出错的地方,本文先通过几个例子来总结静态成员变量和成员函数使用规则,再给出一个实例来加深印象
收藏 0 赞 0 分享

C++类的静态成员初始化详细讲解

通常静态数据成员在类声明中声明,在包含类方法的文件中初始化.初始化时使用作用域操作符来指出静态成员所属的类.但如果静态成员是整型或是枚举型const,则可以在类声明中初始化
收藏 0 赞 0 分享

C++类静态成员与类静态成员函数详解

静态成员不可在类体内进行赋值,因为它是被所有该类的对象所共享的。你在一个对象里给它赋值,其他对象里的该成员也会发生变化。为了避免混乱,所以不可在类体内进行赋值
收藏 0 赞 0 分享

C++中的friend友元函数详细解析

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类。友元函数的特点是能够访问类中的私有成员的非成员函数。友元函数从语法上看,它与普通函数一样,即在定义上和调用上与普通函数一样
收藏 0 赞 0 分享

static全局变量与普通的全局变量的区别详细解析

以下是对static全局变量与普通的全局变量的区别进行了详细的分析介绍,需要的朋友可以过来参考下,希望对大家有所帮助
收藏 0 赞 0 分享

C++ explicit关键字的应用方法详细讲解

C++ explicit关键字用来修饰类的构造函数,表明该构造函数是显式的,既然有"显式"那么必然就有"隐式",那么什么是显示而什么又是隐式的呢?下面就让我们一起来看看这方面的知识吧
收藏 0 赞 0 分享

教你5分钟轻松搞定内存字节对齐

随便google一下,人家就可以跟你解释的,一大堆的道理,我们没怎么多时间,讨论为何要对齐.直入主题,怎么判断内存对齐规则,sizeof的结果怎么来的,请牢记以下3条原则
收藏 0 赞 0 分享
查看更多