对于初学者来说,指针是比较头疼的东西,但是,如果你想深入了解底层的一些东西,指针你又是避不开的。既然逃避不了,干嘛不加入呢?一起继续探索指针中更高级更好玩的技巧吧!😄
1 回顾
指针也指内存地址,指针变量是用来存放内存地址的变量,在32位的操作系统中,它的大小为4个字节,在64位的操作系统中则是8个字节,依次类推…
c语言中常见的指针变量定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
int *p;
int c; p = &c;
int **pp;
int *p_1; pp = &p_1;
int *p[];
int d; p[0] = &d;
int (*p)[4];
int a[4]; p = &a;
|
2 声明推断
从回顾中,我们简单回顾了一些常见的指针用法,下面继续看看一些有意思的声明以及理解这个声明到底是什么意思。
注意: 用于声明变量的表达式和普通的表达式在求值时所使用的规则相同。
示例一
针对这个*i_P
,可以理解成被声明为了一个整数,但它用*
表示了这个表达式,所以可以理解成这是一个指针变量,它指向的地址存储的是整数
示例二
对于表达式*f()
,优先级最高的是()
,所以声明时最先与f
结合的是()
,所以这是一个函数,既然知道了它是一个函数,那么剩下的部分就应该的是它的返回值类型了,所以可以理解成这是一个回整型指针的函数原型
示例三
1 2 3 4
| int (*f)();
int *(*f)();
|
如果能够理解示例一与二的推断方式,那么针对这个示例应该是很容易理解。
表达式(*f)()
中由于括号优先级,可以看到f
先与*
结合,所以f
是一个指针,接下来的是与第二个括号结合,说明这个指针指向的是一个函数,这个函数的返回值为整型。所以整体理解就是这是一个指向/返回整型的函数的/首地址的指针,即函数指针
表达式*(*f)()
这个与(*f)()
声明多了左边的*
,也就是相当于函数的返回类型是整型指针,故可以理解为这是一个指向/返回整型指针的函数的/首地址的指针
示例四
1 2 3 4 5 6 7 8 9
|
int * f[];
int f[]();
int f()[];
|
下面序号1、2、3分别代码中的三个声明:
- 表达式
*f[]
中优先级最高的是[]
,所以这个是一个数组,数组元素存储的是整型指针。
- 表达式
f[]()
中的f
先与[]
结合,所以它是一个数组,然后它的是元素类型是函数,这是错的!,对于一个函数来说,不同的函数长度是不一样的,然而对于数组来说,要求是一样的,所以这个声明无法成立。
f()[]
中的f
先与()
结合,所以它是一个函数,那么它的返回值就是数组,然而函数中不能够返回数组,所以这个声明也是错的。
示例五
这个声明表达式阅读起来确实有点困难!但是依然可以用上面的拆解方式进行解读。*(*f[])()
是由*
,(*f[])
,()
组成,显然(*f[])
优先级更高,所以根据这个括号的内容知道这个f
一定是一个数组,然后它应该存储的是xx类型的指针,要看是什么类型的指针,还得看外面,噢是()
啊,那就是函数,所以它存储的内容是函数的指针。那么这个函数的返回值是什么呢?噢,左边是一个*
,所以它是返回了一个整型的指针。
分析这个表达式实在是头大的话,建议反复阅读这几个示例!
注意,如果函数有参数的话,声明涉及到函数原型时最好注明参数类型,如:
1 2
| int (*f)(int,float); int *(*g[])(int,float);
|
3 函数指针
函数指针上面举例子的时候,已经被举例过了,它就是指向函数的指针变量。在c中,函数也会有存储空间的,也有对应的入口地址,函数名一般就是指向这个入口的地址。
函数指针的初始化与使用
1 2 3 4 5 6 7
| int f(int); int (*pf)(int) = &f;
int ans; ans = f(25); ans = (*pf)(25); ans = pf(25);
|
3.1 函数指针的应用场景一: 回调函数
说起回调函数,其实在很多地方能够见到这个东西,比如点击按钮时,执行什么;发起一个http
异步请求,处理响应结果时等等。
回调函数往往时这样: 把一个函数作为参数传递给其他函数,后者将会”回调“该函数。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
| #include<stdio.h> #include<stdlib.h> #include <error.h> #define DataType char
struct Node{ DataType value; struct Node * next; }; typedef struct Node* LinkNode;
typedef struct Node* LinkList;
LinkList creat_linklist(); void add_element(LinkList list,DataType value);
int search_value(LinkList list,void const* value,int (*compare)(void const * ,void const *)); int compare_self_impl(void const * op1,void const * op2); int compare_self_impl_char(void const * op1,void const * op2); int main() {
LinkList list=creat_linklist(); add_element(list,'a'); add_element(list,'b'); add_element(list,'d'); char c='a'; int result = search_value(list,&c,compare_self_impl_char);
printf("%d", result);
return 0; }
LinkList creat_linklist() { LinkList list = (LinkList) malloc(sizeof(struct Node)); list->next=NULL; return list; }
void add_element(LinkList list,DataType value) { if(!list) exit(ERROR_INVALID_PARAMETER); LinkNode new_node = (LinkNode) malloc(sizeof(struct Node)); new_node->next=list->next; new_node->value=value; list->next=new_node; }
int search_value(LinkList list,void const * value,int (*compare)(void const * ,void const *)) { int result = 0; if(!list) exit(ERROR_INVALID_PARAMETER); LinkNode p = list->next; while (p) { if(compare(&p->value,value) == 1) { result = 1; break; } p=p->next; }
return result; }
int compare_self_impl(void const * op1,void const * op2) { return *((int *)op1) == *((int *)op2) ? 1:0; }
int compare_self_impl_char(void const * op1,void const * op2) { return *((char *)op1) == *((char *)op2) ? 1:0; }
|
search_value
函数用于查找某个值是否在链表中,它的内部必然涉及两个元素进行比较。如果我们的链表只针对某个特定的元素类型,那么它的实现就比较简单,直接内部进行取值比较就行了。但是如果想要更为通用,那么这个比较就不能在函数内部实现了(当然,你直接实现也可以,但是你得在内部进行判别类型)。这时候函数指针的用处就体现出来了,只需声明一个函数指针参数,其他元素类型只需重写这个比较函数,并提供函数地址,由search_value
函数在内部调用即可。
search_value
函数的原型如下:
1
| int search_value(LinkList list,void const * value,int (*compare)(void const * ,void const *));
|
不知道你是否注意到其中的int (*compare)(void const * ,void const *)
参数类型?由于声明这个函数原型的时候,是不知道具体参与比较的元素的类型的,所以为了匹配所有元素,故将类型定为void *
。到时候声明一个符合该函数指针要求的函数时,则需在内部进行强制类型转换即可,这时肯定能知道所需要比较的参数类型了。
3.2 函数指针的应用场景二:转移表
假设,我们想要实现一个简单的加减乘除计算器,常见的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| int select_no,op1,op2,result; switch (select_no) { case ADD: { result= op1 + op2; break; }
case SUB: { result= op1 - op2; break; } case MUL: { result= op1 * op2; break; }
case DIV: { result= op1 / op2; break; } }
|
如果我们想再增加更多的功能模块的话,这个switch
语句将会更长。如果想要达到良好的程序设计,建议把具体操作与选择操作分开,即加减乘除操作的实现与选择分开,所以推荐使用函数实现,以便于后期扩展。
对于switch
,需要知道的是: 表示操作符的分支必须是整数。所以我们可以想到,如果是从0开始的连续的整数,可以用数组来映射的,这就是转移表。所谓数组,就是存储函数指针的数组。
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include<stdio.h>
double self_add(double a,double b){ return a+b;} double self_sub(double a,double b ){return a-b;} double self_mul(double a,double b ){return a*b;} double self_div(double a,double b ){return a/b;}
double (*oper_func[])(double ,double )={ &self_add, &self_sub, &self_mul, &self_div }; enum OP{ADD=0,SUB,MUL,DIV};
int main() { double a=oper_func[ADD](3,1); printf("%lf",a); return 0; }
|
这里需要主要注意的是:
既然用到了数组,就有可能发生越界行为,就可能会造成不可预料的结果,所以建议在使用需要保证在合理的范围内。