为什么 C 和 C + + 编译器在函数签名中允许数组长度,而它们从来没有强制执行?

这是我在学习期间发现的:

#include<iostream>
using namespace std;
int dis(char a[1])
{
int length = strlen(a);
char c = a[2];
return length;
}
int main()
{
char b[4] = "abc";
int c = dis(b);
cout << c;
return 0;
}

所以在变量 int dis(char a[1])中,[1]似乎没有任何作用
全部,因为我可以使用 a[2]。就像 int a[]或者 char *a。我知道数组名是一个指针,也知道如何传递数组,所以我的困惑不在于这一部分。

我想知道的是为什么编译器允许这种行为(int a[1])。还是有其他我不知道的意思?

11643 次浏览

这是将数组传递给函数的语法的一个奇特之处。

实际上,在 C 语言中传递数组是不可能的。如果您编写的语法看起来像是应该传递数组,那么实际发生的情况是传递指向数组第一个元素的指针。

由于指针不包含任何长度信息,函数形式参数列表中 []的内容实际上被忽略。

允许使用这种语法的决定是在20世纪70年代做出的,从那时起就引起了许多混乱..。

首先,C 从不检查数组边界。无论它们是局部的、全局的、静态的、参数的还是其他的。检查数组边界意味着更多的处理,而 C 语言被认为是非常有效的,所以数组边界检查是由程序员在需要时完成的。

其次,有一个技巧可以将数组按值传递给函数。也可以从函数中按值返回数组。您只需使用 struct 创建一个新的数据类型。例如:

typedef struct {
int a[10];
} myarray_t;


myarray_t my_function(myarray_t foo) {


myarray_t bar;


...


return bar;


}

您必须像这样访问元素: foo.a [1]。额外的”。A“可能看起来很奇怪,但是这个技巧为 C 语言增加了很多功能。

忽略第一个维度的长度,但是需要额外维度的长度才能让编译器正确地计算偏移量。在下面的示例中,向 foo函数传递一个指向二维数组的指针。

#include <stdio.h>


void foo(int args[10][20])
{
printf("%zd\n", sizeof(args[0]));
}


int main(int argc, char **argv)
{
int a[2][20];
foo(a);
return 0;
}

第一维度 [10]的大小被忽略; 编译器不会阻止您从末尾索引(注意,形式需要10个元素,但实际只提供2个)。但是,第二维 [20]的大小用于确定每一行的跨度,在这里,形式必须与实际。同样,编译器也不会阻止您在第二维的末尾进行索引。

从数组底部到元素 args[row][col]的字节偏移量由下列因素确定:

sizeof(int)*(col + 20*row)

注意,如果是 col >= 20,那么您实际上将索引到后续行(或整个数组的末尾)。

在我的机器上返回 80,其中 sizeof(int) == 4。但是,如果我尝试采用 sizeof(args),我会得到以下编译器警告:

foo.c:5:27: warning: sizeof on array function parameter will return size of 'int (*)[20]' instead of 'int [10][20]' [-Wsizeof-array-argument]
printf("%zd\n", sizeof(args));
^
foo.c:3:14: note: declared here
void foo(int args[10][20])
^
1 warning generated.

这里,编译器警告说,它只会给出数组衰减到的指针的大小,而不是数组本身的大小。

要告诉编译器 myArray 指向至少10整数的数组:

void bar(int myArray[static 10])

如果您访问 myArray [10] ,优秀的编译器应该会给您一个警告。如果没有“ static”关键字,10就什么都不是了。

这是 C 的一个众所周知的“特性”,之所以传递给 C + + ,是因为 C + + 被认为能够正确地编译 C 代码。

问题来自以下几个方面:

  1. 数组名应该完全等效于指针。
  2. C 语言被认为是快速的,最初的开发者是一种“高级汇编程序”(特别是被设计用来编写第一个“便携式操作系统”: Unix) ,所以它被认为是 没有插入“隐藏”代码; 因此运行时范围检查是“禁止的”。
  3. 为访问静态数组或动态数组(在堆栈中或分配的)而生成的机器代码实际上是不同的。
  4. 由于被调用的函数不能知道作为参数传递的数组的“种类”,因此所有内容都应该是一个指针,并作为指针进行处理。

你可以说数组在 C 语言中并不受支持(正如我之前所说,这并不是真的,但这是一个很好的近似值) ; 一个数组实际上被当作指向一个数据块的指针,并使用指针算法进行访问。 由于 C 没有任何形式的 RTTI,你必须在函数原型中声明数组元素的大小(以支持指针算法)。对于多维数组来说更是如此。

不管怎样,以上这些都不再是真的了

大多数现代的 c/c + + 编译器 都支持边界检查,但是标准要求它在默认情况下是关闭的(对于向下兼容)。例如,最新版本的 gcc 使用“-O3-Wall-Wtra”进行编译时范围检查,使用“-fbound-check”进行全运行时边界检查。

C + + 中存在的问题及解决方法

这个问题已经被广泛地解释了 主持人马特。编译器基本上忽略了数组大小的第一个维度,实际上忽略了传递的参数的大小。

另一方面,在 C + + 中,您可以通过两种方式轻松地克服这个限制:

  • 使用参考文献
  • 使用 std::array(自 C + + 11)

参考文献

如果函数只是试图读取或修改现有数组(而不是复制它) ,那么可以很容易地使用引用。

例如,让我们假设您希望有一个函数来重置一个由10个 int组成的数组,并将每个元素设置为 0。通过使用以下函数签名,您可以很容易地做到这一点:

void reset(int (&array)[10]) { ... }

这不仅将 一切正常,但它也将 强制执行数组的维度

你也可以使用 模板来编写上面的代码 非专利药物:

template<class Type, std::size_t N>
void reset(Type (&array)[N]) { ... }

最后,您可以利用 const的正确性:

void show(const int (&array)[10]) { ... }

通过应用 const限定符,我们是 防止可能的修改


数组的标准库类

如果您像我一样认为上面的语法既丑陋又不必要,那么我们可以把它放在罐子里,改用 std::array(因为 C + + 11)。

下面是重构后的代码:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }

这不是很棒吗? 更不用说我之前教你的 泛型代码技巧泛型代码技巧仍然有效:

template<class Type, std::size_t N>
void reset(std::array<Type, N>& array) { ... }


template<class Type, std::size_t N>
void show(const std::array<Type, N>& array) { ... }

不仅如此,您还可以免费获得拷贝和移动语义。 :)

void copy(std::array<Type, N> array) {
// a copy of the original passed array
// is made and can be dealt with indipendently
// from the original
}

那么,你还在等什么? 去用 std::array

这是一个有趣的功能 C,让你有效地拍摄自己的脚,如果你是如此倾向。我认为原因在于 C仅仅比汇编语言高出一步。检查尺寸类似的安全措施特性已经被删除,以允许达到最高性能,如果程序员非常勤奋,这并不是一件坏事。< br > < br > 另外,为函数参数指定一个 尺寸的好处是,当函数被另一个程序员使用时,他们可能会注意到一个大小限制。仅仅使用 指针并不能将这些信息传递给下一个程序员。

C 不仅将类型为 int[5]的参数转换为 *int; 给定声明为 typedef int intArray5[5];,它还将将类型为 intArray5的参数转换为 *int。在某些情况下,这种行为虽然很奇怪,但是非常有用(特别是像 stdargs.h中定义的 va_list这样的情况,有些实现将 va_list定义为数组)。允许将定义为 int[5]的类型作为参数(忽略维度)而不允许直接指定 int[5]是不合逻辑的。

我发现 C 对数组类型参数的处理是荒谬的,但这是努力采用一种特别的语言的结果,其中很大一部分没有特别好的定义或思考,并试图提出与现有实现对现有程序所做的一致的行为规范。从这个角度来看,C 语言的许多怪异之处都是有意义的,尤其是考虑到当许多怪异之处被发明出来的时候,我们今天所知道的语言的很大一部分还不存在。据我所知,在 C 的前身 BCPL 中,编译器并没有很好地跟踪变量类型。声明 int arr[5];等价于 int anonymousAllocation[5],*arr = anonymousAllocation;; 一旦分配被搁置。编译器既不知道也不关心 arr是指针还是数组。当以 arr[x]*arr访问时,它将被视为一个指针,而不管它是如何声明的。

有一件事还没有得到回答,那就是实际的问题。

已经给出的答案解释了数组不能通过值传递给 C 或 C + + 中的函数。它们还解释说,声明为 int[]的参数被当作具有 int *类型的参数处理,并且可以将 int[]类型的变量传递给这样的函数。

但是他们没有解释为什么显式提供数组长度从来没有出错。

void f(int *); // makes perfect sense
void f(int []); // sort of makes sense
void f(int [10]); // makes no sense

为什么最后一个不是错误?

原因之一是它会导致 typedef 出现问题。

typedef int myarray[10];
void f(myarray array);

如果在函数参数中指定数组长度是错误的,则不能在函数参数中使用 myarray名称。而且由于一些实现使用数组类型作为标准库类型,如 va_list,并且所有的实现都需要使 jmp_buf成为数组类型,如果没有使用这些名称声明函数参数的标准方法,这将是非常有问题的: 没有这种能力,就不可能有一个像 vprintf这样的函数的可移植实现。

允许编译器检查传递的数组大小是否与预期的相同。如果不是这种情况,编译器可能会发出警告。