【C语言】深入理解指针(三)

前言:

在前两讲中,我们掌握了指针的基础概念、与数组的绑定关系,以及二级指针、指针数组等进阶用法。这一讲,我们将聚焦指针与字符、函数的结合,从字符指针的特殊应用,到数组指针的深度解析,再到函数指针与函数指针数组的实战(eg:转移表),逐步揭开C语言中"指针操作复杂数据"的核心逻辑。


一、字符指针变量

字符指针(char*)是最常用的指针类型之一,但其用法不仅限于"指向单个字符"------更常见的场景是指向常量字符串

1.1 字符指针的两种基础用法

用法1:指向单个字符

与整型指针类似,字符指针可指向单个字符变量,通过解引用修改字符值

c 复制代码
#include <stdio.h>
int main() {
    char ch = 'w';
    char* pc = &ch; // 字符指针指向ch的地址
    *pc = 'a';      // 解引用修改ch的值
    printf("ch = %c\n", ch); // 输出'a'
    return 0;
}
用法2:指向常量字符串

更常见的用法是让字符指针指向常量字符串的首地址

(注意:不是将整个字符串存入指针,而是存储字符串首字符的地址。)

c 复制代码
#include <stdio.h>
int main() {
    // 本质:将"hello word."首字符'h'的地址存入pstr
    const char* pstr = "hello word."; 
    // %s打印:从pstr指向的地址开始,直到'\0'结束
    printf("%s\n", pstr); // 输出"hello word."
    return 0;
}

关键点

pstr存储的是首字符地址,而非整个字符串。
printf("%s", pstr)的逻辑是"从首地址开始遍历,直到遇到'\0'终止符"。

1.2 字符指针与数组的区别

我们来看一下以下的程序,以理解字符指针指向常量字符串与数组初始化的差异:

c 复制代码
#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* p110 指针数组 p1先与[]结合,是数组,元素类型为int*
int (*p2)10 数组指针 p2先与*结合,是指针,指向int[10]数组

识别技巧

  • ( ) 的是数组指针(先指针后数组)
  • 不带 ( ) 的是指针数组(先数组后指针)

2.2 数组指针的初始化

数组指针用于存储整个数组的地址 ,而非首元素地址,获取整个数组地址需用**&数组名**。

示例代码:

c 复制代码
#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 arr35 = {{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],编译器会自动解析为数组指针:

c 复制代码
#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的数组指针":

c 复制代码
#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:验证函数地址

先通过代码确认函数有地址:

c 复制代码
#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:定义并初始化函数指针

以加法函数为例,定义函数指针并存储其地址:

c 复制代码
#include <stdio.h>
// 加法函数
int Add(int x, int y) {
    return x + y;
}

int main() {
    // 函数指针pf:指向Add函数(参数和返回值类型匹配)
    // 两种初始化方式均合法:Add或&Add
    int (*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解析为函数地址。

示例代码:

c 复制代码
#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); // 输出5
    printf("ret2 = %d\n", ret2); // 输出8
    return 0;
}

4.3 简化复杂函数指针

函数指针类型复杂(如void(*)(int)),可通过typedef重命名为简洁的类型名,提升代码可读性。

typedef重命名规则:
  • 普通类型:typedef原类型、新类型名(如typedef unsigned int uint);
  • 指针类型:新类型名需放在*右侧(如typedef int* ptr_t);
  • 函数指针类型:新类型名放在*右侧(如typedef void(*pfun_t)(int))。
示例:简化signal函数声明

C标准库中的signal函数声明非常复杂:

c 复制代码
// 原声明:返回值是void(*)(int),参数是int和void(*)(int)
void (*signal(int, void(*)(int)))(int);

typedef重命名后:

c 复制代码
// 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*parr14(int 先与[]结合是数组,但元素类型int* (int)非法
int (*)(int) parr24 语法错误,数组名需紧跟[]
`int (*parr34)(int) 正确:parr3是数组,元素是函数指针

5.2 实战

函数指针数组的核心用途是转移表 (替代switchif-else),减少代码冗余,提升可维护性。以"计算器"为例,对比两种实现方式。

方式1:传统switch实现(冗余)
c 复制代码
#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

c 复制代码
#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-div
    int (*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判断更直接。

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

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

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜10 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa10 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio