进阶指针
导言
大家好,很高兴又和大家见面啦!
在结束了数组指针的学习后,我相信大家对指针与数组的内容应该有了更进一步的认识了。现在C语言的指针和数组的组合我们已经介绍的差不多了,还有一个知识点我们还没有开始介绍——函数。既然数组在内存空间中有自己的地址,并且能够被指针指向,那对于函数来说,它在创建函数栈帧的时候也是有地址的,那它的地址可不可以被指针指向呢?这就是我们今天要探讨的内容——函数指针。
十六、函数指针变量
C语言学习到现在,不知道大家有没有发现一个有趣的事情,那就是C语言的命名特别的简单粗暴:
- 对不同类型的数组命名是字符数组、整型数组、浮点型数组、指针数组……这些数组的前半部分说明了数组元素的数据类型;
- 对不同类型的指针命名是字符指针、整型指针、浮点型指针、数组指针……这些指针的前半部分就说明了指针指向的对象;
根据这个命名特点,我们不难得出函数指针变量即函数指针,它指向的对象应该是一个函数。那它指向的是函数名还是&函数名呢?
下面我们就来探讨第一个问题——函数名是什么?
16.1 函数名
【函数栈帧的创建和销毁】篇章中有提到过,我们在调用函数时,函数会先通过 ebp 和 esp 这两个指针在内存空间中创建一块空间供函数使用,这块空间我们称之为函数栈帧。
- 对于变量来说,变量所在空间的地址就是变量的地址,我们通过&变量名将变量的地址取出来后存放进指针,此时指针指向的就是变量的所在空间;
- 对于数组来说,数组的起始点的地址就是数组的地址,同时也是数组首元素所在空间的地址:
- 我们通过数组名将数组首元素所在空间的地址直接存放进指针,此时指针指向的就是数组的首元素的所在空间;
- 我们通过&数组名将数组在内存空间中的地址的起始地址取出来后放进指针中,此时指针指向的就是数组的起始点;
那对于函数来说,函数名又代表的是什么呢?下面我们就来做个测试:
从这次测试结果的报错中,我们可以得到以下信息:
- 函数名和&函数名都是地址;
- 函数名和&函数名都不能进行+- 整数;
既然函数名和&函数名都是地址,它们的地址又会有什么区别呢?下面我们继续测试:
从反汇编窗口中我们可以看到我们在调用函数的时候得到的函数地址与函数名存放的地址以及&函数名得到的地址是一致的,也就是说函数名和&函数名代表的都是函数的地址;
在进入函数后,我们从内存窗口和监视窗口可以得到以下的信息:
- 函数的地址并不是函数的栈顶;
- 通过指针-指针我们可以发现函数的地址与函数栈顶之间相距
1,095,684
个空间;
通过以上信息,我们可以得到结论:
- 函数名和&函数名代表的都是函数的地址,但是这个地址不是函数的栈顶地址;
- 函数名和&函数名不能进行+-整数;
既然函数名和&函数名都代表的是函数的地址,那既然是地址,就能存入指针中。此时指向函数的指针我们将其称之为函数指针变量,简称函数指针。
我们应该如何创建一个函数指针呢?下面我们来看一下函数指针是如何创建的;
16.2 函数指针变量的创建和初始化
我们在创建函数指针时,需要声明函数的返回类型、函数参数的类型以及函数指针变量名:
//函数指针的创建格式
return_type (*point_name)(parameter_type,……)
//return_type——函数指针指向的函数返回类型
//*point_name——函数指针变量名
//parameter_type——函数参数类型
//return_type (*)(parameter_type,……)——函数指针类型
从函数指针的创建格式中,有几点需要我们注意一下:
- 函数参数类型的数量与参数的数量要一致,参数变量名可以省略;
- 当只有返回类型、指针标志以及参数类型时,这代表的是函数指针的数据类型;
根据函数指针的创建格式,我们就可以来创建一下函数指针了:
//函数指针的创建
int main()
{
//无返回类型函数指针
void(*p1)();
//字符型函数指针
char(*p2)(char);
//指针型函数指针
int* (*p3)(int*, int);
return 0;
}
这里我们创建了三种类型的函数指针——无返回类型的函数指针、字符型的函数指针以及指针型的函数指针。从函数指针的创建信息中我们就可以获得以下信息:
- 函数指针p1为无返回类型的指针,那p1就不能进行解引用以及指针+-整数等操作;
- p1指针指向的函数是一个无返回类型的函数,函数没有参数;
- p2指针指向的函数是一个返回类型为
char
的函数,函数的参数也为char
; - p3指针指向的函数是一个返回类型为
int*
的函数,函数的参数有两个,分别为int*
和int
;
现在我们函数指针创建好了,我们应该如何对其进行初始化呢?
对于指针而言,初识化的方式有两种:明确的指向对象和空指针。函数指针的初始化也是一样,当我们有明确的对象时,我们可以直接将对象的地址赋值给指针进行初始化,没有明确的对象时,就可以将指针先置空:
//函数指针的创建和初始化
//无返回类型
void test1()
{
printf("1 + 2 = %d\n", 1 + 2);
}
int main()
{
//函数指针初始化——置空
void(*p)() = NULL;
//对指针赋值
p = test1;
//函数指针初始化——指向明确对象
void(*p1)() = test1;
void(*p2)() = &test1;
return 0;
}
- 当没有明确的指向对象时,我们先对函数指针置空,在有明确的指向对象后,可以再将指针指向这个对象;
- 当有明确的指向对象时,因为函数名和&函数名代表的都是函数的地址,所以我们初始化的方式既可以通过函数名进行初始化,也可以通过&函数名进行初始化;
16.3 函数指针的使用
当指针指向函数时,此时我们可以认为指针就代表着函数,从而对函数进行直接的调用:
对于无返回类型的指针来说,我们是不能对指针进行解引用的,所以要使用无返回类型的指针调用函数,只有这一种方式。但是对于有返回类型的指针,我们还可以通过解引用的方式来进行函数调用:
相信大家有了前面的知识储备,对这一块的内容应该能很容易的理解了。下面我们要介绍一个新的知识点——关键字typedef
——数据类型重命名;
16.4 关键字typedef
关键字typedef
的作用是给数据类型重新命名,如下所示:
我们现在通过将int重命名为N,并用N创建了一个变量a,通过输出结果我们可以看到,变量可以正常创建,并且我们也能通过sizeof
计算N所占空间的大小。
看到这里有朋友可能就会说了,你这不是多此一举吗?用得着将int重命名吗?
这里我想说的是,如果我们遇到一个比较复杂的数据类型,如前面介绍的函数指针的类型int*(*)(int*,int)
当我们要通过这个类型创建多个函数指针时,每一次都要写一大串的代码是不是就比较麻烦,这时我们就可以通过typedef
来将此类型重命名为一个很简单的名字,如下所示:
可以看到我们此时将int*(*)(int*,int)
这个类型重命名为了p_int
并通过新的类型名创建了两个函数指针,这两个函数指针都是可以正常进行函数掉用的。
- 这里要注意的是,对函数指针类型重命名时,我们需要将新的名字放在指针标志的括号内才能完成重命名;
16.5 有趣的代码
下面我们来看两个有趣的代码:
//代码1
(*(void(*)())0)();
这个代码是在干啥呢?我们能看到的就是两个解引用操作符、一个void、一个0和一堆的括号,这个代码我们应该如何理解呢?下面听我慢慢道来;
要理解这个代码我们首先要找到我们熟悉的部分,比如void
、(*)
、*()
。
这一串我们不熟悉,但是void
我们熟悉呀,它表示的是无类型。
什么东西是无类型的呢?我们看一下它的后面是什么——(*)()
这个是在干什么?
好像还是看不懂,但是如果我们将void与它们连起来看我们就得到了void(*)()
,这个有没有很熟悉的感觉?
没错这个就是一个返回类型为void
的函数指针类型,如果此时我们将这个类型重命名,我们就能得到新的表达式:
(*(void(*)())0)();
//void(*)()——函数指针类型
typedef void(*point)();
//将void(*)()这个数据类型重命名为point
(*(point)0)();
//point——函数指针类型
新得到的这个东西又是啥呢?这里我们需要补充一个知识点——数字0为整型。
我们现在在整型的前面放置了一个(数据类型)
,这是在干嘛?有朋友很快就想到了——强制类型转换。
所以这里其实是将0强制类型转换成了函数指针类型,之后我们再对函数指针类型的0进行解引用,并在后面加了一个函数调用操作符。
最终我们就能得到结论,这里实质上是将0转化为函数指针类型后对其进行函数指针的调用;
//代码2
void (*signal(int, void(*)(int)))(int);
有了前面代码的经验,现在我们继续来看这个代码。在这个代码中,好多字母啊,感觉又看不过来了,别着急,我们还是先找到熟悉的身影——void(*)(int)
,这个是一个函数指针类型,所以为了更好的观察,我们先将他重命名:
void (*signal(int, void(*)(int)))(int);
//void(*)(int)——函数指针类型
typedef void(*P)(int);
//将void(*)(int)重命名为P
void(*signal(int, P))(int);
//P——函数指针类型
现在我们继续观察,有没有什么新的发现?
如果我们将signal(int,P)
拿掉,我们会发现,这句代码的最外层还有一个void(*)(int)
。此时我们将最外面这个数据类型替换成重命名后的数据类型我们就能得到新的代码:P signal(int, P)
;
这句代码的结构为:数据类型 标识符(int, P)
,此时我们但看标识符部分的内容,一个括号加两个数据类型,这里很明显是在进行函数传参。signal
这个函数它有两个参数——一个是int
类型,一个是函数指针类型;
那它实际上是数据类型 标识符(参数类型,参数类型)
,这个格式是函数声明的格式。
所以这里其实是在进行函数声明,声明的是signal
这个函数,它的数据类型为函数指针类型,它有两个参数,参数类型分别为整型和函数指针类型。
结语
函数指针相信大家都能理解了,内容其实不多,我们对函数指针需要掌握的是:
- 函数指针的创建格式;
- 函数指针类型的重命名方式;
- 函数指针的使用——无返回类型和有明确返回类型的函数指针的使用;