数组作为函数参数为什么要多传一个长度_Day10
数组作为函数参数为什么要多传一个长度?——Day10 学习记录,从数组传参到日历打印
学完函数之后,我开始用函数封装各种功能。但很快就遇到了问题——把数组传给函数后,用sizeof计算长度,结果根本不是数组的大小。
后来才知道,这是 C 语言最经典的数组退化为指针规则。
今天就把数组传参、字符串操作函数、作用域和存储类别串起来讲一遍,最后用一个打印日历的例子收尾。
① 数组传参的本质:为什么不是传整个数组?
很多新手(包括我)第一次写数组传参时,都会这么想:“把整个数组传进去不就行了?”
但 C 语言偏不。它不传整个数组,只传首元素的地址。
原因:数组可能很大。如果每次传参都复制整个数组,性能开销太大了。C 语言的设计哲学是"相信程序员,不做多余的事"。
所以数组名做参数时,编译器把它解释为指针——准确地说,是退化为首元素地址。既然函数拿到的只是一个地址,它当然不知道数组有多长。这就是为什么必须额外传一个长度参数。
与之对比:字符数组(字符串)不需要传长度,因为字符串自带\0结束标志。函数只需一直读到\0就知道到哪结束了。
一句总结:数组传参退化为指针,函数拿不到数组大小,所以要么额外传长度,要么靠 ‘\0’ 结尾。
② 一维数组传参怎么写?
错误示例
voidprintArray(inta[]){intlen=sizeof(a)/sizeof(a[0]);// 错!sizeof(a) 是指针大小}这个坑我踩过不止一次。sizeof(a)在函数里返回的是指针的字节数(8 或 4),不是数组的大小。
正确写法
voidprintArray(inta[],intlen){for(inti=0;i<len;i++)printf("%d ",a[i]);}printArray(a,10);// 调用时传入长度字符数组就不一样了:
voidprintStr(chars[]){for(inti=0;s[i]!='\0';i++)printf("%c",s[i]);}printStr("hello");// 不需要传长度一句总结:整型数组传参要加长度参数,字符数组靠 ‘\0’ 结尾。
③ 二维数组传参:为什么列数不能省略?
错误示例
voidoutputArray(inta[][],introw);// 编译错误为什么错
二维数组本质上是"数组的数组"。访问a[i][j]时,编译器需要计算偏移量:
地址 = 首地址 + (i * 列数 + j) * 元素大小列数不指定,这个公式就没法算。行数可以省略,因为编译器不需要行数就能算出地址。
正确写法
voidoutputArray(inta[][4],introw){for(inti=0;i<row;i++){for(intj=0;j<4;j++)printf("%d ",a[i][j]);printf("\n");}}二维字符数组同理:char str[][10],列数必须写,行数可以省略。
一句总结:二维数组传参必须指定列数,因为编译器需要列数来定位元素。
④ 字符串操作函数(自己动手实现)
标准库提供了string.h,但自己实现一遍能更深刻理解原理。
Strlen:计算字符串长度
遇到\0就停,统计\0之前的字符数。
intmyStrlen(constchars[]){inti=0;while(s[i]!='\0')i++;returni;}Strcpy:复制字符串
注意要把\0也复制过去。
voidmyStrcpy(chardest[],constcharsrc[]){inti=0;while(src[i]!='\0'){dest[i]=src[i];i++;}dest[i]='\0';// 别忘了复制结束符}Strcat:拼接字符串
先找到 dest 的末尾(\0的位置),再从那里开始复制 src。
voidmyStrcat(chardest[],constcharsrc[]){inti=0,j=0;while(dest[i]!='\0')i++;// 找到 dest 末尾while(src[j]!='\0'){dest[i]=src[j];i++;j++;}dest[i]='\0';}Strcmp:比较字符串
逐个字符比较 ASCII 码,找到第一个不同的字符就返回差值。
intmyStrcmp(constchars1[],constchars2[]){inti=0;while(s1[i]!='\0'&&s2[i]!='\0'){if(s1[i]!=s2[i])returns1[i]-s2[i];i++;}returns1[i]-s2[i];}一句总结:四个操作函数的本质都是遍历 ‘\0’ 结尾的字符数组。
⑤ 作用域与存储类别
作用域讲的是"在哪能用",生命周期讲的是"啥时候销毁"。
局部变量 vs 全局变量
intg=10;// 全局变量:所有函数都能用voidfunc(){inta=5;// 局部变量:只能在 func 里用}就近原则:如果局部和全局同名,局部优先。
存储类别关键字
| 关键字 | 存储位置 | 生命周期 | 作用域 | 说明 |
|---|---|---|---|---|
| auto | 栈 | 块内 | 块内 | 默认,局部变量 |
| static | 全局区 | 程序始终 | 块内 | 只初始化一次 |
| register | 寄存器(建议) | 块内 | 块内 | 编译器可忽略 |
| extern | 外部存储 | 程序始终 | 全局 | 声明外部变量 |
static最常用——它让局部变量在函数多次调用之间保持值不变,但其他函数又访问不到。
一句总结:作用域决定可见性,static 让局部变量拥有全局生命周期但保持局部访问限制。
⑥ 综合实战:打印日历
今天最后一个实战:打印 2026 年 7 月的日历。
拆成四个函数,每个只做一件事:
#include<stdio.h>intisLeapYear(intyear){return(year%4==0&&year%100!=0)||(year%400==0);}intgetMonthDays(intyear,intmonth){intdays[]={31,28,31,30,31,30,31,31,30,31,30,31};if(month==2&&isLeapYear(year))return29;returndays[month-1];}intgetAllDays(intyear,intmonth){inttotal=0;for(inty=1;y<year;y++)total+=isLeapYear(y)?366:365;for(intm=1;m<month;m++)total+=getMonthDays(year,m);returntotal;}voidprintCal(intyear,intmonth){intdays=getMonthDays(year,month);intweekday=(getAllDays(year,month)+1)%7;// 2026.1.1 是周四printf(" %d 年 %d 月\n",year,month);printf("日 一 二 三 四 五 六\n");for(inti=0;i<weekday;i++)printf(" ");for(intd=1;d<=days;d++){printf("%2d ",d);if((weekday+d)%7==0)printf("\n");}printf("\n");}intmain(){printCal(2026,7);return0;}核心思路:从公元 1 年 1 月 1 日到目标年月的 1 日,算出总天数,总天数 % 7 得到星期几,再按格式输出。
每日一练
运行上面的代码,验证 2026 年 7 月的日历是否正确。你可以把年份和月份改成其他值测试。
今日避坑指南
| 坑 | 原因 | 怎么避 |
|---|---|---|
| 函数内 sizeof(a) 不是数组大小 | 数组名退化为指针 | 额外传 len |
| 二维数组列数省略导致编译错误 | 编译器无法计算偏移 | 形参写int a[][N] |
| Strcpy 忘记复制 \0 | 目标数组不是合法字符串 | 循环结束后加dest[i]='\0' |
| Strcat 目标空间不够 | 拼接后越界 | 确保 dest 容量足够 |
| 全局变量滥用 | 函数间耦合高、难调试 | 能用传参就别用全局 |
| 忘记闰年判断 | 2 月天数算错 | isLeapYear 单独封装 |
今日学习感悟
今天最有价值的收获,是理解了 C 语言"数组传参退化为指针"这条规则。以前写sizeof(a)在函数里算出指针大小时,我只觉得莫名其妙。今天搞明白了背后的原因——性能优先的设计哲学,一切都有道理。
另一个收获是日历打印这个实战。把一个看起来复杂的问题拆成四个小函数,每个函数独立测试,最后组合起来。这个拆分思路在嵌入式开发中特别重要:一个函数只做一件事,做好一件事。
Day10 学习记录,明天继续。