关于函数指针与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的大小.函数指针的意义就不多 ...
随机推荐
- 【LESS系列】一些常用的Mixins
在我们平时的开发中,对于一些使用频率很高的方法函数,我们一般都会将其归纳到一起,整理出一个核心库来. 其实这个思想,借助 LESS 也可以在 CSS 中得以实现. 下面是几个在 W3CPLUS 中偷过 ...
- 【c++】动态绑定
C++的函数调用默认不使用动态绑定.要触发动态绑定,必须满足两个条件: 只有指定为虚函数的成员函数才能进行动态绑定 必须通过基类类型的引用或指针进行函数调用 因为每个派生类对象中都拥有基类部分,所以可 ...
- list-iscroll5.2
简介 iScroll是一个高性能,资源占用少,无依赖,多平台的JavaScript滚动插件. 它可以在桌面,移动设备和智能电视平台上工作.它一直在大力优化性能和文件大小以便在新旧设备上提供最顺畅的体验 ...
- java中的interrupt(),InterruptException和wait(),sleep()
标题中的几个概念大概设计到线程同步以及线程阻塞这两个概念.线程同步,就是同一时刻,只有一个线程能执行指定的代码:另外一个线程阻塞就是当前线程暂时停在某个位置,等待某个条件成立之后再继续往下面执行. ...
- js 打印指定页面部分打印
<!DOCTYPE html><html> <head> <meta charset="UTF-8"> ...
- WebStorm 用法集合
1. 图片宽高提示.<img src="https://pic4.zhimg.com/8345475b687c83a71e0564419b0ac733_b.jpg" ...
- springboot项目作为war包运行
一.首先是pom文件中设置打成war包 < packaging>war< /packaging> 二.然后是修改依赖: <dependency> <group ...
- Spring_Spring与IoC_第一个程序
一.IoC IoC是一种概念,是一种思想,指将传统上由程序代码直接操控的对象调用权交给容器,通过容器来实现对象的装配和管理.控制反转是对对象控制权的转移,从程序代码本身反转到外部容器. 当前IoC比较 ...
- div居中方式
1. position: absolute; top:50%:left: 50%; margin-top: -高度的一半; margin-left: -宽度的一半(此方法适用于固定宽高的元素) 注: ...
- html中块级元素和行内元素
块级元素和行内元素的三个区别 1.行内元素与块级元素直观上的区别: 行内元素会在一条直线上排列,都是同一行,水平方向排列 块级元素独占一行,垂直方向排列.块级元素从新行开始结束接着一个断行 2.块级元 ...