C语言的一座大山——指针3(个人理解+自用笔记)
一、数组名的本质数组首元素的地址先看核心结论大部分情况下数组名就是数组首元素的地址。用代码验证#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; }输出结果里arr[0]和arr的地址完全一样这就是最直接的证明。1.1 数组名的两个 “例外情况”数组名表示首元素地址但有两个特殊情况1.sizeof(数组名)表示整个数组的大小int arr[10] {0}; printf(%d\n, sizeof(arr)); // 输出4010个int每个4字节这里的arr不代表首元素地址而是整个数组计算的是数组占用的总字节数。如果arr只是首元素地址sizeof(arr)应该是 4/8指针大小但这里输出 40说明它表示整个数组。2.数组名表示整个数组的地址(也应该从第一个地址开始int arr[10] {0}; printf(arr[0] %p\n, arr[0]); // 首元素地址 printf(arr %p\n, arr); // 首元素地址 printf(arr %p\n, arr); // 整个数组的地址你会发现三个地址的值是一样的但它们的类型和含义完全不同。1.2 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(arr 1 %p\n, arr1); printf(arr %p\n, arr); printf(arr 1 %p\n, arr1); return 0; }典型输出X86 环境代码块 arr[0] 0077F820 arr[0]1 0077F824 arr 0077F820 arr1 0077F824 arr 0077F820 arr1 0077F848分析一下arr[0] 1和arr 1地址差 4 个字节因为它们是int*类型1 跳过1 个 int 元素4 字节。arr 1地址差 40 个字节0077F820→0077F848差 0x2840因为arr是int(*)[10]类型指向整个数组的指针1 会跳过整个数组10 个 int共 40 字节。1.3 核心总结表表达式含义类型1操作效果arr数组首元素的地址int*跳过 1 个int4 字节arr[0]数组首元素的地址int*跳过 1 个int4 字节arr整个数组的地址int(*)[10]跳过整个数组40 字节sizeof(arr)整个数组的大小无计算数组总字节数 一句话记住除了sizeof(arr)和arr之外所有场景下数组名都代表首元素的地址。二、使用指针访问数组2.1 核心数组下标和指针的等价关系核心就是一句话arr[i] 等价于 *(arr i) p[i] 等价于 *(p i)因为arr是数组首元素的地址p也指向数组首元素所以它们的用法完全互通。2.1.1 代码拆解指针读写数组#include stdio.h int main() { int arr[10] {0}; int i 0; // 计算数组元素个数总大小 / 单个元素大小 int sz sizeof(arr) / sizeof(arr[0]); int* p arr; // p指向数组首元素 // 输入用指针偏移给数组赋值 for(i 0; i sz; i) { scanf(%d, p i); // 等价写法scanf(%d, arr[i]); } // 输出用指针解引用读取数组元素 for(i 0; i sz; i) { printf(%d , *(p i)); // 等价写法printf(%d , arr[i]); // 也可以写成printf(%d , p[i]); } return 0; } 关键点p i是第i个元素的地址和arr[i]完全一样*(p i)就是对这个地址解引用拿到元素的值和arr[i]完全一样2.2 为什么p[i]也能访问数组C 语言的编译器在处理数组下标时本质上都会转换成「地址 偏移」的形式arr[i] → *(arr i) p[i] → *(p i)所以只要p是指向数组首元素的指针p[i]和arr[i]就完全等价你可以随便换着写。arr[ i ] *( arri ) *( pi )三、一维数组传参的本质3.1 核心现象函数里算不出数组长度#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 1❓为什么函数里算出来的长度是 1而不是 103.2 数组传参的本质数组退化成指针1. 关键结论数组传参时传递的不是整个数组而是数组首元素的地址。函数形参里的int arr[]本质上就是一个int*类型的指针。2. 两种等价写法// 写法1写成数组形式语法糖 void test(int arr[]) { ... } // 写法2写成指针形式本质 void test(int* arr) { ... }这两种写法在编译器看来是完全一样的arr都是一个指针变量。3. 为什么sizeof(arr)结果变了在main函数里arr是数组名sizeof(arr)计算的是整个数组的大小40 字节。在test函数里arr退化成了指针sizeof(arr)计算的是指针变量的大小x86 32位环境下是4 字节。所以sz2 4 / 4 1自然算不出正确的数组长度。3.3 正确的传参方式必须把长度也传进去因为函数里无法通过sizeof计算数组长度所以必须把长度作为参数传递给函数#include stdio.h // 正确写法同时接收数组指针和长度 void bubble_sort(int arr[], int sz) { // 这里可以正常使用sz了 for(int i 0; i sz-1; i) { for(int j 0; j sz-1-i; j) { if(arr[j] arr[j1]) { int tmp arr[j]; arr[j] arr[j1]; arr[j1] tmp; } } } } int main() { int arr[10] {10,9,8,7,6,5,4,3,2,1}; int sz sizeof(arr)/sizeof(arr[0]); bubble_sort(arr, sz); // 把数组和长度一起传进去 return 0; }3.4 一句话总结数组传参时会退化成指针形参里的int arr[]和int* arr等价。函数内部无法用sizeof计算数组长度必须把长度作为额外参数传入。四、冒泡排序基础版 优化版冒泡排序的核心思想相邻元素两两比较把大的元素“冒泡” 到后面每一轮都会把当前最大的元素放到它最终的位置上。4.1.基础版代码解析void bubble_sort(int arr[], int sz) { int i 0; // 外层循环控制排序的轮数最多需要 sz-1 轮 for(i 0; i sz-1; i) { int j 0; // 内层循环每轮比较 sz-1-i 次后面i个元素已经排好序了 for(j 0; j sz-1-i; j) { // 相邻元素比较前面比后面大就交换 if(arr[j] arr[j1]) { int tmp arr[j]; arr[j] arr[j1]; arr[j1] tmp; } } } } 关键点每一轮排序后末尾的i个元素已经是有序的所以内层循环的上限可以减去i减少不必要的比较。4.2 优化版提前终止排序笔记里加了flag标记用来判断数组是否已经完全有序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-1-i; j) { if(arr[j] arr[j1]) { flag 0; // 发生了交换说明数组还没完全有序 int tmp arr[j]; arr[j] arr[j1]; arr[j1] tmp; } } if(flag 1) // 这一轮没有任何交换数组已经有序直接结束 break; } }✅ 优化效果如果数组已经接近有序优化版可以提前终止排序减少不必要的循环。4.3. 完整调用示例#include stdio.h // 上面的bubble_sort函数写在这里 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; }五、二级指针int**1. 核心概念指针本身也是变量变量就有自己的地址。二级指针就是用来存放「一级指针变量地址」的指针。对应图中的例子int a 10; // 变量a地址0x0012ff50值为10 int *pa a; // 一级指针pa地址0x0012ff42值为0x0012ff50指向a int **ppa pa; // 二级指针ppa地址0x0012ff30值为0x0012ff42指向pa关系链ppa → pa → a解引用层级ppa存pa的地址→*ppa取pa的值即a的地址 →**ppa最终取到a的值2. 二级指针的运算表达式含义等价操作*ppa对ppa解引用访问它指向的pa直接操作一级指针pa**ppa先通过*ppa找到pa再对pa解引用最终访问a*pa或直接访问a举个代码例子int b 20; *ppa b; // 等价于 pa b; 让pa指向ba的值不变 **ppa 30; // 等价于 *pa 30; 等价于 a 30; 最终修改了a的值六、指针数组到底是指针还是数组一句话结论它是一个数组数组里的每个元素都是指针。类比理解int arr[5];整型数组存放的元素是int类型的整数char arr[5];字符数组存放的元素是char类型的字符int* parr[3];指针数组存放的元素是int*类型的指针地址核心要点parr是一个数组有3 个元素每个元素都是一个int*类型的指针每个指针元素都可以指向一块内存区域比如一个一维数组1.指针数组模拟二维数组代码示例解析#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}; // 指针数组存放3个一维数组的地址 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][j]的工作原理parr[i]访问指针数组的第i个元素它是一个int*指针指向arr1/arr2/arr3的首地址parr[i][j]等价于*(parr[i] j)通过指针偏移j找到对应一维数组的第j个元素2.和真正二维数组的本质区别特性指针数组模拟的 “二维数组”真正的二维数组int arr[3][5]内存连续性每一行是独立的一维数组行与行之间不连续所有元素在内存中是连续存放的元素长度每一行可以长度不同比如arr1长度 5arr2长度 6每一行的长度必须相同类型parr是int*[3]指针数组arr是int[3][5]二维数组 补充常见误区与应用场景误区纠正int* parr[3]不是二维数组它只是“长得像”二维数组本质上是多个独立一维数组的集合。实用场景处理长度不统一的字符串列表灵活管理多个一维数组避免浪费内存空间