【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* p1[10] 指针数组 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 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],编译器会自动解析为数组指针:

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*parr1[4](int 先与[]结合是数组,但元素类型int* (int)非法
int (*)(int) parr2[4] 语法错误,数组名需紧跟[]
`int (*parr3[4])(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判断更直接。

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

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

相关推荐
luoganttcc2 小时前
已知 空间 三个 A,B C 点 ,求 顺序 经过 A B C 三点 圆弧 轨迹 ,给出 python 代码 并且 画出图像
c语言·开发语言·python
今天的砖很烫2 小时前
ThreadLocal 结构设计的精妙之处
java·开发语言
麦麦鸡腿堡3 小时前
Java_HashMap底层机制与原码解读
java·开发语言·jvm
草莓熊Lotso3 小时前
C++ 抽象类与多态原理深度解析:从纯虚函数到虚表机制(附高频面试题)
java·运维·服务器·开发语言·c++·人工智能·笔记
再玩一会儿看代码3 小时前
Ken的Java学习之路——Java中关于面向对象
java·开发语言·经验分享·python·学习
迦蓝叶3 小时前
通过 HelloWorld 深入剖析 JVM 启动过程
java·开发语言·jvm·aot·启动过程·helloword·leyden
m0_565611133 小时前
Java Stream流操作全解析
java·开发语言·算法
_OP_CHEN3 小时前
从零开始的Qt开发指南:(三)信号与槽的概念与使用
开发语言·c++·qt·前端开发·qt creator·信号与槽·gui开发
乄夜3 小时前
嵌入式面试高频!!!C语言(十四) STL(嵌入式八股文)
c语言·c++·stm32·单片机·mcu·面试·51单片机