【C语言】深入理解指针(三)
前言:
在前两讲中,我们掌握了指针的基础概念、与数组的绑定关系,以及二级指针、指针数组等进阶用法。这一讲,我们将聚焦指针与字符、函数的结合,从字符指针的特殊应用,到数组指针的深度解析,再到函数指针与函数指针数组的实战(eg:转移表),逐步揭开C语言中“指针操作复杂数据”的核心逻辑。
一、字符指针变量
字符指针(char*)是最常用的指针类型之一,但其用法不仅限于“指向单个字符”——更常见的场景是指向常量字符串。
1.1 字符指针的两种基础用法
用法1:指向单个字符
与整型指针类似,字符指针可指向单个字符变量,通过解引用修改字符值
#include <stdio.h>
int main() {char ch = 'w';char* pc = &ch; // 字符指针指向ch的地址*pc = 'a'; // 解引用修改ch的值printf("ch = %c\n", ch); // 输出'a'return 0;
}
用法2:指向常量字符串
更常见的用法是让字符指针指向常量字符串的首地址。
(注意:不是将整个字符串存入指针,而是存储字符串首字符的地址。)
#include <stdio.h>
int main() {// 本质:将"hello word."首字符'h'的地址存入pstrconst char* pstr = "hello word."; // %s打印:从pstr指向的地址开始,直到'\0'结束printf("%s\n", pstr); // 输出"hello word."return 0;
}
关键点:
pstr存储的是首字符地址,而非整个字符串。
printf("%s", pstr)的逻辑是“从首地址开始遍历,直到遇到'\0'终止符”。
1.2 字符指针与数组的区别
我们来看一下以下的程序,以理解字符指针指向常量字符串与数组初始化的差异:
#include <stdio.h>
int main() {// 数组:用常量字符串初始化,开辟新内存块char str1[] = "hello word."; char str2[] = "hello word."; // 字符指针:指向常量字符串(只读区域)const char* str3 = "hello word."; const char* str4 = "hello word."; // 比较数组名(首元素地址)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 and str2 are not same
str3 and str4 are same
结果解析:
str1和str2是数组:用相同常量字符串初始化时,编译器会为两个数组分别开辟独立的内存块(存储相同的字符内容),因此str1和str2的首元素地址不同;str3和str4是字符指针:指向的是同一块只读内存中的常量字符串(C/C++为节省空间,相同常量字符串只存储一份),因此str3和str4的值(首字符地址)相同。
二、数组指针变量
在前一讲中,我们学习了“指针数组”(如int* arr[10],是存放指针的数组)。而“数组指针”是另一个易混淆的概念——它是指向数组的指针,本质是指针,而非数组。
2.1 数组指针的定义
数组指针的定义格式为:
数据类型 (*指针名)[数组长度]
例如int (*p)[10],其中括号()是关键——因为[]的优先级高于*,必须用括号保证p先与*结合,确定p是指针。
对比其“指针数组”与“数组指针”的核心差异:
| 表达式 | 本质 | 解析逻辑(优先级:[] > *) |
|---|---|---|
| int* p1[10] | 指针数组 | p1先与[]结合,是数组,元素类型为int* |
| int (*p2)[10] | 数组指针 | p2先与*结合,是指针,指向int[10]数组 |
识别技巧:
- 带 ( ) 的是数组指针(先指针后数组)
- 不带 ( ) 的是指针数组(先数组后指针)
2.2 数组指针的初始化
数组指针用于存储整个数组的地址,而非首元素地址,获取整个数组地址需用**&数组名**。
示例代码:
#include <stdio.h>
int main() {int arr[10] = {0}; // 定义一个10元素的int数组// 数组指针p:指向arr的整个数组,类型为int(*)[10]int (*p)[10] = &arr; // 调试观察:&arr与p的类型完全一致printf("&arr = %p\n", &arr); // 整个数组的地址printf("p = %p\n", p); // 与&arr的值相同return 0;
}
数组指针类型解析(以 int (*p)[10]为例):
- p:数组指针变量名;
- (*p):表示
p是指针; - [10]:表示指针指向的数组有10个元素;
- int:表示数组元素的类型是
int。
三、二维数组传参的本质
有了数组指针的基础,我们就能够来学习一下二维数组传参的底层逻辑了——二维数组可看作“数组的数组”,传参本质是传递第一行的地址(即一个一维数组的地址)。
3.1 二维数组的本质
例如:int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}}
我们可理解为:
- 整个二维数组是一个“包含3个元素的数组”。
- 每个元素又是一个“包含5个int的一维数组”(即每行是一个一维数组)。
因此,二维数组的数组名arr(默认情况下)表示第一行一维数组的地址,其类型是int(*)[5](指向5个int的数组指针)。

3.2 二维数组传参的两种写法
二维数组传参时,形参可写成“数组形式”或“数组指针形式”,本质都是接收第一行的地址。
写法1:数组形式
形参写成int a[3][5],编译器会自动解析为数组指针:
#include <stdio.h>
// 形参:数组形式(3行5列)
void print_arr(int a[3][5], int rows, int cols) {for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; 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}};// 传参:数组名arr(第一行地址)+ 行数+列数print_arr(arr, 3, 5); return 0;
}
写法2:数组指针形式
形参直接写成int (*p)[5],明确接收“指向5个int的数组指针”:
#include <stdio.h>
// 形参:数组指针形式(指向5个int的数组)
void print_arr(int (*p)[5], int rows, int cols) {for (int i = 0; i < rows; i++) {for (int j = 0; j < cols; j++) {// *(*(p+i)+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}};print_arr(arr, 3, 5); // 传参:arr是第一行地址,类型匹配int(*)[5]return 0;
}
关键点:
- 二维数组传参时,列数不能省略(如
int a[][5]合法,int a[3][]非法),因为数组指针需要知道“每行有多少个元素”,才能正确计算下一行的地址(p+i跳过5*sizeof(int)字节)。 - 必须单独传递行数和列数——形参是指针,无法通过
sizeof计算数组大小。
四、函数指针变量
函数也有地址,函数名就是函数的地址(&函数名也可获取地址)。函数指针变量用于存储函数地址,未来可通过地址调用函数,是实现回调函数 “转移表” 的基础。
4.1 函数指针的定义:
函数指针的定义格式为:
返回值类型 (*指针名)(参数类型列表)
例如:int (*pf)(int, int),表示“一个指向返回值为int、参数为两个int的函数的指针”。
步骤1:验证函数地址
先通过代码确认函数有地址:
#include <stdio.h>
void test() {printf("hehe\n");
}int main() {// 函数名和&函数名均表示函数地址printf("test = %p\n", test); // 输出函数地址printf("&test = %p\n", &test); // 与test的值相同return 0;
}
输出结果(地址值为示例):
test = 005913CA
&test = 005913CA
步骤2:定义并初始化函数指针
以加法函数为例,定义函数指针并存储其地址:
#include <stdio.h>
// 加法函数
int Add(int x, int y) {return x + y;
}int main() {// 函数指针pf:指向Add函数(参数和返回值类型匹配)// 两种初始化方式均合法:Add或&Addint (*pf)(int, int) = Add; // int (*pf)(int x, int y) = &Add; // 参数名可省略,不影响匹配return 0;
}
函数指针类型解析(以int (*pf)(int, int)为例):
- pf:函数指针变量名;
- (*pf):表示
pf是指针; - (int, int):表示指针指向的函数有两个int类型参数;
- int:表示函数的返回值类型是int。
4.2 函数指针的使用
通过函数指针调用函数时,(*pf)与pf等价,编译器会自动将pf解析为函数地址。
示例代码:
#include <stdio.h>
int Add(int x, int y) {return x + y;
}int main() {int (*pf)(int, int) = Add;// 两种调用方式均合法,结果相同int ret1 = (*pf)(2, 3); // 显式解引用int ret2 = pf(3, 5); // 隐式调用(更简洁)printf("ret1 = %d\n", ret1); // 输出5printf("ret2 = %d\n", ret2); // 输出8return 0;
}
4.3 简化复杂函数指针
函数指针类型复杂(如void(*)(int)),可通过typedef重命名为简洁的类型名,提升代码可读性。
typedef重命名规则:
- 普通类型:
typedef原类型、新类型名(如typedef unsigned int uint); - 指针类型:新类型名需放在
*右侧(如typedef int* ptr_t); - 函数指针类型:新类型名放在
*右侧(如typedef void(*pfun_t)(int))。
示例:简化signal函数声明
C标准库中的signal函数声明非常复杂:
// 原声明:返回值是void(*)(int),参数是int和void(*)(int)
void (*signal(int, void(*)(int)))(int);
用typedef重命名后:
// 1. 将void(*)(int)重命名为pfun_t
typedef void(*pfun_t)(int);
// 2. 简化signal声明:返回值pfun_t,参数int和pfun_t
pfun_t signal(int, pfun_t);
瞬间简洁易懂!
五、函数指针数组
函数指针数组是“存放函数指针的数组”,数组的每个元素都是一个函数指针,且所有元素指向的函数需满足“相同的返回值类型和参数列表”。
5.1 函数指针数组的定义
定义格式:
返回值类型 (*数组名[数组长度])(参数类型列表)
例如int (*parr[4])(int, int),表示“一个长度为4的数组,每个元素是指向‘返回int、参数为两个int的函数’的指针”。
错误写法对比:
| 表达式 | 问题原因 |
|---|---|
| int*parr1[4](int | 先与[]结合是数组,但元素类型int* (int)非法 |
| int (*)(int) parr2[4] | 语法错误,数组名需紧跟[] |
| `int (*parr3[4])(int) | 正确:parr3是数组,元素是函数指针 |
5.2 实战
函数指针数组的核心用途是转移表(替代switch或if-else),减少代码冗余,提升可维护性。以“计算器”为例,对比两种实现方式。
方式1:传统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("*************************\n");printf(" 1:add 2:sub 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);break;case 2:printf("输入操作数:");scanf("%d %d", &x, &y);ret = sub(x, y);break;// case3、case4逻辑类似,代码重复...case 0: printf("退出程序\n"); break;default: printf("选择错误\n"); ret = 0;}printf("ret = %d\n", ret);} while (input != 0);return 0;
}
问题:每个case的逻辑高度重复(输入操作数、调用函数),新增运算需修改switch,可维护性差。
方式2:转移表实现(简洁)
用函数指针数组parr存储4个运算函数的地址,input直接作为数组下标,省去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;// 函数指针数组parr:下标0留空,1-4对应add-sub-mul-divint (*parr[5])(int, int) = {0, add, sub, mul, div}; do {printf("*************************\n");printf(" 1:add 2:sub 3:mul 4:div\n");printf(" 0:exit\n");printf("*************************\n");printf("请选择:");scanf("%d", &input);if (input >= 1 && input <= 4) {// 输入操作数,通过转移表调用函数printf("输入操作数:");scanf("%d %d", &x, &y);ret = parr[input](x, y); // 直接用input作为下标printf("ret = %d\n", ret);} else if (input == 0) {printf("退出计算器\n");} else {printf("输入有误\n");}} while (input != 0);return 0;
}
优势:
- 代码简洁:省去重复的
case逻辑; - 可维护性高:新增运算只需添加函数,并在数组中补充地址,无需修改核心逻辑;
- 效率高:数组下标访问比
switch判断更直接。
至此,我们的学习已覆盖指针的核心应用场景,从基础的变量、数组,到复杂的函数、字符串。而指针的灵活性源于对“地址”的直接操作,掌握其底层逻辑(如类型意义、优先级、内存分布),是写出高效语言代码的关键。后续我们还将探索指针与动态内存、回调函数的结合,敬请关注!
