虚函数指针sizeof不为sizeof(void*)
ref:http://bbs.csdn.net/topics/360249561
一个继承了两个虚基类又增加了自己的一个虚函数pif的类,sizeof(指向pif的指针)竟然是8(X86)。
我是从这里http://www.codeproject.com/KB/cpp/FastDelegate.aspx看到的。
试验代码(VS2010,Win32)
#include <iostream>
#include <cstdlib>
class CBase
{
public:
int a;
int b;
int c;
int d;
int e;
virtual void fa(){std::cout<<"base fa"<<std::endl;}
virtual void fb(){std::cout<<"base fb"<<std::endl;}
};
class CBase2
{
public:
int a;
int b;
int c;
int d;
int e;
virtual void f2a(){std::cout<<"base2 fa"<<std::endl;}
virtual void f2b(){std::cout<<"base2 fb"<<std::endl;}
};
class CInh:public CBase,public CBase2
{
public:
virtual void fa(){std::cout<<"inh fa"<<std::endl;}
virtual void fb(){std::cout<<"inh fb"<<std::endl;}
virtual void f2a()
{
std::cout<<"inh f2a"<<std::endl;
CInh *p=this;
}
//virtual void f2b(){std::cout<<"inh f2b"<<std::endl;}
virtual void fia(){};
int a;
};
int main()
{
typedef void (CInh::* pf_t)(void);
std::cout<<sizeof(pf_t)<<std::endl;//输出8
pf_t pft=&CInh::fia;
int n=5;
int *pn=&n;
void *pv=pn;
pf_t *ppp=(pf_t *)pv;
typedef void (CBase2::* func_t)(void);
std::cout<<sizeof(func_t)<<std::endl;
CInh *a=new CInh;
a->f2a();
func_t pf=&CBase2::f2b;
(a->*pf)();
std::cout<<&a<<std::endl;
std::cout<<(CBase *)(&a)<<std::endl;
std::cout<<(CBase2 *)(&a)<<std::endl;
std::cout<<(CInh *)(CBase2 *)(&a)<<std::endl;
CBase2 *p=a;
p->f2a();
std::system("pause");
return 0;
}
问题:
标准里对指针的size是怎么规定的?
如果指针的size是编译器相关的话,那么用int来保存各种指针岂不是不可靠的了?就是说将指针转换为int可能会丢失信息?可是印象中好多代码是这样写的啊?
在编程工作中常会遇到在一个“类”中通过函数指针调用成员函数的要求,如,当在一个类中使用了C++标准库中的排序函数qsort时,因qsort参数需要一个“比较函数”指针,如果这个“类”使用某个成员函数作“比较函数”,就需要将这个成员函数的指针传给qsort供其调用。本文所讨论的用指针调用 “类”的成员函数包括以下三种情况:
(1)将 “类”的成员函数指针赋予同类型非成员函数指针,如:
C/C++ code
#include <stdio.h>
#include <stdlib.h>
typedef void (*Function1)(); //定义一个函数指针类型。
Function1 f1;
class Test1
{
public:
// 被调用的成员函数
void Memberfun1()
{
printf("%s \n", "Calling Test1::Memberfun1 OK");
}
void Memberfun2()
{
f1 = reinterpret_cast<Function1>(Memberfun1); // 将成员函数指针赋予普通函数指针f1,编译出错
f1();
}
};
int main()
{
Test1 t1;
t1.Memberfun2();
return 0;
}
(2)在一个“类”内,有标准库函数,如qsort, 或其他全局函数,用函数指针调用类的成员函数。如:
C/C++ code
#include <stdio.h>
#include <stdlib.h>
class Test2
{
public:
int __cdecl Compare(const void* elem1, const void* elem2) // 成员函数
{
printf("%s \n", "Calling Test2::Memberfun OK");
return *((int*)elem1) - *((int*)elem2);
}
void Memberfun()
{
data[0] = 2;
data[1] = 5;
qsort(data, 2, sizeof(int), Compare); // 标准库函数调用成员函数,编译出错
}
private:
int data[2];
};
int main()
{
Test2 t2;
t2.Memberfun(); //调用成员函数。
return 0;
}
(3)同一个“类”内,一个成员函数调用另一个成员函数, 如:
C/C++ code
#include <stdio.h>
#include "stdlib.h"
class Test3
{
public:
void Memberfun1(void(*f2)())
{
f2(); // 成员函数1调用成员函数2
}
//成员函数
void Memberfun2()
{
printf("%s \n", "Calling Test3::Memberfun2 OK");
}
void Memberfun3()
{
Memberfun1(Memberfun2); // 编译出错
}
};
int main()
{
Test3 t3;
t3.Memberfun3(); //调用成员函数。
return 0;
}
以上三种情况的代码语法上没有显著的错误,在一些较早的编译环境中,如,VC++ 4.0,通常可以编译通过,或至多给出问题提醒(Warning)。后来的编译工具,如,VC++6.0和其他一些常用的C++编译软件,不能通过以上代码的编译,并指出错误如下(以第三种情况用VC++ 10.0编译为例):
编译错误信息
error C3867: 'Test3::Memberfun2': function call missing argument list; use '&Test3::Memberfun2' to create a pointer to member
即:Memberfun1参数中所调用的函数类型不对。
按照以上提示,仅通过改变函数的类型无法消除错误,但是,如果单将这几个函数从类的定义中拿出来,不作任何改变就可以消除错误通过编译,仍以第三种情况为例,以下代码可通过编译:
C/C++ code
#include <stdio.h>
#include <stdlib.h>
typedef void (*Function1)(); //定义一个函数指针类型。
Function1 f1;
// 被调用的成员函数
void Memberfun1()
{
printf("%s \n", "Calling Memberfun1 OK");
}
class Test1
{
public:
void Memberfun2()
{
f1 = reinterpret_cast<Function1>(Memberfun1); // 将成员函数指针赋予普通函数指针f1,编译出错
f1();
}
};
int main()
{
Test1 t1;
t1.Memberfun2();
return 0;
}
第1、 2种情况和第3种情况完全相同。
由此可以的得出结论,以上三种情况编译不能通过的原因表面上并不在于函数类型调用不对,而是与 “类”有关。没通过编译的情况是用函数指针调用了 “类”的成员函数,通过编译的是用函数指针调用了非成员函数,而函数的类型完全相同。那么, “类”的成员函数指针和非成员函数指针有什么不同吗?
在下面的程序中,用sizeof()函数可以查看各种“类”的成员函数指针和非成员函数指针的长度(size)并输出到屏幕上。
C/C++ code
#include <iostream>
#include <typeinfo.h>
class Test;
// 一个未定义的类。
class Test2 // 一个空类。
{
};
class Test3 // 一个有定义的类。
{
public:
void (*memberfun)();
void Memberfun1(void(*f2)())
{
f2(); //成员函数1调用成员函数2
}
void Memberfun2(); //成员函数2。
};
class Test4 : virtual Test3, Test2 // 一个有virtual继承的类(derivative class)
{
public:
void Memberfun1(void(*f2)())
{
f2();
}
};
class Test5 : Test3, Test2 // 一个继承类(derivative class)
{
public:
void Memberfun1(void(*f2)())
{
f2();
}
};
int main()
{
std::cout << "一般函数指针长度= " << sizeof(void(*)()) << std::endl;
std::cout << std::endl << "类的成员函数指针长度:" << std::endl << std::endl;
std::cout << "Test3类成员函数指针长度=" << sizeof(void(Test3::*)()) << std::endl;
std::cout << "Test5类成员函数指针长度=" << sizeof(void(Test5::*)()) << std::endl;
std::cout << "Test4类成员函数指针长度=" << sizeof(void(Test4::*)()) << std::endl;
std::cout << "Test类成员函数指针长度=" << sizeof(void(Test::*)()) << std::endl;
return 0;
}
输出结果为(VC++10.0编译,运行于Win7操作系统,其他操作系统可能有所不同):
输出结果
一般函数指针长度= 4
类的成员函数指针长度:
Test3类成员函数指针长度=4
Test5类成员函数指针长度=8
Test4类成员函数指针长度=12
Test类成员函数指针长度=16
以上结果表明,在32位win7操作系统中,一般函数指针的长度为4个字节(32位),而类的成员函数指针的长度随类的定义与否、类的继承种类和关系而变,从无继承关系类(Test3)的4字节(32位)到有虚继承关系类(Virtual Inheritance)(Test4)的12字节(96位),仅有说明(declaration)没有定义的类(Test)因为与其有关的一些信息不明确成员函数指针最长为16字节(128位)。显然, 与一般函数指针不同,指向“类”的成员函数的指针不仅包含成员函数地址的信息,而且包含与类的属性有关的信息,因此,一般函数指针和类的成员函数指针是根本不同的两种类型,当然,也就不能用一般函数指针直接调用类的成员函数,这就是为什么本文开始提到的三种情况编译出错的原因。尽管使用较早版本的编译软件编译仍然可以通过,但这会给程序留下严重的隐患。
至于为什么同样是指向类的成员函数的指针,其长度竟然不同,从32位到128位,差别很大,由于没有看到微软官方的资料只能推测VC++10.0在编译时对类的成员函数指针进行了优化,以尽量缩短指针长度,毕竟使用128位或96位指针在32位操作系统上对程序性能会有影响。但是,无论如何优化,类的成员函数指针包含一定量的对象(Objects)信息是确定的。其他的操作系统和编译软件是否进行了类似的处理,读者可以用以上程序自己验证。
大致原理:
对于Mircosoft来说,成员函数指针实际上分两种:
一种需要调节this指针,一种不需要调节this指针。
先分清楚那些情况下成员函数指针需要调整this指针,那些情况下不需要。
可以总结如下:
如果一个类对象obj含有一些子对象subobj,这些子对象的首地址&subobj和对象自己的首地址&obj不等的话,就有可能需要调整this指针。因为我们有可能把subobj的函数当成obj自己的函数来使用。
根据这个原则,可以知道下列情况不需要调整this指针:
1.继承树最顶层的类。
2.单继承,若所有类都不含有虚拟函数。
3.单继承,若最顶层的类含有虚函数。
下列情况可能进行this指针调整:
1.多继承的类。
2.单继承,最顶的类不含有虚函数,但继承类含虚函数。
Microsoft把这两种情况分得很清楚。所以成员函数的内部表示大致分下面两种:
struct pmf_type1{
void* vcall_addr; // 成员函数的地址
};
struct pmf_type2{
void* vcall_addr; // 编译器生成的函数的地址
int delta; // 调整this指针用
};
这两种表示导致成员函数指针的大小可能不一样,pmf_type1大小为4,pmf_type2大小为8。
上面两个结构中出现的vcall_addr是一个指针,这个指针隐藏了它所指的函数是虚拟成员函数还是普通成员函数。
若它所指的是一个普通成员函数,那么包含的地址也就是这个成员函数的函数地址。
若它所指的是一个虚拟成员函数,那么包含的地址就是指向一小段编译器生的代码,这段代码会根据this指针和虚函数表索引号寻找出真正的虚拟成员函数地址,然后跳转(注意是跳转jmp,而不是函数调用call)到真实的函数地址处执行。
Microsoft的这种实现需要对一个类的每个用到了的虚函数,都分别产生这样的一段代码。
这一小段编译器生的代码就像一个template函数:
template <int index>
void vcall(void* this){
jmp this->vptr[index]; // 此处为伪代码
}
虚拟函数表的每个不同的索引号都要产生一个实例。
Microsoft就是采用这样的方式实现了虚成员函数指针的调用。
但GCC对于成员函数指针的实现和Microsoft的方式有很大的不同。
GCC对于成员函数指针统一使用类似下面的结构进行表示:
struct{
void* __pfn; // 成员函数地址,或者是虚拟函数表的索引号
long __delta; // 用来进行this指针调整
};
先来看看GCC是如何区分普通成员函数和虚拟成员函数的。
不管是普通成员函数,还是虚拟成员函数,信息都记录在__pfn里面。
这里有个小小的技巧,我们知道一般来说因为对齐的关系,函数地址都至少是4字节对齐的。这就意味这一个函数的地址,最低位两个bit总是0。(就算没有这个对齐限制,编译器也可以这样实现。) GCC充分利用了这两个bit。如果是普通的函数,__pfn记录该函数的真实地址,最低位两个bit就是全0,如果是虚拟成员函数,最后两个bit不是0,剩下的30bit就是虚拟成员函数在函数表中的索引号。
使用的时候,GCC先取出最低位两个bit看看是不是0,若是0就拿这个地址直接进行函数调用。若不是0,就取出前面30位包含的虚拟函数索引,通过计算得到真正的函数地址,再进行函数调用。
GCC和Microsoft对这个问题最大的不同就是GCC总是动态计算出函数地址,而且每次调用都要判断是否为虚拟函数,开销自然要比Microsoft的实现要大一些。这也差不多可以算成一种时间换空间的做法。
在this指针调整方面,GCC和Mircrosoft的做法是一样的。不过GCC在任何情况下都会带上__delta这个变量,如果不需要调整,__delta=0。
这样GCC的实现比起Microsoft来说要稍简单一些。在所有场合其实现方式都是一样的。而且这样的实现也带来多一些灵活性。而且这样的实现也带来多一些灵活性。这一点下面“语言限制与陷阱”中详细说明。
虚函数指针sizeof不为sizeof(void*)的更多相关文章
- C++ 类的多态三(多态的原理--虚函数指针--子类虚函数指针初始化)
//多态的原理--虚函数指针--子类虚函数指针初始化 #include<iostream> using namespace std; /* 多态的实现原理(有自己猜想部分) 基础知识: 类 ...
- 由剑指offer引发的思考——对象中虚函数指针的大小
先看一个简单的问题: 一.定义一个空的类型,对于其对象我们sizeof其大小,是1字节.因为我们定义一个类型,编译器必须为其分配空间,具体分配多少是编译器决定,vs是1字节,分配在栈区. 那,这一个字 ...
- sizeof运算符、虚函数、虚继承考点(待修改)
参考: http://blog.csdn.net/wangyangkobe/article/details/5951248 下面的文章解释有错误,不要看.......... 记住几句话: 编译器为每个 ...
- 虚函数列表: 取出方法 // 虚函数工作原理和(虚)继承类的内存占用大小计算 32位机器上 sizeof(void *) // 4byte
#include <iostream> using namespace std; class A { public: A(){} virtual void geta(){ cout < ...
- 含有虚函数的类sizeof大小
#include <iostream> using namespace std; class Base1{ virtual void fun1(){} virtual void fun11 ...
- 从零开始学C++之虚函数与多态(一):虚函数表指针、虚析构函数、object slicing与虚函数
一.多态 多态性是面向对象程序设计的重要特征之一. 多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态的实现: 函数重载 运算符重载 模板 虚函数 (1).静态绑定与动态绑 ...
- 2014 0416 word清楚项目黑点 输入矩阵 普通继承和虚继承 函数指针实现多态 强弱类型语言
1.word 如何清除项目黑点 选中文字区域,选择开始->样式->全部清除 2.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据 ...
- c++ 虚函数多态、纯虚函数、虚函数表指针、虚基类表指针详解
静态多态.动态多态 静态多态:程序在编译阶段就可以确定调用哪个函数.这种情况叫做静态多态.比如重载,编译器根据传递给函数的参数和函数名决定具体要使用哪一个函数.动态多态:在运行期间才可以确定最终调用的 ...
- C++中的虚函数(表)实现机制以及用C语言对其进行的模拟实现
tfref 前言 C++对象的内存布局 只有数据成员的对象 没有虚函数的对象 拥有仅一个虚函数的对象 拥有多个虚函数的对象 单继承且本身不存在虚函数的继承类的内存布局 本身不存在虚函数(不严谨)但存在 ...
随机推荐
- BZOJ2957: 楼房重建(分块)
题意 题目链接 Sol 自己YY出了一个\(n \sqrt{n} \log n\)的辣鸡做法没想到还能过.. 可以直接对序列分块,我们记第\(i\)个位置的值为\(a[i] = \frac{H_i}{ ...
- DOM的查找,新增,删除操作
查找 1. document.getElementById() 通过ID获取元素,由于ID唯一,所以获取的是一个元素 2. document.getElementsByTagName() 通过标签名 ...
- C# Newtonsoft.Json反序列化为dynamic对象之后的使用
通过Newtonsoft.Json将一个json类型的字符串反序列化为dynamic后直接使用报错 源代码: namespace ConsoleApplication1 { class Program ...
- 2018-10-27 22:44:33 c language
2018-10-27 22:44:33 c language 标准的C语言并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字.并不是所有的编译器都支持二进制数字,只有一部分编译 ...
- 【疑难杂症04】EOFException异常详解
最近线上的系统被检测出有错误日志,领导让我检查下问题,我就顺便了解了下这个异常. 了解一个类,当然是先去看他的API,EOFException的API如下: 通过这个API,我们可以得出以下信息: 这 ...
- React Native中的约束规范
参考资料:https://github.com/sunyardTime/React-Native-CodeStyle 感谢情书哥无私奉献 ##一.编程规约 ###(一) 命名规约 [强制] 代码中命名 ...
- 【Python】插入sqlite数据库
import sqlite3 from datetime import datetime conn = sqlite3.connect('data.db') print("Opened da ...
- spring定时任务表达式
@Scheduled 注解 cron表达式 一个cron表达式有至少6个(也可能7个)有空格分隔的时间元素. 按顺序依次为 秒(0~59) 分钟(0~59) 小时(0~23) 天(月)(0~31,但是 ...
- jquery实现显示textarea输入字符数
起初会想到使用keyup.keydown.keypress或者是onchange事件,onchange需要失去焦点才触发, 其它三个有些对按住键盘某个键不放不生效,有些对使用中文输入法正在输入时统计不 ...
- Fabric密码保存
参考:https://segmentfault.com/a/1190000000497630 多个IP分别使用不同的账号.密码 from fabric.api import * env.hosts = ...