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

Why do C and C++ compilers allow array lengths in function signatures when they're never enforced?

本文关键字:数组 强制执行 编译器 C++ 许函数 函数 为什么      更新时间:2023-10-16

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

#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])。还是有我不知道的其他含义?

将数组传递给函数是语法上的一个怪怪。

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

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

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

第一个维度的长度被忽略,但是额外维度的长度是允许编译器正确计算偏移量所必需的。在下面的示例中,传递给foo函数一个指向二维数组的指针。

#include <stdio.h>
void foo(int args[10][20])
{
    printf("%zdn", 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,那么您将实际索引到后续行(或整个数组的末尾)。

sizeof(args[0]),在我的机器上返回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("%zdn", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

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

c++中的问题及解决方法

这个问题已经被pat和Matt详细地解释过了。编译器基本上忽略了数组大小的第一个维度,实际上忽略了传递的参数的大小。

另一方面,在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的正确性。让我们考虑一个输出包含10个元素的数组的函数:

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只是比汇编语言高一步。大小检查类似的安全特性已经被删除,以允许峰值性能,如果程序员非常勤奋,这不是一件坏事。

另外,给函数实参赋size的好处是,当函数被另一个程序员使用时,他们有可能会注意到大小限制。仅仅使用指针并不能将该信息传递给下一个程序员。

首先,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]。额外的"。可能看起来很奇怪,但是这个技巧为C语言增加了很棒的功能。

告诉编译器myArray指向一个至少包含10个int型整数的数组:

void bar(int myArray[static 10])
如果你访问myArray[10],一个好的编译器应该给你一个警告。如果没有"static"关键字,10就没有任何意义。

这是C的一个众所周知的"特性",传递给c++,因为c++应该正确编译C代码。

问题产生于几个方面:

  1. 数组名应该完全等同于指针。
  2. C应该是快速的,最初被开发为一种"高级汇编器"(特别设计用于编写第一个"可移植操作系统":Unix),因此不应该插入"隐藏"代码;
  3. 访问静态数组或动态数组(堆栈中或已分配)的机器码实际上是不同的。
  4. 由于被调用的函数无法知道作为参数传递的数组的"类型",因此所有内容都应该是指针并被视为指针。

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

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

大多数现代C/c++编译器都支持边界检查,但标准要求它在默认情况下关闭(为了向后兼容)。例如,最近的gcc版本使用"-O3 -Wall -Wextra"进行编译时范围检查,使用"-fbounds-checking"进行完整运行时边界检查。

C不仅将类型为int[5]的参数转换为*int;给定声明typedef int intArray5[5];,它将把类型为intArray5的参数也转换为*int。在某些情况下,这种行为虽然奇怪,但很有用(特别是在stdargs.h中定义的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这样的函数的可移植实现。

允许编译器能够检查传递的数组的大小是否与期望的相同。如果不是这样,编译器可能会警告一个问题。