深入理解指针(中):数组与指针的进阶之旅

深入理解指针(中):数组与指针的进阶之旅

从数组名到指针数组,从冒泡排序到二维数组传参,一步步揭开指针的神秘面纱

前言

在上一讲中,我们学习了指针的基础概念,包括内存地址、指针变量、const修饰符以及野指针的规避。这一讲我们将更进一步,探讨指针与数组的密切关系,这是C语言指针学习中的核心内容。


一、数组名的深入理解

1.1 数组名就是地址

先来看一段代码:

c 复制代码
#include <stdio.h>
int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    printf("&arr[0] = %p\n", &arr[0]);
    printf("arr = %p\n", arr);
    return 0;
}

输出结果中,两行打印的地址完全相同 。这说明:数组名就是数组首元素的地址

1.2 两个重要的例外

但是,有同学会发现下面的代码输出是40,而不是4或8:

c 复制代码
int arr[10] = {1,2,3,4,5,6,7,8,9,10};
printf("%d\n", sizeof(arr));  // 输出 40(10个int × 4字节)

数组名就是首元素地址这个规则有两个例外:

例外情况 说明
sizeof(数组名) 这里的数组名表示整个数组,计算的是整个数组的大小
&数组名 这里的数组名表示整个数组,取出的是整个数组的地址

1.3 &arr0、arr、&arr 的区别

通过指针运算来看它们的区别:

c 复制代码
printf("&arr[0] = %p\n", &arr[0]);
printf("&arr[0]+1 = %p\n", &arr[0]+1);  // +4字节
printf("arr = %p\n", arr);
printf("arr+1 = %p\n", arr+1);          // +4字节
printf("&arr = %p\n", &arr);
printf("&arr+1 = %p\n", &arr+1);        // +40字节(整个数组)
  • &arr[0]arr 都是首元素地址,+1 跳过 1个元素(4字节)
  • &arr 是整个数组的地址,+1 跳过 整个数组(40字节)

二、使用指针访问数组

有了前面的知识,我们可以很方便地使用指针来访问数组:

c 复制代码
#include <stdio.h>
int main() {
    int arr[10] = {0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = arr;  // p指向数组首元素
    
    // 输入
    for (int i = 0; i < sz; i++) {
        scanf("%d", p + i);  // 或 scanf("%d", arr + i);
    }
    
    // 输出 - 两种写法等价
    for (int i = 0; i < sz; i++) {
        printf("%d ", *(p + i));  // 指针方式
        // printf("%d ", p[i]);   // 数组方式,完全等价
    }
    return 0;
}

核心结论

  • p[i] 等价于 *(p + i)
  • arr[i] 等价于 *(arr + i)

编译器处理数组元素访问时,都是转换成"首元素地址 + 偏移量"的方式,然后解引用访问。


三、一维数组传参的本质

3.1 一个常见问题

c 复制代码
void test(int arr[]) {
    int sz2 = sizeof(arr) / sizeof(arr[0]);
    printf("sz2 = %d\n", sz2);  // 输出 1(而不是10)
}

int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz1 = sizeof(arr) / sizeof(arr[0]);
    printf("sz1 = %d\n", sz1);  // 输出 10
    test(arr);
    return 0;
}

为什么函数内部无法正确获取数组长度?

3.2 本质原因

数组传参传递的是数组首元素的地址,而不是整个数组。

  • test(arr) 中的 arr 是首元素地址
  • 形参 int arr[] 本质上是一个指针,等价于 int* arr
  • sizeof(arr) 在函数内部计算的是指针的大小(4或8字节),而不是数组的大小
c 复制代码
// 以下两种写法完全等价
void test(int arr[]) { }   // 写成数组形式
void test(int* arr) { }    // 写成指针形式(本质)

结论:一维数组传参时,形参可以写成数组形式,也可以写成指针形式。但需要在函数外部计算好数组长度,作为参数传入。


四、冒泡排序

冒泡排序的核心思想是:两两相邻元素进行比较,每一轮将最大(或最小)的元素"冒泡"到末尾。

c 复制代码
void bubble_sort(int arr[], int sz) {
    for (int i = 0; i < sz - 1; i++) {
        int flag = 1;  // 假设已经有序
        for (int j = 0; j < sz - i - 1; j++) {
            if (arr[j] > arr[j + 1]) {
                flag = 0;  // 发生交换,说明无序
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
        if (flag == 1) {  // 这一趟没有交换,已经有序
            break;
        }
    }
}

int main() {
    int arr[] = {3, 1, 7, 5, 8, 9, 0, 2, 4, 6};
    int sz = sizeof(arr) / sizeof(arr[0]);
    bubble_sort(arr, sz);
    for (int i = 0; i < sz; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

优化点 :使用 flag 标记,如果某一趟没有发生任何交换,说明数组已经有序,可以提前结束排序。


五、二级指针

指针变量也是变量,也有自己的地址。存放指针变量地址的指针,就是二级指针

c 复制代码
int a = 10;
int* pa = &a;   // pa是一级指针
int** ppa = &pa; // ppa是二级指针

二级指针的运算

c 复制代码
*ppa == pa;           // 解引用ppa,得到pa
**ppa == *pa == a;    // 两次解引用,得到a

*ppa = &b;            // 等价于 pa = &b
**ppa = 30;           // 等价于 a = 30

六、指针数组

6.1 什么是指针数组?

指针数组是存放指针的数组

类比理解:

  • 整型数组:存放整型的数组 → int arr[10]
  • 字符数组:存放字符的数组 → char arr[10]
  • 指针数组:存放指针的数组 → int* arr[10]
c 复制代码
int a = 10, b = 20, c = 30;
int* parr[3] = {&a, &b, &c};  // 每个元素都是int*类型

6.2 指针数组模拟二维数组

利用指针数组可以模拟出二维数组的效果:

c 复制代码
#include <stdio.h>
int main() {
    int arr1[] = {1, 2, 3, 4, 5};
    int arr2[] = {2, 3, 4, 5, 6};
    int arr3[] = {3, 4, 5, 6, 7};
    
    // parr是一个指针数组,每个元素指向一个一维数组
    int* parr[3] = {arr1, arr2, arr3};
    
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 5; j++) {
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

注意 :这种方式模拟的二维数组,各行在内存中不一定是连续的,而真正的二维数组是连续存放的。


七、字符指针变量

7.1 基本用法

c 复制代码
char ch = 'w';
char* pc = &ch;
*pc = 'a';  // 修改ch的值为'a'

7.2 指向字符串的字符指针

c 复制代码
const char* pstr = "hello bit.";
printf("%s\n", pstr);  // 输出:hello bit.

重点理解pstr 中存放的不是整个字符串,而是字符串首字符 'h' 的地址

7.3 经典笔试题

c 复制代码
int main() {
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";
    
    if (str1 == str2)
        printf("same\n");
    else
        printf("not same\n");  // 输出:not same
        
    if (str3 == str4)
        printf("same\n");      // 输出:same
    else
        printf("not same\n");
    return 0;
}

原因分析

  • str1str2 是数组,用常量字符串初始化时会开辟不同的内存空间
  • str3str4 是指针,指向同一个常量字符串(存储在只读数据区),所以地址相同

八、数组指针变量

8.1 什么是数组指针?

数组指针是指向数组的指针变量

c 复制代码
int (*p)[10];  // p是一个指针,指向一个包含10个int元素的数组

注意区分

  • int* p[10] → 指针数组(有10个元素,每个是int*)
  • int (*p)[10] → 数组指针(1个指针,指向int10数组)

8.2 数组指针的初始化

c 复制代码
int arr[10] = {0};
int (*p)[10] = &arr;  // p指向整个数组

九、二维数组传参的本质

9.1 二维数组的内存模型

二维数组可以看做是"每个元素是一维数组"的数组:

  • 二维数组的首元素是第一行(一个一维数组)
  • 数组名 arr 表示第一行的地址,类型是 int(*)[5]

9.2 传参的两种写法

c 复制代码
// 写法1:形参写成二维数组
void test1(int arr[3][5], int r, int c) { }

// 写法2:形参写成数组指针
void test2(int (*p)[5], int r, int c) { }

int main() {
    int arr[3][5] = {{1,2,3,4,5}, {2,3,4,5,6}, {3,4,5,6,7}};
    test1(arr, 3, 5);
    test2(arr, 3, 5);  // 完全等价
    return 0;
}

访问二维数组元素*(*(p + i) + j) 等价于 p[i][j]


十、函数指针变量

10.1 函数的地址

函数也有地址,函数名就是函数的地址:

c 复制代码
void test() {
    printf("hehe\n");
}

int main() {
    printf("%p\n", test);   // 输出函数地址
    printf("%p\n", &test);  // 同样输出函数地址
    return 0;
}

10.2 函数指针的定义和使用

c 复制代码
int Add(int x, int y) {
    return x + y;
}

int main() {
    int (*pf)(int, int) = Add;  // pf是函数指针变量
    
    // 两种调用方式等价
    printf("%d\n", (*pf)(2, 3));  // 5
    printf("%d\n", pf(3, 5));     // 8
    return 0;
}

函数指针类型解析

复制代码
int (*pf)(int x, int y)
│    │      │
│    │      └── 参数类型
│    └── 指针变量名
└── 返回类型

10.3 typedef 简化函数指针

c 复制代码
// 重命名函数指针类型
typedef void(*pfun_t)(int);  // pfun_t 是 void(*)(int) 的别名

// 使用
pfun_t signal(int, pfun_t);

十一、函数指针数组(转移表)

11.1 定义

函数指针数组是存放函数指针的数组

c 复制代码
int (*parr[5])(int, int);  // parr是一个数组,有5个元素,每个元素是函数指针

11.2 应用:计算器的转移表实现

传统计算器使用 switch-case 语句,代码冗长。使用函数指针数组(转移表)可以让代码更简洁:

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;
    // 转移表:下标0对应exit,1对应add,2对应sub,3对应mul,4对应div
    int (*p[5])(int, int) = { NULL, add, sub, mul, div };
    
    do {
        printf("1:add 2:sub 3:mul 4:div 0:exit\n");
        printf("请选择:");
        scanf("%d", &input);
        
        if (input >= 1 && input <= 4) {
            printf("输入操作数:");
            scanf("%d %d", &x, &y);
            int ret = p[input](x, y);  // 通过转移表调用对应函数
            printf("ret = %d\n", ret);
        } else if (input == 0) {
            printf("退出程序\n");
        } else {
            printf("输入有误\n");
        }
    } while (input);
    
    return 0;
}

转移表的优势

  • 代码更简洁,易于扩展
  • 避免了冗长的 switch-case 语句
  • 新增功能只需添加函数和更新转移表

总结

知识点 核心要点
数组名 通常是首元素地址;sizeof(arr)&arr是例外
指针访问数组 arr[i] == *(arr + i) == p[i] == *(p + i)
一维数组传参 传递的是首元素地址,形参可以写成数组或指针
冒泡排序 两两比较,优化:使用flag标记提前结束
二级指针 存放指针变量的地址,**ppa访问原始变量
指针数组 存放指针的数组,int* arr[10]
字符指针 指向常量字符串时,多个指针指向同一地址
数组指针 指向数组的指针,int (*p)[10]
二维数组传参 形参用数组指针 int (*p)[列数] 接收
函数指针 存放函数地址,int (*pf)(int,int)
转移表 函数指针数组,实现高效的多路分支

下一讲我们将继续深入,探讨 const 与指针的更多细节、指针运算的高级应用,以及函数指针数组等更加进阶的内容!


欢迎大家点赞收藏并在评论区留言!

相关推荐
朔北之忘 Clancy2 小时前
2026 年 3 月青少年软编等考 C/C++ 一级真题解析
c语言·开发语言·c++·青少年编程·题解·考级
小成202303202652 小时前
C++~01面向对象基础
开发语言·c++
郝学胜-神的一滴3 小时前
干货版《算法导论》07:递归视角下的选择排序与归并排序
java·数据结构·c++·python·程序人生·算法·排序算法
暖焰核心3 小时前
C++内存管理和模板初阶
开发语言·c++
Irissgwe4 小时前
c++智能指针
开发语言·c++
西梅汁4 小时前
C++ 线程间通信(一)
c++
hautcyh4 小时前
C++new和delete
c++
不会C语言的男孩4 小时前
C++ Primer Plus 第10章:对象和类
开发语言·c++
不会C语言的男孩4 小时前
C++ Primer Plus 第11章:使用类
开发语言·c++