C语言指针的理解六:函数指针与typedef
1.函数指针
1.1 函数指针的实质
函数指针的实质还是指针,还是指针变量。在64位系统中,所有的指针都是8字节,32位系统中都是4字节。函数指针、数组指针、普通指针之间并没有本质区别,区别在于指针指向的东西的类型不同。
函数的实质是一段代码,这一段代码在内存中是连续分布的,一个函数的大括号括起来的所有语句将来编译出来生成的可执行程序在内存中是连续的,所以对于函数来说很关键的就是函数中的第一句代码的地址,这个地址就是所谓的函数地址,在C
语言中用函数名这个符号来表示。
例如,可以通过printf()
打印函数名:
#include<stdio.h>
void func(void)
{
printf("in func.\n");
}
int main(int argc,char**argv)
{
printf("%p.\n",func);
return 0;
}
输出:
0x400566.
结合函数的实质,函数指针其实就是一个普通变量,这个普通变量的类型是函数指针变量类型,它里面存储的值就是某个函数的地址,也就是它的函数名这个符号在编译器中对应的值。
1.2 函数指针的语法和分析方法
C
语言本身是强类型语言,每一个变量都有自己的变量类型,编译器可以帮我们做严格的类型检查。所有的指针变量类型其实本质都是一样的,但是为什么在C
语言中要去区分它们,写法不一样呢,比如int
类型指针就写作int *p
,数组指针就写作int (*p)[5]
,函数指针就得写得更复杂,主要就是给编译器提供信息做类型检查,同时在指针解引用时匹配正确的解析方式。
函数指针的语法是:
原函数返回值类型 (*函数指针名) (原函数参数类型列表)
例如有个函数是:
void func(void);
则对应的函数指针:
void (*p)(void);
该指针的类型是:void (*)(void)
。
举个栗子:
#include<stdio.h>
void func(void)
{
printf("in func.\n");
}
int add(int a,int b)
{
return a+b;
}
int main(int argc,char**argv)
{
void (*pFunc)(void);
pFunc=func; //等价于pFunc=&func;
pFunc();
int (*pAdd)(int,int);
pAdd=add;
printf("%d.\n",pAdd(10,20));
return 0;
}
运行结果:
in func.
30.
函数名和数组名最大的区别就是:函数名做右值时加不加&
效果和意义都是一样的,但是数组名做右值时加不加&
意义就不一样。
一个复杂的函数指针的实例:string.h
中的strcpy
函数:
char *strcpy(char *dest, const char *src);
对应的函数指针是:
char *(*pStrCopy)(char*, const char*);
pStrCopy=strcpy;
1.3 总结
函数指针的分析方法也是源于优先级与逐层剥离。
2.函数指针的应用
2.1 用函数指针调用执行函数
最简单的函数指针来调用函数的示例,在上面一部分中已经写过了。本部分用函数指针指向不同的函数来实现同一个调用执行不同的结果。如果学过C++
或者Java
等面向对象的语言,就会知道面向对象三大特征中有一个多态,多态就是同一个执行实际结果不一样。
举个栗子,实现一个简单的运算器:
#include<stdio.h>
int add(int a,int b);
int sub(int a,int b);
int multiply(int a,int b);
int divide(int a,int b);
typedef int (*pFunc)(int,int);
int main(int argc,char**argv)
{
pFunc p1 = NULL;
int a,b;
char c;
printf("请输出操作类型: + or - or * or /.\n");
scanf("%c",&c);
switch(c)
{
case '+':
p1=add;break;
case '-':
p1=sub;break;
case '*':
p1=multiply;break;
case '/':
p1=divide;break;
default:
p1=NULL;
}
printf("请入要计算的2个值:\n");
scanf("%d %d",&a,&b);
printf("计算结果为:%d.\n",p1(a,b));
return 0;
}
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
int multiply(int a,int b)
{
return a*b;
}
int divide(int a,int b)
{
return a/b;
}
运行结果:
请输出操作类型: + or - or * or /.
+
请入要计算的2个值:
20 30
计算结果为:50.
2.2 结构体内嵌函数指针实现分层
程序为什么要分层?因为复杂程序东西太多一个人搞不定,需要更多人协同工作,于是乎就要分工。要分工先分层,分层之后各个层次由不同的人完成,然后再彼此调用组合共同工作。
本部分要完成一个计算器,但是设计了2个层次:上层是framework.c
,实现应用程序框架;下层是cal.c
,实现计算器。实际工作时cal.c
是直接完成工作的,但是cal.c
中的关键部分是调用的framework.c
中的函数来完成的。
先写framework.c
,由一个人来完成。这个人在framework.c
中需要完成计算器的业务逻辑,并且把相应的接口写在对应的头文件中发出来,将来别的层次的人用这个头文件来协同工作。
cal.h
:
#ifndef CAL_H
#define CAL_H
typedef int (*pFunc)(int, int);
// 结构体是用来做计算器的,计算器工作时需要计算原材料
struct cal_t
{
int a;
int b;
pFunc p;
};
// 函数原型声明
int calculator(const struct cal_t *p);
#endif
framewordk.c
:
#include "cal.h"
// framework.c中应该写实际业务关联的代码
// 计算器函数
int calculator(const struct cal_t *p)
{
return p->p(p->a, p->b);
}
另一个人来完成cal.c,实现具体的计算器;这个人需要framework层的工作人员提供头文件来工作(但是不需要framework.c)
cal.c
:
#include "cal.h"
#include <stdio.h>
int add(int a, int b)
{
return a + b;
}
int sub(int a, int b)
{
return a - b;
}
int multiply(int a, int b)
{
return a * b;
}
int divide(int a, int b)
{
return a / b;
}
int main(int argc,char**argv)
{
int ret = 0;
struct cal_t myCal;
myCal.a = 12;
myCal.b = 4;
myCal.p = divide;
ret = calculator(&myCal);
printf("ret = %d.\n", ret);
return 0;
}
2.3总结:
第二部分和第一部分部分实际完成的是同一个习题,但是采用了不同的程序架构。对于简单问题来说,上节的不分层反而容易理解,反而简单;本节的分层代码不好理解,看起来有点把简单问题复杂化的意思。原因在于这个问题本身确实是简单问题,而简单问题就应该用简单方法处理。明知错误还要这样做的目的是演示这种分层的写代码的思路和方法。
分层写代码的思路是:有多个层次结合来完成任务,每个层次专注各自不同的领域和任务,不同层次之间用头文件来交互。分层之后上层为下层提供服务,上层写的代码是为了在下层中被调用。
上层注重业务逻辑,与我们最终的目标相直接关联,而没有具体干活的函数。下层注重实际干活的函数,注重为上层填充变量,并且将变量传递给上层中的函数,其实就是调用上层提供的接口函数来完成任务。下层代码中其实核心是一个结构体变量,比如这里的struct cal_t
,写下层代码的逻辑其实很简单:第一步先定义结构体变量;第二步填充结构体变量;第三步调用上层写好的接口函数,把结构体变量传给它既可,在Linux
内核中大量应用这种编程思想。
3.再论typedef
3.1 typedef关键字的用法
typedef
是C
语言中一个关键字,作用是用来定义,或者叫重命名类型。C语言中的类型一共有2种:一种是编译器定义的原生类型,基础数据类型,如int
、double
之类的;另二种是用户自定义类型,不是语言自带的,如数组类型、结构体类型、函数类型等。
有时候自定义类型太长了,用起来不方便,所以用typedef
给它重命名一个短点的名字。注意:typedef
是给类型重命名,也就是说typedef
加工出来的都是类型,而不是变量。
举个栗子,用typedef
重命名int
类型:
#include<stdio.h>
typedef int mSize_t;
int main(int argc,char**argv)
{
mSize_t a =10;
printf("a = %d.\n",a);
return 0;
}
typedef
定义,或者叫重命名的是类型而不是变量,类型是一个数据模板,变量是一个实在的数据,类型是不占内存的,而变量是占内存的。在面向对象的语言中:类型就是类class
,变量就是对象。
3.2 typedef与#define宏的区别
typedef char* pChart;
#define pChard char*;
pchart p1,p2; //p1,p2都是char*类型的指针变量
pchard p3,p4; //p3是char*类型的指针变量,p4是char类型的字符变量
3.3 typedef与const
来看下面两重写法:
typedef int *PINT;
const PINT p2; //相当于是int *const p2;
typedef int *PINT;
PINT const p2; //相当于是int *const p2;
如果想得到const int *p;
这种效果,只能:
typedef const int *CPINT;
CPINT p1;
3.4 使用typedef的重要意义
-
1.简化类型的描述。
-
2.创造平台无关类型。
很多编程体系下倾向于不使用int
、double
等C
语言内建类型,因为这些类型本身和平台是相关的,比如int
在16位机器上是16位的,在32位机器上就是32位的。为了解决这个问题,很多程序使用自定义的中间类型来做缓冲,比如Linux
内核中大量使用了这种方法:内核中先定义:typedef int size_t;
然后在特定的编码需要下用size_t
来替代int
。STM32
的标准库中全部使用了自定义类型,比如typedef volatile unsigned int vu32;
。
为什么要用size_t
代替int
?
size_t
的真实类型与操作系统有关,在32位OS中被普遍定义为:
typedef unsigned int size_t;
而在64位OS中被定义为:
typedef unsigned long size_t;
以memcpy()
函数为例,其函数原型如下:
void *memcpy(void *s1, void const *s2, size_t n);
该函数的作用是将一段内存中数据复制到另一端段内存中去,其中n就是复制内存块的大小,该大小显然是一个大于等于0的数,而int
也可以表示负数,这个时候如果用unsigned int
来修饰n,则n可表示的有用的数据范围会比int
大上一倍,因为int
表示的那一半负数取值范围是用不上的。同理对于64位机器,其内存可寻址空间有大了很多,这个时候就可以用unsigned long
来扩大n的可表示数据范围。使用size_t
的函数很多,通过typedef
就可以不用一一修改函数原型里的参数类型而直接修改size_t
对应的类型就可以了,例如可以这样定义:
#if 32位操作系统
typedef unsigned int size_t;
#endif
#if 64位操作系统
typedef unsigned long size_t;
#endif