ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C语言指针(二)-- 一维、二维数组与指针的关系

C语言指针(二)-- 一维、二维数组与指针的关系 文章目录从纠正一句常见说法开始1、从 arr[i] 开始数组下标如何定位元素2、数组名何时转换为首元素地址何时保留数组身份2.1 常见表达式array 表现为 array[0]2.2 sizeof(array)数组不会转换2.3 array取到的是“整个数组”的地址3、一维数组传参传的是首元素地址不是整个数组副本3.1 为什么函数内部不能用 sizeof(arr) 求长度3.2 两种遍历方式下标和首尾指针4、字符数组、字符指针和字符串都能打印不代表可随便修改4.1 字符串为什么需要 \04.2 字符数组数据属于数组可以修改4.3 字符指针通常指向只读字符串字面量4.4 用指针理解字符串长度5、指针数组数组元素保存的是多个地址6、指针数组和数组指针只差括号含义完全不同6.1 int *p[3]先看 p[3]7.2 int (*p)[3]括号先把 *p 绑在一起7、二维数组传参为何第一维可省列数却要保留8、练习对照内存关系仔细思考8.1 用指针求一维数组的最大值8.2 判断两个声明并说明 1 的含义8.3 编写二维数组每列求和函数总结从纠正一句常见说法开始你可能听过数组名就是指针。这句话便于入门却不够准确。它会让人误以为下面两者完全相同intarray[5];int*pointerarray;实际上array是包含 5 个int的数组对象pointer是一个保存地址的指针变量。数组有固定的元素个数和完整大小指针只保存一个地址。两者在很多表达式中表现相似是因为数组名经常转换成首元素地址而不是因为数组变成了指针变量。1、从arr[i]开始数组下标如何定位元素intscores[4]{60,75,88,92};printf(%d\n,scores[2]);printf(%d\n,*(scores2));两行都会输出88。在大多数表达式中scores会转换为scores[0]也就是首元素地址再向后移动 2 个int元素最后解引用得到第三个元素。因此可以写成scores[i]*(scoresi)这不是说下标形式和指针形式在所有语境下都能随意替换而是说明数组访问的底层定位方式。对初学者来说下标形式通常更直观当你需要理解传参、二维数组或指针遍历时*(scores i)才更有价值。#includestdio.hintmain(){intscores[4]{60,75,88,92};int*pscores;printf(scores[1] %d\n,scores[1]);printf(*(p 1) %d\n,*(p1));printf(p[1] %d\n,p[1]);return0;}运行结果三种写法都定位到第二个元素 75。p[1]能成立是因为下标运算本质上也会按“地址加偏移再解引用”的方式理解。2、数组名何时转换为首元素地址何时保留数组身份2.1 常见表达式array表现为array[0]intdata[3]{1,2,3};int*pdata;// 等价于 int *p data[0];printf(%d\n,*data);// 1printf(%d\n,*(data1));// 2传给普通函数、赋给指针、参与data i时数组名通常会转换为首元素地址。正是这条规则让数组可以自然地传给接收int *的函数。2.2sizeof(array)数组不会转换#includestdio.hintmain(){intdata[3]{1,2,3};printf(整个数组占 %zu 字节\n,sizeof(data));printf(元素个数是 %zu\n,sizeof(data)/sizeof(data[0]));return0;}运行结果此处sizeof(data)得到整个数组所占字节数而不是指针大小。sizeof(data) / sizeof(data[0])是在数组定义所在作用域计算元素数目的常见方法。但把数组传进函数后这个办法就失效了函数参数中的数组会调整为指针sizeof(arr)得到的只是指针变量大小。2.3array取到的是“整个数组”的地址intdata[3]{1,2,3};int*element_ptrdata;int(*array_ptr)[3]data;element_ptr指向一个intarray_ptr指向一个含 3 个int的完整数组。它们在打印时可能显示相同的起始位置但类型和移动步长不同element_ptr1;// 移到 data[1]array_ptr1;// 跳过整个 data[3] 数组3、一维数组传参传的是首元素地址不是整个数组副本下面两种函数声明在参数位置是等价的voidPrintA(intarr[],size_tcount);voidPrintB(int*arr,size_tcount);arr[]写法在函数参数里更像是在告诉读者“这里希望传数组”编译器会把它调整为int *arr。因此函数能访问并修改调用者的数组元素voidSetFirstTo100(intarr[],size_tcount){if(arr!NULLcount0){arr[0]100;}}intmain(){intdata[]{1,2,3};SetFirstTo100(data,3);printf(%d\n,data[0]);// 100return0;}函数没有复制数组它拿到首元素地址后仍然在访问调用者的那一块内存。若函数只需要读取数据应该用const把意图写出来intSum(constint*arr,size_tcount){size_ti;inttotal0;if(arrNULL){return0;}for(i0;icount;i){totalarr[i];}returntotal;}const int *arr说明函数不能通过arr修改元素。它并不能自动检查调用者是否传对了长度所以“地址 长度”必须作为一组契约理解arr指向开头count表示可访问元素数。3.1 为什么函数内部不能用sizeof(arr)求长度voidShowSize(intarr[]){printf(%zu\n,sizeof(arr));}这里arr已经是int *所以输出的是指针大小不是调用者数组的大小。C 语言不会在数组首地址里偷偷记录长度一个地址本身无法知道后面有多少个元素。正确的调用方式是由调用者计算真实数组长度再传给函数intdata[]{3,1,4,1,5};intcountsizeof(data)/sizeof(data[0]);PrintA(data,count);请只对真正的数组使用sizeof(data) / sizeof(data[0])。如果data已经是函数参数或普通指针该表达式就不再代表元素个数。3.2 两种遍历方式下标和首尾指针#includestdio.hvoidPrintByPointer(constint*arr,size_tcount){constint*cur;constint*end;if(arrNULL){return;}curarr;endarrcount;while(cur!end){printf(%d ,*cur);cur;}}cur是当前元素位置end是尾后位置。循环只在cur ! end时解引用所以不会读取尾后位置。若count为 0cur一开始就等于end循环自然不执行。在写代码时使用下标还是指针遍历可以按可读性选择。重要的是始终掌握边界下标要小于count指针要在[arr, arr count)这个范围内移动。4、字符数组、字符指针和字符串都能打印不代表可随便修改4.1 字符串为什么需要\0charword[]hello;这不是 5 个字符而是 6 个h、e、l、l、o和结尾的空字符\0。C 的字符串函数通过\0判断字符串在哪里结束。printf(%zu\n,sizeof(word));// 6strlen(word)的结果则是 5因为它统计可见字符不把结束符算进长度。4.2 字符数组数据属于数组可以修改charword[]hello;word[0]H;printf(%s\n,word);// Hello数组有自己的一块可写空间所以在不越界的前提下可以修改元素。注意替换一个字符不会自动增加容量若数组只有 6 个位置就不能再向里面塞入更长的字符串。4.3 字符指针通常指向只读字符串字面量constchar*messagehello;printf(%s\n,message);message[0]H;//不要这样做字符串字面量的内容不应被修改因此推荐使用const char *。即使*message没有加const有些开发环境在你运行代码后也会提示写入访问的权限冲突而暂停运行。下面的两句看起来类似存储和修改权限却不同charname[]Ada;// 数组拥有一份可修改的字符constchar*titleAda;// 指针指向只读字面量4.4 用指针理解字符串长度#includestdio.hsize_tMyStrlen(constchar*str){constchar*pstr;if(strNULL){return0;}while(*p!\0){p;}return(size_t)(p-str);}p从首字符开始一直移动到\0所在位置p - str得到跨过的字符数。这个函数隐含一个重要前提str必须指向一段以\0结束的有效字符序列。若调用者给出未结束的字符数组循环会越界寻找结束符。5、指针数组数组元素保存的是多个地址#includestdio.hintmain(){constchar*colors[]{red,green,blue};for(inti0;i3;i){printf(%s\n,colors[i]);}return0;}colors是一个数组但每个元素不是字符而是const char *每个格子都保存一个字符串的首地址。因此三段字符串可以有不同长度。不要把它和二维字符数组混淆了constchar*colors[]{red,green,blue};charlabels[][8]{red,green,blue};前者存的是多个地址字符串本身可以分散后者是一块连续的二维数组每一行固定有 8 个字符位置。两种结构都能写colors[i]或labels[i]但内存布局和可修改性不同。6、指针数组和数组指针只差括号含义完全不同6.1int *p[3]先看p[3]int*p[3];[]比*结合优先级更高所以先得到p[3]p是一个有 3 个元素的数组再看元素类型每个元素是int *。这叫指针数组。7.2int (*p)[3]括号先把*p绑在一起int(*p)[3];括号使*p先结合因此p是一个指针它指向“有 3 个int的数组”。这叫数组指针常用于二维数组的行指针。intmatrix[2][3]{{1,2,3},{4,5,6}};int(*row)[3]matrix;​printf(%d\n,row[1][2]);// 6row 1会跳到下一整行而普通int *的 1只会跳到下一个整数。7、二维数组传参为何第一维可省列数却要保留#includestdio.hvoidPrintMatrix(intmatrix[][3],introws){for(inti0;irows;i){for(intj0;j3;j){printf(%d ,matrix[i][j]);}printf(\n);}}参数int matrix[][3]在函数中会调整为int(*matrix)[3]它表示matrix指向一行每行恰好有 3 个int。行数作为rows单独传入列数写在类型中用来计算每次移动一行的步长。8、练习对照内存关系仔细思考8.1 用指针求一维数组的最大值要求函数不能修改数组当参数为空或长度为 0 时返回 0。intMax(constint*arr,intcount);思路arr指向第一个元素。先用第一个元素初始化最大值再从第二个元素开始比较没有元素时不能访问arr[0]。参考答案:#includestdio.hintMax(constint*arr,intcount){intmax;if(arrNULL||count0){return0;}maxarr[0];for(inti1;icount;i){if(arr[i]max){maxarr[i];}}returnmax;}8.2 判断两个声明并说明 1的含义int*a[4];int(*b)[4];参考答案a是含 4 个int *的指针数组a 1移到下一个指针元素b是指向“含 4 个int的数组”的指针b 1跨过整整 4 个整数。8.3 编写二维数组每列求和函数要求处理 3 列整数矩阵结果写入长度为 3 的sum数组。参考答案#includestdio.hvoidSumColumns(constintmatrix[][3],introws,intsum[3]){if(matrixNULL||sumNULL){return;}//首先确保sum数组内初始值为0for(intj0;j3;j){sum[j]0;}for(inti0;irows;i){for(intj0;j3;j){sum[j]matrix[i][j];}}}验证方式对{{1, 2, 3}, {4, 5, 6}}调用后sum应为{5, 7, 9}。参数声明中的[3]不能随意删除因为函数需要知道一行有几列。总结一维数组让我们看到数组名如何转换为首元素地址字符串让我们看到地址之外还需要结束符和容量二维数组则进一步说明指针移动必须知道目标类型的大小和结构。理解了“每一行也是一个数组”int (*p)[3]和二维数组传参就不再只是奇怪的语法。
返回列表