为什么C和C++编译器在从未强制实施时允许函数签名中的数组长度?

Fan*_*anl 131 c c++ arrays

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

#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;
}  
Run Code Online (Sandbox Code Playgroud)

所以在变量中int dis(char a[1]),[1]似乎什么都不做,根本不起作用
,因为我可以使用a[2].就像int a[]char *a.我知道数组名称是一个指针,以及如何传达一个数组,所以我的谜题不是这个部分.

我想知道的是为什么编译器允许这种行为(int a[1]).或者它有其他我不知道的含义?

M.M*_*M.M 155

这是将数组传递给函数的语法的怪癖.

实际上,无法在C中传递数组.如果您编写的语法看起来应该通过数组,那么实际发生的是传递指向数组第一个元素的指针.

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

允许这种语法的决定是在20世纪70年代做出的,并且自从......以来引起了很大的混乱.

  • 为"允许这种语法的决定是在20世纪70年代制定的,并且从那以后引起了很多混乱......" (21认同)
  • 作为一名非C程序员,我发现这个答案非常容易理解.+1 (20认同)
  • -1作为C程序员,我发现这个答案不正确.在多维数组中不会忽略`[]`,如pat的答案所示.所以包括数组语法是必要的.此外,即使在单维数组上,也没有什么能阻止编译器发出警告. (14认同)
  • 这是正确的,但也可以使用`void foo(int(*somearray)[20])`语法传递__just的数组__just.在这种情况下,在呼叫者站点上强制执行20. (8认同)
  • 通过"你[]的内容",我正在具体谈论问题中的代码.这种语法怪癖根本不是必需的,通过使用指针语法可以实现同样的目的,即如果传递指针则需要将参数作为指针声明符.例如,在pat的例子中,`void foo(int(*args)[20]);`而且,严格来说C不具有多维数组; 但它有数组,其元素可以是其他数组.这不会改变任何事情. (7认同)
  • @MattMcNabb实际上C标准确实使用了术语(N1570.pdf 6.5.2.1.3):*"连续的下标运算符指定多维数组对象的元素."*.虽然`[]`对于1D数组并不是绝对必要的,但它更好地记录了意图:*"此函数采用对象数组而不是单个对象"*.但既然你的回答有一个单词"你的",我会删除-1,如果可以的话. (4认同)

pat*_*pat 143

忽略第一个维度的长度,但需要额外维度的长度以允许编译器正确计算偏移量.在下面的示例中,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;
}
Run Code Online (Sandbox Code Playgroud)

[10]忽略第一个维度的大小; 编译器不会阻止你索引结束(请注意,正式需要10个元素,但实际只提供2个元素).但是,第二个维度的大小[20]用于确定每一行的步幅,这里,形式必须与实际匹配.同样,编译器也不会阻止您索引第二个维度的末尾.

从数组基址到元素的字节偏移量由下式args[row][col]确定:

sizeof(int)*(col + 20*row)
Run Code Online (Sandbox Code Playgroud)

请注意,如果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("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.
Run Code Online (Sandbox Code Playgroud)

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

  • 是的,这就是`int(*)[20]`类型; "指向20个整数数组的指针". (9认同)

Sho*_*hoe 33

问题以及如何在C++中克服它

这个问题已经得到了广泛的解释拍拍马特.编译器基本上忽略了数组大小的第一个维度,有效地忽略了传递参数的大小.

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

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

参考

如果您的函数只是尝试读取或修改现有数组(而不是复制它),则可以轻松使用引用.

例如,假设您想要一个函数来重置int每个元素的十进制数组0.您可以使用以下函数签名轻松完成此操作:

void reset(int (&array)[10]) { ... }
Run Code Online (Sandbox Code Playgroud)

这不仅可以正常工作,而且还可以强制执行数组的维度.

您还可以使用模板使上述代码具有通用性:

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

最后你可以利用const正确性.让我们考虑一个打印10个元素数组的函数:

void show(const int (&array)[10]) { ... }
Run Code Online (Sandbox Code Playgroud)

通过应用const限定符,我们正在阻止可能的修改.


数组的标准库类

如果您认为上述语法既丑陋又不必要,就像我一样,我们可以把它放在can中并使用std::array(因为C++ 11).

这是重构的代码:

void reset(std::array<int, 10>& array) { ... }
void show(std::array<int, 10> const& array) { ... }
Run Code Online (Sandbox Code Playgroud)

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

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) { ... }
Run Code Online (Sandbox Code Playgroud)

不仅如此,你还可以免费获得复制和移动语义.:)

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
}
Run Code Online (Sandbox Code Playgroud)

你还在等什么?去使用std::array.

  • @kietz,我很抱歉你的建议编辑遭到拒绝,但我们[自动假设正在使用C++ 11](http://meta.stackexchange.com/a/112650/152998),除非另有说明. (2认同)

bil*_*ill 9

这是C的一个有趣的特点,如果你如此倾向,它可以让你有效地射击自己.

我认为原因是C只比汇编语言高出一步.大小检查类似的安全功能已被删除,以便获得最佳性能,这是不是一件坏事,如果程序员正在很勤快.

此外,分配大小的函数参数的优点是当函数被另一个程序员,有一个机会,他们会发现一个大小限制.仅使用指针不会将该信息传达给下一个程序员.

  • 14年前,我在C编程中切入了牙齿.在我所有的教授中,有一句话比其他所有人更加坚持我,"C是程序员为程序员编写的." 这种语言非常强大.(准备陈词滥调)正如本叔叔教导我们的那样,"有了巨大的力量,就会有很大的责任感." (7认同)
  • 是.C旨在通过编译器信任程序员.如果你如此公然地索引数组的结尾,你必须做一些特别和有意的事情. (3认同)

use*_*814 6

首先,C从不检查数组边界.如果它们是本地的,全局的,静态的,参数等等都无所谓.检查数组边界意味着更多的处理,并且C应该是非常有效的,因此数组边界检查由程序员在需要时完成.

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

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

myarray_t my_function(myarray_t foo) {

  myarray_t bar;

  ...

  return bar;

}
Run Code Online (Sandbox Code Playgroud)

你必须访问这样的元素:foo.a [1].额外的".a"可能看起来很奇怪,但这个技巧为C语言增添了很多功能.

  • 您使用编译时类型检查混淆运行时边界检查. (7认同)
  • @ user34814编译时边界检查在类型检查的范围内.一些高级语言提供此功能. (2认同)

gna*_*729 5

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

void bar(int myArray[static 10])
Run Code Online (Sandbox Code Playgroud)

如果访问myArray [10],一个好的编译器应该会给你一个警告.如果没有"static"关键字,那么10就没有任何意义.

  • `error:在'static'之前预期的primary-expression从未见过这种语法.这不太可能是标准的C或C++. (3认同)
  • @ v.oddou,它在C99中指定,在6.7.5.2和6.7.5.3中. (3认同)
  • 我不认为这是对标准的正确解读.`[静态]`允许编译器在你用*int [5]`调用*`bar`时发出警告.它没有规定你在*`bar`中可以访问*.责任完全在来电方面. (2认同)

Zio*_*yte 5

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

问题来自几个方面:

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

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

无论如何,以上所有都不再是真的:p

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