我对在C/C++中传递数组有点困惑。我看到一些情况下签名是这样的:
void f(int arr[])
有些是这样的
void f(int arr[], int size)
有谁能详细说明一下它们的区别以及在什么情况下如何使用它们?
我对在C/C++中传递数组有点困惑。我看到一些情况下签名是这样的:
void f(int arr[])
有些是这样的
void f(int arr[], int size)
有谁能详细说明一下它们的区别以及在什么情况下如何使用它们?
首先,传递给函数的数组实际上会传递一个指向数组第一个元素的指针,例如,如果你有:
int a[] = { 1, 2, 3 };
f(a);
然后,f()
接收到传递给它的 &a[0]
。 所以,在编写函数原型时,以下内容是等价的:
void f(int arr[]);
void f(int *arr);
这意味着数组的大小丢失了,f()
通常不能确定大小。(这是我更喜欢 void f(int *arr)
形式而不是 void f(int arr[])
的原因。)f()
不需要这些信息,在这两种情况下,没有额外的参数给它也是可以的。arr
中有一些特殊约定的值,调用者和 f()
都把它当作“结尾”的意思。例如,可以约定一个值 0
表示“完成”。int a[] = { 1, 2, 3, 0 }; /* make sure there is a 0 at the end */
int result = f(a);
并定义f()
的方式如下:
int f(int *a)
{
size_t i;
int result = 0;
for (i=0; a[i]; ++i) /* loop until we see a 0 */
result += a[i];
return result;
}
显然,上述方案仅在调用者和被调用者都同意一项约定并遵循它时才有效。一个例子是C库中的strlen()函数。它通过查找0
来计算字符串的长度。如果传递的参数没有以0
结尾,那么所有的投注都是不确定的,你会进入未定义的行为领域。
第二种情况是当你实际上并没有一个数组。在这种情况下,f()函数需要一个指向对象的指针(在你的示例中是int
)。所以:
int change_me = 10;
f(&change_me);
printf("%d\n", change_me);
使用
void f(int *a)
{
*a = 42;
}
没问题: f()
并没有操作数组。
void f( int *arr, int size )
C和C++不是同一种语言,但它们有一些共同的子集。你在这里观察到的是,当将“第一个”数组维度传递给函数时,始终只会传递一个指针。一个被声明为
函数的“签名”(C不使用这个术语)void toto(double A[23]);
始终如一
void toto(double *A);
就是说,上面的 23
在编译器中有点多余且不被使用。现代 C 语言(即 C99)在这里有一个扩展,可以让你声明 A 始终具有 23 个元素:
void toto(double A[static 23]);
或者指针被const
修饰
void toto(double A[const 23]);
如果您增加了其他维度,图片会发生变化,那么数组大小将被使用:
void toto(double A[23][7]);
void toto(double (*A)[7]);
这是一个指向包含 7
个元素的数组的指针。在 C++ 中,这些数组边界必须是一个整数常量。在 C 中可以是动态的。
void toto(size_t n, size_t m, double A[n][m]);
需要注意的是,在参数列表中,n
和 m
必须在 A
之前声明。因此最好总是按照这个顺序声明函数的参数。
在C或C++中传递数组时只传递其地址。因此第二种情况非常常见,其中第二个参数是数组中元素的数量。函数仅通过查看数组的地址无法知道它应该包含多少个元素。
第一个签名只是传递数组,没有办法知道数组的大小,可能会导致越界错误和/或安全漏洞。
第二个签名是更安全的版本,因为它允许函数根据数组的大小进行检查,以防止第一个版本的缺陷。
除非这是作业,否则原始数组有点过时。使用std::vector代替。它允许在不必手动传递大小的情况下传递向量,因为它会自动处理。
数组的大小不会随着数组本身一起传递。因此,如果其他函数需要大小,则将其作为参数。
问题在于,有些函数隐含地理解数组的大小。因此,它们不需要明确指定大小。例如,如果您的函数操作包含3个浮点数的数组,则无需用户告诉您它是包含3个浮点数的数组。只需取一个数组即可。
然后还有那些函数(让我们称它们为“可怕的”),它们将使用任意数据填充数组,直到由该数据定义的点。sprintf可能是最好的例子。它将继续将字符放入该数组中,直到完成写入。这非常糟糕,因为用户和函数之间没有明确或隐含的协议来确定该数组的大小。sprintf将写入一些字符,但在一般情况下,用户无法知道确切写入了多少个字符。
这就是为什么您永远不应该使用sprintf的原因;根据您的编译器使用snprintf或_snprintf。
每当您需要知道数组的大小时,都需要提供它。传递数组本身的两种形式并没有什么特别之处;无论哪种方式,第一个参数都是相同的。第二种方法只是提供了需要知道数组大小的信息,而第一种方法则没有。
有时候,数组本身保存有关其大小的信息。例如,在您的第一个示例中,也许 arr[0]
设置为数组的大小,实际数据从 arr[1]
开始。或者考虑 C 字符串的情况... 您只提供一个 char[]
,并且假定数组在第一个等于 \0
的元素处结束。在您的示例中,负值可能充当类似的标记。或者,该函数根本不关心数组的大小,并且将简单地假定它足够大。
这些方法本质上是不安全的,因为很容易忘记设置arr[0]
或者意外覆盖空终止符。然后,f
突然就不知道有多少可用空间了。始终优先显式提供大小,可以通过像您展示的size
参数或使用指向数组末尾的第二个指针来实现。后者是C++标准库函数通常采用的方法。但是,您仍然存在提供错误大小的问题,这就是为什么在C++中不建议首先使用此类数组...而应该使用实际的容器来跟踪该信息。
区别在于第二个包括指示数组大小的参数。逻辑上的结论是,如果您不使用这样的参数,函数就不知道数组大小是多少。事实证明确实如此。实际上,它不知道您有一个数组。事实上,您不必拥有一个数组来调用该函数。
这里的数组语法,方括号内没有指定大小,是一种欺骗行为。该参数实际上是一个指针。有关更多信息,请参见http://c-faq.com/aryptr/index.html,特别是第4节。