目录
一、字符指针
二、数组指针
1.数组指针的定义
2.数组指针的初始化
3. 二维数组传参的本质
三、函数指针
1.函数指针的创建
2.函数指针的使用
3.有趣的代码(1)
4.有趣的代码(2)
四、typedef关键字
1.typedef的使用方法
2.typedef和#define的区别
五、函数指针数组
六、转移表
1.使用函数指针数组实现简易计算器
2.使用函数指针实现简易计算器
正文开始
一、字符指针
在了解了指针的类型后我们都知道有一种指针为字符指针char*。我们一般是这么使用字符指针的:
int main()
{
char ch = 'w';
char *pc = &ch;
*pc = 'w';
return 0;
}
在学习了const关键词后,还有这么一种使用方法:
int main()
{
const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
printf("%s\n", pstr);
return 0;
}
其中对于代码const char* pstr = "hello bit.";特别容易让同学以为是把字符串hello bit放到字符指针pstr里了,但是其本质是把字符串hello bit.的首字符的地址放到了pstr中。
那么简而言之上面代码的意思就是把一个常量字符串的首字符h的地址存放到指针变量pstr中。
接下来我们来看下面代码:
#include <stdio.h>int main()
{char str1[] = "hello bit.";char str2[] = "hello bit.";const char *str3 = "hello bit.";const char *str4 = "hello bit.";if(str1 ==str2)printf("str1 and str2 are same\n");elseprintf("str1 and str2 are not same\n"); if(str3 ==str4)printf("str3 and str4 are same\n");elseprintf("str3 and str4 are not same\n");return 0;
}
我们先来分析一下代码:首先我们先创建了两个字符数组str1和str2,随后又创建了两个字符串常量str3和str4。随后要分别判断两个字符数组str1和str2是否相等,两个字符串常量str3和str4是否相等。
在讲述数组的时候C语言:数组-CSDN博客我们就已经知道在创建数组的时候编译器会向内存中随机申请一块空间来存放这个数组,这也就意味着,无论是两个数组的大小相同还是内容相同,这两个数组的地址是永远不会重叠在一起的。那么在上述代码中,字符数组str1和str2就不相等。
再来分析str3和str4。我们先引入一个例子,假设我们在某次编写代码时创建了两个整型变量n1和n2,随后将两个变量都赋值为10。然后我们再判断这两个变量是否相等。显然是相等的。我们为这两个变量赋值后本质上我们可以将这两个变量视为常量了,即使是n1和n2在内存中的地址是不同的。同样的,字符串常量也是如此。字符串常量字符串常量,都是常量了,即便是它们的地址是不同的,它们在数值上也是相等的。
那么上述代码的输出结果就是:
实际上只是为了便于理解才有上述对整型变量比较的类比。这里str3和str4指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域, 当几个指针指向同一个字符串的时候,它们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
二、数组指针
1.数组指针的定义
之前我们学习了指针数组。指针数组是一种数组,在其中存放的是地址(指针)。那么数组指针变量是指针变量?还是数组?数组指针,数组指针,即数组的指针,所以数组指针存放的是指针变量。在前面我们学习指针的时候接触到了我们目前为止最为频繁的两个指针变量:整型指针变量和浮点型指针变量。
整形指针变量:int* pint;//存放的是整形变量的地址,能够指向整形数据的指针。
浮点型指针变量:float* pf;//存放浮点型变量的地址,能够指向浮点型数据的指针。
那么数组指针应该是存放数组的地址,能够指向数组的指针变量。
那么下面代码哪个是数组指针变量?
int *p1[10];
int (*p2)[10];
数组p1是int*类型,存放的是整型指针,所以p1是指针数组。那么p2就是我们在这一小节重点介绍的数组指针。
数组指针变量的写法:
int (*p)[10];
p先和*结合,说明p是一个指针变量,然后指着指向的是一个大小为10个整型的数组。所以p是一个指向一个数组的指针,叫做数组指针。这里要注意:[]的优先级是要高于*号的,所以必须加上()来保证p先和*结合。
2.数组指针的初始化
数组指针是用来存放数组地址的,那该怎么获得数组的地址呢?我们之前已经知道并理解了该如何获得一个数组的地址:C语言:指针详解(2)-CSDN博客。就是利用&数组名来获取整个数组的地址。
int arr[10] = {0};
&arr;//得到的就是数组的地址
如果要存放个数组的地址,就需要存放在数组指针中:
int(*p)[10] = &arr;
我们需要调试来观察一下数组的地址是否真正地被存放到了数组指针中:
观察到,数组指针p的值与&arr的值是相同的,这说明数组的地址的确被存放到了数组指针p当中。那么我们可以对数组指针有一个更深的认识:
有了对数组指针的理解,接下来我们来进一步剖析二维数组。
3. 二维数组传参的本质
在以前我们要让一个二维数组传参给一个函数的时候,我们通常是这样写的:
#include <stdio.h>void test(int a[3][5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", a[i][j]);{printf("\n");}
}int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
也就是类比一维数组传参那样,将二维数组的数组名、行数和列数传给函数。那么在了解了数组指针后我们是否可以通过数组指针来增加二维数组传参的手法呢?答案是可以的。
首先我们再次理解一下二维数组。二维数组可以看做是每个元素是一维数组的数组,那么⼆维数组的首元素就是第一行的第一个一维数组:
然后根据数组名是数组首元素的地址这个规则,⼆维数组的数组名表示的就是第一行数组的地址。根据上面的例子,第一行的一维数组的类型是int[5],所以第一行的地址的类型就是数组指针类型 int(*)[5]。这也就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:
#include <stdio.h>void test(int (*p)[5], int r, int c)
{int i = 0;int j = 0;for(i=0; i<r; i++){for(j=0; j<c; j++){printf("%d ", *(*(p+i)+j));}printf("\n");}
}int main()
{int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6},{3,4,5,6,7}};test(arr, 3, 5);return 0;
}
其中的printf("%d ", *(*(p+i)+j));可能会有同学不太理解。我们在利用指针打印一维数组的时候是这么写的:
#include <stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++)printf("%d ", *(p + i));return 0;
}
在这里我们知道p取的是数组arr的首元素地址,当我们建立一个循环让其加上i后再解引用就可以依次得到后续的元素,然后打印。
同样的,printf("%d ", *(*(p+i)+j));这行代码其实就是在打印二维数组的元素,也就是一维数组。其中i是行数,j是列数。我们一步步从里到外慢慢分析:
我们已经知道p是数组指针,而且在这里特指二维数组的首元素——即第一个(第一行)的一维数组。那么也就是说可以将p+i理解为指针p向后移动i个整型数组的长度即第i行一维数组。随后再对其解引用,得到的就是第i行一维数组。
在*(p+i)的基础上再加上列数j,就表示在第i行的整型数组中向后移动j个整型元素的长度,即指向第i行第j列的整型元素的指针。
然后再对*(p+i)+j进行解引用,即*(*(p+i)+j),表示取出指针*(p+i)+j所指向的整型元素的值,即第i行第j列的整型元素。
综上所述,*(*(p+i)+j)就可以理解为取出二维数组中第i行第j列的元素值。
所以,我们在进行二维数组传参的时候,既可以写成数组的形式,也可以写成指针的形式。
三、函数指针
1.函数指针的创建
什么是函数指针呢?根据前面学习的内容,我们进行类比之后不难得出结论:函数指针变量就是用来存放函数地址的。我们可以通过函数指针来调用函数。既然有函数指针的存在,那么函数就一定存在地址:
#include <stdio.h>void test()
{printf("hehe\n");
}int main()
{printf("test: %p\n", test);printf("&test: %p\n", &test);return 0;
}
结果(VS2022 x86环境下):
可以发现,我们在对test()函数进行取地址操作后的确输出了一个地址,这就说明函数确实是存在地址的。然后我们又直接打印了test()函数的函数名,输出的结果也是一个地址,而且和&test的结果是一样的,这说明函数名就是函数的地址。
如果我们要将函数的地址存放起来,那就得创建函数指针。函数指针的写法其实和数组指针非常类似:
void test()
{
printf("hehe\n");
}
void (*pf1)() = &test;
void (*pf2)()= test;
int Add(int x, int y)
{
return x+y;
}
int(*pf3)(int, int) = Add;
int(*pf3)(int x, int y) = &Add;//x和y写上或者省略都是可以的
函数指针类型解析:
2.函数指针的使用
假设我们引出一个加法函数Add(),我们在以前调用Add()函数时是这么写的:
#include <stdio.h>int Add(int x, int y)
{return x+y;
}int main()
{printf("%d\n",Add(2,3));printf("%d\n",Add(3,5));return 0;
}
在知道了函数指针后,我们或许可以通过函数指针来实现函数的调用。
首先我们肯定要先创建一个函数指针来指向Add()函数,我们为这个指针起一个名字,叫做pAdd,那么以函数指针的形式来表示的话就是:
int (*pAdd)(int x,int y) = &Add;
当然,我们知道函数名和数组名一样,其含义都是本身的地址。那么上行代码也可以写成:
int (*pAdd)(int x,int y) = Add;
当然,这里的形参变量其实也可以省略,只写出形参部分的类型:
int (*pAdd)(int x,int y) = &Add;
int (*pAdd)(int x,int y) = Add;
既然pAdd是一个指向Add()函数的指针,那么我们对其解引用应该就会得到Add()函数,那么我们就可以将以前调用函数的方法改写为解引用指针来调用函数:
printf("%d\n", (*pAdd)(2, 3));
需要注意的是,这里的(*pAdd)与我们在声明函数指针用到的(*pAdd)是不一样的。在这里的意思是对函数指针变量pAdd进行解引用而间接调用Add()函数,而在声明时(*pAdd)代表的含义是一个名为pAdd的函数指针。
无论是&函数名还是函数名都可以表示函数指针,相应的,指针pAdd的值是Add()函数的地址也就是:pAdd=&Add等价于pAdd=Add。也就是函数指针可以直接转化为函数名本身,那上述调用函数的语句也可以写成:
printf("%d\n", pAdd(2, 3));
#include <stdio.h>int Add(int x, int y)
{return x+y;
}int main()
{int(*pf3)(int, int) = Add;printf("%d\n", (*pf3)(2, 3));printf("%d\n", pf3(3, 5));return 0;
}
结果:
3.有趣的代码(1)
(*(void (*)())0)();
我们在拿到这种代码的时候大多都是比较懵的,但是如果静下心来分析,一切都会非常清晰。
这里我们由里到外逐个分析。最里层的(void (*)())这个语句块,根据我们刚才了解的函数指针的相关概念,可以知道这是一个指向某个返回类型为void的函数、没有形参、没有命名的一个函数指针。
然后再看外层的左边:(*(void (*)())0)。这部分其实可以看成两个小部分:第一个部分就是(void (*))())0这部分。既然void (*)()是一个函数指针,那么用括号括起来后就代表它是一个类型。我们在之前已经知道了强制类型转换——(类型)C语言:操作符详解-CSDN博客。那么这一部分的操作就是将0给强制转换成函数指针类型。啊这时肯定就会有人问啦,0是一个整型啊,怎么能被转换成一个指针类型呢?怎么不行呢?强制类型转换中的强制是很强制的,无论你是啥类型都能被转换成我所需要的类型。况且0在这里的真正含义其实是空指针NULL,当然是可以被转换成另一种指针类型的。然后再加上左边的解引用操作符就不难理解了,就是对这个被强转后的指针进行解引用操作。
最后再加上外层的右边:(*(void (*)())0)()。我们已经知道了,函数指针可以直接写成函数名,它们俩是等价的。而且无论函数指针是否进行解引用,其表达的含义其实就是函数名。根据这一点我们就不难理解了,左边一坨再加上右边的一个小圆括号,其实就是在调用一个函数啊!
但是呢这行代码只是用来检验大家的学习成果,并没有实际的意义,如果尝试执行它可能会发生报错:
4.有趣的代码(2)
void (*signal(int , void(*)(int)))(int);
还是一样的,遇到类似这样的代码不要慌张,我们慢慢的将其从里到外拆开来看~
首先看最内层的:signal(int , void(*)(int))。我们可以很清晰地分析出这个语句要表达的含义:它其实就是代表一个有两个形参分别是int类型和函数指针类型(形参为int类型且无返回类型)、名为signal的函数(返回类型我们暂且不说)。
理解了内层后呢外层其实就很好理解了:void (*signal(int , void(*)(int)))(int)。既然内层的本质实际上是一个函数,那我们不妨先将其简写为signal(即舍去形参部分)。那么整个语句就可以写成这样的:void (*signal)(int)。诶!我们就会发现,外层居然也是一个指向无返回类型的、形参为int类型的函数、名为signal的函数指针!那么到这里这行代码就分析完全了~
四、typedef关键字
1.typedef的使用方法
在C语言中提供了typedef关键字,由此我们可以依靠它来为某种类型取一个新的名字,进而可以将复杂的类型简单化。typedef的语法如下:
typedef 原始数据类型 新类型名称;
比如,如果你觉得unsigned int写起来不方便,想将其改写为uint,那么我们可以这样写:
typedef unsigned int uint;//将unsigned int重命名为uint
如果是指针类型,也可以对其进行重命名。比如将int*重命名为ptr_t:
typedef int* ptr_t;
但是需要注意的是对于数组指针和函数指针稍微有点区别。比如我们有一个数组指针类型int(*)[5],我们需要将其重命名为parr_t,那可以这样写:
typedef int(*parr_t)[5];//新的类型名必须在*的右边
函数指针的重命名也是一样的。比如将void(*)(int)类型重命名为pf_t,就可以这样写:
typedef void(*pf_t)(int);//新的类型名必须在*的右边
用上typedef来简化上述的有趣的代码(2):
typedef void(*pf_t)(int);
pf_t signal(int, pf_t);
当然typedef也可以用来对结构体进行重命名,这在后续讲解结构体的时候会再次强调。同时,typedef重命名对未来学习数据结构的时候也是非常重要的一个语法,可以帮助我们简化代码,让代码的可读性更强。
2.typedef和#define的区别
尽管我们目前还没有遇到过利用#define来重命名类型的情况,其中遇到的名词也可能是陌生的,我们会在后续文章中进行解释。但是这里还是需要先记住typedef和#define对类型的重命名之间的区别:
①typedef与#define不同,typedef创建的符号名只受限于类型,不能用于值
②typedef由编译器解释,不是预处理器
③在其受限范围内,typedef比#define更灵活
这里再提一下typedef和#define在语法上的区别。
如果我们事先将char*类型用typedef重命名为STRING,在后续创建变量的时候是没有任何问题的。其中name变量和sign变量都是char*类型的:
但是,如果我们将typedef替换为#define,这种做法是未定义的:
如果我们写成这样:
我们发现,编译器并没有发生任何报错,但是这种做法真的和typedef的做法一样吗?
我们可以发现,name的类型是char*,而sign的类型居然是char。由此我们可以总结出:
①对于typedef char* STRING,当利用STRING创建变量时,例如变量sign和name,其效果就等价于char* sign,* name;
②对于#define STRING char*,当利用STRING创建变量时,例如变量sign和name,其效果就等价于char* sign,name;
五、函数指针数组
数组是一个存放相同类型数据的连续的数据结构。我们学习了指针数组:
int *arr[10];//数组的每个元素是int*
如果把函数的地址存到一个数组中,那么这个数组就叫函数指针数组,其中每个元素都是指向若干个函数的指针。那函数指针数组该如何定义呢?定义函数指针数组的基本语法如下:
返回类型 (*数组名[数组大小])();
如果函数没有返回值,则返回类型为void;如果函数没有参数,则参数列表为空。例如:
int (*parr1[3])();
就是一个函数指针数组。
六、转移表
函数指针数组的定义意味着我们可以将若干个函数的地址存放在一个数组当中,因此我们调用函数的方式又多了一种——即访问数组的下标来进行调用函数。那么依此我们可以写出转移表这个对函数指针数组的应用实例。
比如我们要实现一个简易计算器,我们该如何实现呢?一个简易计算器,一般只含有加减乘除这四则运算,那我们在实现的时候就要将四个运算的函数写出来。随后就是写出一大串代码来供操作人员进行选择运算法则。这种用一般方法实现简易计算器应该是不难的,我们直接放代码:
#include <stdio.h>int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch(input){case 1:printf("输入操作数:");scanf("%d %d", &x, &y);ret = add(x, y);printf("ret = %d\n", ret);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);printf("ret = %d\n", ret);break;case 3:printf("输入操作数:");scanf("%d %d", &x, &y);ret = mul(x, y);printf("ret = %d\n", ret);break;case 4:printf("输入操作数:");scanf("%d %d", &x, &y);ret = div(x, y);printf("ret = %d\n", ret);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}}while(input);return 0;
}
但是大家有没有发现一个很明显的问题,就是在该代码中的switch-case语句中的子语句太过冗余:
就上述的4个小语句,就分别需要重复4遍,这会让代码显得非常冗余,使代码的观感和可读性变得很差。有问题就要分析,分析出来就要优化。既然这些代码变得非常冗余,我们可以将这个4行非常相似的代码放在一个函数当中,然后来进行调用使用。随后计算方面的问题就由我们刚刚了解到的函数指针数组来完成,当要完成相应的计算时,访问相应的数组元素即可完成计算。这里有两种方法进行优化,我们先讲述其中比较简单的一种。
1.使用函数指针数组实现简易计算器
第一种方法其实与传统实现简易计算器的代码大差不差,但是用到了函数指针数组进而才让这个代码显得不那么冗余。我们来分析一下,当我们创建好一个函数指针数组的时候,我们就需要把四则运算法则的相对应的函数的地址存入到这个数组当中,当然也包括0(用来退出计算器)。既然我们已经提前设计好了我们输入哪些值进行相应的运算,那我们也应该将相应的函数的标号与与之对应的数组下标对齐:
int (*p[5])(int x, int y) = { 0, add, sub, mul, div };//转移表
这就是对函数指针数组的一个应用。当我们需要进行加法运算的时候,我们就访问下标为1的元素,也就是间接地调用了add()函数;我们需要进行乘法运算的时候,我们就访问下标为3的元素,也就是间接地调用了mul()函数;当我们不再使用计算器的时候,我们就访问下标为0的元素,即退出计算器,程序就会结束运行。
根据以上分析,我们其实也不难写出以下代码:
#include <stdio.h>int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a*b;
}int div(int a, int b)
{return a / b;
}int main()
{int x, y;int input = 1;int ret = 0;int(*p[5])(int x, int y) = { 0, add, sub, mul, div };//转移表do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf( "请选择:" );scanf("%d", &input);if ((input <= 4 && input >= 1)){printf( "输入操作数:" );scanf( "%d %d", &x, &y);ret = (*p[input])(x, y);printf( "ret = %d\n", ret);}else if(input == 0){printf("退出计算器\n");}else{printf( "输入有误\n" ); }}while(input);return 0;
}
2.使用函数指针实现简易计算器
既然传统实现简易计算器的代码在switch-case语句中显得非常冗余,那我们就从根本上解决问题,我们将这些冗余的代码放在同一个函数体当中,这样只需要调用四次这个函数就可以实现计算器的功能,同时也减少了代码量。
我们不妨将这个函数命名为calc(),当我们转移到switch-case语句中应该是这样的(以add函数为例):
case 1:
calc(add);
break;
因为add是一个函数,所以calc函数的形参部分应该是函数指针,它负责打印和计算等一系列的操作,所以它的返回类型是void类型。那么calc()函数的基本结构就已经定下来了:
void calc(int (*pf)(int,int))
然后calc()函数首先要执行的就是输入输出,我们将之前冗余的部分写入,就可以将这冗余的部分变成公共的部分。当然,calc主要进行的还是计算这个操作,我们就要在其中创建三个临时变量进行计算,随后打印出所需的结果:
void calc(int (*pf)(int,int))
{
int x = 0,y = 0,z = 0;
printf("输入操作数:");
scanf("%d %d", &x, &y);
z = pf(x, y);printf("ret = %d\n", ret);
}
我们在调用所需函数的时候,只需要写calc(函数名)即可,这样calc()就会通过传址调用这一手段来找到相对应的函数,然后在z = pf(x,y)这行代码中将pf替换成对应的函数,然后再进行计算。那么通过函数指针来优化简易计算器的代码如下:
#include <stdio.h>int add(int a, int b)
{return a + b;
}int sub(int a, int b)
{return a - b;
}int mul(int a, int b)
{return a * b;
}int div(int a, int b)
{return a / b;
}void calc(int (*pf)(int,int))
{int x = 0,y = 0,z = 0;printf("输入操作数:");scanf("%d %d", &x, &y);z = pf(x, y);printf("ret = %d\n", ret);
}int main()
{int x, y;int input = 1;int ret = 0;do{printf("*************************\n");printf(" 1:add 2:sub \n");printf(" 3:mul 4:div \n");printf(" 0:exit \n");printf("*************************\n");printf("请选择:");scanf("%d", &input);switch(input){case 1:calc(add);break;case 2:calc(sub);break;case 3:calc(mul);break;case 4:calc(div);break;case 0:printf("退出程序\n");break;default:printf("选择错误\n");break;}}while(input);return 0;
}
通过函数指针这一方法实现计算器其实也涉及到了主调函数和回调函数的相关概念,相关概念我会在下一篇文章中体现。
完