C语言经验分享:二维指针与二维数组的两种错误用法

Posted 一起学编程

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C语言经验分享:二维指针与二维数组的两种错误用法相关的知识,希望对你有一定的参考价值。

工作好几年了,一直用的都是C。自认为指针应该很熟悉了。然而,前段时间我对二维指针和二维数组的一个混用,并且我们项目组的一个大牛(博士毕业,工作10+年)在review我的代码也没发现问题,导致代码上线后出现一个异常。我才觉得我对指针只是学废了。找了一些指针和数组的博客资料,记录一下。希望下次不会再犯类似的错误。

引子

首先看一段代码:

void test(int *p)




int main()

    int arr[]= 30, 450,14,5;
    test(arr);
    return 0;

毫无疑问,上面这段代码是运行OK的。因为C语言标准中有以下规则:在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针

第一种错误

那下面这段代码,会正确运行吗:

#include <stdlib.h>

void test(int **p)



int main()

    int arr[]=30,450,14,5;
    test(&arr);
    return 0;

可能有的同学认为这段代码是正确的,因为数组作为函数参数时退化成一个指针,那么我对数组进行取址,这样&arr就是一个二维指针了,所以可以作为函数test的入参。

这个理由貌似有道理,但很遗憾,这段代码会报编译错误:

main.c:3:17: note: expected 'int **' but argument is of type 'int (*)[4]'
 void test(int **p)
           ~~~~~~^

这个错误是说,test函数期望的入参类型是int **。但是实际传入的参数类型是 int (*)[4],即实际传入的类型是指向数组的指针。

C语言标准中是定义了:在函数参数的声明中,数组名被编译器当作指向该数组第一个元素的指针。但是你不能因为数组在函数参数中当成一个指针,你对数组名取地址&arr就认为它的类型就是指向指针的指针(int **),这样以为是错的,因为不具备这样的传递性。C语言规范中只规定了数组名作为函数的入参时会被当做一个指针,但是并没有规定对数组取地址会被当成指向指针的指针。

根据上面代码的报错信息提示,将不能编译通过的代码片段作一个小的修改,就会编译通过了:

#include <stdlib.h>
#include <stdio.h>

void test(int (*p)[4])

    (*p)[2] = 10;


int main()

    int arr[] = 30, 450, 14, 5;
    test(&arr);
    printf("%d\\n", arr[2]);
    return 0;

输出: 10

但其实上面的这种用法非常的怪。至少我在平时的工作中没遇到过这样的写法。这个函数的意图是改变数组的某个元素的内容。那下面的这种写法更加清晰和常见:

void test(int *p)

    p[2] = 10;


int main()

    int arr[] = 30, 450, 14, 5;
    test(arr);
    printf("%d\\n", arr[2]);
    return 0;

第二种错误

这种错误就是我之前在工作中犯的,从而导致出现异常的。

假如有一段代码:

#include <stdlib.h>
#include <stdio.h>
void foo(int **arr, int m, int n)

    int i,j;
    for (i = 0; i < m; i++) 
        for (j = 0; j < n; j++) 
            printf("%d ", arr[i][j]);
        
        printf("\\n");
    


int **alloc_2d(int m, int n)

    int **arr = malloc(m * sizeof(*arr));
    int i;
    for (i = 0; i < m; i++) 
        arr[i] = malloc(n * sizeof(**arr));
    
    return arr;


int main()

    int **joe = alloc_2d(2, 3);
    joe[0][0] = 1;
    joe[0][1] = 2;
    joe[0][2] = 3;
    joe[1][0] = 4;
    joe[1][1] = 5;
    joe[1][2] = 6;
    return 0;

上面的代码至此还是能正常工作的。

现在,我想用函数foo来打印一个二维的数组,那我能用下面的这段代码吗?

int moe[2][3];
moe[0][0] = 1;
moe[0][1] = 2;
moe[0][2] = 3;
moe[1][0] = 4;
moe[1][1] = 5;
moe[1][2] = 6;

foo(moe, 2, 3);

很遗憾,这段代码同样会报编译错误:

ain.c:42:9: warning: passing argument 1 of 'foo' from incompatible pointer type [-Wincompatible-pointer-types]
     foo(moe, 2, 3);
         ^~~
main.c:3:16: note: expected 'int **' but argument is of type 'int (*)[3]'
 void foo(int **arr, int m, int n)

将二维数组作为入参传入foo函数的原因可能是错误的认为二维数组就是二维指针。为什么会这么想呢? 我以前的一个思考过程是这样的:一维数组作为函数参数的时候,编译器会把数组名当作指向该数组第一个元素的指针。所以我想当然的以为:既然一维数组和一维指针在函数参数中等价,那二维数组应该就等价于二维指针。

但是很遗憾,二维数组作为函数参数并等价于二维指针。因为数组作为函数参数时转换为指针没有传递性。也就是说你不能认为一维数组和一维指针作为函数参数等价,就认为二维数组和二维指针就等价了。在C语言中没有这样的传递性。

其实仔细想想,也是很容易明白的。二维数组其实就是一个数组的数组(即它是一个一维数组,只不过它的每个元素又都是一个一维数组)。当二维数组作为函数入参时,比如 int a[3][4]; 它的第一维数组会被转换成指针,相当于是传入了一个指向数组的指针。即作为函数参数, int a[3][4]和 int (*p)[4]等价。那 int (*p)[4]和 int **pp等价吗?肯定不等价, p指针指向类型是 int [4],而pp指向的类型是int *。

那有什么办法能让上面的foo函数编译通过呢?

最直接的方式是将二维数组作为函数的参数传入:

void foo(int arr[2][3], int m, int n)

事实上,上面的第一维的参数可以去掉:

void foo(int arr[][3], int m, int n)

上面也分析了,二维数组作为函数入参和 int (*p)[3]等价,所以也可以写成下面的形式:

void foo(int (*p)[3], int m, int n)

总结

上面说了2维数组作为函数参数的情况,那3维数组呢?

和二维一样:首先将类似三维数组arr[2][3][4]传入到 int ***类型的函数参数是错误的。但是可以将这个三维数组传入到参数类型为 int arr[][3][4]或者 int (*arr)[3][4]的函数中。

"数组名被改写成一个指针参数"规则并不是递归定义的(没有传递性)。数组的数组会被改写为"数组的指针",而不是"指针的指针"。

你之所以能在main()函数中看到char **argv这样的参数,是因为argv是个指针数组(即 char *argv[])。这个表达式被编译器改写为指向数组第一个元素的指针,也就是一个指向指针的指针。如果argv参数事实上被声明为一个数组的数组(也就是char argv[10][5]), 它将被编译器改写为 char (*argv)[15],也就是一个字符数组的指针,而不是char **argv。

作者:Elec

对啦对啦!另外的话为了帮助大家,轻松,高效学习C语言/C++,我给大家分享我收集的资源,从最零基础开始的教程到C语言项目案例,帮助大家在学习C语言的道路上披荆斩棘!

 可以来我粉丝群领取哦~最重要的是你可以在群里面交流提问编程问题哦!

以上是关于C语言经验分享:二维指针与二维数组的两种错误用法的主要内容,如果未能解决你的问题,请参考以下文章

C语言指针(指针数组数组指针函数指针传参回调函数等)超详细

C语言指向二维数组的指针

C语言中关于二维数据指针的问题?

C语言指针指向一维数组与二维数组?

一个关于C语言的指针与二维数组的问题

C语言中二维数组行指针是啥