C语言指针2:数组名、二级指针、指针数组,一次把“指针和数组“讲透
个人主页https://blog.csdn.net/2501_93119704?spm1000.2115.3001.5343欢迎大佬们互三写在前面指针和数组其实是一家人上一讲我们把地址和指针变量搞明白了。这一讲很多人会栽在两个地方一是数组名到底是不是指针一会儿说是、一会儿又说 sizeof 例外二是二级指针、指针数组这些套娃到底怎么回事。其实只要抓住一句话——数组名在绝大多数情况下就是首元素地址——后面的指针访问数组、数组传参、指针数组就全都串起来了。这篇复盘我按先吃透数组名 → 再用指针玩数组 → 最后上二级指针和指针数组的顺序把这一讲重新捋了一遍配上图争取一次讲透。一、数组名的理解1.1 数组名就是首元素的地址上一讲用指针访问数组时我们都是这样写的int arr[10] {1,2,3,4,5,6,7,8,9,10}; int *p arr[0];用arr[0]拿到数组第一个元素的地址。但数组名arr本身就是地址而且就是首元素的地址。做个测试#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 但是有两个例外有同学马上会问如果数组名是首元素地址那下面的代码怎么解释#include stdio.h int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; printf(%d\n, sizeof(arr)); // 输出 40不是 4/8 return 0; }输出是4010 个 int × 4 字节。如果arr是首元素地址sizeof(arr)应该输出 432 位或 864 位才对。所以数组名是首元素地址这个说法基本对但有两个例外sizeof(数组名)sizeof里单独放数组名这里的数组名表示整个数组算的是整个数组的大小字节。数组名这里的数组名表示整个数组取出的是整个数组的地址整个数组的地址和首元素地址1 时的表现是有区别的马上讲。除此之外任何地方使用数组名数组名都表示首元素的地址。1.3 arr 和 arr 到底差在哪有好奇的同学再试这段#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); printf(arr %p\n, arr); return 0; }三个打印结果一模一样都是同一个地址。那arr和arr有啥区别看1就露馅了#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); printf(arr %p\n, arr); printf(arr1 %p\n, arr1); printf(arr %p\n, arr); printf(arr1 %p\n, arr1); return 0; }输出结果arr[0] 0077F820 arr[0]1 0077F824 arr 0077F820 arr1 0077F824 arr 0077F820 arr1 0077F848看差值arr[0]和arr[0]1差4 个字节arr和arr1也差 4 字节——因为arr[0]和arr都是首元素地址1就是跳过一个元素一个 int 是 4 字节。arr和arr1差40 个字节——因为arr是整个数组的地址1是跳过整个数组10 个 int 40 字节。 结论数组名是首元素地址但有 2 个例外——sizeof(数组名)表示整个数组数组名表示整个数组的地址。arr1跳过整个数组arr1只跳过首元素。二、使用指针访问数组有了数组名的基础再结合数组连续存放的特点就能方便地用指针访问数组了#include stdio.h int main() { int arr[10] {0}; int i 0; int sz sizeof(arr)/sizeof(arr[0]); int* p arr; // arr 就是首元素地址直接赋给 p // 输入 for(i 0; i sz; i) { scanf(%d, pi); // scanf(%d, arri); // 也可以这样写 } // 输出 for(i 0; i sz; i) { printf(%d , *(pi)); } return 0; }既然arr是首元素地址可以赋给p那arr和p在这里是等价的。那arr[i]能访问元素p[i]能不能也访问呢把上面输出里的*(pi)换成p[i]一样能正常打印for(i 0; i sz; i) { printf(%d , p[i]); } 结论p[i]本质上等价于*(pi)同理arr[i]等价于*(arri)。数组元素的访问编译器处理时也是首元素地址 偏移量求出元素地址再解引用和指针是一回事。三、一维数组传参的本质先抛个问题数组能传给函数那能不能在函数内部求数组元素个数#include stdio.h void test(int arr[]) { 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}; int sz1 sizeof(arr)/sizeof(arr[0]); printf(sz1 %d\n, sz1); test(arr); return 0; }运行发现sz1是 10正确但函数里的sz2是错的32 位下输出 1因为 4/4。函数内部没有正确获得数组元素个数。原因就是数组传参的本质上一节我们说了数组名是首元素地址数组传参时传的是数组名所以本质上传的是首元素的地址。函数形参部分应该用指针变量来接收首元素地址。既然形参本质是指针sizeof(arr)在函数里算的就是一个地址的大小而不是数组的大小。所以函数形参写成数组形式还是指针形式本质一样都是指针void test(int arr[]) // 参数写成数组形式本质上还是指针 { printf(%d\n, sizeof(arr)); // 算的是指针变量大小 } void test(int* arr) // 参数写成指针形式 { printf(%d\n, sizeof(arr)); // 也是指针变量大小 } int main() { int arr[10] {1,2,3,4,5,6,7,8,9,10}; test(arr); return 0; } 总结一维数组传参形参部分可以写成数组形式int arr[]也可以写成指针形式int* arr两者等价。因为传的是首元素地址所以函数里算不出数组长度需要把元素个数另外传进来。IMG3四、冒泡排序冒泡排序的核心思想就一句两两相邻的元素进行比较。方法 1基础版#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-i-1; j) { if(arr[j] arr[j1]) { int tmp arr[j]; arr[j] arr[j1]; arr[j1] 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); int i 0; for(i 0; i sz; i) printf(%d , arr[i]); return 0; }方法 2优化版加一个flag如果某一趟一次都没交换说明已经有序直接提前结束省掉后面的无用趟数#include stdio.h void bubble_sort(int arr[], int sz) { int i 0; for(i 0; i sz-1; i) { int flag 1; // 假设这一趟已经有序 int j 0; for(j 0; j sz-i-1; j) { if(arr[j] arr[j1]) { flag 0; // 发生交换说明还无序 int tmp arr[j]; arr[j] arr[j1]; arr[j1] 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); int i 0; for(i 0; i sz; i) printf(%d , arr[i]); return 0; }这里正好用上了上一节数组传参的结论bubble_sort的形参是int arr[]本质是指针所以函数里求不了元素个数必须把sz也传进去。五、二级指针指针变量也是变量是变量就有地址。那指针变量的地址存放在哪——二级指针。int a 10; int* pa a; // pa 是一级指针存 a 的地址 int** ppa pa; // ppa 是二级指针存 pa 的地址二级指针的运算就两条*ppa对 ppa 里的地址解引用找到的是pa所以*ppa访问的就是pa。int b 20; *ppa b; // 等价于 pa b;**ppa先*ppa找到pa再对 pa 解引用*pa找到的是a。**ppa 30; // 等价于 *pa 30; // 等价于 a 30; 一句话*ppa就是pa**ppa就是a。每一层*都往指向的对象走一层。六、指针数组指针数组是指针还是数组类比一下整型数组是存放整型的数组字符数组是存放字符的数组——那指针数组就是存放指针的数组。int* parr[3]; // 有 3 个元素的数组每个元素都是 int* 类型存放 int 的地址指针数组的每个元素都用来存放地址指针每个元素又是一个地址可以指向一块区域。判断数组还是指针的小技巧看*和[]谁离名字近。int* parr[3]里parr先和[3]结合所以它是数组数组里放的是int*。而int (*parr)[3]数组指针下一讲是parr先和*结合是指针。七、指针数组模拟二维数组指针数组的每个元素可以指向一块区域那就可以用它模拟出二维数组的效果#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}; // 数组名是首元素地址类型是 int*可以放进 parr 数组 int* parr[3] {arr1, arr2, arr3}; int i 0, j 0; for(i 0; i 3; i) { for(j 0; j 5; j) { printf(%d , parr[i][j]); } printf(\n); } return 0; }parr[i]访问的是parr数组的元素parr[i]找到的这个元素一个指针指向了一个整型一维数组parr[i][j]就是那个整型一维数组里的元素。注意上面只是模拟出了二维数组的效果实际上它并非真正的二维数组因为每一行arr1/arr2/arr3在内存里并不是连续的。真二维数组是整块连续的这个区别下一讲会再展开。总结最后把这一讲的骨架拎出来数组名绝大多数情况是首元素地址只有sizeof(数组名)和数组名两个例外都表示整个数组。arr1跳过 1 个元素arr1跳过整个数组。指针访问数组arr[i]*(arri)p[i]*(pi)数组下标本质是首地址 偏移再解引用。数组传参传的是首元素地址形参int arr[]和int* arr等价函数里求不了数组长度要把sz另外传进去。冒泡排序两两相邻比较加flag优化某一趟没交换就提前结束。二级指针指针变量的地址用二级指针存*ppa是pa**ppa是a。指针数组存放指针的数组每个元素是一个地址可用它模拟二维数组但行与行不连续。一句话收尾把数组名 首元素地址这句话刻进脑子这一讲的内容就全打通了。拓展学习数组指针int (*p)[3]与指针数组的区别《深入理解指针(3)》会继续讲一维/二维数组在内存中的真实排布以及真正的二维数组怎么用指针访问函数指针与回调函数后续章节上一讲深入理解指针(1)地址、指针变量、const、野指针、传址调用