第13讲:深⼊理解指针-字符,数组,函数-指针变量
THE MATRIX
C语言知识点大总结
本讲主要讨论的是指针、数组、函数指针变量的相关知识。
目录
字符指针变量
数组指针变量
二维数组传参的本质
函数指针变量
函数指针数组
转移表
正文开始
字符指针变量
在指针的类型中我们知道有一种指针类型为字符指针
char*
;
一般使用:
|
还有一种使用方式如下:
|
画图理解:
代码 const char* p = "abcdef"; 特别容易让同学以为是把字符串
abcdef放到字符指针
p⾥了,但是本质是把字符串
abcdef⾸字符的地址放到了
p中。
《剑指offer》中收录了一道和字符串相关的笔试题,我们一起来学习一下:
|
输出结果:
画图理解:
这里str 3 和str 4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str 1 和str 2 不同,str 3 和str 4 相同。
数组指针变量
数组指针变量是什么?
指针数组是一种数组,数组中存放的是地址(指针)。
数组指针变量是指针变量?还是数组?
答案是:指针变量
。
类比:
字符指针 -char* -指向字符的指针 - 字符指针变量中存放字符变量的地址。
char ch = 'w';
char* p = &ch;
整型指针 -int* -指向整型的指针 - 整型指针变量中存放整型变量的地址。
int a = 10;
int* p = &a;
数组指针 -char* -指向数组的指针 - 数组指针变量中存放
数组的地址。
**&数组名**
运行结果:`// 包含标准输入输出库
// 主函数
int main()
{
// 定义并初始化大小为10的字符数组
char arr[10] = {1 , 2 , 3 , 4 , 5};
// 定义指向数组的指针p,p中存放的是数组arr的地址
char (*p)[10] = &arr; //p就是是数组指针,p中存放的是数组的地址
//int(*)[10] = int(*)[10]
//arr --int* arr+1跳过4个字节
//&arr[0] --int(*)[10] &arr[0]+1跳过4个字节
//&arr --int(*)[10] &arr+1跳过40个字节
// 返回0
return 0;
}
1 2 3 4 5 |
我们已经熟悉:
整形指针变量 :
int * pint
;存放整形变量的地址
,能够指向整形数据的指针
。浮点型指针变量 :
float * pf
;存放浮点型变量的地址
,能够指向浮点型数据的指针
。数组指针变量:
存放的应该是数组的地址,能够指向数组的指针变量
。
练习:
|
运行结果:
00000000 |
下面代码哪个是数组指针变量?
int *p1[10]; |
思考一下:p 1 ,p 2 分别是什么?
数组指针变量
int (*p)[10]; |
解释:p先和*结合,说明p是一个指针变量,然后指针指向的是一个大小为 10 个整型的数组。所以p是一个指针,指向一个数组,叫 **数组指针**
。
注意:
[]的优先级要高于*号的,所以必须加上()来保证p先和*结合
。
数组指针变量怎么初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?就是我们之前学习的&数组名。
int arr[10] = { 0 }; |
如果要存放个数组的地址,就得存放在 数组指针变量 中,如下:
1 int(*p)[ 10 ] = &arr; |
我们调试也能看到&arr和p的类型是完全一致的
。
数组指针类型解析:int (*p) [10] = &arr;
| | |
| | |
| | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
二维数组传参的本质
有了数组指针的理解,我们就能够讲一下二维数组传参的本质了。
过去我们有一个二维数组的需要传参给一个函数的时候,我们是这样写的
:
// 定义一个名为Print的函数,接受一个二维整型数组a,以及两个整型参数r(行数)和c(列数) |
运行结果:
1 2 3 4 5 |
类比
:
一维数组传参
:
数组名是首元素地址
一维数组在传参的时候,传递的是数组的地址,也就是数组的首元素的地址。
函数的参数可以写成数组,也可以写成指针形式。
二维数组传参
:
- 数组名是首元素地址
二维数组可以理解为一维数组的数组,也就是每个元素是一个一维数组。
二维数组的每一行可以看做是一个一维数组,所以二维数组其实是一个一维数组,二维数组的首元素就是它的第一行。
所以二维数组的数组名表示的就是第一行的地址,是一维数组的地址。
如下图:
根据上面的例子,第一行的一维数组的类型就是int [5],所以第一行的地址的类型就是数组指针类型int(*)[5]。那就意味着
二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址 ,那么形参也是可以写成指针形式的
。如下:
void Print(int(*arr)[5], int r, int c) |
运行结果
:
1 2 3 4 5 |
总结: 二维数组传参,形参的部分可以写成数组,也可以写成指针形式
。
补充解释
:
第9行代码:*(*(arr+i)+j)
函数指针变量
函数指针变量的创建
什么是函数指针变量呢?
根据前面学习整型指针,数组指针的时候,我们的类比关系,我们不难得出结论
:
函数指针变量应该是用来存放函数地址的,未来通过地址能够调用函数的。
那么函数是否有地址呢?
做个测试
:
|
|
输出结果如下
:
test: 005913 CA |
确实打印出来了地址,所以函数是有地址的,函数名就是函数的地址,当然也可以通过&函数名的方
式获得函数的地址。
如果我们要将函数的地址存放起来,就得创建函数指针变量咯,函数指针变量的写法其实和数组指针
非常类似
。如下:
// 定义一个无返回值、无参数的函数test |
函数指针类型解析
:
int (*pf3) (int x, int y) |
函数指针变量的使用
通过函数指针调用指针指向的函数。
|
输出结果
:
5 |
两段有趣的代码(了解一下
)
代码 1
(*(void (*)()) 0 )(); |
代码 2
void (*signal(int , void(*)(int)))(int); |
两段代码均出自:《C陷阱和缺陷》这本书
typedef关键字(了解一下
)
typedef是用来类型重命名的,可以将复杂的类型,简单化。
比如,你觉得unsigned int
写起来不方便,如果能写成uint
就方便多了,那么我们可以使用:
typedef unsigned int uint; |
如果是指针类型,能否重命名呢?其实也是可以的,比如,将int*
重命名为ptr_t
,这样写:
1 typedef int* ptr_t; |
但是对于数组指针和函数指针稍微有点区别:
比如我们有数组指针类型int(*)[5]
,需要重命名为parr_t
,那可以这样写:
1 typedef int(*parr_t)[5]; //新的类型名必须在*的右边 |
函数指针类型的重命名也是一样的,比如,将void(*)(int)
类型重命名为pf_t
,就可以这样写:
1 typedef void(*pfun_t)(int);//新的类型名必须在*的右边 |
那么要简化代码 2 ,可以这样写:
typedef void(*pfun_t)(int); |
函数指针数组
数组是一个存放相同类型数据的存储空间,我们已经学习了指针数组,
比如:int * arr[ 10 ];
//数组的每个元素是int*
那要把函数的地址存到一个数组中,那这个数组就叫 函数指针数组 ,那函数指针的数组如何定义呢?
int (*parr1[ 3 ])(); |
答案是:parr 1
parr1
先和[]
结合,说明parr 1 是数组,数组的内容是什么呢?
是int (*)()
类型的函数指针。
举例
:
//指针数组 |
运行结果
:
6 |
转移表
函数指针数组
的用途: 转移表
举例:计算器的一般实现
:
(1)
// 原始实现的计算器功能
// 定义加法函数,接收两个整数参数,返回它们的和
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;
}(2)
|
运行结果
:
******************************** |
特殊情况
:
(1)(2)缺点
:
- 代码冗余:代码中有很多重复的函数定义,增加了代码的复杂度。
- 效率低:每一次用户输入都需要遍历数组,效率低。
(3)
在(2)的基础上,使用函数指针数组
来实现转移表。
|
运行结果
:
******************************** |
(4)
再次优化,Calc()
函数可以接收一个函数指针作为参数,并调用该函数。
|
运行结果
:
******************************** |
总结
- 指针变量:指向内存中其他变量的变量,可以用来存放地址、函数指针、数组指针等。
- 指针数组:存放指针变量的数组,可以用来存放不同类型的指针变量。
- 函数指针数组:存放函数指针的数组,可以用来实现转移表。
- 转移表:根据用户输入的选项,调用相应的函数。
完