上节课咱们讲值传递的时候说过:你把变量传给函数,函数收到的是它的“复印件”,在函数里面改,外面不变。你传一个整数,传一个浮点数,都是这么个道理。
但是有个东西特别奇怪——数组。你传一个数组给函数,在函数里面改了数组的元素,外面的数组也跟着变了。这到底怎么回事?今天咱们就把这个事彻底讲明白。
先说结论:数组作为函数参数的时候,它不走寻常路。它传进去的不是整个数组的“复印件”,而是数组的地址。
回忆一下咱们之前说的“数组名就是地址”——你写int a[5],这里的a本身就是数组首元素的地址,也就是数组在内存里从哪儿开始的。
所以你写func(a),实际上传进去的是数组的地址。函数拿着这个地址,就能直接找到内存里的数组原件,它改的就是原件本身。这就是为什么数组作为参数的时候,函数里改了外面也会变。
咱们看个最简单的例子:
#include <stdio.h> void change(int arr[]) { arr[0] = 100; arr[1] = 200; } int main() { int a[5] = {1, 2, 3, 4, 5}; change(a); printf("%d %d\n", a[0], a[1]); // 输出 100 200 return 0; }
你看到没?change函数里把arr[0]改成了100,外面a[0]也跟着变了。如果按照普通变量的规则,这根本就不可能——复印件改了,原件应该没事才对。
但数组就是这么个脾气:传进去的是地址,改的是原件。
有人可能会问:“你这函数参数写的是int arr[],这不是一个数组吗?怎么就变成地址了?”
这里有一个关键知识点你得记住:在函数参数里,int arr[]和int *arr完全等价。 也就是说,你写成void change(int arr[]),编译器看到的就是void change(int *arr)。它就是个指针,不是数组。
用指针的写法也一样:
void change(int *arr) { arr[0] = 100; // 等价于 *(arr + 0) = 100 }
你写成arr[]也好,写成*arr也好,本质都是一个指向int的指针。所以你在函数里用arr[0]去访问,实际上是“从arr这个地址开始,向后移动0个int,取那个位置的值”。改的就是原数组的位置。
所以数组传参的本质是:你传了一个地址,函数通过地址去操作你的原数组。
那问题又来了:既然传的是地址,那数组的大小信息会跟着一起传过去吗?
答案是:不会。
你想想,地址就是一个“起始位置”,它只告诉你从哪儿开始,没告诉你到哪儿结束。函数只知道“这是个int的地址”,但不知道后面跟着多少个int。
看这个例子:
#include <stdio.h> void printArray(int arr[]) { int size = sizeof(arr) / sizeof(arr[0]); printf("函数内部算的大小:%d\n", size); // 这玩意儿不是数组长度 } int main() { int a[10] = {1,2,3,4,5,6,7,8,9,10}; printArray(a); return 0; }
你猜输出啥?不是10。
因为sizeof(arr)算的是指针的大小(在64位系统上是8字节),sizeof(arr[0])算的是int的大小(4字节),8除以4等于2——这完全不是数组长度。
所以你必须把数组长度也作为参数传给函数。
#include <stdio.h> void printArray(int arr[], int n) { for (int i = 0; i < n; i++) { printf("%d ", arr[i]); } printf("\n"); } int main() { int a[10] = {1,2,3,4,5,6,7,8,9,10}; printArray(a, 10); // 把长度也传进去 return 0; }
这个“传长度”的习惯,你以后写代码得记在脑子里。绝大多数数组处理函数,都是“数组首地址 + 数组长度”这样配对出现的。
说到数组长度,还有一个常见的写法——在main里用sizeof(a) / sizeof(a[0])算长度,然后再传给函数:
int a[10] = {1,2,3,4,5,6,7,8,9,10}; int len = sizeof(a) / sizeof(a[0]); // len = 10 printArray(a, len);
这一步只能在main里做,不能在函数里做。因为main里的a是真正的数组,sizeof(a)能算出整个数组占的字节数。而在函数里,arr已经退化成指针了,sizeof(arr)算的是指针大小。
接下来咱们说二维数组作为函数参数。
二维数组传参比一维数组麻烦一点。你定义的时候是int a[3][4],3行4列。那函数参数怎么写?
void print2D(int arr[][4], int row) { for (int i = 0; i < row; i++) { for (int j = 0; j < 4; j++) { printf("%d ", arr[i][j]); } printf("\n"); } }
注意了:列数必须写,行数可以空着。
为什么?因为二维数组在内存里是连续存储的,编译器要知道每一行有多长,才能正确计算偏移量。比如你写arr[i][j],编译器要算“从起始地址开始,跳过i行,每行4个int,再跳过j个int”,它得知道那个4是多少。
行数可以空着,因为传参的时候你额外传了一个row参数告诉它有多少行。
如果你把二维数组当一维指针来处理,还有另一种写法:
void print2D(int *arr, int row, int col) { for (int i = 0; i < row; i++) { for (int j = 0; j < col; j++) { printf("%d ", arr[i * col + j]); // 手动计算偏移 } printf("\n"); } }
调用的时候传print2D(&a[0][0], 3, 4),把首元素的地址传进去,然后手动算位置。这种写法灵活性更高,但可读性差一点。
对于刚学的同学,我建议第一种写法,列数写死,清晰好懂。 等后面你熟练了再玩灵活的那套。
对了,有一种情况容易把人绕晕——函数内部想修改指针的指向,该咋办?
看这个例子:
#include <stdio.h> void allocate(int *p) { p = (int*)malloc(sizeof(int)); *p = 100; } int main() { int *p = NULL; allocate(p); printf("%d\n", *p); // 崩了 return 0; }
你可能会想:“我把指针p传到函数里,函数给它分配内存,外面p应该能用了吧?”
结果是:外面还是NULL。
为啥?因为指针本身也是个变量,你传进去的是指针的值(也就是一个地址),函数拿到的是这个地址的复印件。你给复印件重新赋值,原件还是原来的NULL。
正确的做法是传指针的地址,也就是二级指针:
#include <stdio.h> #include <stdlib.h> void allocate(int **p) { // 接收的是指针的地址 *p = (int*)malloc(sizeof(int)); **p = 100; } int main() { int *p = NULL; allocate(&p); // 传指针的地址 printf("%d\n", *p); // 输出100 free(p); return 0; }
或者,也可以用返回值:
int* allocate() { int *p = (int*)malloc(sizeof(int)); *p = 100; return p; } int main() { int *p = allocate(); printf("%d\n", *p); free(p); return 0; }
这种写法更清晰,推荐新手用这种方式。
再回到数组上来,咱们还要提一嘴字符串作为函数参数。
字符串本质上就是char数组,以\0结尾。它作为参数传递的时候,跟普通数组一样,传的是地址。
#include <stdio.h> int my_strlen(char *s) { int len = 0; while (s[len] != '\0') { len++; } return len; } int main() { char str[] = "Hello"; int len = my_strlen(str); // str是地址 printf("%d\n", len); // 5 return 0; }
my_strlen的参数既可以是char *s,也可以是char s[],这两种写法在函数参数里等价。你传一个字符数组进去,函数顺着地址找到字符,一个接一个往后数,直到碰到\0为止。
最后总结一下这节课的核心:
1. 数组名就是地址,传数组就是传地址。 函数通过地址操作原数组,所以函数里改了外面也会变。这跟传普通变量完全不同。
2. 数组作为参数,长度会“丢失”。 因为传过去的只是首地址,函数不知道数组有多长。所以你得额外传一个n参数表示长度。sizeof在函数里算的是指针大小,不是数组大小。
3. 二维数组作为参数,列数必须写,行数可以空着。 因为编译器需要列数来计算偏移。
4. 如果想在函数里修改指针本身的指向,你得传二级指针,或者用返回值。 因为指针也是变量,传进去的也是复印件。
5. 字符串传参跟数组一样,传的是地址。 函数顺着地址遍历到\0结束。
好了,数组作为函数参数,最关键的就这几点。回去之后自己写几个例子跑一跑:
-
写个函数把数组里所有元素乘以2
-
写个函数找数组里的最大值
-
写个函数打印二维矩阵
你会发现,只要掌握了“传地址”这个核心,写起来就不费劲了。