
指针数组与二维传参: 我们将从指针数组的本质和声明规则入手,理解其如何灵活地模拟二维数组。随后,揭示二维数组传参的秘密,强调必须使用数组指针来正确接收参数,以确保指针运算(行间跳转)的正确性。 字符指针的本质: 深入辨析字符指针指向字符串常量时的内存机制,通过经典笔试题区分常量字符串和字符数组在内存中的存储区域和可修改性。 函数指针与转移表: 详细学习函数指针的声明和调用方式,它是实现回调机制的基础。最后,利用函数指针数组构建高效的转移表(Jump Table),以取代冗长的 switch-case 或 if-else 结构,优化程序逻辑。
经过前面对指针基础、数组名和二级指针的学习,我们现在将迎来两个容易混淆但至关重要的概念:指针数组和数组指针。 本章,我们首先聚焦于指针数组,它将带你进入更灵活、更动态的内存管理世界。
当我们看到指针数组这个名称时,我们首先要抓住它的核心: 定义:存放指针的数组 指针数组,本质上是一个数组。 类比一下:
int 类型数据的数组。char 类型数据的数组。指针数组的每一个元素本身都是一个地址,这个地址可以指向内存中的一块独立区域。
理解声明规则的关键在于C语言的运算符优先级:[](数组操作符)的优先级高于 *(指针操作符)。
声明格式:
int* arr[5];把数组名去掉也就是指针数组类型:
int * [5]。
声明解析:
arr 首先与 [] 结合,表明 arr 是一个大小为 5 的数组。* 和 int,表明这个数组的每个元素都是 int* 类型,即整型指针。声明部分 | 结合顺序 | 含义 |
|---|---|---|
arr[5] | 优先结合 | arr 是一个大小为 5 的数组。 |
int* | 其次结合 | 数组的每个元素类型是 int*(整型指针)。 |
指针数组最强大、最常见的应用,就是用来管理多个一维数组,从而达到模拟二维数组的效果。 思考: 为什么不直接用二维数组?
int main()
{
// 1. 定义三个独立且长度不等的一维数组
int arr1[] = { 10, 20, 30 };
int arr2[] = { 40, 50, 60, 70 }; // 长度可以不同!
int arr3[] = { 80, 90, 100 };
// 2. 定义指针数组 parr,存储这三个数组的首地址
int* parr[3] = { arr1, arr2, arr3 };
// parr[0] 存储 arr1 的地址
// parr[1] 存储 arr2 的地址
// parr[2] 存储 arr3 的地址
// 3. 通过指针数组访问元素 (例如访问 arr2 中的元素 60)
// arr2 的索引是 1,60 在 arr2 中的索引是 2
int row = 1;
int col = 2;
printf("目标元素: %d\n", parr[row][col]); // 输出 60
printf("\n遍历模拟的二维数组:\n");
for (int i = 0; i < 3; i++) // 遍历行 (指针数组的元素)
{
// 关键:parr[i] 就是第 i 个一维数组的首地址
int size = (i == 1) ? 4 : 3; // 动态确定每一行的元素个数
for (int j = 0; j < size; j++) // 遍历列
{
// parr[i][j] 等价于 *(parr[i] + j) || *(*(parr+i)+j)
printf("%d ", parr[i][j]);
}
printf("\n");
}
return 0;
}
通过这个例子,我们可以清晰地看到,指针数组就像一个包含了多个地址的列表,每个地址指向一个独立的数据块(一行数组)。当我们使用 parr[i][j] 时,它会先通过 parr[i] 定位到第 i 个地址,再通过 [j] 的偏移量访问该地址指向的数据。
在理解了指针数组可以用来模拟二维数组后,我们现在来看看真正的二维数组在函数传参时有什么特殊要求。 二维数组的传参规则与一维数组类似,也是传递地址。但由于二维数组的特殊内存结构,接收参数时必须采用能体现“行”和“列”信息的指针类型。
一个 int arr[3][5] 的二维数组,在内存中是连续存储的一块内存。
arr 传入函数时,它代表的不是首元素地址 (int*),而是第一行数组的地址。arr 的类型是 int (*)[5](指向一个包含 5 个 int 元素的数组的指针)。arr + 1 才会正确地跳过一整行(即 5 * sizeof(int) 字节),定位到下一行的起始地址。C语言在进行指针运算(尤其是多维数组的行间跳转)时,需要知道指针所指向对象的大小。对于一个指向数组的指针,它必须知道这个数组的完整大小(即列数)。
如果你不指定列数,编译器就不知道 arr + 1 应该跳过多少字节,从而无法定位下一行元素,导致访问错误。
基于上述原理,接收二维数组参数的形参必须能够体现指向数组的特性,并包含列数信息。
方式一:数组形式(最常用且直观)
这是最直观的写法,但必须指定列数。行数(3)可以省略,但列数(5)不能省略。
// 列数不能省略
void print_2d_array(int arr[][5], int row, int col)
{
// ...
}方式二:数组指针(最能体现本质) 这是最能体现二维数组传参本质的写法:用一个数组指针来接收地址。
// int (*p)[5] 表示 p 是一个指针,它指向一个有 5 个 int 元素的数组
void print_2d_array_ptr(int (*p)[5], int row, int col)
{
// p + i 找到第 i 行的首地址
// p[i][j] 语法依然生效,等价于 *(*(p + i) + j)
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
printf("%d ", p[i][j]);
}
printf("\n");
}
}结果展示:

结论: 二维数组传参时,形参必须是数组指针(如 int (*p)[5])或等价的数组形式(如 int arr[][5]),以确保编译器知道每一行的大小,从而进行正确的行间跳转(指针加法)。
本章我们将聚焦于三种具有特殊意义的指针变量:字符指针(已介绍)、函数指针和函数指针数组。
char*)字符指针变量 char* 是用来存放字符变量地址的指针。
这是最基础的用法,char* 存储一个 char 变量的地址,用于间接访问和修改该字符。
将一个字符串常量赋值给 char* 类型的指针。
const char* pstr = "hello Extreme35.";关键:pstr 中存放的不是整个字符串!
代码 const char* pstr = "hello Extreme35." 特别容易让同学以为是把字符串 hello Extreme35.放到字符指针 pstr 里了, 但是本质是把字符串 hello Extreme35. 首字符的地址放到了 pstr 中。
内存结构与指针指向:
"hello Extreme35." 被存储在内存的只读数据区。pstr 仅仅存储了这个字符串的起始地址(即首字符的地址)。给个小例子,别看答案自己尝试一下:
int main()
{
char str1[] = "hello Extreme35.";
char str2[] = "hello Extreme35.";
const char* str3 = "hello Extreme35.";
const char* str4 = "hello Extreme35.";
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相同。
函数指针,顾名思义,是存放函数地址的指针。函数的名称本身就代表了函数的地址(类似于数组名代表数组首元素地址)。
声明方式:去函数名,加括号和星号
要声明一个函数指针,我们只需将函数声明中的函数名替换为 (*指针名) 即可。
// 假设有一个函数声明如下:
// int Add(int x, int y);
// 对应的函数指针声明:
int (*pfun)(int, int);
// 解读:pfun 先与 * 结合,表明 pfun 是一个指针;
// 它指向一个返回类型为 int,接受两个 int 类型参数的函数。元素 | 作用 |
|---|---|
int | 指针所指向函数的返回类型。 |
(*pfun) | 声明 pfun 是一个指针。 |
(int, int) | 指针所指向函数的参数列表。 |
使用方式:存储与调用 函数指针常用于将函数作为参数传递给其他函数,或用于实现回调机制。
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 1. 声明并初始化函数指针
int (*padd)(int, int) = &Add; // 或者直接写 = Add;
// 2. 通过函数指针调用函数 (两种调用方式等效)
int sum1 = (*padd)(10, 20); // 方式一:先解引用,再传参
int sum2 = padd(10, 20); // 方式二:直接使用指针名 (编译器自动解引用)
printf("Sum1 = %d, Sum2 = %d\n", sum1, sum2); // 输出 30, 30
return 0;
}结果展示:

这里可以看到两种方式都可以成功调用函数Add,但感觉好像也没差多少,那是因为还没学到回调函数,当我们下节接触到回调函数qsort的时候,就知道学函数指针有什么用了。
函数指针数组,本质上是数组,数组的每个元素都是一个函数指针。它常用于实现转移表(Jump Table),用于简化多分支条件的判断。 声明方式: 函数指针数组的声明是在函数指针的基础上增加数组的维度:
// 假设函数指针类型是 int (*)(int, int)
int (*pfa[4])(int, int);
// 解读:pfa 先与 [] 结合,表明 pfa 是一个大小为 4 的数组;
// 数组的每个元素都是一个函数指针,指向 int(int, int) 类型的函数。经典应用:实现转移表 将多个功能相似(参数和返回值相同)的函数地址存入一个数组中,可以根据用户输入(数组下标)快速调用对应的函数。 代码示例(简化的计算器):
int Add(int x, int y){ return x + y; }
int Sub(int x, int y){ return x - y; }
int Div(int x, int y){ return x / y; }
int Mul (int x, int y){ return x * y; }
int main()
{
int(*calc[5])(int, int) = { 0,Add,Sub,Mul,Div };//转移表
int choice = 0;//计算器选择
int x = 0;
int y = 0;
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", &choice);
if ((choice <= 4 && choice >= 1))
{
printf("输入操作数:>");
scanf("%d %d", &x, &y);
// 根据 choice (下标) 调用对应的函数,实现多分支跳转
// calc[choice] 找到函数地址
// (*calc[choice])(x, y) 调用函数
ret = (*calc[choice])(x, y);
printf("ret = %d\n", ret);
}
else if (choice == 0)
printf("退出计算器\n");
else
printf("输入有误\n");
} while (choice);
return 0;
}结果展示:

通过函数指针数组构建的转移表,可以避免大量的 if-else 或 switch 语句,使程序结构更加清晰和高效。