原文  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++多态的更多相关文章

  1. 使用函数指针模拟C++多态

    #include <iostream> using namespace std; class Base { public : void display() { cout << ...

  2. 使用函数指针和多态代替冗长的if-else或者switch-case

    在编程中,if-else和switch-case是很常见的分支结构,很少在程序中不用这些控制语句.但是不能否认,在一些场景下,由于分支结构过分长,导致代码不美观且不容易维护,在<重构>一书 ...

  3. C 语言实现多态的原理:函数指针

    C语言实现多态的原理:函数指针 何为函数指针?答案:C Programming Language. 能够查阅下,从原理上来讲,就是一个内存地址.跳过去运行相应的代码段. 既然如此,在运行时决定跳到哪个 ...

  4. C++ 类的多态三(多态的原理--虚函数指针--子类虚函数指针初始化)

    //多态的原理--虚函数指针--子类虚函数指针初始化 #include<iostream> using namespace std; /* 多态的实现原理(有自己猜想部分) 基础知识: 类 ...

  5. 2014 0416 word清楚项目黑点 输入矩阵 普通继承和虚继承 函数指针实现多态 强弱类型语言

    1.word 如何清除项目黑点 选中文字区域,选择开始->样式->全部清除 2.公式编辑器输入矩阵 先输入方括号,接着选择格式->中间对齐,然后点下面红色框里的东西,组后输入数据   ...

  6. typedef 函数指针 数组 std::function

    1.整型指针 typedef int* PINT;或typedef int *PINT; 2.结构体 typedef struct { double data;}DATA,  *PDATA;  //D ...

  7. 类成员函数指针 ->*语法剖析

    在cocos2d-x中,经常会出现这样的调用,如 ->*,这个是什么意思呢,如下面得这个例子: , 其实这是对类的成员函数指针的调用,在cocos2dx中,这种形式多用于回调函数的调用.如我们经 ...

  8. C++中怎么获取类的成员函数的函数指针?

    用一个实际代码来说明. class A { public: staticvoid staticmember(){cout<<"static"<<endl;} ...

  9. C语言函数指针与 c#委托和事件对比

    C语言: 函数指针可以节省部分代码量,写类似具有多态的函数,比如要比较最大值,如果不用函数指针就只能写比较某一类型比如int类型的max函数,这个max无法比较string的大小.函数指针的意义就不多 ...

随机推荐

  1. 【c++】输出文件的每个单词、行

    假设文件内容为 1. hello1 hello2 hello3 hello4 2. dsfjdosi 3. skfskj ksdfls 输出每个单词 代码 #include <iostream& ...

  2. .NET加密技术概述

    微软.NET 的System.Security.Cryptography中的类实现了各种具体的加密算法和技术.这些类,有一些是非托管 Microsoft CryptoAPI 的包装,而另一些则是纯粹的 ...

  3. 面向对象 OOP

    [面向对象编程OOP]   1 语言的分类 面向机器 :汇编语言 面向过程 :c语言 面向对象 :c++ Java PHP等   2 面向过程与面向对象 面向过程:专注于如何去解决一个问题的过程,编程 ...

  4. JQuery extend()与工具方法、实例方法

    使用jQuery的时候会发现,jQuery中有的函数是这样使用的: $.get(); $.post(); $.getJSON(); 有些函数是这样使用的: $('div').css(); $('ul' ...

  5. 数据结构(二) --- 伸展树(Splay Tree)

    文章图片和代码来自邓俊辉老师课件 概述 伸展树(Splay Tree),也叫分裂树,是一种二叉排序树,它能在O(log n)内完成插入.查找和删除操作.它由丹尼尔·斯立特Daniel Sleator ...

  6. java 散列运算浅分析 hash()

            文章部分代码图片和总结来自参考资料 哈希和常用的方法 散列,从中文字面意思就很好理解了,分散排列,我们知道数组地址空间连续,查找快,增删慢,而链表,查找慢,增删快,两者结合起来形成散列 ...

  7. 纯手写实现HashMap

    1.hashmap的实现 ① 初始化 1)定义一个Node<K, V>的数组来存放元素,但不立即初始化,在使用的时候再加载 2)定义数组初始大小为16 3)定义负载因子,默认为0.75, ...

  8. class path resource [logback.xml] cannot be resolved to URL because it does not exist 问题解决

    今天在自己搭建Springboot 框架的时候,在配置 logging.config=classpath:logback.xml 出现找不到这个文件的错误 经发现是maven的一个写法问题,本来我是打 ...

  9. lintcode题目记录4

    Russian Doll Envelopes    Largest Divisible Subset     Two Sum - Input array is sorted Russian Doll ...

  10. wampserevr安装redis和mongo扩展

    1.下载redis对应的扩展.dll文件(php_redis.dll)和php_igbinary.pdb文件以及php_mongo.dll文件(对应版本)慢慢试 2.把找好的对应好的版本放到 D:\p ...