【C语言-函数-数组作为参数】数组作为函数参数——数组名本身就是地址,这题不按套路出牌

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 260
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

上节课咱们讲值传递的时候说过:你把变量传给函数,函数收到的是它的“复印件”,在函数里面改,外面不变。你传一个整数,传一个浮点数,都是这么个道理。

但是有个东西特别奇怪——数组。你传一个数组给函数,在函数里面改了数组的元素,外面的数组也跟着变了。这到底怎么回事?今天咱们就把这个事彻底讲明白。

先说结论:数组作为函数参数的时候,它不走寻常路。它传进去的不是整个数组的“复印件”,而是数组的地址。

回忆一下咱们之前说的“数组名就是地址”——你写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

  • 写个函数找数组里的最大值

  • 写个函数打印二维矩阵

你会发现,只要掌握了“传地址”这个核心,写起来就不费劲了。