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. 指针数组可以模拟二维数组,但内存布局和真正的二维数组有区别。

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

相关推荐
Dovis(誓平步青云)3 分钟前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
isyangli_blog8 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008119 小时前
FastAPI APIRouter
开发语言·python
Benszen9 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆9 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木9 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充9 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~9 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball6169 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
春生野草10 小时前
反射、Tomcat执行
java·开发语言