在C语言中将数组作为参数传递给函数

134

我写了一个包含数组作为参数的函数,并通过以下方式传递数组值来调用它。

void arraytest(int a[])
{
    // changed the array a
    a[0] = a[0] + a[1];
    a[1] = a[0] - a[1];
    a[0] = a[0] - a[1];
}

void main()
{
    int arr[] = {1, 2};
    printf("%d \t %d", arr[0], arr[1]);
    arraytest(arr);
    printf("\n After calling fun arr contains: %d\t %d", arr[0], arr[1]);
}

我发现即使我传入值来调用 arraytest() 函数,int arr[] 的原始副本仍然会被更改。
您能否解释一下为什么会这样?

3
你正在通过引用传递数组,但你正在修改其内容,这就是为什么你看到数据发生变化的原因。 - Shaun Wilde
1
main()函数必须返回int类型。 - underscore_d
这是这个问题的自然延伸:如何在C和C++中将多维数组传递给函数。这里是我对该问题的几种方法 - Gabriel Staples
11个回答

208

在将数组作为参数传递时,这个

void arraytest(int a[])

意思和...完全相同。

void arraytest(int *a)

所以你正在修改main函数中的值。

由于历史原因,数组不是一等公民,不能通过值传递。


5
在哪些情况下使用哪种符号表示法更好? - Heberto Mayorquin
39
@Ramon - 我会选择第二个选项,因为它似乎不那么令人困惑,并且更好地表明您不会得到数组的副本。 - Bo Persson
4
能否解释一下"历史原因"?我认为传递值需要复制,因此会浪费内存...谢谢。 - Jacquelyn.Marquardt
13
在 C 语言最初的版本中,除了单个值以外,它没有采用传值方式。直到 struct 结构体被添加到语言中后,这一点才得以改变。然而对于数组,改变规则已经太晚了,因为已经有成百上千的用户在使用该规则。 :-) - Bo Persson
1
意思与void arraytest(int a[1000])完全相同,详见此处扩展答案:https://dev59.com/GWw15IYBdhLWcg3wfsBy#51527502。 - Gabriel Staples

47

要传递2D(或更高维度)数组,请参见我的其他答案:

  1. 如何在C和C ++中将多维[C风格]数组传递给函数,以及这里:
  2. 仅通过std :: vector<std :: vector <int>>&将多维数组传递给C ++函数

在C(和C ++)中将1D数组作为函数参数传递

1. C中的标准数组用法,自然类型衰减(调整)从数组到ptr

@Bo Persson 在他的出色回答此处中正确地陈述:

When passing an array as a parameter, this

void arraytest(int a[])

means exactly the same as

void arraytest(int *a)

让我对这两个代码片段添加一些注释,以增加清晰度:

// param is array of ints; the arg passed automatically "adjusts" (frequently said
// informally as "decays") from `int []` (array of ints) to `int *` 
// (ptr to int)
void arraytest(int a[])

// ptr to int
void arraytest(int *a)

然而,我也要补充一点,上述两种形式也:
  1. mean exactly the same as

     // array of 0 ints; automatically adjusts (decays) from `int [0]`
     // (array of zero ints) to `int *` (ptr to int)
     void arraytest(int a[0])
    
  2. which means exactly the same as

     // array of 1 int; automatically adjusts (decays) from `int [1]`
     // (array of 1 int) to `int *` (ptr to int)
     void arraytest(int a[1])
    
  3. which means exactly the same as

     // array of 2 ints; automatically adjusts (decays) from `int [2]`
     // (array of 2 ints) to `int *` (ptr to int)
     void arraytest(int a[2])
    
  4. which means exactly the same as

     // array of 1000 ints; automatically adjusts (decays) from `int [1000]`
     // (array of 1000 ints) to `int *` (ptr to int)
     void arraytest(int a[1000])
    
  5. etc.

在上述所有数组示例中,正如下面代码中的示例调用所示,输入参数类型会调整(衰减)为int *,即使打开了构建选项-Wall -Wextra -Werror(有关这3个构建选项的详细信息,请参见我的存储库),也可以无警告和错误地调用,如下所示:
int array1[2];
int * array2 = array1;

// works fine because `array1` automatically decays from an array type
// to a pointer type: `int *`
arraytest(array1);
// works fine because `array2` is already an `int *` 
arraytest(array2);

实际上,这里数组参数中的“size”值([0][1][2][1000]等)显然仅用于美学/自我记录目的,可以是任何正整数(我认为是size_t类型)!
然而,在实践中,您应该使用它来指定您希望函数接收到的数组的最小大小,以便在编写代码时轻松跟踪和验证。MISRA-C-2012标准(在此处购买/下载2012版236页标准PDF,售价15.00英镑)甚至明确规定(强调添加):

Rule 17.5 The function argument corresponding to a parameter declared to have an array type shall have an appropriate number of elements.

...

If a parameter is declared as an array with a specified size, the corresponding argument in each function call should point into an object that has at least as many elements as the array.

...

The use of an array declarator for a function parameter specifies the function interface more clearly than using a pointer. The minimum number of elements expected by the function is explicitly stated, whereas this is not possible with a pointer.

换句话说,他们建议使用显式大小格式,即使C标准在技术上没有强制执行 - 至少它可以帮助开发者以及使用代码的其他人明确该函数期望你传递的数组大小。

2. 在C语言中强制数组类型安全

(不建议(更正:有时候建议,尤其是对于固定大小的多维数组),但是这是可能的。请参见我在结尾反对这样做的简要论点。另外,如果想了解我的多维数组[例如:2D数组]版本,请看这里。)

正如@Winger Sendon在我的答案下方的评论中指出的那样,我们可以通过数组大小来强制C对待数组类型不同!

首先,你必须认识到,在我上面的例子中,使用int array1[2];像这样:arraytest(array1);会导致array1自动衰减为int *。然而,如果你取array1的地址并调用arraytest(&array1),你会得到完全不同的行为!现在它不会衰减成int *了!这是因为如果你取数组的地址,那么你已经有了一个指针类型,而指针类型不会调整到其他指针类型。只有数组类型才能调整到指针类型。所以,&array1的类型是int (*)[2],这意味着"指向大小为2的int数组的指针",或"指向类型为int的大小为2的数组的指针",也可以说成"指向2个int数组的指针"因此,你可以通过传递显式的数组指针来强制C检查数组的类型安全性,就像这样:

// `a` is of type `int (*)[2]`, which means "pointer to array of 2 ints"; 
// since it is already a ptr, it can NOT automatically decay further
// to any other type of ptr 
void arraytest(int (*a)[2])
{
    // my function here
}

这个语法很难读,但与函数指针类似。在线工具cdecl告诉我们int (*a)[2]的意思是:"声明a为指向由2个int组成的数组的指针"(指向2个int的数组的指针)。不要将其与没有括号的版本混淆:int * a[2],它的意思是:"声明a为包含2个指向int的指针的数组"(又称:包含2个指向int的指针的数组,又称:包含2个int*的数组)。
现在,这个函数要求你使用地址运算符(&)调用它,使用正确大小的数组的指针作为输入参数!:
int array1[2];

// ok, since the type of `array1` is `int (*)[2]` (ptr to array of 
// 2 ints)
arraytest(&array1); // you must use the & operator here to prevent
                    // `array1` from otherwise automatically decaying
                    // into `int *`, which is the WRONG input type here!

然而,这将会产生一个警告:

int array1[2];

// WARNING! Wrong type since the type of `array1` decays to `int *`:
//      main.c:32:15: warning: passing argument 1 of ‘arraytest’ from 
//      incompatible pointer type [-Wincompatible-pointer-types]                                                            
//      main.c:22:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
arraytest(array1); // (missing & operator)

您可以在此处测试此代码

为了强制C编译器将此警告转换为错误,以便您必须始终仅使用正确大小和类型的输入数组(在本例中为int array1[2];)调用arraytest(&array1);,请将-Werror添加到构建选项中。如果在onlinegdb.com上运行上面的测试代码,请单击右上角的齿轮图标,然后单击“额外的编译器标志”以键入此选项。现在,这个警告:

main.c:34:15: warning: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Wincompatible-pointer-types]                                                            
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’    

将会转化为这个构建错误:

main.c: In function ‘main’:
main.c:34:15: error: passing argument 1 of ‘arraytest’ from incompatible pointer type [-Werror=incompatible-pointer-types]
     arraytest(array1); // warning!
               ^~~~~~
main.c:24:6: note: expected ‘int (*)[2]’ but argument is of type ‘int *’
 void arraytest(int (*a)[2])
      ^~~~~~~~~
cc1: all warnings being treated as errors

请注意,您还可以创建指向给定大小数组的“类型安全”指针,如下所示:

{{像这样}}

int array[2]; // variable `array` is of type `int [2]`, or "array of 2 ints"

// `array_p` is a "type safe" ptr to array of size 2 of int; ie: its type
// is `int (*)[2]`, which can also be stated: "ptr to array of 2 ints"
int (*array_p)[2] = &array;

...但我并不一定推荐这样做(在C中使用这些“类型安全”的数组),因为它让我想起了C++中用于强制实现类型安全的花招,这样做的代价是语言语法复杂度、冗长性和难以架构代码,而我对此很不喜欢,并且之前多次发表过抨击言论(例如:请参见“我的C++思考”)。


如需进行额外的测试和实验,请查看下面的链接。

参考资料

请参阅上方链接。还有:

  1. 我的在线代码实验:https://onlinegdb.com/B1RsrBDFD

另请参阅:

  1. 我在多维数组(例如2D数组)上的回答,扩展了上述内容,并在多维数组的类型安全方法可行时使用 "类型安全" 方法:如何将多维数组传递给C和C++中的函数

3
void arraytest(int (*a)[1000]) 更好,因为如果大小不正确,编译器会出错。 - wingerse
@WingerSendon,我知道这里有一些微妙之处需要验证,并且语法很令人困惑(就像函数指针语法一样令人困惑),所以我花了些时间,最终更新了我的答案,并添加了一个名为“在C中强制类型安全的数组”的大型新部分,涵盖了你的观点。 - Gabriel Staples
@GabrielStaples,谢谢。你的回答非常有帮助。你能给我推荐一个学习高级C语言的参考资料吗? - daryooosh
请注意,经典的 C 语言教材是 K&R 的《C程序设计语言》一书:https://en.wikipedia.org/wiki/The_C_Programming_Language。 - Gabriel Staples
@daryooosh,你想看点酷的东西吗?看看这个:在另一个程序的main()函数之前或之后动态注入你自己的函数调用。这是又一个发现。获取这种知识是一个漫长而缓慢的过程;我不知道快速获得这种知识的方法,但可以通过尝试解决棘手的问题,然后在无法解决时进行深入研究,最后编写像这样的优秀示例,在第一次学习时就能掌握,并且以后再次学习时也不会忘记在哪里寻找。 - Gabriel Staples
显示剩余3条评论

9
如果你想在函数中将一维数组作为参数传递,你可以通过以下三种方式之一声明形式参数,这三种声明方法都会产生类似的结果,因为每种方法都告诉编译器将接收一个整型指针。
int func(int arr[], ...){
    .
    .
    .
}

int func(int arr[SIZE], ...){
    .
    .
    .
}

int func(int* arr, ...){
    .
    .
    .
}

所以,您正在修改原始值。
谢谢!

2
我在寻找你的第二个例子,你能详细说明每种方法的优点吗? - Puck

9

你没有传递数组的副本,它只是一个指向内存中第一个元素地址的指针。


8

将多维数组作为参数传递给函数。 将一维数组作为参数传递更为普遍,但是传递二维数组会更有趣。 在C语言中,不能使用指向指针(int **)来代替二维数组。 让我们举个例子:

void assignZeros(int(*arr)[5], const int rows) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < 5; j++) {
            *(*(arr + i) + j) = 0;
            // or equivalent assignment
            arr[i][j] = 0;
        }
    }

我这里定义了一个函数,第一个参数是指向包含5个整数的数组的指针。 我可以传递任何有5列的二维数组作为参数:

int arr1[1][5]
int arr1[2][5]
...
int arr1[20][5]
...

您可能会想到定义一个更通用的函数,该函数可以接受任何二元数组,并将函数签名更改为以下形式:
void assignZeros(int ** arr, const int rows, const int cols) {
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            *(*(arr + i) + j) = 0;
        }
    }
}

这段代码虽然可以编译,但在尝试以与第一个函数相同的方式分配值时会出现运行时错误。 因此,在C语言中,多维数组与指向指针的指针...到指针不同。 int(*arr)[5]是指向包含5个元素的数组的指针, int(*arr)[6]是指向包含6个元素的数组的指针,它们是不同类型的指针!
好的,那如何定义更高维度的函数参数呢?很简单,我们只需按照模式进行操作!这里是调整为接受三维数组的同一函数:
void assignZeros2(int(*arr)[4][5], const int dim1, const int dim2, const int dim3) {
    for (int i = 0; i < dim1; i++) {
        for (int j = 0; j < dim2; j++) {
            for (int k = 0; k < dim3; k++) {
                *(*(*(arr + i) + j) + k) = 0;
                // or equivalent assignment
                arr[i][j][k] = 0;
            }
        }
    }
}

正如您所期望的那样,该函数可以将任何第二维有4个元素,第三维有5个元素的三维数组作为参数。以下任何类型的数组都可以:

arr[1][4][5]
arr[2][4][5]
...
arr[10][4][5]
...

但我们必须指定所有尺寸,直到第一个尺寸。


8
你正在传递数组的第一个成员的内存位置的值。
因此,当你在函数内部开始修改数组时,你正在修改原始数组。
记住,a[1]*(a+1)

1
我想应该是缺少了括号,*a+1 应该改为 *(a+1)。 - ShinTakezou
@Shin 谢谢,我已经有一段时间没有玩过 C 了。 - alex

7
在大多数情况下,C中的数组会被转换成指向数组第一个元素的指针。更详细地说,传递到函数中的数组总是被转换为指针。
这里引用自K&R2nd的一句话:
“当将数组名传递给函数时,传递的是初始元素的位置。在被调用的函数内部,该参数是一个局部变量,因此数组名参数是一个指针,即一个包含地址的变量。”
写作方面:
void arraytest(int a[])

与写作具有相同的含义:

void arraytest(int *a)

虽然您没有明确地编写它,但就好像您传递了一个指针一样,因此您正在修改主函数中的值。

此外,我建议阅读this以获取更多信息。

此外,您可以在SO上找到其他答案here


7
你正在传递数组的第一个元素的地址。

6
在C语言中,除了少数特殊情况外,数组引用总是会“衰变”为指向数组第一个元素的指针。因此,不可能通过值传递数组。在函数调用中传递数组将作为指针传递给函数,这类似于按引用传递数组。
编辑:有三种特殊情况,其中数组不会衰减为指向其第一个元素的指针:
  1. sizeof asizeof (&a[0]) 不同。
  2. &a&(&a[0]) 不同(也不完全相同于 &a[0])。
  3. char b[] = "foo"char b[] = &("foo") 不同。

如果我将一个数组传递给一个函数。例如,我创建了一个数组int a[10]并为每个元素分配了随机值。现在,如果我使用int y[]int y[10]int *y将此数组传递到函数中。然后,在该函数中使用sizeof(y),答案将是指针已分配的字节数。因此,在这种情况下,它将会衰减为一个指针,如果您能包括这一点,那将非常有帮助。请参见https://postimg.org/image/prhleuezd/。 - Suraj Jain
如果我在函数中使用sizeof操作符来操作我们最初定义的数组,那么它将会退化为一个数组,但是如果我将其传递给其他函数,那么在那个函数中使用sizeof操作符时,它将会退化为一个指针。 - Suraj Jain
我知道这是老问题。如果有人看到了,我有两个问题 :) 1. @ThomSmith写道,当a是一个数组时,&a&a[0]并不完全相同。为什么?在我的测试程序中,无论是在声明数组的函数中还是在传递到另一个函数中时,两者都显示为相同。 2. 作者写道,“char b [] =“foo””与“char b [] =&(“foo”)”不同。对我来说,后者甚至无法编译。只有我这样吗? - Aviv Cohn

1

如果你使用a[]或者*a,那么数组总是按引用传递:

int* printSquares(int a[], int size, int e[]) {   
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}

int* printSquares(int *a, int size, int e[]) {
    for(int i = 0; i < size; i++) {
        e[i] = i * i;
    }
    return e;
}

我正在点赞这个。我不确定为什么它被踩了。 - Gabriel Staples
@GabrielStaples 我不是那个给你点踩的人,但也许是因为“按引用传递”这个术语在这里非常模糊(甚至可以说是错误的)。引用是仅存在于C++中的一种东西,在那里它的意思恰好相反(即对函数中的非指针引用参数所做的更改会反映在函数外部)。因此,在我们谈论标准C中的指针衰减到指针时,我们真的不应该使用引用这个术语。 - Binarus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接