C语言指针系列文章目录
入门篇
强化篇
进阶篇
文章目录
- C语言指针系列文章目录
- 1. 字符指针变量
- 2. 数组指针变量
- 2. 1 概念
- 2. 2 数组指针变量的初始化
- 3. 二维数组传参的本质
- 4. 函数指针变量
- 4. 1 函数指针变量的创建
- 4. 2 指针变量的使用
- 4. 3 两个有趣的代码
- 4. 3. 1 代码一
- 4. 3. 1 代码二
- 4. 3. 3 typedef 关键字
- 5. 函数指针数组
- 6. 转移表
1. 字符指针变量
在指针的类型中我们知道有一种指针类型为字符指针 char* 。
一般的是使用方式:
int main()
{char ch = 'w';char* pc = &ch;*pc = 'w';return 0;
}
这里介绍另一种使用方式:
#include<stdio.h>
int main()
{const char* pstr = "hello world.";//这里是把一个字符串放到pstr指针变量里了吗?printf("%s\n", pstr);return 0;
}
代码 const char* pstr ="hello world.";
特别容易让同学以为是把字符串 hello world 放到字符指针 pstr 里了,但是本质是把字符串 hello world. 的首字符的地址放到了pstr中。
所以上面代码的意思是把一个常量字符串的首字符 h 的地址存放到指针变量 pstr 中。
《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下!
#include <stdio.h>
int main()
{char str1[] = "hello world.";char str2[] = "hello world.";const char* str3 = "hello world.";const char* str4 = "hello world.";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 一样。
因为这里str3和str4指向的是一个同一个常量字符串。C / C++ 会把常量字符串存储到单独的一个内存区域(文字常量区),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存,所以str3和str4相同。
2. 数组指针变量
2. 1 概念
之前我们学习了指针数组:
指针数组是一种数组,数组中存放的是地址(指针)
那么数组指针变量是指针变量还是数组?
答案是:指针变量。
我们已经熟悉:
整形指针变量 :int *pint;存放整形变量的地址,能够指向整形数据的指针变量。
浮点型指针变量:float *pf;存放浮点型变量的地址,能够指向浮点型数据的指针变量。
那数组指针变量应该是:存放的应该是数组的地址,能够指向数组的指针变量。
下面代码哪个是数组指针变量?
int *p1[10];
int (*p2)[10];
想一想,这两个变量分别是什么类型的?
int (*p2)[10];
是数组指针变量。
解释:p先和 * 结合,说明 p 是一个指针变量,然后指针指向的是一个大小为10个整型的数组。所以p是一个指针,指向一个数组,叫数组指针。
这里要注意:[] 的优先级要高于 * 号,所以必须加上()来保证p先和 * 结合。
p的类型是什么?
我们类比 int double 等我们熟悉的变量,可以发现:在变量声明中去掉变量名就是变量类型,所以 p 的类型就是:int (*)[10]
2. 2 数组指针变量的初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是之前的博客中提及的 &数组名。
int arr[10] = { 0 };
&arr;//得到的就是数组的地址
如果要存放单个数组的地址,就得存放在数组指针变量中,比如:
int (*p)[10] = &arr;
我们通过调试来看一看
可以发现,p 和 &arr 的类型是相同的。
总结:
int(*p)[10] = &arr;| | || | || | p指向数组的元素个数| p是数组指针变量名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;
}
实际上,这也可以解开一个疑惑:为什么写成二维数组的形式传参时,形参的行可以省略,而列不行,因为这是和以指针的形式传参保持一致的。
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
4. 函数指针变量
4. 1 函数指针变量的创建
什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们的利用的类比关系,我们不难得出结论:
函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。
那么函数是否有地址呢?
我们做个测试:
#include <stdio.h>
void test()
{printf("hehe\n");
}
int main()
{printf("test : %p\n", test);// %p 打印地址printf("&test: %p\n", &test);return 0;
}
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过 &函数名 的方式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量,函数指针变量的写法其实和数组指针非常类似。如下:
我们以一个简单的加法函数做例子:
int Add(int a, int b)
{return a + b;
}
我们将它的声明变成指针:
int (*Add)(int a,int b);
那么按照之前类比出来的规则:在变量声明中去掉变量名就是变量类型,可以得知 Add 的类型为:
int (*)(int a,int b)
除此之外,函数形参里的变量名称也是可以省略的,即:
int (*)(int ,int)
这样就知道函数指针变量的类型了。
那么就有:
#include<stdio.h>
void test()
{printf("hehe\n");
}int Add(int x, int y)
{return x + y;
}
int main()
{void (*pf1)() = &test;void (*pf2)() = test;int(*pf3)(int, int) = Add;int(*pf4)(int x, int y) = &Add;//x和y写上或者省略都是可以的return 0;
}
函数指针变量分析:
int (*pf3) (int x, int y)| | ------------ | | || | pf3指向函数的参数类型和个数的交代| 函数指针变量名pf3指向函数的返回类型
int (*) (int x, int y) //pf3函数指针变量的类型
4. 2 指针变量的使用
通过函数指针调用指针指向的函数:
#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)); //因为函数名也是地址,所以 pf 无论是否解引用return 0; //都可以进行传参
}
4. 3 两个有趣的代码
4. 3. 1 代码一
(*(void (*)())0)();
来分析一下
实际上,这个代码是用来模拟实现开机的(开机时会调用 0 地址处的函数)
4. 3. 1 代码二
void (*signal(int , void(*)(int)))(int);
所以说,上面的代码实际上是对函数 signal 的声明,至于为什么不把整个返回类型放在函数名的前面,是因为C语言的语法要求(函数名放在 * 的右边)。
signal 返回类型为 void ,两个参数分别为 int 和 函数指针变量(这个函数指针变量返回类型为 void ,参数为 int),返回类型为函数指针类型(这个函数指针变量的类型为:void(*)(int)
返回类型为 void ,参数为 int)。
两段代码均出自《C陷阱和缺陷》这本书
4. 3. 3 typedef 关键字
typedef 是用来类型重命名的,可以将复杂的类型简单化。
比如说可以将 unsigned int 这个很长的变量名称改短:
typedef unsigned int uint;
//将unsigned int 重命名为uint
这样在之后就可以用 unit 代替 unsigned int 了。
typedef 同样适用于指针类型。
typedef int* ptr_t;
但是对于数组指针和函数指针稍微有点区别:
比如:有数组指针类型 int(*)[5]
,需要重命名为 parr_t ,那可以这样写:
typedef int(*parr_t)[5]; //新的类型名必须在*的右边
函数指针类型的重命名也是一样的,比如,将 void(*)(int)
类型重命名为 pf_t,就可以这样写:
typedef void(*pfun_t)(int);//新的类型名必须在*的右边
这样,我们可以尝试简化一下第两个有趣的代码:
typedef void(*pfun_t)(int);
pfun_t signal(int, pfun_t);
这样就简单多了,更方便代码阅读了。
5. 函数指针数组
如果把函数的地址存到一个数组中,那这个数组就叫函数指针数组,那函数指针的数组如何定义呢?
应该是下面这三个中的哪一个呢?
int (*parr1[3])();
int *parr2[3]();
int (*)() parr3[3];
答案是第一个。
我们来分析一下第一个:
parr1 是数组名,首先与[3]
结合,说明它是一个数组,那么剩下的部分就是这个数组存储的变量类型(int (*)()
)。
注意:只有类型返回类型,形参完全相同的几个函数的指针才能放进一个函数指针数组中。
6. 转移表
那么函数指针数组有什么用呢?
我们来实现一个计算器:
不使用函数指针数组:
#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;
}
可以发现,这样写的话,在 main 函数中会十分得臃肿,有大量的相同的重复内容,我们可以使用函数指针数组优化这个代码:
#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;
}
那么在 main 函数中使用的函数数组就是转移表,是函数指针数组的最主要功能。
谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会尽快更新完毕指针全系列!