深⼊理解指针(3)
1. 数组名的理解数组名就是数组⾸元素(第⼀个元素)的地址。sizeof(数组名)sizeof中好的我们来详细解释一下C语言中数组名的含义。数组名在不同上下文中确实有不同的含义理解这一点对于掌握指针和数组操作至关重要。核心概念总结上下文数组名arr的含义说明普通使用首元素地址 (arr[0])大多数情况下数组名代表数组第一个元素的地址sizeof(arr)整个数组单独放在sizeof运算符内时代表整个数组的大小arr整个数组的地址取地址操作得到的是指向整个数组的指针详细解释与代码验证1.1数组名通常表示首元素地址#includestdio.hintmain(){intarr[5]{1,2,3,4,5};printf(arr %p\n,(void*)arr);// 首元素地址printf(arr[0] %p\n,(void*)arr[0]);// 首元素地址return0;}输出示例arr 0x7ffd4a3b2b10 arr[0] 0x7ffd4a3b2b10重点arr和arr[0]的值相同证明数组名在普通使用时等价于首元素地址。1.2sizeof(数组名)表示整个数组#includestdio.hintmain(){intarr[5]{1,2,3,4,5};printf(sizeof(arr) %zu\n,sizeof(arr));// 整个数组大小printf(sizeof(arr[0]) %zu\n,sizeof(arr[0]));// 指针大小通常4或8字节return0;}输出示例sizeof(arr) 20 // 5个int × 4字节 20 sizeof(arr[0]) 8 // 64位系统指针大小为8字节重点sizeof(arr)计算的是整个数组占用的内存大小5 × sizeof(int)。若arr仅是首元素地址sizeof(arr)应返回指针大小如8字节但实际返回20证明此处arr代表整个数组。1.3数组名表示整个数组的地址#includestdio.hintmain(){intarr[5]{1,2,3,4,5};printf(arr %p\n,(void*)arr);// 整个数组的地址printf(arr %p\n,(void*)arr);// 首元素地址printf(arr 1 %p\n,(void*)(arr1));// 跳过整个数组printf(arr 1 %p\n,(void*)(arr1));// 跳过一个元素return0;}输出示例arr 0x7ffd4a3b2b10 arr 0x7ffd4a3b2b10 arr 1 0x7ffd4a3b2b24 // 比 arr 大20字节5×4 arr 1 0x7ffd4a3b2b14 // 比 arr 大4字节1个int重点arr和arr的数值相同但类型不同arr的类型是int(*)[5]指向长度为5的整型数组的指针。arr的类型是int*指向整型的指针。指针运算时arr 1跳过整个数组20字节而arr 1仅跳过一个元素4字节。总结首元素地址除sizeof(arr)和arr外数组名arr始终等价于arr[0]。整个数组sizeof(arr)中单独使用的arr代表整个数组的大小。数组地址arr获取的是指向整个数组的指针类型为int(*)[n]如int(*)[5]而非int*。2. 使⽤指针访问数组2.1 数组名与指针的关系在C语言中数组名本质上是数组首元素的地址。例如intarr[5]{10,20,30,40,50};arr等价于arr[0]首元素地址。因此可以通过指针操作访问数组元素。2.2 指针访问数组的三种方式(1) 下标法传统方式for(inti0;i5;i){printf(%d ,arr[i]);// 输出10 20 30 40 50}(2) 指针变量法int*ptrarr;// ptr指向数组首地址for(inti0;i5;i){printf(%d ,*ptr);// 输出当前指针指向的值ptr;// 指针移动到下一个元素地址}(3) 数组名作为指针常量for(inti0;i5;i){printf(%d ,*(arri));// 等价于 arr[i]}2.3 指针运算的核心机制地址偏移计算指针加减整数n时实际移动的字节数为偏移量n×sizeof(数据类型) \text{偏移量} n \times \text{sizeof(数据类型)}偏移量n×sizeof(数据类型)例如ptr 2在int数组中移动2×482 \times 4 82×48字节假设int占4字节。解引用操作*ptr表示获取指针当前指向的值。2.4 内存布局示例假设数组arr起始地址为0x1000地址 | 值 0x1000 | 10 (arr[0]) 0x1004 | 20 (arr[1]) 0x1008 | 30 (arr[2]) ...ptr arr→ 指向0x1000ptr 1→ 指向0x1004地址增加sizeof(int)2.5 完整代码示例#includestdio.hintmain(){intarr[5]{10,20,30,40,50};int*ptrarr;// ptr指向arr[0]// 方法1指针遍历printf(指针遍历: );for(inti0;i5;i){printf(%d ,*(ptri));// 输出arr[i]}// 方法2指针自增printf(\n指针自增: );ptrarr;// 重置指针for(inti0;i5;i){printf(%d ,*ptr);ptr;// 移动到下一个元素}// 方法3数组名作为指针printf(\n数组名指针: );for(inti0;i5;i){printf(%d ,*(arri));// 等价于arr[i]}return0;}输出指针遍历: 10 20 30 40 50 指针自增: 10 20 30 40 50 数组名指针: 10 20 30 40 502.6 注意事项数组名是常量指针arr非法不能修改常量指针但ptr合法。越界访问危险指针移动超出数组范围会导致未定义行为。类型匹配指针类型需与数组元素类型一致如int *对应int[]。总结指针访问数组的核心是地址计算与解引用。三种访问方式本质相同但指针变量更灵活。理解*(arr i) arr[i]的等价性是关键。通过指针操作可高效遍历数组尤其在动态内存和函数参数传递中优势显著。3. ⼀维数组传参的本质核心观点数组名的本质在大多数情况下数组名arr代表数组首元素arr[0]的地址。即arr等价于arr[0]。传参的本质当将数组名作为参数传递给函数时实际上传递的是这个首元素的地址。形参的灵活性因为函数接收的是一个地址指针所以形参部分可以有两种等效的写法数组形式int arr[]指针形式int *arr重点详解3.1 数组名即首元素地址考虑以下数组intmain(){intarr[5]{1,2,3,4,5};// 定义一个整型数组printf(arr: %p\n,(void*)arr);// 打印数组名本身的值printf(arr[0]: %p\n,(void*)arr[0]);// 打印首元素地址return0;}运行这段代码你会发现arr和arr[0]打印出来的地址值是完全相同的。这证明了arr在表达式中除了sizeof(arr)和arr等少数情况代表的就是数组首元素的地址。3.2 传参传递地址当我们将数组名arr传递给函数func时func(arr);// 传递数组名实际上传递的是arr[0]的地址相当于传递了arr[0]。3.3 形参的两种等效写法函数接收到的参数是一个指向int的指针。因此在定义函数时形参可以有下面两种写法它们是完全等价的写法一数组形式 (语法糖)voidfunc(intarr[],intsize){// 看起来像数组实际是指针for(inti0;isize;i){printf(%d ,arr[i]);// 依然可以使用下标访问}}int arr[]看起来像是接受一个数组但这只是C语言提供的一种便利的写法语法糖。编译器看到int arr[]时自动将其解释为int *arr。方括号[]在这里只是表明arr指向的是数组的首元素它并不意味着函数内会创建一个新的数组副本。方括号内的数字如果有也会被编译器忽略。函数内部仍然可以使用下标arr[i]来访问元素这是因为指针支持下标操作。写法二指针形式 (本质)voidfunc(int*arr,intsize){// 明确写成指针for(inti0;isize;i){printf(%d ,*(arri));// 通过指针运算和解引用访问// 等价于 printf(%d , arr[i]);}}int *arr直接表明形参arr是一个指向整型的指针。在函数内部可以通过指针算术(arr i)和解引用*(arr i)来访问数组元素这等价于使用下标arr[i]。关键验证sizeof操作符验证形参本质是指针而非数组的最直接方法是使用sizeof操作符voidcheckSize(intarr[]){printf(Sizeof(arr) inside function (array form): %zu bytes\n,sizeof(arr));// 通常打印 8 (64位系统) 或 4 (32位系统)}intmain(){intmyArr[10];printf(Sizeof(myArr) in main: %zu bytes\n,sizeof(myArr));// 打印 10 * sizeof(int) 40 (假设 int 为 4 字节)checkSize(myArr);return0;}在main函数中sizeof(myArr)计算的是整个数组myArr在内存中占用的总字节数 (这里是10×44010 \times 4 4010×440字节)。在checkSize函数内部sizeof(arr)计算的却是一个指针变量的大小在 64 位系统上通常是 8 字节在 32 位系统上是 4 字节而不是数组的大小。这个差异明确无误地证明了函数内部接收到的arr是一个指针存储地址的变量而不是整个数组本身。总结传递地址一维数组传参时传递的是数组首元素的地址 (arr[0])。形参等效函数形参int arr[]和int *arr在功能和意义上完全等价。编译器都将arr视为一个指向int的指针。语法糖int arr[]这种写法是一种语法糖让代码看起来像是直接传递了数组更直观易读但其底层实现依然是指针。操作本质在函数内部对形参arr的下标操作arr[i]会被编译器转换为基于指针的算术和解引用操作*(arr i)。影响原数组由于传递的是地址在函数内通过指针或下标修改形参arr指向的内存会直接影响调用函数中的原数组元素。需要传递大小因为函数内部无法通过形参arr获取原数组的长度信息sizeof(arr)得到的是指针大小所以通常需要额外传递一个参数如int size来指明数组的元素个数。4. 冒泡排序核心思想重复遍历待排序序列依次比较相邻元素如果顺序错误则交换它们。每轮遍历会将一个最大或最小元素“浮”到正确位置如同气泡上浮。算法步骤比较相邻元素如果第一个比第二个大则交换它们。对每一对相邻元素做同样工作从开始第一对到最后一对。此时最后一个元素应是最大值。对所有元素重复上述步骤除了最后一个已排序的元素。持续每次对越来越少的元素重复上述步骤直到没有元素需要比较。时间复杂度最好情况已有序O(n)O(n)O(n)最坏/平均情况O(n2)O(n^2)O(n2)C语言代码示例#includestdio.hvoidprint_arr(intarr[],intsz){inti0;for(i0;isz;i){printf(%d ,arr[i]);}printf(\n);}voidbubble_sort(intarr[],intsz){inti0;//确定趟数for(i0;isz;i){intflag1;//标记是否有序假设已经有序//一趟内部比较intj0;for(j0;jsz-1-i;j){if(*(arrj)*(arrj1)){flag0;inttmp*(arrj);*(arrj)*(arrj1);*(arrj1)tmp;}}if(flag1){break;}}}intmain(){intarr[]{9,8,7,6,5,4,3,2,1,0};//对数组进行排序 - 升序intszsizeof(arr)/sizeof(arr[0]);bubble_sort(arr,sz);//输出print_arr(arr,sz);return0;}5. 二级指针定义指向指针的指针。即一个指针变量存储的是另一个指针变量的地址。用途动态创建指针数组。在函数中修改一级指针本身的值例如改变指针指向的地址。处理多维动态数组。声明int **pp;pp是一个二级指针。*pp是一个一级指针指向int。**pp是一个int值。C语言代码示例#includestdio.hintmain(){intvalue42;int*pvalue;// p 是一级指针指向 valueint**ppp;// pp 是二级指针指向 pprintf(value: %d\n,value);// 直接访问printf(*p: %d\n,*p);// 通过一级指针访问printf(**pp: %d\n,**pp);// 通过二级指针访问// 通过二级指针修改 value 的值**pp100;printf(Modified value: %d\n,value);return0;}6. 指针数组定义一个数组其每个元素都是指针。声明int *arr[5];arr是一个包含 5 个元素的数组。每个元素arr[i]的类型是int *指向int的指针。特点数组本身在栈上分配连续内存用于存储指针。每个指针元素可以指向不同大小的内存块通常在堆上动态分配。用途存储多个字符串字符串数组。模拟二维数组。存储不同长度的数据集合。C语言代码示例#includestdio.h#includestdlib.hintmain(){// 声明一个指针数组包含 3 个 int 指针int*ptrArr[3];// 为每个指针动态分配内存并赋值for(inti0;i3;i){ptrArr[i](int*)malloc(sizeof(int));// 分配一个 int 空间*ptrArr[i]i*10;// 通过指针赋值}// 访问和打印for(inti0;i3;i){printf(ptrArr[%d] points to value: %d\n,i,*ptrArr[i]);}// 释放内存for(inti0;i3;i){free(ptrArr[i]);}return0;}7. 指针数组模拟二维数组核心思想利用指针数组一级指针数组来模拟二维数组的行为。创建一个指针数组int **arr二级指针指向指针数组。为指针数组的每个元素一级指针分配一个一维数组代表二维数组的一行。访问元素使用arr[i][j]的形式类似于二维数组。优势行可以有不同的长度不规则二维数组。动态分配内存大小可运行时确定。内存不一定连续与真正的二维数组不同。C语言代码示例#includestdio.h#includestdlib.hintmain(){introws3,cols4;int**simulated2D;// 二级指针// 步骤1分配行指针数组 (模拟有多少行)simulated2D(int**)malloc(rows*sizeof(int*));if(simulated2DNULL)exit(1);// 步骤2为每一行分配内存 (模拟每行的列)for(inti0;irows;i){simulated2D[i](int*)malloc(cols*sizeof(int));if(simulated2D[i]NULL)exit(1);}// 步骤3赋值 (使用双重下标)for(inti0;irows;i){for(intj0;jcols;j){simulated2D[i][j]i*colsj;// 或 *(*(simulated2D i) j)}}// 步骤4打印printf(Simulated 2D Array:\n);for(inti0;irows;i){for(intj0;jcols;j){printf(%2d ,simulated2D[i][j]);}printf(\n);}// 步骤5释放内存 (先释放每行再释放行指针数组)for(inti0;irows;i){free(simulated2D[i]);}free(simulated2D);return0;}关键点simulated2D指向一个指针数组int *数组。simulated2D[i]指向第i行的一维数组。simulated2D[i][j]访问第i行第j列的元素。内存释放顺序很重要先释放每一行的内存再释放存储行指针的数组。