关于函数指针与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的私有函数。