目录
- 一、字符指针
-
- [1. 指向变量](#1. 指向变量)
- [2. 指向字符串常量](#2. 指向字符串常量)
- [3. 一道与字符串相关的笔试题](#3. 一道与字符串相关的笔试题)
- 二、数组指针
-
- [1. 创建和初始化数组指针](#1. 创建和初始化数组指针)
- [2. 通过数组指针打印数组元素](#2. 通过数组指针打印数组元素)
- 三、二维数组传参的本质
- 四、函数指针
-
- [1. 创建和初始化函数指针](#1. 创建和初始化函数指针)
- [2. 使用函数指针](#2. 使用函数指针)
- [3. 两段有趣的代码](#3. 两段有趣的代码)
- [4. 简化第二段代码](#4. 简化第二段代码)
- 五、函数指针数组
-
- [1. 创建函数指针数组](#1. 创建函数指针数组)
- [2. 使用函数指针数组来模拟计算器](#2. 使用函数指针数组来模拟计算器)
一、字符指针
1. 指向变量
字符指针我们通常都是用来指向一个字符或者指向字符串和字符数组的开头。如下代码:
c
char c = 'c'; // 字符
char str1[] = "abcdef"; // 字符串
char str2[] = { 'a','b','c','d','e','f' }; // 字符数组
// 使用字符指针分别指向三个变量
char* pc1 = &c;
char* pc2 = str1;
char* pc3 = str2;
然后通过指针来打印这三个变量:
可以看到指针 pc1 和 pc2 正常输出,但是指针 pc3 后面输出了一堆垃圾值。这里就要说到 printf() 函数的特性了,当给 printf() 函数传递一个字符指针时,它会从该地址开始往后打印每个字符,直到遇到空字符才停止。 str1 是一个字符串,它的末尾有空字符;而 str2 是一个字符数组,末尾没有空字符。
2. 指向字符串常量
字符指针还可以指向字符串常量:
c
char* ps = "abcdef"; // 字符串指向字符串常量
然后我们可以使用 printf() 函数来进行打印:
根据以上种种特征,我们可以得出结论,ps 存储了字符串常量 "abcdef" 的首元素 'a' 的地址。也就是说字符串常量实际上是其首元素的地址,跟数组名是数组首元素的地址有点类似。
3. 一道与字符串相关的笔试题
c
#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");
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;
}
分析一下上述代码,创建了两个字符串 str1 和 str2,它们的内容都是 "hello bit.";创建了两个字符指针,它们都指向字符串常量 "hello bit."。如下图:
在 vs 编译器下,相同的字符串常量通常只有一个实例。所以,str1 != str2,str3 == str4。运行结果如下:
二、数组指针
我们知道 &数组名 取出的是整个数组的地址,只要是地址就可以存放在相应的指针中。所以,我们就可以创建一个指向数组的指针来存储数组的地址,也就是数组指针。
1. 创建和初始化数组指针
我们先创建一个数组:
c
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
数组指针首先是一个指针,所以首先需要跟符号 * 结合,*parr,然后去掉 *parr 后,剩下的类型就是该数组的类型,也就是 int [10],所以该数组指针如下:
c
int (*parr)[10] = &arr; // 数组指针
由于解引用操作符的优先级低于下标运算符,所以需要添加圆括号。
2. 通过数组指针打印数组元素
c
int arr[10] = { 1,2,3,4,5,6,7,8,9,0 };
int(*parr)[10] = &arr;
指针 parr 指向整个数组 arr,那么 *parr 就等价于 arr,所以 (*parr)[i] 就等价于 arr[i]。那么,就可以通过如下方式访问整个数组:
三、二维数组传参的本质
我们知道一维数组传参有两种方式,指针传参和数组传参,但本质上都是指针传参。而二维数组也可以看成一维数组,只不过它的每个元素都是一维数组。那么,二维数组传参的本质实际上应该和一维数组一致,都是传递首元素的地址,也就是指针传参。
如下代码:
c
// 二维数组打印函数
// 数组传参
void print1_2arr(int arr[][10], int row, int col)
{
int i;
for (i = 0; i < row; ++i)
{
int j;
for (j = 0; j < 10; ++j)
printf("%d ", arr[i][j]);
// 下一行
printf("\n");
}
}
// 指针传参
void print2_2arr(int(*parr)[10], int row, int col)
{
int i;
for (i = 0; i < row; ++i)
{
int j;
for (j = 0; j < 10; ++j)
printf("%d ", parr[i][j]);
// 下一行
printf("\n");
}
}
int main()
{
int arr[10][10] = { 0 };
print1_2arr(arr, 10, 10); // 数组传参
print2_2arr(arr, 10, 10); // 指针传参
}
数组 arr 代表其首元素的地址,而它的首元素是一个数组,大小为 10,每个元素的类型是 int。所以 arr 的值应该存放在类型为 int (*)[10] 的数组指针中。也就对应了上面的指针传参函数 print2_2arr(),而数组传参只是形式上和数组对应,更加容易理解和使用,其本质上还是指针传参。
所以,不管是几维数组,都可以看成一维数组,传参时传递的都是其首元素的地址。因为传递的都是数组名,而数组名除了两种特殊情况,都是代表数组首元素的地址。
运行程序进行验证:
所以,数组传参都可以写成数组传递和指针传递两种形式,但是要知道其本质都是指针传递。
四、函数指针
不管是库函数还是自定义函数,都是要存放在内存空间中的,那么我们就可以使用指针存放这些函数的地址,然后通过指针调用这些函数。
1. 创建和初始化函数指针
假设有如下的加法函数:
c
// 加法函数
int Add(int x, int y)
{
return x + y;
}
那么如何创建指向该函数的函数指针 pf ?首先,pf 需要和解引用操作符结合,表示 pf 是一个指针,然后去掉 *pf 剩下的就是该函数的类型,而函数 Add 的类型就是去掉函数名之后剩下的部分。所以,函数指针 pf 的创建如下:
c
int (*pf)(int, int) = &Add; // 函数指针 pf
而实际上,函数名就是函数的地址,也就是说 Add 和 &Add 是等价的,如下代码:
可以看到两个输出结果是同一个地址,所以函数指针初始化就可以有两种形式:
c
int (*pf1)(int, int) = &Add; // 函数指针 pf1
int (*pf2)(int, int) = Add; // 函数指针 pf2
2. 使用函数指针
从上述代码中我们可以得出结论:*pf1 等价于 Add,而 pf2 也等价于 Add。那么理论上,我们可以通过 *pf1 或者 pf2 来调用该函数。如下代码:
可以看到,三条打印语句的结果都是一样的。
3. 两段有趣的代码
(1)
c
(*(void (*)())0)();
我们通过画图板来逐步解析这条语句,首先,void (*)() 是一个函数指针,该指针指向一个函数,该函数没有参数也不返回任何值。
然后相邻外边的圆括号就对数字 0 进行强制类型转换,把 0 转换成该类型的函数指针。
然后左边的解引用操作符和外面的圆括号一起,对函数指针 0 进行解引用操作,因为函数调用操作符的优先级高于解引用操作符。
最后通过函数调用操作符调用该函数。
所以,该代码的运行是:先把数字 0 强制类型转换成函数指针,该指针指向没有参数没有返回值的函数。然后对其解引用得到该函数,然后调用该函数。
(2)
c
void (*signal(int , void(*)(int)))(int);
首先,signal 先和后面的圆括号结合,表明其是一个函数,其参数为 int 和一个指向没有参数也没有返回值的函数的函数指针。
然后去掉函数名和参数列表,得到该函数的返回类型。
该函数的返回类型也是一个指向没有参数也没有返回值的函数的函数指针。
所以,上述代码就是一个函数声明,该函数有两个参数,一个 int,一个函数指针,然后返回类型也是一个函数指针。
4. 简化第二段代码
关键字 typedef 的作用是为类型重命名,那么通过就可以使用它来为函数指针 void (*)() 重命名,从而简化代码。如下:
c
// 重命名函数指针 void (*)()
typedef void (*pf)();
现在 pf 就代表 void (*)() 函数指针类型,那么第二段带代码就可以按照如下形式编写:
c
pf signal(int, pf);
五、函数指针数组
1. 创建函数指针数组
假设需要创建一个函数指针数组 arr_pf,大小为 4,每个元素都是指向有两个 int 参数且返回值为 int 的函数指针。
首先,arr_pf 需要先跟下标运算符结合,表示 arr_pf 是一个数组,然后去掉 arr_pf[4] 之后,剩下的就是数组元素的类型,也就是 int (*)(int, int)。所以,代码如下:
c
int (*arr_pf[4])(int, int); // 函数指针数组
2. 使用函数指针数组来模拟计算器
这里只模拟两个整数进行加减乘除的简单计算器。那么就需要支持两个整数进行加减乘除的四个函数:
c
// 加法
int Add(int x, int y)
{
return x + y;
}
// 减法
int Sub(int x, int y)
{
return x - y;
}
// 乘法
int Mul(int x, int y)
{
return x * y;
}
// 除法
int Div(int x, int y)
{
return x / y;
}
然后使用一个函数指针数组来存放这四个函数:
c
int (*arr_pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
这里为什么把数组的大小设置为 5,第一个元素设置为空,是为了与下面的 switch 选择语句配合。
下面就是通过一个循环,打印菜单,让用户选择加减乘除,然后进行相应的运算,直到用户退出程序。
c
// 菜单
void menu()
{
printf("*********************************************\n");
printf("********* 1. Add 2. Sub **********\n");
printf("********* 3. Mul 4. Div **********\n");
printf("********* 0. Exit **********\n");
printf("*********************************************\n");
}
// 计算
void calc(int (*pf)(int, int))
{
int x, y;
printf("请输入需要计算的两个数:");
scanf("%d %d", &x, &y);
printf("结果为:%d\n", pf(x, y));
}
int main()
{
int (*arr_pf[5])(int, int) = { NULL, Add, Sub, Mul, Div };
int select = 0;
// 模拟计算机实现
do
{
// 菜单
menu();
// 选择
printf("请选择:");
scanf("%d", &select);
// 判断
switch (select)
{
case 1:
calc(arr_pf[1]); // 加法
break;
case 2:
calc(arr_pf[2]); // 减法
break;
case 3:
calc(arr_pf[3]); // 乘法
break;
case 4:
calc(arr_pf[4]); // 除法
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (select);
return 0;
}
上述代码额外创建了一个函数 calc(),通过传递的函数指针来调用相应的函数。这样提高了代码的可读性,也减少了代码的冗余。而在 calc() 中通过函数指针被调用的函数也叫回调函数。
回调函数是一种通过函数指针调用的函数,使得某个特定的动作在特定的时间点被执行。
代码运行测试结果如下: