在看函数指针的时候,发现自己对这种方式完全没有使用的习惯,也很少见到类似代码,很疑惑这种看起来很重要的使用方式到底重要在何处。看到所起到的“函数替换”功能,有有些像是虚函数,恰好想起对于虚函数的虚函数表和虚函数的工作机会并不是十分清楚,所以打算彻底攻克这一部分。
函数指针
什么是函数指针
函数指针是指向函数的指针变量。 因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是大体一致的。函数指针有两个用途:调用函数和做函数的参数。
函数指针的用法
用法1:调用函数
int func(int x); /* 声明一个函数 */
int (*f) (int x); /* 声明一个函数指针 */
f=func; /* 将func函数的首地址赋给指针f */
或者使用下面的方法将函数地址赋给函数指针:
f = &func;
f(1);
赋值时函数func不带括号,也不带参数,由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。赋给函数指针的函数应该和函数指针所指的函数原型是一致的。
用法2:做函数的参数(又称回调函数)
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。
#include <stdio.h>
void printWelcome(int len)
{
printf("欢迎欢迎 -- %d/n", len);
}
void printGoodbye(int len)
{
printf("送客送客 -- %d/n", len);
}
void callback(int times, void (* print)(int))
{
int i;
for (i = 0; i < times; ++i)
{
print(i);
}
printf("/n我不知道你是迎客还是送客!/n/n");
}
void main(void)
{
callback(10, printWelcome);
callback(10, printGoodbye);
printWelcome(5);
}
typedef函数指针的作用
typedef char (*PTRFUN)(int);
PTRFUN pFun;
char glFun(int a){ return;}
void main()
{
pFun = glFun;
(*pFun)(2);
}
在typedef 函数指针定义后,可以把函数指针当成一种数据类型来定义对应的函数指针,是一种方便的用法。
虚函数
回调函数的一些用法让我想起了虚函数,二者有很多相似之处。在调用之前,很可能不知道到底是需要调用哪个函数,不同层次的虚函数的参数都是一样的等等。这里探究一下回调函数和虚函数的关系,顺便仔细研究下从来没搞清楚过的虚函数表。
什么是虚函数
在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数,用法格式为:virtual 函数返回类型 函数名(参数表) {函数体};实现多态性,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。
简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
class A
{
public:
virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
public:
void print(){cout<<"ThisisB"<<endl;}
};
int main()
{
//main2
A a;
B b;
A *p1 = &a;
A *p2 = &b;
p1->print();
p2->print();
return 0;
}
虚函数表
class A{//虚函数示例代码
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
};
class B : public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};
由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表。那段数据叫做vptr指针,指向那个表。那个表叫做vtbl,每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图
类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址
值得注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。 虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。 编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
虚函数表在类中的存放示意图
内存中的开始存放虚函数表,单继承如下:
多继承如下:
下面是对于虚函数指针的取值,通过实际执行,说明虚函数在虚函数表中的存放方式:
- 虚函数表存放在类的最一开始
- 对于单继承,虚函数在虚函数表的第一个位置按照声明顺序存放
- 对于多继承,每个继承类会在虚函数表中新拓展一行
- 重写的虚函数会有一个新的函数地址,没重写的虚函数使用父类的函数地址
具体使用方式在下述代码中有详细体现。
#include<stdio.h>
#include<iostream>
using namespace std;
typedef void (*Fun)();
class A
{//虚函数示例代码
public:
virtual void fun(){cout<<1<<endl;}
virtual void fun2(){cout<<2<<endl;}
virtual void fun3(){cout<<3<<endl;}
};
class C
{
public:
virtual void fun4(){cout<<11<<endl;}
virtual void fun5(){cout<<12<<endl;}
};
class B : public A, public C
{
public:
void fun(){cout<<4<<endl;}
void fun2(){cout<<5<<endl;}
void fun4(){cout<<21<<endl;}
void fun5(){cout<<22<<endl;}
};
int main()
{
A a;
B b;
printf("class A 虚函数表地址%p\n",(int*)&a);
printf("class A fun地址%p\n",(int*)*(int*)(&a));
printf("class A fun2地址%p\n",(int*)*(int*)(&a)+1);
printf("class B 虚函数表地址%p\n",(int*)&b);
printf("class B fun地址%p\n",(int*)*(int*)(&b));
printf("class B fun2地址%p\n",(int*)*(int*)(&b)+1);
printf("class B fun4地址%p\n",(int*)*((int*)(&b)+1));
printf("class B fun5地址%p\n",(int*)*((int*)(&b)+1)+1);
Fun pFun = (Fun)(*((int*)*((int*)(&b)+1)+1));//class B fun5函数赋值给p
pFun();//输出了22
return 0;
}