C语言8之函数指针与指针函数

时间:2018.3.3 作者:Tom   工作:HWE 说明:如需转载,请注明出处。
说明:本文主要参考朱有鹏老师linux嵌入式C语言高级篇笔记,已注明转载。

1.函数指针

1.1函数指针的实质(还是指针变量)

1)函数指针的实质还是指针,它指向一个函数本身占4字节(在32位系统中,所有的指针都是4字节)。

2)函数的实质是一段代码,这一段代码在内存中是连续分布的(一个函数的大括号括起来的所有语句将来编译出来生成的可执行程序是连续的),所以对于函数来说很关键的就是函数中的第一句代码的地址,这个地址就是所谓的函数地址,在C语言中用函数名这个符号来表示。

3) 函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的东西是个什么玩意。结合函数的实质,函数指针其实就是一个普通变量,这个普通变量的类型是函数指针变量类型,它的值就是某个函数的地址(也就是它的函数名这个符号在编译器中对应的值)。

4)函数指针的定义:返回值类型 (*p)(参数列表); 提示:定义一个指向函数的指针变量时,一定要使用括号

5)函数指针的解引用:

a)非指针引用:函数名(参数列表)

b)指针引用:(函数指针)(参数列表)

从上述例子可以看出什么问题?????好乱啊,什么鬼???

    至少发现一个问题,fun的数值和&fun的数值是一样的!!!

但p和&p数值不是一样的,这个可以理解,因为p是指针变量,p与&p必然不一样!!!

解引用这五种方法都是可以的,因此我们可以说fun是一个指向自己的指针。

我们好像能确定函数名就是函数指针,由此我们发现函数名作为指针无法被赋值。对此,在网上发现两种解释

第一种:函数名与函数指针p都是函数指针。fun是一个函数指针常量,p是一个函数数指针变量。

        虽然通过常量与变量来解释函数名无法赋值可以帮助理解,但是我们发现对fun赋值时编译器给的错误提示并不是说对常量进行赋值,而是告诉我们=号两端格式不匹配。对此,第二种理解更合理。

第二种:函数名和数组名实际上都不是指针,但是,在使用时可以退化成指针,即编译器可以帮助我们实现自动的转换。

        这也可以解释为什么当我们在=号右侧使用函数名时,无论是取值还是取地址都没有问题,因为编译替我们做了相当于强制类型转换的工作,而在当函数名在=号左侧时,右侧的函数指针并没有这个功能,毕竟他们俩不是同一种结构。

科普:本质的区别:立即数是编译时常量,而像const限定符变量和函数名则是运行时的常量。

1.2函数指针的书写和分析方法

1)C语言本身是强类型语言(每一个变量都有自己的变量类型),编译器可以帮我们做严格的类型检查。

2)所有的指针变量类型其实本质都是一样的,但是为什么在C语言中要去区分它们,写法不一样呢(譬如int类型指针就写作int *p; 数组指针就写作int (*p)[5],函数指针就得写得更复杂),因为在C语言编程中处理的问题不一样,问题的复杂度也不一样,为了对问题进行分类,自然而然出现了不同的数据类型与其对应的指针表达形式!

3)假设我们有个函数是:void func(void); 那么对应的函数指针:void (*p)(void); 类型是:void (*)(void);

4)函数名和数组名最大的区别就是:函数名做右值时加不加&效果和意义都是一样的;但是数组名做右值时加不加&意义就不一样。

5)写一个复杂的函数指针的实例:譬如函数是strcpy函数(char *strcpy(char *dest, const char *src);),对应的函数指针是:char *(*pFunc)(char *dest, const char *src);

2.指针函数

1)指针函数是一个函数,它的返回值是一个指针!

2)指针函数的定义:

    int* f(int a, int b);

让指针标志 * 与int紧贴在一起,而与函数名f间隔开,这样看起来就明了些了,f是函数名,返回值类型是一个int类型的指针。

所以,指针函数就是返回一个地址给调用者,用于需要地址的情况。需要注意的是:这个返回的地址是子函数内部的指针变量的地址,为什么可以用在子函数外部呢?是因为没有用free(p),没有释放就一直存在。

3.函数指针数组

函数指针数组,是每一个元素为函数指针的数组。根据指针数组的定义去定义一个函数指针数组就非常简单了。

主要注意的是:上述函数指针定义:void (*pfun1)() = fun1,也是可以的!!

总结:函数指针的分析方法也是源于优先级与逐层剥离的基本理论。

4.typedef关键字的用法

1)typedef是C语言中一个关键字,作用是用来定义(或者叫重命名类型)

2)C语言中的类型一共有2种:一种是编译器定义的原生类型(基础数据类型,如int、double之类的);第二种是用户自定义类型,不是语言自带的是程序员自己定义的(譬如数组类型、结构体类型、函数类型•••••)。

3)我们今天讲的数组指针、指针数组、函数指针等都属于用户自定义类型。

4)有时候自定义类型太长了,用起来不方便,所以用typedef给它重命名一个短点的名字。

5)注意:typedef是给类型重命名,也就是说typedef加工出来的都是类型,而不是变量。

typedef char* (*pType)(char *, const char *);

//重命名了一个新的数据类型,名字叫Type,类型是:char* (*)(char *, const char *);

请记住:函数指针在运行时的动态调用(例如函数回调)中应用广泛。但是直接定义复杂的函数指针会由于有太多的括号而使代码的可读性下降。使用typedef可以让函数指针更直观和易维护。拒绝晦涩难懂的函数指针定义,拒绝函数定义中成堆的括号。

5.函数指针实战1

5.1用函数指针调用执行函数

1)最简单的函数指针来调用函数的示例,在上节课中已经演示过了。

2)本节演示的是用函数指针指向不同的函数来实现同一个调用执行不同的结果。

3)如果学过C++或者Java或者C#等面向对象的语言,就会知道面向对象三大特征中有一个多态。多态就是同一个执行实际结果不一样,跟我们这里看到的现象其实是一样的。

4)刚才的调试过程,可以得到很多信息:

第一:当程序出现段错误时,第一步先定位段错误。定位的方法就是在可疑处加打印信息,从而锁定导致段错误的语句,然后集中分析这句为什么会段错误。

第二:linux中命令行默认是行缓冲的,意思就是说当我们程序printf输出的时候,linux不会一个字一个字的输出我们的内容,而是将其缓冲起来放在缓冲区等一行准备完了再一次性把一行全部输出出来(为了效率)。linux判断一行有没有完的依据就是换行符'\n'(windows中换行符是\r\n, linux中是\n,iOS中是\r)。也就是说你printf再多,只要没有遇到\n(或者程序终止,或者缓冲区满)都不会输出而会不断缓冲,这时候你是看不到内容输出的。因此,在每个printf打印语句(尤其是用来做调试的printf语句)后面一定要加\n,否则可能导致误判。

第三:关于在linux命令行下用scanf写交互性代码的问题,想说以下几点:

1. 命令行下的交互程序纯粹是用来学习编程用的,几乎没有实践意义,大家别浪费时间了。

2. scanf是和系统的标准输入打交道,printf和标准输出打交道。要完全搞清楚这些东西得把标准输入标准输出搞清楚。

3. 我们用户在输入内容时结尾都会以\n结尾,但是程序中scanf的时候都不会去接收最后的\n,导致这个回车符还存留在标准输入中。下次再scanf时就会先被拿出来,这就导致你真正想拿的那个数反而没机会拿,导致错误。

14函数指针实战2

主题:结构体内嵌函数指针实现分层

1)程序为什么要分层?因为复杂程序东西太多一个人搞不定,需要更多人协同工作,于是乎就要分工。要分工先分层,分层之后各个层次由不同的人完成,然后再彼此调用组合共同工作。

2)本程序要完成一个计算器,我们设计了2个层次:上层是framework,实现应用程序框架;下层是fun_pointer_use.h.c,实现计算器。实际工作时fun_pointer_use.h.c是直接完成工作的,但是fun_pointer_use.h.c中的关键部分是调用的framework.c中的函数来完成的。

3)先写framework.c,由一 个人来完成。这个人在framework.c中需要完成计算器的业务逻辑,并且把相应的接口写在对应的头文件中发出来,将来别的层次的人用这个头文件来协同工作。

4)另一个人来完成fun_pointer_use.h.c,实现具体的计算器;这个人需要framework层的工作人员提供头文件来工作(但是不需要framework.c)

5)总结:

第一:本节和上节实际完成的是同一个习题,但是采用了不同的程序架构。

第二:对于简单问题来说,上节的不分层反而容易理解,反而简单;本节的分层代码不好理解,看起来有点把简单问题复杂化的意思。原因在于我们这个问题本身确实是简单问题,而简单问题就应该用简单方法处理。我们为什么明知错误还要这样做?目的是向大家演示这种分层的写代码的思路和方法。

第三:分层写代码的思路是:有多个层次结合来完成任务,每个层次专注各自不同的领域和任务;不同层次之间用头文件来交互。

第四:分层之后上层为下层提供服务,上层写的代码是为了在下层中被调用。

第五:上层注重业务逻辑,与我们最终的目标相直接关联,而没有具体干活的函数。

第六:下层注重实际干活的函数,注重为上层填充变量,并且将变量传递给上层中的函数(其实就是调用上层提供的接口函数)来完成任务。

第七:下层代码中其实核心是一个结构体变量(譬如本例中的struct cal),写下层代码的逻辑其实很简单:第一步先定义结构体变量;第二步填充结构体变量;第三步调用上层写好的接口函数,把结构体变量传给它既可。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值