前两篇我们搞定了指针基础、数组与指针的绑定关系,这一篇我们要啃下指针的硬核进阶知识点—— 字符指针、数组指针、函数指针以及函数指针数组。这些内容是 C 语言指针的分水岭,也是面试高频考点,掌握它们,你才算真正迈入 C 语言的进阶大门!
目录
1.3 经典笔试题:字符串比较的坑(出自《剑指 offer》)
一、 字符指针变量 char*:不止能存字符地址
字符指针是最常用的指针类型之一,它有两种典型用法,其中第二种很容易踩坑。
1.1 用法 1:指向单个字符
这是最基础的用法,字符指针存储单个字符变量的地址:
#include <stdio.h>
int main()
{
char ch = 'w';
char* pc = &ch; // pc指向字符ch
*pc = 'a'; // 修改ch的值为a
printf("%c\n", ch); // 输出:a
return 0;
}
1.2 用法 2:指向常量字符串(重点 + 易错点)
这种用法是面试的常客,核心是字符指针存储的是常量字符串首字符的地址,而非整个字符串。
#include <stdio.h>
int main()
{
// 易错理解:不是把字符串存到pstr里,而是存首字符'h'的地址
const char* pstr = "hello bit.";
// 正确用法:打印字符串,从首地址开始直到'\0'
printf("%s\n", pstr); // 输出:hello bit.
// 错误操作:常量字符串不可修改,加const就是防止误修改
// *pstr = 'H'; // 程序崩溃!
return 0;
}

上⾯代码的意思是把⼀个常量字符串的⾸字符 h 的地址存放到指针变量 pstr 中。
1.3 经典笔试题:字符串比较的坑(出自《剑指 offer》)
这道题能帮你彻底理解字符串存储的本质:
#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是数组:用相同字符串初始化时,会开辟两块独立的内存空间,地址自然不同str3和str4是字符指针:指向的是常量字符串,C/C++ 会把常量字符串存到单独的内存区域,相同常量字符串只存一份,所以两个指针指向同一块地址
二、 数组指针变量:指向整个数组的指针
这是最容易和指针数组混淆的概念,先明确结论:数组指针是指针,指向一个完整的数组。
2.1 区分:数组指针 vs 指针数组
先看两个写法,你能分清吗?
int *p1[10]; // 指针数组
int (*p2)[10]; // 数组指针
关键区分原则:看运算符优先级,[]优先级高于*
p1先和[]结合 → 是数组,数组元素是int*→ 指针数组p2先和*结合 → 是指针,指针指向int[10]的数组 → 数组指针
2.2 数组指针的定义与初始化
数组指针的定义格式:元素类型 (*指针名)[数组元素个数]
#include <stdio.h>
int main()
{
int arr[10] = {0};
// &arr是整个数组的地址,类型是int(*)[10],赋值给数组指针p
int (*p)[10] = &arr;
return 0;
}

类型解析:int (*p)[10]
int→ 指向数组的元素类型(*p)→ 强调 p 是指针[10]→ 指向的数组有 10 个元素
2.3 数组指针的核心用途:配合二维数组使用
数组指针单独用的场景不多,它的主战场是二维数组传参,我们下一节详细讲。
三、 二维数组传参的本质:传递一维数组的地址
要搞懂二维数组传参,首先要明确:二维数组可以看作 “数组的数组”。比如int arr[3][5],可以理解为:
- arr 有 3 个元素,每个元素都是
int[5]的一维数组 - 二维数组的数组名
arr,表示首元素的地址 → 即第一行一维数组的地址,类型是int(*)[5]
3.1 二维数组传参的两种写法
写法 1:数组形式(便于理解)
#include <stdio.h>
// 形参写成二维数组形式,行数可省略,列数不能省
void test(int a[][5], int r, int c)
{
for(int i=0; i<r; i++)
{
for(int 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;
}
写法 2:数组指针形式(贴近本质)
#include <stdio.h>
// 形参写成数组指针,指向int[5]的一维数组
void test(int (*p)[5], int r, int c)
{
for(int i=0; i<r; i++)
{
for(int j=0; j<c; j++)
{
// 等价于 p[i][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.1 函数指针的定义与初始化
函数指针的定义格式:返回值类型 (*指针名)(参数类型列表)
#include <stdio.h>
// 加法函数
int Add(int x, int y)
{
return x + y;
}
int main()
{
// 函数名Add就是地址,赋值给函数指针pf
int (*pf)(int, int) = Add;
// 等价写法:&Add也能取函数地址
// int (*pf)(int, int) = &Add;
return 0;
}
类型解析:int (*pf)(int, int)
int→ 指向函数的返回值类型(*pf)→ pf 是指针(int, int)→ 指向函数的参数类型
4.2 通过函数指针调用函数
有两种调用方式,效果完全一样:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
// 方式1:解引用调用(符合指针的使用逻辑)
int ret1 = (*pf)(2, 3);
// 方式2:直接调用(函数名本质是地址,可省略*)
int ret2 = pf(3, 5);
printf("ret1 = %d\n", ret1); // 输出:5
printf("ret2 = %d\n", ret2); // 输出:8
return 0;
}
4.3 用typedef简化函数指针类型
函数指针的写法比较繁琐,typedef可以帮我们重命名类型:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
// 重命名函数指针类型:pfun_t等价于int(*)(int, int)
typedef int (*pfun_t)(int, int);
int main()
{
// 用简化后的类型定义函数指针
pfun_t pf = Add;
printf("%d\n", pf(2, 3)); // 输出:5
return 0;
}
4.4 两段有趣的代码(出自《C 陷阱和缺陷》)
这两段代码能帮你加深对函数指针的理解:
(*(void (*)())0)();- 把 0 强制转换成
void (*)()类型的函数指针 - 解引用该指针,调用地址为 0 的函数
- 把 0 强制转换成
void (*signal(int, void(*)(int)))(int);signal是一个函数,参数有两个:int和void(*)(int)的函数指针- 返回值是
void(*)(int)的函数指针 - 用
typedef简化:typedef void(*pfun_t)(int); pfun_t signal(int, pfun_t);
五、 函数指针数组:存放函数地址的数组
函数指针数组是数组,数组的每个元素都是函数指针,它的核心用途是实现转移表。
5.1 函数指针数组的定义
格式:返回值类型 (*数组名[元素个数])(参数类型列表)
// 定义一个函数指针数组,有4个元素,每个元素是int(*)(int,int)类型
int (*parr[4])(int, int);
解析:parr先和[]结合 → 是数组,数组元素是函数指针。
5.2 核心用途:转移表(以计算器为例)
普通计算器用switch实现,代码冗余;用函数指针数组(转移表)实现,简洁高效。
普通switch版本
#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, input, ret;
do
{
printf("1:add 2:sub 3:mul 4:div 0:exit\n");
printf("请选择:");
scanf("%d", &input);
switch(input)
{
case 1:
scanf("%d %d", &x, &y);
ret = add(x, y);
break;
case 2:
scanf("%d %d", &x, &y);
ret = sub(x, y);
break;
case 3:
scanf("%d %d", &x, &y);
ret = mul(x, y);
break;
case 4:
scanf("%d %d", &x, &y);
ret = div(x, y);
break;
case 0:
printf("退出\n");
return 0;
default:
printf("选择错误\n");
}
printf("ret = %d\n", ret);
} while(1);
return 0;
}
转移表版本(函数指针数组)
#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, input, ret;
// 函数指针数组:下标对应功能号,0位置放NULL
int (*p[5])(int, int) = {NULL, add, sub, mul, div};
do
{
printf("1:add 2:sub 3:mul 4:div 0:exit\n");
printf("请选择:");
scanf("%d", &input);
if(input >= 1 && input <=4)
{
scanf("%d %d", &x, &y);
// 通过下标直接调用对应函数,无需switch
ret = p[input](x, y);
printf("ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出\n");
}
else
{
printf("选择错误\n");
}
} while(1);
return 0;
}
对比优势:
- 代码更简洁,减少冗余的
case分支 - 扩展性更强:新增功能只需添加函数和数组元素,无需修改逻辑
总结与系列预告
本篇我们攻克了指针的高阶知识点:
- 字符指针指向常量字符串的坑,以及经典笔试题解析
- 数组指针与指针数组的区分,核心看运算符优先级
- 二维数组传参的本质是传递一维数组的地址,形参有两种写法
- 函数指针的定义与调用,用
typedef简化类型 - 函数指针数组实现转移表,简化多分支逻辑
下一篇我们将进入指针的终极应用:回调函数与qsort 函数的模拟实现,带你理解指针在通用算法中的强大作用!
如果本篇内容对你有帮助,欢迎点赞、收藏、关注!有任何疑问,评论区留言交流~
542



被折叠的 条评论
为什么被折叠?



