目录
1.前言
挑兮踏兮,终于等到了指针的完结篇。这篇是指针与函数,数组等章节的串烧。相信通过这篇博客,我们对指针的使用和理解可以更上一层楼。
2. 字符指针变量
字符指针变量,顾名思义即指针指向对象的类型为char。既然如此,它不是和整形指针变量一样吗,有什么可说的呢?按照指针变量常规的用法自然不用多费口舌。所以我们来看一看非常规的:
int main()
{
//char arr[] = "hello world";
char* str = "hello world";
printf("%c", *(str + 1));
return 0;
}
不知各位看到上面的代码是否也会和我一样产生疑惑——正常来讲,创建一个字符串不应该是和被注释了的代码一样,char类型数组的方式创建字符串吗?为啥它能直接将字符串当做地址赋给指针变量啊?
我们先将这个问题放在一边,现在着重思考指针变量str的问题:
不言而喻,str中储存的是定字符串的地址, 那么该地址到底是整个字符串还是字符串的首字符的地址呢?分类讨论呗!
①若str中储存的是字符串首字符的地址,那么上述代码的打印结果便是字符e②若str中储存的是整个字符串的地址,那么代码就越界访问了。
运行结果:
论: 字符指针变量str中储存的是字符串首字符的地址。
我们再来看一道经典的笔试题:
int main()
{
char str1[] = "hello wrold";
char str2[] = "hello wrold";
char* str3 = "hello wrold";
char* str4 = "hello wrold";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
return 0;
}
请问上面代码的运行结果是什么?
在做题之前呢,先来普及一点点知识:
程序中以“xxxx”形式出现的字符串叫做常量字符串。C/C++会把常量字符串存储到单独的⼀个内存区域, 当几个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。
根据这个结论,str3和str4中储存的地址就是相同的。至于str1和str2是使用相同的常量字符串去初始化不同的数组。在C/C++眼中它们与普通的数组没什么两样,自然会开辟出不同的内存块。所以str1和str2中储存的地址是不一样的。
3.数组指针变量
这里有个烧脑的问题——请问数组指针和指针数组有什么区别?
①指针数组,重点在数组,那它就是各个元素均为指针的一种数组。②数组指针,重点在指针,它便是一种指向对象的类型为数组的指针。
那么数组指针变量自然就是存放数组指针的一种指针变量。
既然如此,下面哪一个是数组指针变量呢?
int *p1[10];
int (*p2)[10];
请问你们是根据什么来快速判断的呢?我先来分享一下我的方法:①找出变量名,比如这里的p1,p2②看变量名先和谁结合,由于下标访问操作符“[ ]”的优先级大于解引用操作符“*”。所以在不加括号的情况下,变量名先和下标访问操作符“[ ]”结合。此时的变量类型就是指针数组。倘若在变量名和解引用操作符外加上括号,变量名就和解引用操作符“*”相结合。此时的变量类型就是数组指针。
所以以后只要看到p和解引用操作符“*”结合就说明p是一个指针变量,这里就是下面那个为数组指针。我们知道一个变量是由类型和变量名组成,那也就是说去掉变量名剩下的就是类型咯。这里p2的类型就是int(*) [10],代表指针变量p2所指向的变量的类型是一个含有十个元素的整型数组。
3.1数组指针变量的初始化
数组指针变量是用来存放数组地址的,想要初始化数组指针变量就得有数组的地址,那该如何获得数组的地址呢?那不得是&数组名。
言语总显得有些苍白,我们来写个代码调试看看:
如图,二者的类型完全相同 。
4.二维数组传参本质
有了对数组指针的理解,我们就可以来挖掘二维数组传参的本质了。和一维数组传参一样,二维数组传参我们一般习惯将形参写成数组的形式。
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;
}
我们知道一维数组传参时形参可以写成指针的形式,那么二维数组传参时形参又如何写成指针的形式呢?
我们先来理解下二维数组:二维数组可以看做各个元素为一位数组的数组,也就是二维数组的每一个元素就是一个一维数组,那么二维数组的首元素就是第一行的整个数组:
那么二维数组的首元素在一般情况下(即除了sizeof数组名和&数组名)是第一行整个数组的地址,即数组指针。而第一行数组的类型是int [5],那么arr的类型就是int(*)[5]。
结论:二维数组传参传的是数组名,而数组名就是第一行一维数组的地址,所以二维数组传参本质上传递的是第一行一维数组的地址。
那么将上面的代码形参部分写成指针的形式:
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;
}
这里有两个点需要解释下:
①第一行一维数组是含有5元素的整形数组,所以用int(*)[5]的变量p来接收②这里的*(*(p + i) + j))该如何理解呢?内层的p + i表示第i+1行数组的地址,再解引用就得到了这一行数组首元素的地址外层的*(p + i) + j表示这一行数组中第j+1个元素,再解引用得到的就是这个元素本身了。
5.函数指针变量
说完了字符指针变量,数组指针变量,那就轮到了的函数指针变量啦。与前面二者相同,函数指针变量就是一种用来存放函数地址指针变量。
5.1函数指针
这个时候你可能会好奇——函数,不是直接调用就行了吗,竟然还有地址,就算有地址又有什么用呢?
那就先来先来写个代码,看看函数是否有地址 :
void test()
{
printf("hello world\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
如图所示,函数是有地址的,不过更令人眼前一亮的是函数名本身就是函数的地址
5.2函数指针变量的创建
我们有了函数的地址,想要将他保存起来,这就要用到函数指针变量。
那么请问声明一个函数,须要哪几个要素?①返回值类型②函数名③参数个数和类型
既然是函数的指针变量那不得包含一些函数的要素:
int test(int x, int y)
{
return x + y;
}
int (*pf)(int x, int y) = test;
int (*pd)(int, int) = test;//参数名可以省略
如图,函数指针变量包含函数返回值类型,变量名,参数个数,参数名。去掉变量名就是函数指针变量的类型了。函数指针变量和数组指针变量长得类似,但一个是圆括号,一个方括号可不要弄混了。
5.3函数指针变量的使用
下面就来说说,函数指针变量到底有啥用。
5.3.1通过指针变量调用函数
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;
}
上面提到,函数名实际上就是函数的地址, 那么我们可以先用函数指针变量接收函数指针,在调用函数时就可以直接使用或者解引用后使用指针变量来代替函数名。当然这种用法比较鸡肋,给人一种多此一举的感觉。
5.3.2代码二段
现在是不是感觉函数指针挺简单的,现在我们来给它上点颜色:请看下面这两段代码,并分析其中含义:
int main()
{
//代码①
(*(void (*)())0)();
//代码②
void (*signal(int, void(*)(int)))(int);
return 0;
}
先来看代码一:
如图所示,先将常量0强制类型转化为void(*)(),此时的0就是一个函数指针,再解引用0得到的就是地址为0的函数,最后再调用该函数。
再来说代码二:
代码二乍一看也挺唬人的,但实际上也是“纸老虎”。代码二实际上就是一个参数类型为int和void(*)(int),返回值类型为void(*)(int)的函数。
5.3.3typedef关键字
我们在以后写代码时难免会遇到像上面那样“令人望而生畏”的代码,那有没有一种方法能够让“又臭又长”的函数名变得“短小精悍”呢?这就该typedef上场啦。
typedef unsigned int uint;
//将unsigned int 重命名为uint
那让我们感到不适的指针类型如何重命名呢,一般变量类型的指针变量的重命名和上面的类似。我们来说说数组指针和函数指针:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
那么请问该如何使用关键字typedef简化5.3.2中的代码2?
我们可以这样:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
怎么样是不是有种柳暗花明的感觉。
6.函数指针数组
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
我们知道一个数组是由元素类型,数组名,[ ],元素个数组成。比如int parr [3]。而函数指针数组则只需要将元素类型换成函数指针类型即可。所以答案是parr1。
7.转移表
存在即合理,那么函数指针数组的用途是什么呢?别急,且听我娓娓道来!
假设我们要运用函数实现一种可以完成加、减、乘、除运算的计算器。
那实现运算的函数部分应该是这样:
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;
}
写完之后我们发现代码十的冗长,实现的效率不高,尤其是一部分重复出现的代码——打印,输入,函数调用,输出结果。
可否用函数指针数组来减少这些重复的代码呢?当然啦:
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;
}
这里我们直接将数组下标当做变量,输入几,就访问数组中对应的元素。而元素刚好是函数指针,所以我们可以直接用它来调用函数,这样就简洁多了。
8.回调函数
说完了用使用函数指针调用函数的数组,再来说说用函数指针来调用的函数——回调函数,我们先来看它的定义。
计算器使用回调函数:
void calc(int(*pf)(int, int))
{//回调函数
int ret = 0;
int x, y;
printf("输⼊操作数:");
scanf("%d %d", &x, &y);
ret = pf(x, y);
printf("ret = %d\n", ret);
}
int main()
{
int input = 1;
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);
9.总结
我们较为详细的介绍了指针在字符,数组,函数中一些较为高阶的用法。C语言指针章节也终于告一段落了,感谢大家的支持与鼓励,下期见。