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

前言:

在上一讲,我们掌握了指针的基础概念、类型意义与运算规则,而C语言中指针的核心应用场景,离不开与数组的结合。这一讲我们将聚焦指针与数组的关系 ,从数组名的本质入手,拆解数组传参的底层逻辑,再延伸到二级指针、指针数组等进阶知识点,最终通过实战案例(如冒泡排序、模拟二维数组)帮你打通"指针+数组"的应用链路。


一、数组名的理解

很多初学者会误以为"数组名就是首元素地址"------这个说法不完全错,但有两个关键例外。理解数组名的真实含义,是掌握"指针操作数组"的前提。

1.1 数组名的默认含义

先看一个简单实验:打印数组名与首元素地址,观察结果:

c 复制代码
#include <stdio.h>
int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 打印首元素地址 &arr[0]
    printf("&arr[0] = %p\n", &arr[0]); 
    // 打印数组名 arr
    printf("arr     = %p\n", arr);     
    return 0;
}

输出结果(地址值可能不同,但两者必相等):

复制代码
&arr[0] = 004FF9CC
arr     = 004FF9CC

这说明:默认情况下,数组名等价于数组首元素的地址 。我们可以直接将数组名赋值给指针变量(如int* p = arr),本质就是让指针指向数组首元素。

1.2 数组名的两个"例外"

当数组名出现在以下两种场景时,它不再是"首元素地址",而是代表整个数组

例外1:sizeof(数组名)------计算整个数组的大小

如果直接对数组名使用sizeof,得到的是"整个数组占用的字节数",而非指针大小(4/8字节)。例如:

c 复制代码
#include <stdio.h>
int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // int占4字节,10个元素共40字节
    printf("sizeof(arr) = %zd\n", sizeof(arr)); 
    return 0;
}

输出结果sizeof(arr) = 40(若int为4字节)。

例外2:&数组名------取出整个数组的地址

&arr(取数组名的地址)得到的是"整个数组的地址",它与"首元素地址"的值相同,但意义完全不同。我们通过"地址+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[0]+1  = %p\n", &arr[0]+1);  // 首元素地址+1(跳1个int)
    printf("arr        = %p\n", arr);        // 等价于首元素地址
    printf("arr+1      = %p\n", arr+1);      // 首元素地址+1(跳1个int)
    printf("&arr       = %p\n", &arr);       // 整个数组的地址(值与首元素地址相同)
    printf("&arr+1     = %p\n", &arr+1);     // 整个数组的地址+1(跳整个数组)
    return 0;
}

输出结果分析 (假设首地址为0077F820):

表达式 地址值 地址差原因
&arr[0] 0077F820 首元素地址
&arr[0]+1 0077F824 跳1个int(4字节)
arr 0077F820 等价于首元素地址
arr+1 0077F824 跳1个int(4字节)
&arr 0077F820 整个数组的地址(值相同)
&arr+1 0077F848 跳整个数组(10×4=40字节)

关键结论:

  • arr&arr[0]是"首元素地址",+1跳过1个元素;
  • &arr是"整个数组地址",+1跳过整个数组(大小由数组长度决定)。

二、使用指针访问数组

既然数组名默认是首元素地址,我们就可以用指针灵活访问数组元素,甚至实现与arr[i]完全等价的操作。

2.1 指针访问数组的核心等价关系

C语言编译器在处理数组元素访问时,会将arr[i]自动转换为*(arr + i)------本质是"首元素地址 + 偏移量i,再解引用"。

同理,若定义指针int* p = arr(p指向首元素),则:

  • p[i] 等价于 *(p + i)
  • arr[i] 等价于 *(arr + i)
  • 甚至 i[arr] 也等价于 *(i + arr)(语法允许,但不推荐,可读性差)

用代码验证这一关系:

c 复制代码
#include <stdio.h>
int main() {
    int arr[5] = {10,20,30,40,50};
    int* p = arr; // p指向数组首元素
    
    // 以下四种访问方式完全等价,均输出30(第3个元素,下标2)
    printf("arr[2]   = %d\n", arr[2]);
    printf("*(arr+2) = %d\n", *(arr+2));
    printf("p[2]     = %d\n", p[2]);
    printf("*(p+2)   = %d\n", *(p+2));
    return 0;
}

输出结果 :四种方式均输出30

2.2 用指针完成数组的输入与输出

利用指针的灵活性,我们可以脱离数组下标,直接通过指针偏移实现数组的输入和输出:

c 复制代码
#include <stdio.h>
int main() {
    int arr[10] = {0};
    int sz = sizeof(arr) / sizeof(arr[0]);
    int* p = arr; // 指针指向数组首元素
    
    // 1. 用指针输入数组元素
    printf("请输入10个整数:\n");
    for (int i = 0; i < sz; i++) {
        // p+i 是第i个元素的地址,等价于 &arr[i]
        scanf("%d", p + i); 
    }
    
    // 2. 用指针输出数组元素
    printf("数组元素为:\n");
    for (int i = 0; i < sz; i++) {
        // *(p+i) 是第i个元素的值,等价于 arr[i]
        printf("%d ", *(p + i)); 
    }
    return 0;
}

逻辑说明p+i计算第i个元素的地址(偏移i个int大小),scanf通过地址写入值,printf通过解引用读取值,完全替代下标操作。


三、一维数组传参的本质

很多初学者会疑惑:"为什么数组传给函数后,在函数内用sizeof求不出数组大小?"答案藏在数组传参的底层本质里。

先看一个反例:尝试在函数内计算数组元素个数,结果会出乎我们意料:

c 复制代码
#include <stdio.h>
// 函数参数为数组形式
void test(int arr[]) {
    // 期望计算数组大小,实际得到的是指针大小(4或8字节)
    int sz2 = sizeof(arr) / sizeof(arr[0]); 
    printf("函数内 sz2 = %d\n", sz2);
}

int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    // 外部计算数组大小,正确得到10
    int sz1 = sizeof(arr) / sizeof(arr[0]); 
    printf("函数外 sz1 = %d\n", sz1);
    test(arr); // 数组传参
    return 0;
}

输出结果

函数外 sz1 = 10

函数内 sz2 = 1 ( 32位平台:4/4=1;64位平台:8/4=2)

为什么会出现这种情况呢?原因很简单:一维数组传参时,传递的不是整个数组,而是数组首元素的地址 。函数形参列表中的int arr[]只是"语法糖"------编译器会自动将其解析为int* arr(指针变量)。

也就是说,以下两种函数声明完全等价:

c 复制代码
// 等价写法1:数组形式(语法糖)
void test(int arr[]); 
// 等价写法2:指针形式(本质)
void test(int* arr); 

因此,函数内的sizeof(arr)计算的是"指针变量的大小 "(32位4字节,64位8字节),而非数组大小。这也意味着:函数内无法通过形参获取数组长度,必须单独传递数组大小sz

正确的数组传参示例

修正上述代码,通过单独传递sz实现数组处理:

c 复制代码
#include <stdio.h>
// 形参:指针(接收首元素地址) + sz(数组大小)
void print_arr(int* arr, int sz) {
    for (int i = 0; i < sz; i++) {
        printf("%d ", *(arr + i));
    }
}

int main() {
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr) / sizeof(arr[0]);
    print_arr(arr, sz); // 传递首元素地址和大小
    return 0;
}

输出结果:(正确打印数组)

1 2 3 4 5 6 7 8 9 10


四、冒泡排序

冒泡排序是经典的排序算法,核心思想是"两两相邻元素比较,逆序则交换"。结合指针操作数组,我们可以高效实现冒泡排序,并优化其性能。

基础版冒泡排序

思路:

  1. 外层循环控制"排序趟数":n个元素需要n-1趟(每趟确定1个最大值);
  2. 内层循环控制"每趟比较次数":第i趟需要比较n-i-1次(已确定的最大值无需再比较);
  3. 用指针访问数组元素,逆序则交换

代码实现:

c 复制代码
#include <stdio.h>
// 冒泡排序:升序排列
void bubble_sort(int* arr, int sz) {
    // 1. 外层循环:控制趟数(sz-1趟)
    for (int i = 0; i < sz - 1; i++) {
        // 2. 内层循环:控制每趟比较次数(sz-i-1次)
        for (int j = 0; j < sz - i - 1; j++) {
            // 指针访问:arr[j] 等价于 *(arr+j)
            if (*(arr + j) > *(arr + j + 1)) {
                // 交换两个元素
                int tmp = *(arr + j);
                *(arr + j) = *(arr + j + 1);
                *(arr + j + 1) = tmp;
            }
        }
    }
}

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;
}

输出结果

0 1 2 3 4 5 6 7 8 9

优化版冒泡排序(加上了提前终止)

基础版存在冗余:若某一趟未发生任何交换,说明数组已有序,无需继续后续趟数。我们可以用一个flag标记是否发生交换,实现优化:

c 复制代码
#include <stdio.h>
void bubble_sort(int* arr, int sz) {
    for (int i = 0; i < sz - 1; i++) {
        int flag = 1; // 1:假设本趟有序,0:本趟无序
        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;
        }
    }
}

// main函数与基础版一致
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;
}

优化效果 :若数组本身有序(如[1,2,3,4,5]),仅需1趟比较就终止,时间复杂度从O(n²)降至O(n)。(时间复杂度将会在未来的数据结构部分向大家进行介绍,敬请期待)


五、二级指针

指针变量也是变量,变量就有地址------那么"指针变量的地址"存放在哪里?答案是二级指针(指向指针的指针)。

5.1 定义与内存关系

定义格式:

数据类型 ** 二级指针名`= &一级指针名。

例如:

c 复制代码
#include <stdio.h>
int main() {
    int a = 10;        // 普通变量
    int* pa = &a;      // 一级指针:指向a,存储a的地址
    int** ppa = &pa;   // 二级指针:指向pa,存储pa的地址
    return 0;
}

内存关系图解(地址为示例值):

变量 变量值(地址/数据) 存储地址
a 10 0x0012ff50
pa 0x0012ff50(a的地址) 0x0012ff48
ppa 0x0012ff48(pa的地址) 0x0012ff40

5.2 核心运算

二级指针有两种关键运算:*ppa**ppa,分别对应不同的访问层级:

  1. *ppa:访问一级指针pa

    对二级指针解引用一次,得到的是一级指针pa的值(即a的地址),等价于pa

    例如:*ppa = &b 等价于 pa = &b(让pa指向新变量b)。

  2. **ppa:访问普通变量a

    对二级指针解引用两次,先通过*ppa找到pa,再通过*pa找到a,等价于a

    例如:**ppa = 30 等价于 *pa = 30,最终等价于 a = 30

我们用代码来验证一下:

c 复制代码
#include <stdio.h>
int main() {
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    
    printf("a = %d\n", a);        // 输出10
    printf("*pa = %d\n", *pa);    // 输出10(通过一级指针访问a)
    printf("**ppa = %d\n", **ppa); // 输出10(通过二级指针访问a)
    
    // 修改a的值:通过二级指针
    **ppa = 30;
    printf("修改后 a = %d\n", a);  // 输出30
    
    // 让pa指向新变量b:通过二级指针
    int b = 20;
    *ppa = &b;
    printf("*pa = %d\n", *pa);    // 输出20(pa现在指向b)
    return 0;
}

输出结果

a = 10

*pa = 10

**ppa = 10

修改后 a = 30

*pa = 20


六、指针数组

类比"整型数组(存放int)""字符数组(存放char)",指针数组是"存放指针的数组"------数组的每个元素都是一个指针(地址)。

指针数组的定义

定义格式:

数据类型* 数组名[数组长度]

例如:int* parr[3] 表示"一个长度为3的数组,每个元素是int*类型的指针"。

数组结构示意

数组元素 元素类型 存储内容(示例)
parr[0] int* 0x0012ff50(某int的地址)
parr[1] int* 0x0012ff60(某int的地址)
parr[2] int* 0x0012ff70(某int的地址)

七、指针数组模拟二维数组

普通二维数组(如int arr[3][5])的行是连续存储的,而指针数组可以通过"存储多个一维数组的首地址",模拟出二维数组的访问效果(但行不连续)。

实现步骤:
  1. 定义3个一维数组(模拟二维数组的3行);
  2. 定义指针数组parr,存储这3个一维数组的首地址;
  3. 通过双重循环访问:parr[i][j] 等价于 *(parr[i] + j)(访问第i行第j列元素)。

代码实现:

c 复制代码
#include <stdio.h>
int main() {
    // 3个一维数组(模拟二维数组的3行)
    int arr1[] = {1,2,3,4,5};
    int arr2[] = {2,3,4,5,6};
    int arr3[] = {3,4,5,6,7};
    
    // 指针数组:存储3个一维数组的首地址
    int* parr[3] = {arr1, arr2, arr3};
    
    // 双重循环:模拟二维数组访问
    for (int i = 0; i < 3; i++) { // 控制"行"(3行)
        for (int j = 0; j < 5; j++) { // 控制"列"(5列)
            // parr[i][j] 等价于 *(parr[i] + j)
            printf("%d ", parr[i][j]);
        }
        printf("\n"); // 每行结束换行
    }
    return 0;
}

输出结果(模拟3行5列的二维数组):

1 2 3 4 5

2 3 4 5 6

3 4 5 6 7

关键说明:
  • 这不是"真正的二维数组":普通二维数组的行是连续存储的,而指针数组的"行"(即3个一维数组)在内存中可能不连续;
  • 优势:灵活度高,可将不同长度的一维数组"组合"成类似二维数组的结构,适合处理不规则数据。

通过这一讲,我们打通了"指针+数组"的应用链路------从基础概念到实战排序,再到进阶的二级指针与指针数组,这些知识点是C语言高效操作内存的核心。下一讲我们将进一步探索指针与函数、字符串的结合,敬请期待!

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

相关推荐
m0_748248022 小时前
C++中的位运算符:与、或、异或详解
java·c++·算法
沐浴露z2 小时前
详解【限流算法】:令牌桶、漏桶、计算器算法及Java实现
java·算法·限流算法
兜有米啦2 小时前
python练习题3
开发语言·python
王哈哈^_^2 小时前
【完整源码+数据集】草莓数据集,yolov8草莓成熟度检测数据集 3207 张,草莓成熟度数据集,目标检测草莓识别算法系统实战教程
人工智能·算法·yolo·目标检测·计算机视觉·视觉检测·毕业设计
Wzx1980122 小时前
go基础语法练习
开发语言·后端·golang
程序员东岸3 小时前
数据结构杂谈:双向链表避坑指南
数据结构·链表
忧郁的蛋~3 小时前
.NET异步编程中内存泄漏的终极解决方案
开发语言·前端·javascript·.net
2301_795167203 小时前
玩转Rust高级应用. ToOwned trait 提供的是一种更“泛化”的Clone 的功能,Clone一般是从&T类型变量创造一个新的T类型变量
开发语言·后端·rust
油泼辣子多加3 小时前
【实战】自然语言处理--长文本分类(3)HAN算法
算法·自然语言处理·分类