C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)
C语言指针入门到理解:一篇文章系统梳理指针核心知识(3)
前两篇文字我们已经把指针的基础和数组相关内容系统梳理过了,比如:
- 指针的本质
- 指针和数组的关系
- 一维数组、二维数组的传参
- 二级指针
- 指针数组
这一篇继续往下走,进入指针相关的更进一步的内容:
- 字符指针变量到底存的是什么?
- 数组指针和指针数组怎么区分?
- 二维数组传参为什么可以写成指针形式?
- 函数指针到底是什么?
- 函数指针数组有什么实际用途?
- 什么是转移表?
这部分是 C 语言指针体系里看上去比较困难,但实际上只要抓住“类型决定意义”这条主线,其实并不难。
一、字符指针变量:它存的不是字符串本身
我们先看一个最常见的字符指针写法:
char ch = 'w';
char *pc = &ch;
*pc = 'w';
这个很好理解:pc 是一个字符指针,里面存的是字符变量 ch 的地址。
但很多同学真正困惑的是下面这种写法:
const char* pstr = "hello world.";
printf("%s\n", pstr);
很多人第一眼会误以为:
是把字符串
"hello world."整体放进了指针变量pstr
其实不是。
这句代码的本质是:
把字符串常量
"hello world."的首字符地址,存放到字符指针pstr中。
也就是说,pstr 里存放的是首字符 'h' 的地址,而不是整个字符串对象本身。
这里为什么要写 const char*
因为字符串字面量通常存放在常量区,不应该通过指针去修改它,所以更合理的写法是:
const char* pstr = "hello bit.";
如果这里无法理解的话请联想一些常量的赋值是违规的操作,例如:
3 = 5 //这种操作
二、一个经典面试题:为什么有的字符串地址相同,有的不同
看下面这段代码:
#include <stdio.h>
int main()
{
char str1[] = "hello world.";
char str2[] = "hello world.";
const char *str3 = "hello world.";
const char *str4 = "hello world.";
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
为什么会这样?
1. str1 和 str2 为什么不同
因为:
char str1[] = "hello world.";
char str2[] = "hello world.";
这是用同样的内容初始化了两个不同的数组。
数组初始化时,会各自开辟独立空间,所以 str1 和 str2 不是同一块内存。
2. str3 和 str4 为什么相同
因为:
const char *str3 = "hello world.";
const char *str4 = "hello world.";
这里不是创建数组,而是让两个指针都去指向同一个字符串常量。
编译器通常会把相同的字符串常量放到同一块常量区内存中,所以 str3 和 str4 很可能相等。
ps:这也同样可以进一步佐证前文为何要加const,因为这是一个常量,所以我们不希望它被修改。
总结
- 数组名比较的是各自数组首元素地址
- 字符指针比较的是它们指向的常量字符串地址
所以这个例子非常适合理解:
“字符数组”和“字符指针”虽然都能处理字符串,但底层模型并不一样。
三、数组指针变量:它是指针,不是数组
这一块是最容易和“指针数组”混掉的地方。
先看两个定义:
int *p1[10];
int (*p2)[10];
很多人会懵:到底哪个是数组指针?
答案是:
int (*p2)[10];
才是数组指针变量。
四、什么是数组指针
数组指针,本质上是:
一个指针变量,这个指针指向的是数组。
例如:
int (*p)[10];
怎么理解这句?
先看优先级
因为 [] 的优先级高于 *,所以必须加括号:
(*p)
这表示先说明 p 是一个指针。
然后再看:
(*p)[10]
表示 p 指向一个有 10 个元素的数组。
如果数组元素类型是 int,那最终它的含义就是:
p是一个指针,指向一个int[10]类型的数组。
五、数组指针怎么初始化
既然数组指针是“指向数组的指针”,那它存的就应该是数组的地址。
而数组的地址怎么取?
用:
&数组名
例如:
int arr[10] = {0};
int (*p)[10] = &arr;
这里:
&arr是整个数组的地址p是数组指针变量p和&arr类型一致
数组指针拆解理解
int (*p)[10] = &arr;
可以拆成三层:
p是变量名*p说明p是指针(*p)[10]说明它指向一个有 10 个int元素的数组
所以记忆方法很简单:
数组指针是指针,指针数组是数组。
六、二维数组传参的本质:传的是第一行的地址
先看一个常见写法:
#include <stdio.h>
void test(int a[3][5], int r, int c)
{
int i = 0;
int j = 0;
for(i = 0; i < r; i++)
{
for(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;
}
这段代码大家通常都见过,但很多人并没有真正理解:
二维数组传参,传过去的到底是什么?
七、二维数组本质上是什么
二维数组可以理解为:
每个元素都是一维数组的数组
比如:
int arr[3][5];
它可以理解成:
- 整个数组有 3 行
- 每一行是一个
int[5]的一维数组
也就是说,二维数组的首元素不是一个 int,而是第一行这个一维数组。
所以根据“数组名表示首元素地址”的规则:
arr
表示的不是单个 int 的地址,而是第一行的地址。
第一行的类型是:
int [5]
那么第一行地址的类型就是:
int (*)[5]
这正好就是数组指针类型。
八、所以二维数组形参也可以写成数组指针
于是二维数组传参,完全可以写成下面这样:
#include <stdio.h>
void test(int (*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for(i = 0; i < r; i++)
{
for(j = 0; j < c; 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;
}
这里:
p + i表示走到第i行*(p + i)表示第i行这个一维数组*(p + i) + j表示第i行第j个元素地址*(*(p + i) + j)才是真正的元素值
ps:这里如果忘记了*操作符的作用请回顾我上篇文章
所以结论是:
二维数组传参时,形参既可以写成:
int a[3][5]
也可以写成:
int (*p)[5]
但无论哪种写法,本质上都是在接收第一行的地址。
九、函数指针变量:它存放的是函数地址
前面学了很多种指针:
- 整型指针:存整型变量地址
- 字符指针:存字符地址
- 数组指针:存数组地址
那自然就会想到:
能不能有一种指针,专门用来存函数地址?
答案当然是可以,这就是函数指针变量。
十、函数真的有地址吗?
先看例子:
#include <stdio.h>
void test()
{
printf("hehe\n");
}
int main()
{
printf("test: %p\n", test);
printf("&test: %p\n", &test);
return 0;
}
你会发现:
test&test
都能打印出函数地址,而且结果通常一样。
这说明:
函数名本身就表示函数地址,也可以用
&函数名来取地址。
十一、函数指针怎么定义
例如有这样一个函数:
int Add(int x, int y)
{
return x + y;
}
那对应的函数指针可以写成:
int (*pf)(int, int) = Add;
也可以写成:
int (*pf)(int, int) = &Add;
这两种写法都可以。
如何理解这个定义
int (*pf)(int, int)
拆开看:
pf是变量名*pf说明它是一个指针(int, int)说明它指向的函数参数是两个int- 最前面的
int说明该函数返回值类型是int
所以这句的完整含义是:
pf是一个函数指针,指向的函数形参是(int, int),返回值类型是int
十二、函数指针怎么调用函数
有了函数指针之后,可以通过它调用对应函数:
#include <stdio.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
int (*pf)(int, int) = Add;
printf("%d\n", (*pf)(2, 3));
printf("%d\n", pf(3, 5));
return 0;
}
输出:
5
8
这里两种调用方式都对:
(*pf)(2, 3);
pf(3, 5);
因为对于函数指针来说,前面的* 在写的时候可以省略,所以平时更常写的是第二种,简洁一些。
十三、复杂函数指针看不懂怎么办?用 typedef
函数指针类型一复杂,代码可读性会迅速下降。
比如说:
void (*signal(int, void(*)(int)))(int);
这种写法一眼看过去确实头大。
这时候最好的解决方案就是:
用 typedef 给复杂类型起别名
例如:
typedef void(*pfun_t)(int);
typedef long long LL;
这表示把:
void(*)(int)
重命名为:
pfun_t
那上面那句复杂定义就能简化为:
pfun_t signal(int, pfun_t);
可读性一下就上来了。
除了函数指针,数组指针也可以这样简化
例如:
typedef int(*parr_t)[5];
以后看到 parr_t,你就知道它是“指向 int[5] 的数组指针类型”。
十四、函数指针数组:数组里存的是函数地址
前面学过:
int *arr[10];
这是指针数组,表示数组中每个元素都是 int*。
那如果数组中每个元素都是“函数指针”,就得到了:
函数指针数组
例如:
int (*parr1[3])();
这个定义中:
parr1先和[]结合,说明它是数组- 数组元素类型是
int (*)(),也就是函数指针
所以 parr1 是一个函数指针数组。
十五、函数指针数组最典型的用途:转移表
函数指针数组最经典的应用场景,就是转移表。
比如我们要实现一个简单计算器:
- 1:加法
- 2:减法
- 3:乘法
- 4:除法
传统写法通常会用 switch-case:
switch (input)
{
case 1:
ret = add(x, y);
break;
case 2:
ret = sub(x, y);
break;
case 3:
ret = mul(x, y);
break;
case 4:
ret = div(x, y);
break;
}
这种写法当然能用,但如果功能越来越多,switch-case 会越来越臃肿。
这时就可以用函数指针数组优化。
十六、用函数指针数组实现计算器
#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;
int input = 1;
int ret = 0;
int (*p[5])(int, int) = {0, add, sub, mul, div}; // 转移表
do
{
printf("*************************\n");
printf(" 1:add 2:sub \n");
printf(" 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 = (*p[input])(x, y);
printf("ret = %d\n", ret);
}
else if(input == 0)
{
printf("退出计算器\n");
}
else
{
printf("输入有误\n");
}
} while(input);
return 0;
}
这里最关键的一句
int (*p[5])(int, int) = {0, add, sub, mul, div};
这就是一个函数指针数组,也就是转移表。
它的含义是:
p[1]指向addp[2]指向subp[3]指向mulp[4]指向div
之后只需要根据用户输入,直接调用:
(*p[input])(x, y);
这样就把“菜单选择 -> 函数跳转”的流程做成了表驱动结构。
这就是所谓的:
转移表
它的优势在于:
- 结构更清晰
- 扩展更方便
- 代码更容易维护
十七、这一篇的几个易错点:
最后把本篇最容易出错的地方集中总结一下。
1. 字符指针不等于字符数组
const char *p = "hello";
这里 p 里存的是首字符地址,不是把整个字符串“装进了指针”。
2. 相同内容的字符数组不一定地址相同
char str1[] = "abc";
char str2[] = "abc";
这是两个不同数组,地址不同。
3. 相同内容的字符串常量指针可能地址相同
const char *p1 = "abc";
const char *p2 = "abc";
它们可能指向同一个常量区字符串。
4. 数组指针是指针,不是数组
int (*p)[10];
p 是指针,指向一个 int[10] 数组。
5. 指针数组是数组,不是指针
int *arr[10];
arr 是数组,元素类型是 int*。
6. 二维数组传参本质上传的是第一行地址
所以形参可以写成:
int a[][5]
也可以写成:
int (*p)[5]
7. 函数指针的关键是先看 pf 和谁结合
int (*pf)(int, int);
先看 (*pf),说明 pf 是指针;再看后面的参数列表和前面的返回值类型。
8. 函数指针数组的本质还是数组
只是数组里的元素,不再是普通数据,而是“函数地址”。
十八、总结
这一篇我们的主要结论如下
- 字符指针存的是字符地址,字符串字面量本质上传递的是首字符地址。
- 数组指针是“指向数组的指针”,指针数组是“存放指针的数组”。
- 二维数组传参本质上传的是第一行的地址,所以形参可以写成数组指针。
- 函数名就是函数地址,函数指针变量就是用来存函数地址的。
- 函数指针数组可以构造转移表,让代码从分支驱动变成表驱动,从而更好的维护自己的项目。
实际上在学习指针这一块的时候,有一个很大的诀窍,那就是:
先认清变量是谁,再看它指向什么。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)