目录
- 1.字符指针变量
- 2.数组指针变量
-
- [2.1 数组指针变量是什么?](#2.1 数组指针变量是什么?)
- [2.2 数组指针如何初始化?](#2.2 数组指针如何初始化?)
- 3.二维数组传参的本质
- 4.函数指针变量
-
- [4.1 函数指针变量的创建](#4.1 函数指针变量的创建)
- [4.2 函数指针变量的使用](#4.2 函数指针变量的使用)
- [4.3 两段有趣的代码](#4.3 两段有趣的代码)
-
- [4.3.1 typedef 关键字](#4.3.1 typedef 关键字)
- 5.函数指针数组
- 6.转移表
1.字符指针变量
指针类型为char*的指针叫做字符指针变量。
一般我们这样使用:
c
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'h';
return 0;
}
还有一种使用方式如下:
c
int main()
{
//char str[] = "abcdef";
//const 的作用是因为是常量字符串,不能随意修改内容
const char* ps = "abcdef"; // "abcdef" 是常量字符串,这里的赋值是把首元素a的地址赋值给ps
printf("%s\n", ps); //注意,用字符%s来打印字符串时,不用加上*
printf("%c\n", *ps);
return 0;
}

《剑指offer》中收录了⼀道和字符串相关的笔试题,通过这一段代码让我们更好认识字符指针数组:
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;
}
这段代码打印出的结果是:

我们一起来分析这段代码:

str3和str4指向的是同一块常量字符串,C/C++会把常量字符串存储到单独的⼀个内存区域,当几个指针指向同一块字符串时,他们实际会指向同⼀块内存,但是⽤相同的常量字符串去初始化不同的数组 的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。
2.数组指针变量
2.1 数组指针变量是什么?
我们又可以来类比一下:
- 整型指针变量
int* pa:指向整型变量的指针,存放的是整型变量的地址 - 字符指针变量
char* pc:指向的是字符类型变量的指针,存放的是字符类型变量的地址
得出结论:数组指针变量就是指向数组类型变量的指针,存放的是数组的地址。
数组指针变量应该怎么写:
c
int (*p)[10];
我们来拆分一下:
- p先和*结合在一起,说明p是指针变量。
- 去掉
*p,剩下int [10],说明指针指向的是一个包含 10 个 int 元素的数组 - p是一个指针,指向的是一个数组,所以叫做数组指针。
2.2 数组指针如何初始化?
数组指针存放的是整个数组的地址,而不是数组首元素的地址。
c
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int (*p)[10] = &arr; //存放的是整个数组的地址
return 0;
}
注意:数组指针变量的类型,就是把变量名去掉后剩下的部分。比如 int (*p)[10] 中,变量名是 p,去掉 p 后,类型就是 int (*)[10],表示"指向含有 10 个 int 元素数组的指针"。

数组指针类型的解析:

3.二维数组传参的本质
在之前我们的二维数组需要传参给一个函数时,我们是这样写的:
c
void print(int arr[3][5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", arr[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} };
print(arr, 3, 5);
return 0;
}
二维数组可以理解为:由多个一维数组组成的数组 。例如 arr[0] 表示二维数组中的第一个一维数组,也就是第一行。
例如:
c
int arr[3][5];

根据数组名是首元素的地址这个特点,我们可以得出二维数组中数组名其实就是首元素的地址,也就是第一行的地址,是一维数组的地址,由此可知,上面arr数组中第一行一维数组的类型就是int [5],所以第一行数组指针的类型就是int (*)[5].
结论:二维数组传参本质上传递的是地址,传递的是第一行这个一维数组的地址,形参也可以写成指针的形式,如下:
c
// 一维数组中数组指针变量p的类型
void print(int (*p)[5],int r, int c)
{
for (int i = 0; i < r;i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", *(p[i] + j));
// p[i][j]
// *(*(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} };
// arr传参时表示第一行 arr[0] 的地址,类型是 int (*)[5]
print(arr, 3, 5);
return 0;
}
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针的形式。
4.函数指针变量
4.1 函数指针变量的创建
根据前面的学习,我们进行一下类比不难得出结论:函数指针变量是用来存放函数的地址的,指向的是函数的指针变量。
函数的地址是什么,看下面代码:
c
void test()
{
printf("hehe\n");
}
int main()
{
printf("%p\n", test);
printf("%p\n", &test);
return 0;
}
输出结果:

从上述代码我们可以得出,函数名就是函数的地址,我们也可以通过&函数名的方式来获得函数的地址。那我们就需要创建函数指针变量来存放函数的地址,函数指针变量的写法跟数组指针非常相似。如下:
c
void test()
{
printf("hello world\n");
}
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p1)(int, int) = Add;
int (*p2)(int x, int y) = &Add; //x 和 y 写上或者省略都可以
void (*p3)() = test;
//void (*)() -> 函数指针变量p3的类型
//int (*)(int, int) -> 函数指针变量p1,p2的类型
return 0;
}
函数指针类型解析:

4.2 函数指针变量的使用
通过函数指针调用所指向的函数
c
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*p1)(int, int) = Add;
int r = (*p1)(4, 5);
int l = p1(4, 5);
printf("%d\n", r);
printf("%d\n", l);
return 0;
}
输出结果:

函数指针变量调用函数的两种写法:

4.3 两段有趣的代码
代码1:
c
(*(void (*)())0)();
代码解析:
- 上述代码完成的是一次函数调用,调用的是0地址处的函数,这个函数没有参数,也没有返回类型。
- 代码中的
void (*)()是函数指针类型,void (*)()0类型放在括号中的意思是强制类型转化 ,将整型0强制类型转化这种函数指针类型,将0当作函数的地址。 *(void (*)())0,前面加上一个*,表示调用0地址处的这个函数,根据函数指针变量来看,这个函数没有返回值,也没有参数。
画图演示:

代码2:
c
void (*signal(int , void(*)(int)))(int);
代码解析:
- 上述代码其实是一次函数声明,
- 声明的函数名叫做
signal,函数的参数有两个,一个是int类型,另一个是void(*)(int)函数指针类型。函数的返回值类型也是void(*)(int)。
画图演示:

4.3.1 typedef 关键字
typedef 是用来给类型重命名的,可以将复杂的类型简单化。
下面我来类举常见用typedef重命名的情况:
- 对于整型类型的重命名,用
unit来重命名unsigned int,用ll重命名long long,用ull重命名unsigned long long.
c
typedef unsigned int uint
- 对于指针变量的重命名,将
int*重命名为ptr_t
c
typedef int* ptr_t
- 对于数组指针和函数指针稍微有点区别:
- 在数组指针类型中,比如数组指针类型是
int(*)[5],需要重命名为parr_t,那么可以这么写:
c
typedef int(*parr_t)[5]
//新的类型名必须在 * 的右边
- 函数指针类型的重命名也是⼀样的,比如,将
void(*)(int)重命名为pf_t,我们可以这么写:
c
typedef void(*pf_t)(int)
//新的类型名必须在 * 的右边
这样我们就可以简化代码2了:
c
typedef void(*pf_t)(int);
int main()
{
void (* signal( int, void(*)(int) ) ) (int);
pf_t signal(int, pf_t);
return 0;
}
5.函数指针数组
之前我们学习了数组,数组是一个存放相同类型数据的存储空间,函数指针数组就是把函数的地址存放在一个数组中,函数指针数组该如何定义。
c
int (*parr[3])()
函数指针数组的拆解:

parr 先和 [] 结合,说明 parr 是数组;再看 *,说明数组中的每个元素是指针(函数指针变量);最后看 () 和最前面的 int,说明这些指针指向返回类型为 int 的函数。因此,parr 是函数指针数组。
下面这张图是用来说明parr为什么先和[]结合,而不是(),相关知识点在C语言操作符详解中讲到过。

6.转移表
函数指针数组的用途:计算器的一般实现:
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;
}
void menu()
{
printf("-----------------------------------\n");
printf("------ 1. add 2. sub --------\n");
printf("------ 3. mul 4. div --------\n");
printf("------ 0. exit --------\n");
printf("-----------------------------------\n");
}
int main()
{
int x = 0;
int y = 0;
int r = 0;
int input = 1;
menu();
do
{
printf("请选择:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入两个数字:");
scanf("%d %d", &x, &y);
r = Add(x,y);
printf("结果是:%d\n", r);
break;
case 2:
printf("请输入两个数字:");
scanf("%d %d", &x, &y);
r = Sub(x, y);
printf("结果是:%d\n", r);
break;
case 3:
printf("请输入两个数字:");
scanf("%d %d", &x, &y);
r = Mul(x, y);
printf("结果是:%d\n", r);
break;
case 4:
printf("请输入两个数字:");
scanf("%d %d", &x, &y);
r = Div(x, y);
printf("结果是:%d\n", r);
break;
case 0:
printf("退出计算器\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
使用函数指针数组的方式来实现:
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;
}
void menu()
{
printf("-----------------------------------\n");
printf("------ 1. add 2. sub --------\n");
printf("------ 3. mul 4. div --------\n");
printf("------ 0. exit --------\n");
printf("-----------------------------------\n");
}
int main()
{
int x = 0;
int y = 0;
int r = 0;
int input = 1;
//函数指针数组 - 转移表
int (*p[])(int, int) = { NULL,Add,Sub,Mul,Div }; //这样做是为了在用户输入选项时,根据下标直接找到对应的函数指针
//这里 p[0] 放的是 NULL,不是函数指针,目的是让菜单编号和数组下标对应:
// 下标: 0 1 2 3 4
// 功能:退出 加 减 乘 除
menu();
do
{
printf("请选择:>");
scanf("%d", &input);
if (input > 0 && input < 5)
{
printf("请输入两个数:");
scanf("%d %d", &x, &y);
printf("结果是:%d\n", p[input](x, y)); // 通过 input 作为下标,取出对应的函数指针并调用
}
else if (input == 0)
{
printf("退出计算器\n");
}
else
printf("请重新输入\n");
} while (input);
return 0;
}
如果大家对于这段代码还有疑问的,可以看下面这张图片,我又补充讲解了函数指针数组的调用,其实和函数指针变量 的调用语法格式差不多:

完