
🏠个人主页:黎雁
🎬作者简介: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 修饰、野指针规避与传址调用
想要学好本篇内容,先巩固前两篇的关键要点:
- 指针基础 :地址就是指针,指针变量用于存储地址,通过
&取地址、*解引用操作变量。 - const与指针 :
const在*左右位置不同,限制的对象不同,可保护数据不被意外修改。 - 野指针规避 :指针必须初始化、避免越界、不用时置
NULL、不返回局部变量地址。 - 传址调用:通过传递地址,函数可直接修改主调函数中的变量,是指针的核心实用场景。
一、数组名的本质:首元素地址的代名词 🔍
在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. 数组名的两个例外情况 🚨
数组名并非在所有场景下都代表首元素地址,有两个特殊场景,数组名表示整个数组:
- 例外1 :
sizeof(数组名)------ 计算的是整个数组的字节大小 - 例外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=10,sz2=1(指针大小4字节,4/4=1) - 64位系统:
sz1=10,sz2=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是整型变量,&a是a的地址,类型是int*。pa是一级指针变量,&pa是pa的地址,类型是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):解引用得到目标元素的值。
💡 注意:指针数组模拟的二维数组,各一维数组在内存中不一定连续,而真正的二维数组内存是连续的。
写在最后 📝
本篇的核心是打通指针与数组的关联,记住这几个关键结论:
- 数组名默认是首元素地址,仅在
sizeof(数组名)和&数组名时代表整个数组。 - 一维数组传参本质传地址,函数形参是指针变量。
- 二级指针用于操作一级指针变量,指针数组是存放指针的数组。
- 指针数组可以模拟二维数组,但内存布局和真正的二维数组有区别。
指针的学习到这里已经覆盖了大部分核心知识点,后续可以结合字符串、函数指针等内容进一步深化。多敲代码、多调试,观察内存地址的变化,才能真正掌握指针的精髓!