仅通过基类的接口,程序调用了正确的函数,编译器是如何知道正确代码的位置的呢?
其实,编译器在编译时并不知道要调用的函数体的正确位置,但它插入了一段能找到正确的函数体的代码。这称之为 晚捆绑(late binding) 或 运行时捆绑(runtime binding) 技术。
通过virtual 关键字创建虚函数能引发晚捆绑,编译器在幕后完成了实现晚捆绑的必要机制。它对每个包含虚函数的类创建一个表(称为VTABLE),用于放置虚函数的地址。在每个包含虚函数的类中,编译器秘密地放置了一个称之为vpointer(缩写为VPTR)的指针,指向这个对象的VTABLE。
关于构造函数不能为虚函数的原因:
1.在定义该派生类对象时,先调用其基类的构造函数,然后再初始化VPTR,最后再调用派生类的构造函数。在定义该派生类对象时,先调用其基类的构造函数,然后再初始化VPTR,最后再调用派生类的构造函数。
从实现上来说,每个对象的VPTR是需要构造函数来初始化的(当然是由编译系统自动加进去的代码来实现),在构造函数没有调用之前,VPTR没有形成,根本就不可能实现动态绑定。
简单来说,构造函数做的事情顺序如下:
基类的构造函数,由深到浅(若有虚基类,先虚基类),从左到右
初始化虚拟函数表的指针
执行初始化列表(按声明顺序,若有对象,递归第1步)
构造函数的执行体部分
2.只有基类指针指向子类对象时,虚函数才用意义。当一个基类指针指向子类对象时,子类对象已经构造好了,已经没有动态绑定的必要了,所以构造函数不能是虚函数。
3.从实现上来说,每个对象的VPTR是需要构造函数来初始化的(当然是由编译系统自动加进去的代码来实现),在构造函数没有调用之前,VPTR没有形成,根本就不可能实现动态绑定。
4.如前所述,虚函数机制只有在应用于地址时才有效,因为地址在编译阶段提供的类型信息不完全。构造函数的功能是为一个对象在内存中分配空间,也就是说,此时该对象的类型已经确定了,编译系统确切的知道应该调用哪一个类的构造函数,不需要也不可能应用动态绑定。
差不多就这么个意思
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
关于虚函数的使用方法,我在这里不做过多的阐述。大家可以看看相关的C++的书籍。在这篇文章中,我只想从虚函数的实现机制上面为大家 一个清晰的剖析。
当然,相同的文章在网上也出现过一些了,但我总感觉这些文章不是很容易阅读,大段大段的代码,没有图片,没有详细的说明,没有比较,没有举一反三。不利于学习和阅读,所以这是我想写下这篇文章的原因。也希望大家多给我提意见。
言归正传,让我们一起进入虚函数的世界。
虚函数表
对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。 在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了 这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
这里我们着重看一下这张虚函数表。在C++的标准规格说明书中说到,编译器必需要保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证正确取到虚函数的偏移量)。 这意味着我们通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
听我扯了那么多,我可以感觉出来你现在可能比以前更加晕头转向了。 没关系,下面就是实际的例子,相信聪明的你一看就明白了。
假设我们有这样的一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
按照上面的说法,我们可以通过Base的实例来得到虚函数表。 下面是实际例程:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// Invoke the first virtual function
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
实际运行经果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)
虚函数表地址:0012FED4
虚函数表 — 第一个函数地址:0044F148
Base::f
通过这个示例,我们可以看到,我们可以通过强行把&b转成int *,取得虚函数表的地址,然后,再次取址就可以得到第一个虚函数的地址了,也就是Base::f(),这在上面的程序中得到了验证(把int* 强制转成了函数指针)。通过这个示例,我们就可以知道如果要调用Base::g()和Base::h(),其代码如下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f()
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
这个时候你应该懂了吧。什么?还是有点晕。也是,这样的代码看着太乱了。没问题,让我画个图解释一下。如下所示:
注意:在上面这个图中,我在虚函数表的最后多加了一个结点,这是虚函数表的结束结点,就像字符串的结束符“\0”一样,其标志了虚函数表的结束。这个结束标志的值在不同的编译器下是不同的。在WinXP+VS2003下,这个值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,这个值是如果1,表示还有下一个虚函数表,如果值是0,表示是最后一个虚函数表。
下面,我将分别说明“无覆盖”和“有覆盖”时的虚函数表的样子。没有覆盖父类的虚函数是毫无意义的。我之所以要讲述没有覆盖的情况,主要目的是为了给一个对比。在比较之下,我们可以更加清楚地知道其内部的具体实现。
一般继承(无虚函数覆盖)
下面,再让我们来看看继承时的虚函数表是什么样的。假设有如下所示的一个继承关系:
请注意,在这个继承关系中,子类没有重载任何父类的函数。那么,在派生类的实例中,其虚函数表如下所示:
对于实例:Derive d; 的虚函数表如下:
我们可以看到下面几点:
1)虚函数按照其声明顺序放于表中。
2)父类的虚函数在子类的虚函数前面。
我相信聪明的你一定可以参考前面的那个程序,来编写一段程序来验证。
一般继承(有虚函数覆盖)
覆盖父类的虚函数是很显然的事情,不然,虚函数就变得毫无意义。下面,我们来看一下,如果子类中有虚函数重载了父类的虚函数,会是一个什么样子?假设,我们有下面这样的一个继承关系。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子:
我们从表中可以看到下面几点,
1)覆盖的f()函数被放到了虚表中原来父类虚函数的位置。
2)没有被覆盖的函数依旧。
这样,我们就可以看到对于下面这样的程序,
Base *b = new Derive();
b->f();
由b所指的内存中的虚函数表的f()的位置已经被Derive::f()函数地址所取代,于是在实际调用发生时,是Derive::f()被调用了。这就实现了多态。
多重继承(无虚函数覆盖)
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数。
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
多重继承(有虚函数覆盖)
下面我们再来看看,如果发生虚函数覆盖的情况。
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以任一静态类型的父类来指向子类,并调用子类的f()了。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
安全性
每次写C++的文章,总免不了要批判一下C++。这篇文章也不例外。通过上面的讲述,相信我们对虚函数表有一个比较细致的了解了。水可载舟,亦可覆舟。下面,让我们来看看我们可以用虚函数表来干点什么坏事吧。
一、通过父类型的指针访问子类自己的虚函数
我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
Base1 *b1 = new Derive();
b1->f1(); //编译出错
任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)
二、访问non-public的虚函数
另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
如:
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{
};
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun();
}
发表评论
-
析构函数为虚函数的原因
2012-09-09 11:42 830我们知道,用C++开发的时候,用来做基类的类的析构函数 ... -
hash的应用
2012-08-31 23:02 959第一部分为一道百度面试题Top K算法的详解;第二部分为关 ... -
微软智力题
2012-08-29 19:59 565第一组1.烧一根不均匀的绳,从头烧到尾总共需要1个小时。现在有 ... -
C++不能被继承的类
2012-08-27 20:16 1055一个类不能被继承, ... -
括号对齐问题
2012-08-27 10:47 1407解法一:左右括号成一对则抵消 可以 ... -
树的遍历
2012-08-19 10:43 717/****************************** ... -
堆排序
2012-08-16 14:24 882堆:(二叉)堆数据结构是一种数组对象。它可以被视为一棵完全 ... -
多态赋值
2012-08-14 16:16 828#include <iostream> usi ... -
static变量与static函数(转)
2012-08-13 10:15 744一、 static 变量 static变量大致分为三种用法 ... -
不用sizeof判断16位32位
2012-08-10 15:21 1701用C++写个程序,如何判断一个操作系统是16位还是3 ... -
找出连续最长的数字串(百度面试)
2012-08-09 15:15 1144int maxContinuNum(const char*in ... -
顺序栈和链栈
2012-08-06 10:01 796顺序栈:话不多说直接上代码 #include ... -
队列的数组实现和链表实现
2012-08-05 16:20 1023话不多少,数组实现上代码: #include<i ... -
KMP算法详解
2012-08-02 21:40 880KMP算法: 是在一个“主文本字符串” ... -
字符串的最长连续重复子串
2012-08-01 15:05 9768两种方法: 循环两次寻找最长的子串: <方法一> ... -
寻找一个字符串连续出现最多的子串的方法(转)
2012-07-31 21:19 974算法描述首先获得后缀数组,然后1.第一行第一个字符a,与第二行 ... -
字符串的循环移位
2012-07-31 16:52 974假设字符串:abcdefg 左循环两位:cdefgab 右 ... -
一次谷歌面试趣事(转)
2012-07-31 15:26 764很多年前我进入硅谷 ... -
约瑟夫环问题(循环链表)
2012-07-30 21:31 1287题目描述:n只猴子要选大王,选举方法如下:所有猴子按 1, ... -
面试之单链表
2012-07-30 20:18 7261、编程实现一个单链表的建立/测长/打印。 ...
相关推荐
当一个函数被声明为虚函数时,编译器会为该类创建一个虚函数表,其中包含了这个类的所有虚函数地址。这个表在类的每个实例中都存在,并且是在对象构造时初始化的。这样,即使通过基类指针调用虚函数,也能正确地调用...
本文将深入探讨C++虚函数表的工作原理、用途以及如何解析它。 虚函数表是一个由编译器自动生成的隐式数据结构,通常用于动态绑定(即运行时确定函数调用)。在类层次结构中,如果一个基类包含至少一个虚函数,那么...
2. **构造函数不能是虚函数**:C++不支持虚构造函数,因为构造函数的调用是在对象创建时进行的,此时还没有确定对象的实际类型。 3. **析构函数默认是虚函数**:从C++11开始,析构函数默认为虚函数,这确保在销毁...
### C++多态性与虚函数知识点解析 #### 一、多态性的概念 多态性是面向对象编程的一个核心特性,它允许我们通过基类的接口来操作派生类的对象。这种特性使得代码更加灵活且易于扩展。在C++中,多态性主要通过虚函数...
即使在没有显式定义构造函数的情况下,如果类含有虚函数,编译器也会自动生成一个默认构造函数,以确保虚表指针的正确设置。析构函数则负责清理对象,同样会处理虚表指针。如果类具有析构函数,那么在对象销毁时,析...
5. **构造函数与析构函数**:构造函数不能声明为虚函数,因为对象类型在构造过程中已经确定。而析构函数通常应该是虚函数,以便在删除基类指针指向的派生类对象时,能够正确调用派生类的析构函数。 6. **虚函数表...
- **解释**: 析构函数可以是虚函数,而构造函数不能是虚函数。析构函数通常用于释放动态分配的资源。 8. **答案**: virtual - **解释**: 为了使`Base`类中的`fun`成为虚函数,需要在声明前加上`virtual`关键字。 ...
内容概要:本文档提供了一系列针对C++开发者常见的面试题目及其解答,涵盖了构造函数与析构函数的概念、深拷贝与浅拷贝的区别、虚函数的作用、RAII编程准则的应用、实现多重继承的方法及可能引发的问题,还有智能...
下面的例子说明了为什么应该将基类的析构函数声明为虚函数: ```cpp #include using namespace std; class A { public: A() { p = new char[10]; cout ()" ; } ~A() { delete[] p; cout ~A" ; } private: ...
静态成员函数不能是虚函数。这是因为静态成员函数与类关联而不是与对象关联,而虚函数机制依赖于运行时的类型信息。 6. **构造函数与析构函数** - 构造函数不能是虚函数。 - 析构函数可以是虚函数,通常用于...
在构造函数中,这个指针可能指向基类的虚函数表,而在析构函数中也可能如此,除非派生类的析构函数被调用,这时虚函数表才被更新为派生类的虚函数表。 4. **示例输出解析**:从示例程序的输出可以看出,即使在构造...
构造函数和析构函数不能声明为虚函数,因为它们不参与动态联编。答案C表示的构造函数不能声明为虚函数。 7. **虚函数调用** 如果`p`是一个指向基类对象的指针,而`func()`是虚函数,那么`p->A::func()`会明确调用...
3. 构造函数可以重载但不能是虚函数,而析构函数可以是虚函数。 关于指针转换和访问规则: 1. 公有派生类的对象可以用基类指针指向,但私有派生类不行。 2. 基类指针只能访问派生类继承自基类的成员,不能直接访问...
当一个类中定义了虚函数,编译器就会为该类的每个实例添加一个虚指针,这个虚指针(vptr)指向一个称为虚函数表(virtual table)的数据结构。虚函数表(vtbl)包含了类中所有虚函数的函数指针,每个函数指针对应类中的一...
对于派生类,它的虚函数表会包含基类的虚函数,以及自身重写的那些。因此,即使`ptr`指向`Derived`对象,`Base`类的虚函数表也能正确地调用到`Derived`的`print`实现。 除了基本的虚函数,C++还提供了纯虚函数(`=0...
(1)每个类对象在被构造时不用去关心是否有其他类从自己派生,也不需要关心自己是否从其他类派生,只要按照一个统一的流程:在自身的构造函数执行之前把自己所属类(即当前构造函数所属的类)的虚函数表的地址绑定...
当我们在程序中声明一个对象时,编译器为调用构造函数(如果有的话),而这个调用将通常是外部的,也就是说它不属于class对象本身的调用,假如构造函数是私有的,由于在class外部不允许访问私有成员,所以这将导致...
C++类继承之子类调用父类的构造函数的实例详解 C++类继承是一种重要的面向对象编程技术,通过继承可以实现代码的重用和模块化。在C++类继承中,子类可以继承父类的成员变量和成员函数,但是在子类中调用父类的构造...
4. **构造函数与析构函数**:C++标准规定构造函数不能是虚函数,但析构函数默认是虚函数,以便在删除基类指针指向的派生类对象时能正确调用派生类的析构函数。 总之,虚函数是C++中实现多态性的重要工具,通过它...