当前位置: 首页 > news >正文

【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

结果解析

  1. str1str2是数组:用相同常量字符串初始化时,编译器会为两个数组分别开辟独立的内存块(存储相同的字符内容),因此str1str2的首元素地址不同;
  2. str3str4是字符指针:指向的是同一块只读内存中的常量字符串(C/C++为节省空间,相同常量字符串只存储一份),因此str3str4的值(首字符地址)相同。

二、数组指针变量

在前一讲中,我们学习了“指针数组”(如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 实战

函数指针数组的核心用途是转移表(替代switchif-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判断更直接。

至此,我们的学习已覆盖指针的核心应用场景,从基础的变量、数组,到复杂的函数、字符串。而指针的灵活性源于对“地址”的直接操作,掌握其底层逻辑(如类型意义、优先级、内存分布),是写出高效语言代码的关键。后续我们还将探索指针与动态内存、回调函数的结合,敬请关注!

以上就是本期博客的全部内容了,感谢各位的阅读以及关注。如有内容存在疏漏或不足之处,恳请各位技术大佬不吝赐教、多多指正。

http://www.dtcms.com/a/600875.html

相关文章:

  • BHYRA:当金融的信任,开始由收益来证明
  • 安装paddle_ocr踩坑(使用PP-OCRv5_server_rec)
  • ClickHouse查看数据库、表、列等元数据信息
  • 场外衍生品系统架构解密:TRS收益互换与场外个股期权的技术实现与业务创新
  • PQ:软件的UX - 快速分析并提议改进
  • MoonBit Pearls Vol.15: 使用 MoonBit 和 Wassette 构建安全的 WebAssembly 工具
  • 旅游机票网站建设2022年时事新闻摘抄
  • 【Linux网络】基于UDP的Socket编程,实现简单聊天室
  • IntelliJ IDEA 快捷键全解析与高效使用指南
  • openGauss 实战手册:gsql 常用命令、认证配置与运维工具全解
  • 如何入门Appium-移动端自动化测试框架?
  • 解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域
  • 【ZeroRange WebRTC】KVS WebRTC 示例中的 HTTP 通信安全说明
  • 软件测试之单元测试知识总结
  • 如何更改asp网站自定义产品顺序网站推广优化建设方案
  • 手机网站优点linux服务器wordpress
  • HPC性能新纪元!AWS Hpc7g实例: Graviton3E芯片开启200Gbps超算之旅
  • LeetCode 422 - 有效的单词方块
  • windows docker 配置镜像
  • 《数据结构:从0到1》-11-字符串与模式匹配
  • 【轴承故障诊断】基于SE-TCN和SE-TCN-SVM西储大学轴承故障诊断研究(Matlab代码实现)
  • 容器云质量加固方案
  • HarmonyOS OCR文字识别应用开发:深度指南与分布式实践
  • C语言 | LeetCode 414. 第三大的数
  • 360优化大师官方网站网站运营成本
  • 北京网站seo优化推广招聘网最新招聘信息网
  • 【代码】TorchCFM(Conditional Flow Matching library)代码入门
  • C++主流日志库深度剖析:从原理到选型的全维度指南
  • CAD/CASS 无法复制到剪贴板
  • C语言在线编译环境 | 轻松学习C语言编程,随时随地在线编程