关于函数指针与c++多态
原文 https://www.cnblogs.com/zhchngzng/p/4013031.html
虚函数是实现多态的重要元素,请看:
class A
{
public:
void a0(){cout <<"a0"<<endl;}
virtual void a1(){cout <<"a1"<<endl;}
}; class B: public A
{
public:
void a0(){cout <<"b.a0"<<endl;};
void a1(){cout <<"b.a1"<<endl;};
}; main()
{
A * a0 = new B ();
a0->a0();
a0->a1();
A a1 = B();
a1.a0();
a1.a1();
delete a;
}
输出是a0, b.a1, a0, a1。喻示了两点:其一,C++多态是通过指针来实现的,这和Java中通过类型转换(C#中称为装箱和拆箱)不同,因为执行A a1=B()时,首先调用了B de 构造函数构造出B对象,然后调用A的复制构造函数构造A,因此,最终调用的是A的复制构造函数,在调用函数时当然也调用A的函数了;其二,virtual的功能是使用多态时,子类的同名同参数的函数得以覆盖父类函数,而对于非虚函数,C++中在通过对象调用成员函数时,函数的入口在编译时就静态地确定了,而编译器是不在乎指针在赋值后会指向什么对象的。
这一切来自于C++的虚函数表机制。虚函数表是一个连续的内存空间,保存着一系列虚函数指针。在构造一个子对象时,内存空间最开始的4B保存一个虚函数表的入口地址。如上例中,A的虚函数表为<A::a1>,B继承A并重写了虚函数a1,因此B的虚函数表为<B::a1>,即在继承的时候,用B::a1的函数地址覆盖了A::a1的地址。于是有了下面的代码:
class A
{
public:
void a0(){cout <<"a0"<<endl;}
virtual void a1(){cout <<"a1"<<endl;}
virtual void a2(){cout <<"a2"<<endl;}
}; class B: public A
{
public:
void a0(){cout <<"b.a0"<<endl;};
void a1(){cout <<"b.a1"<<endl;};
void a2(){cout <<"b.a2"<<endl;}
}; type void ( * Function)(); main()
{
A * a = new B ();
Function p = (void (*)(void))*( (int *) *(int*)(a) + 0 ); //这里的(void (*)(void))是表明这个函数返回值为void,参数为空,等于 Function
p();
delete a;
}
其中:
a是对象的地址,
(int *) a是对象最开始4字节的地址
* (int *)a是对象的最开始的4字节
(int *) * (int *)a是对象最开始的四个字节,实际上是个地址,是什么地址呢,是虚函数表存放的地址
* (int *) * (int *)a是虚函数表的第一项,也就是第一个虚函数的地址,因此+0表示第一个函数,+1表示第二个函数,以此类推
下面分析一下这里为什么用int*不用void*,因为int* 不仅表示了指针存放的地址,也表示了指针所指的区域长度,所以可以解引用,下面参考一下别人分析指针:
链接:https://www.zhihu.com/question/46663525/answer/102300274
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
指针由两个信息构成:地址和长度(这就是为什么上面说“两个int组成的struct”)。
像这样,多于一个的信息复合在一起构成的类型,就属于复合数据类型。
指针由两个信息构成,所以指针是和int、char、float等有着本质区别的复合数据类型---切记。
题主所说的“直接用int来存放地址”,这是指针里的“地址”信息,然而指针还需要另外一个信息:长度信息-----如果只拿一个类似int的存放用于地址信息,那么长度信息就丢失了。
比如,int * p = &i;
这个p由两个信息决定,一个是p里存放的地址,另一个是int的size。
如果假设我们有一个叫pointer的类型(类似void *,但是这里就不用void *了,害怕影响初学者学习void *)用来只存放地址:pointer p = &i; 很显然,size信息就丢掉了,即我们只能通过p找到i的地址,却无法得知它所占据内存的长度。
那么为什么要把指针设定成复合数据类型呢?
举个例子来说明:比如,你要告诉计算机,去0x0001取出一个数据,然后计算机会问你:取到哪里?然后你就需要告诉计算机:取四个长度。
于是计算机就会把0x0001 0x0002 0x0003 0x0004的数据拿出来组成一个完整的数据给你。
如果指针变成了一个只保存地址的基本类型(就像上面虚构的pointer类型),那么你每次使用指针的时候,你都得显式地告诉计算机你要通过这个指针对多长的存储空间进行操作(可能你就不得不类似这样:pointer p = pointer(&i, 4);)。很显然这样很不方便且易出错。
万一你说,我就是要看看某个占4个字节的int类型的前两个字节里存放的数据,那么你可以通过位运算来轻松实现这一点。很显然位运算比起(&i, 2)要好得多。
最后举两个例子(第二个例子初学者可以先不看):
// 例子一
double d = 1.1;
double *p = &d;
cout << *p;
// 继续使用p
// 例子二
int i = 1;
void * q = &i;
cout << *q; // ERROR!!! void * is not a pointer-to-object type
cout << *static_cast<int *>(q); // correct
当编译器看到p被声明定义的时候(第二行),编译器会记住两个信息:p指向数据的类型(double)和p的初始值(d的地址,比如是0x0001)。此后,不论你在什么地方对p进行解引用操作(*p),编译器都可以立刻知道:去0x0001,以此为起点,开始取sizeof(double)长度的数据。
在第二个例子中,由于q只有一个地址信息而没有type信息,所以无法对其做解引用运算除非在解引用前先通过强制转换给它一个type信息。
因此,再强调一次,指针由两个信息共同决定,所以指针是复合数据类型。
再看一个例子:
1,通过函数指针访问子类的私有函数
2,通过函数指针访问父类的私有函数
class A
{
virtual void a0()
{
printf("A::a0 (private)\n");
}
public:
explicit A(){}
virtual void a1()
{
printf("A::a1 (public)\n");
}
}; class B : public A
{
public:
explicit B(){}
private:
int y;
virtual void a1()
{
printf("B::a1 (private)\n");
}
}; typedef void (* Function)(); main()
{
A * a = new B();
Function p;
p = ( void (*)(void) ) *( (int *) *(int*)(a) + 0 );
p();
p = (Function) *( (int*) *(int*)(a) + 1 );
p();
a->a1();
delete a;
}
其中A的虚函数表是<private A::a0, public A::a1>, B的虚函数表是<private A::a0, private B::a1>。
在第一次调用时p指向private A::a0,而第二次调用时p指向private B::a1。权限的检查是在编译阶段,因此动态的指针调用绕过了权限检查。
在第三次调用时(a->a1())时,由于对权限的检查是在编译阶段,而编译器不检查a到底指向什么对象(因为这是动态的),只看a的类型。编译器发现a是A的指针,而a1()在类A中是public函数,因此权限检查顺利地pass。随后开始执行,此时a->a1()的指针指向B::a1(),于是乎,我们成功地用父类指针A * a调用了子类B的私有函数。
关于函数指针与c++多态的更多相关文章
- 使用函数指针模拟C++多态
#include <iostream> using namespace std; class Base { public : void display() { cout << ...
- 使用函数指针和多态代替冗长的if-else或者switch-case
在编程中,if-else和switch-case是很常见的分支结构,很少在程序中不用这些控制语句.但是不能否认,在一些场景下,由于分支结构过分长,导致代码不美观且不容易维护,在<重构>一书 ...
- C 语言实现多态的原理:函数指针
C语言实现多态的原理:函数指针 何为函数指针?答案:C Programming Language. 能够查阅下,从原理上来讲,就是一个内存地址.跳过去运行相应的代码段. 既然如此,在运行时决定跳到哪个 ...
- C++ 类的多态三(多态的原理--虚函数指针--子类虚函数指针初始化)
//多态的原理--虚函数指针--子类虚函数指针初始化 #include<iostream> using namespace std; /* 多态的实现原理(有自己猜想部分) 基础知识: 类 ...
- 2014 0416 word清楚项目黑点 输入矩阵 普通继承和虚继承 函数指针实现多态 强弱类型语言
1.word 如何清除项目黑点 选中文字区域,选择开始->样式->全部清除 2.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据 ...
- typedef 函数指针 数组 std::function
1.整型指针 typedef int* PINT;或typedef int *PINT; 2.结构体 typedef struct { double data;}DATA, *PDATA; //D ...
- 类成员函数指针 ->*语法剖析
在cocos2d-x中,经常会出现这样的调用,如 ->*,这个是什么意思呢,如下面得这个例子: , 其实这是对类的成员函数指针的调用,在cocos2dx中,这种形式多用于回调函数的调用.如我们经 ...
- C++中怎么获取类的成员函数的函数指针?
用一个实际代码来说明. class A { public: staticvoid staticmember(){cout<<"static"<<endl;} ...
- C语言函数指针与 c#委托和事件对比
C语言: 函数指针可以节省部分代码量,写类似具有多态的函数,比如要比较最大值,如果不用函数指针就只能写比较某一类型比如int类型的max函数,这个max无法比较string的大小.函数指针的意义就不多 ...
随机推荐
- 关于Jquery事件绑定的心得
今日在工作的时候,遇上了一些和事件绑定有关的问题无法解决,于是在网上找到了以下资料,稍加整理了下. 对于事件绑定,jQuery的 bind / unbind 大多数时候可能并不会用到,取而代之的是直接 ...
- 关于 double sort 这道题的思考
声明 笔者最近意外的发现 笔者的个人网站http://tiankonguse.com/ 的很多文章被其它网站转载,但是转载时未声明文章来源或参考自 http://tiankonguse.com/ 网站 ...
- js时间字符串转时间戳
字符串形如:2016-06-20 10:41 转换为时间戳: var date = "2016-06-20 10:41"; date = date.substring(,); da ...
- jsp实现html页面静态化
一.实现原因 1.网站访问量过大,导致服务器压力加大以及数据库数据交换频繁.生成静态页面提供访问以缓解压力. 2.静态页面是动态页面的备份,若动态页面出现异常,静态页面可以暂时替代. 二.使用场合 ...
- UNION ALL 视图 'ImprotHIS2012.dbo.ImportHISData' 不可更新,因为没有找到分区依据列。 Severity 16 State 12
-- 3 更正措施,使约束check一次 Alter Table ImprotHIS_Bak_2011.dbo.ImportHISData with check Check Constraint al ...
- 第1章 初识CSS3
什么是CSS3? CSS3是CSS2的升级版本,3只是版本号,它在CSS2.1的基础上增加了很多强大的新功能. 目前主流浏览器chrome.safari.firefox.opera.甚至360都已经支 ...
- 一个对inner jion ...on 的sql多表联合查询的练习
create database practiceSql; use practiceSql; -- create table student( `id` bigint not null auto_inc ...
- Python-网络编程(一)
首先我们python基础部分已经学完了,而socket是我们基础进阶的课程,也就是说,你自己现在完全可以写一些小程序了,但是前面的学习和练习,我们写的代码都是在自己的电脑上运行的,虽然我们学过了模块引 ...
- PeekMessage&GetMessage
原文:http://www.cnblogs.com/faceang/archive/2010/05/25/1743757.html PeekMessage与GetMessage的对比相同点:PeekM ...
- Python爬虫教程-34-分布式爬虫介绍
Python爬虫教程-34-分布式爬虫介绍 分布式爬虫在实际应用中还算是多的,本篇简单介绍一下分布式爬虫 什么是分布式爬虫 分布式爬虫就是多台计算机上都安装爬虫程序,重点是联合采集.单机爬虫就是只在一 ...