C 语言指针精讲:数组与指针深度绑定 + 二级指针 + 指针数组全解析

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

    • [前景回顾:前两篇指针核心速记 📝](#前景回顾:前两篇指针核心速记 📝)
    • [一、数组名的本质:首元素地址的代名词 🔍](#一、数组名的本质:首元素地址的代名词 🔍)
      • [1. 数组名 = 首元素地址](#1. 数组名 = 首元素地址)
      • [2. 数组名的两个例外情况 🚨](#2. 数组名的两个例外情况 🚨)
    • [二、用指针访问数组:灵活操作的新姿势 ✨](#二、用指针访问数组:灵活操作的新姿势 ✨)
      • [1. 指针访问数组的多种方式](#1. 指针访问数组的多种方式)
      • [2. 核心原理:下标引用符`[]`的本质](#2. 核心原理:下标引用符[]的本质)
    • [三、一维数组传参的本质:传递的是地址 📤](#三、一维数组传参的本质:传递的是地址 📤)
      • [1. 代码演示:数组传参的陷阱](#1. 代码演示:数组传参的陷阱)
      • [2. 结论](#2. 结论)
    • [四、冒泡排序:指针思想的经典实战 💡](#四、冒泡排序:指针思想的经典实战 💡)
      • [1. 冒泡排序的核心逻辑](#1. 冒泡排序的核心逻辑)
      • [2. 完整代码实现](#2. 完整代码实现)
    • [五、二级指针:指向指针的指针 🎯](#五、二级指针:指向指针的指针 🎯)
      • [1. 二级指针的定义与理解](#1. 二级指针的定义与理解)
      • [2. 二级指针的解引用](#2. 二级指针的解引用)
    • [六、指针数组:存放指针的数组 📦](#六、指针数组:存放指针的数组 📦)
      • [1. 指针数组的定义](#1. 指针数组的定义)
      • [2. 指针数组的使用](#2. 指针数组的使用)
    • [七、指针数组模拟二维数组:巧妙的伪装 🎭](#七、指针数组模拟二维数组:巧妙的伪装 🎭)
      • [1. 模拟原理](#1. 模拟原理)
      • [2. 核心等价关系](#2. 核心等价关系)
    • [写在最后 📝](#写在最后 📝)

继前两篇指针基础与进阶内容后,本篇聚焦指针与数组的核心关联,同时讲解二级指针、指针数组的用法,带你打通指针与数组的任督二脉,彻底搞懂这些易混淆的知识点!

前景回顾:前两篇指针核心速记 📝

指针第一讲:从内存到运算,吃透指针核心逻辑
指针第二讲:const 修饰、野指针规避与传址调用

想要学好本篇内容,先巩固前两篇的关键要点:

  1. 指针基础 :地址就是指针,指针变量用于存储地址,通过&取地址、*解引用操作变量。
  2. const与指针const*左右位置不同,限制的对象不同,可保护数据不被意外修改。
  3. 野指针规避 :指针必须初始化、避免越界、不用时置NULL、不返回局部变量地址。
  4. 传址调用:通过传递地址,函数可直接修改主调函数中的变量,是指针的核心实用场景。

一、数组名的本质:首元素地址的代名词 🔍

在C语言中,数组名和指针有着密不可分的关系,核心结论先记住:数组名默认是数组首元素的地址。

1. 数组名 = 首元素地址

看代码验证:

c 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("arr    = %p\n", arr);
    printf("&arr[0] = %p\n", &arr[0]);
    printf("&arr   = %p\n", &arr);
    return 0;
}

运行结果中三个地址完全相同,这说明arr&arr[0]都指向数组第一个元素的地址,&arr虽然地址值相同,但含义却不一样。

2. 数组名的两个例外情况 🚨

数组名并非在所有场景下都代表首元素地址,有两个特殊场景,数组名表示整个数组:

  • 例外1sizeof(数组名) ------ 计算的是整个数组的字节大小
  • 例外2&数组名 ------ 取出的是整个数组的地址

我们用代码看区别:

c 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {0};
    printf("arr    = %p\n", arr);
    printf("arr+1  = %p\n", arr+1); // 跳过1个int,偏移4字节
    printf("&arr[0] = %p\n", &arr[0]);
    printf("&arr[0]+1 = %p\n", &arr[0]+1); // 跳过1个int,偏移4字节
    printf("&arr   = %p\n", &arr);
    printf("&arr+1 = %p\n", &arr+1); // 跳过整个数组,偏移40字节
    return 0;
}

关键区别

  • arr+1&arr[0]+1:指针类型是int*+1跳过1个int(4字节)。
  • &arr+1&arr的类型是int (*)[10](指向包含10个int的数组的指针),+1跳过整个数组(10×4=40字节)。

二、用指针访问数组:灵活操作的新姿势 ✨

既然数组名是首元素地址,那我们就可以用指针来遍历和操作数组,多种写法效果完全一致。

1. 指针访问数组的多种方式

c 复制代码
#include <stdio.h>
int main()
{
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    int sz = sizeof(arr)/sizeof(arr[0]);
    int *p = arr; // p指向数组首元素
    int i = 0;
    for(i=0; i<sz; i++)
    {
        // 以下四种写法等价
        printf("%d ", arr[i]);       // 下标法,最常用
        printf("%d ", *(arr+i));    // 数组名+偏移量
        printf("%d ", p[i]);        // 指针变量下标法
        printf("%d ", *(p+i));      // 指针变量+偏移量
        // 还有一种特殊写法(不推荐)
        // printf("%d ", i[arr]); // 等价于*(i+arr) = *(arr+i)
    }
    return 0;
}

2. 核心原理:下标引用符[]的本质

C语言规定:arr[i]等价于*(arr+i),这是下标引用符的本质。

  • arr是首元素地址,+i表示跳过i个元素的地址。
  • *解引用这个地址,就得到了第i个元素的值。

三、一维数组传参的本质:传递的是地址 📤

很多人会疑惑,为什么数组传参后,用sizeof计算的大小和原数组不一样?答案很简单:一维数组传参,本质传递的是数组首元素的地址。

1. 代码演示:数组传参的陷阱

c 复制代码
#include <stdio.h>
void test(int arr[]) // 这里的arr本质是int*指针,不是数组
{
    int sz2 = sizeof(arr)/sizeof(arr[0]);
    printf("sz2 = %d\n", sz2);
}
int main()
{
    int arr[10] = {0};
    int sz1 = sizeof(arr)/sizeof(arr[0]);
    printf("sz1 = %d\n", sz1); // 输出10,计算的是整个数组元素个数
    test(arr); 
    return 0;
}

运行结果

  • 32位系统:sz1=10sz2=1(指针大小4字节,4/4=1)
  • 64位系统:sz1=10sz2=2(指针大小8字节,8/4=2)

2. 结论

函数形参写int arr[]和写int* arr是完全等价的,形参接收的是一个指针变量,不是整个数组。

所以数组传参时,必须额外传递数组的元素个数,否则函数内部无法正确获取数组长度。

四、冒泡排序:指针思想的经典实战 💡

冒泡排序是入门级排序算法,核心思想是两两相邻元素比较,逆序则交换,每一趟排序都会让最大的元素浮到末尾。

1. 冒泡排序的核心逻辑

  • 数组有sz个元素,需要进行sz-1趟排序(最后一个元素无需再比较)。
  • i趟排序时,只需要比较前sz-1-i个元素(后面i个元素已经有序)。

2. 完整代码实现

c 复制代码
#include <stdio.h>
// 冒泡排序函数
void bubble_sort(int arr[], int sz)
{
    int i = 0;
    // 控制排序趟数
    for(i=0; i<sz-1; i++)
    {
        int j = 0;
        // 控制每一趟的比较次数
        for(j=0; j<sz-1-i; j++)
        {
            if(arr[j] > arr[j+1])
            {
                // 交换两个元素
                int tmp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = tmp;
            }
        }
    }
}
// 打印数组函数
void print_arr(int arr[], int sz)
{
    int j = 0;
    for(j=0; j<sz; j++)
    {
        printf("%d ", arr[j]);
    }
    printf("\n");
}
int main()
{
    int arr[] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr)/sizeof(arr[0]);
    printf("排序前:");
    print_arr(arr, sz);
    bubble_sort(arr, sz);
    printf("排序后:");
    print_arr(arr, sz);
    return 0;
}

优化技巧:如果某一趟排序没有发生任何交换,说明数组已经有序,可以提前结束排序,提升效率。

五、二级指针:指向指针的指针 🎯

当一个指针变量的地址被另一个指针存储时,这个指针就是二级指针,它的作用是操作一级指针变量本身。

1. 二级指针的定义与理解

c 复制代码
#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;  // pa是一级指针,存储a的地址
    int** ppa = &pa; // ppa是二级指针,存储pa的地址
    return 0;
}
  • a是整型变量,&aa的地址,类型是int*
  • pa是一级指针变量,&papa的地址,类型是int**

2. 二级指针的解引用

通过二级指针可以间接访问目标变量的值,需要两次解引用:

c 复制代码
#include <stdio.h>
int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = &pa;
    printf("%d\n", a);      // 直接访问a,输出10
    printf("%d\n", *pa);    // 一级解引用,输出10
    printf("%d\n", **ppa);  // 二级解引用,输出10
    return 0;
}

六、指针数组:存放指针的数组 📦

指针数组,本质是数组,只是数组中的每个元素都是指针类型的变量。

1. 指针数组的定义

格式:类型* 数组名[元素个数];

  • 例如int* parr[4];:数组parr有4个元素,每个元素的类型是int*(整型指针)。
  • 对比普通数组:int arr[4];的元素是int类型,char arr[4];的元素是char类型。

2. 指针数组的使用

c 复制代码
#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;
    // 指针数组存储四个整型变量的地址
    int* parr[4] = {&a, &b, &c, &d};
    int i = 0;
    for(i=0; i<4; i++)
    {
        printf("%d ", *(parr[i])); // 解引用每个元素,输出10 20 30 40
    }
    return 0;
}

七、指针数组模拟二维数组:巧妙的伪装 🎭

二维数组在内存中是连续存储的,而指针数组可以通过存储多个一维数组的首地址,来模拟二维数组的效果。

1. 模拟原理

定义三个一维数组,再用一个指针数组存储它们的首地址,通过两层循环访问:

c 复制代码
#include <stdio.h>
int main()
{
    int arr1[5] = {1,2,3,4,5};
    int arr2[5] = {2,3,4,5,6};
    int arr3[5] = {3,4,5,6,7};
    // 指针数组存储三个一维数组的首地址
    int* parr[3] = {arr1, arr2, arr3};
    int i = 0;
    for(i=0; i<3; i++)
    {
        int j = 0;
        for(j=0; j<5; j++)
        {
            // parr[i][j] 等价于 *(*(parr+i)+j)
            printf("%d ", parr[i][j]);
        }
        printf("\n");
    }
    return 0;
}

2. 核心等价关系

parr[i][j] = *(*(parr+i)+j)

  • parr+i:找到指针数组的第i个元素的地址。
  • *(parr+i):解引用得到第i个一维数组的首地址。
  • *(parr+i)+j:找到第i个一维数组的第j个元素的地址。
  • *(*(parr+i)+j):解引用得到目标元素的值。

💡 注意:指针数组模拟的二维数组,各一维数组在内存中不一定连续,而真正的二维数组内存是连续的。

写在最后 📝

本篇的核心是打通指针与数组的关联,记住这几个关键结论:

  1. 数组名默认是首元素地址,仅在sizeof(数组名)&数组名时代表整个数组。
  2. 一维数组传参本质传地址,函数形参是指针变量。
  3. 二级指针用于操作一级指针变量,指针数组是存放指针的数组。
  4. 指针数组可以模拟二维数组,但内存布局和真正的二维数组有区别。

指针的学习到这里已经覆盖了大部分核心知识点,后续可以结合字符串、函数指针等内容进一步深化。多敲代码、多调试,观察内存地址的变化,才能真正掌握指针的精髓!

相关推荐
再__努力1点2 小时前
【78】HOG+SVM行人检测实践指南:从算法原理到python实现
开发语言·人工智能·python·算法·机器学习·支持向量机·计算机视觉
leiming62 小时前
MobileNetV4 (MNv4)
开发语言·算法
llxxyy卢2 小时前
反序列化之PHP
开发语言·php
雨落在了我的手上2 小时前
C语言入门(三十一):预处理详解(1)
c语言·开发语言
BD_Marathon2 小时前
关于JS和TS选择的问题
开发语言·javascript·ecmascript
YJlio3 小时前
Python 一键拆分 PDF:按“目录/章节”建文件夹 + 每页单独导出(支持书签识别&正文识别)
开发语言·python·pdf
IT方大同3 小时前
C语言进制转化
c语言·开发语言
SELSL3 小时前
标准IO总结
linux·c语言·标准io·stdio·标准io与文件io的区别