为什么C和C ++编译器从不强制执行函数签名中的数组长度?


131

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

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


6
那是因为您实际上无法将数组传递给函数。
Ed S.

37
我认为这里的问题是,为什么C允许您将参数声明为数组类型,但无论如何它都将表现得完全像指针。
2014年

8
@Brian:我不确定这是支持还是反对行为的参数,但是如果参数类型是typedefwith数组类型,它也适用。因此,参数类型中的“指向指针的衰变”不仅仅是将语法糖替换[]*,它实际上是在类型系统中进行的。这对于某些va_list可能使用数组或非数组类型定义的标准类型具有现实意义。
R .. GitHub停止帮助ICE,2014年

4
@songyuanyao您可以使用指针在C(和C ++)中完成并非完全不同的操作int dis(char (*a)[1])。然后,将指针传递给数组:dis(&b)。如果您愿意使用C ++中不存在的C功能,那么您也可以说诸如void foo(int data[static 256])int bar(double matrix[*][*]),但这是蠕虫的另一种形式。
斯图尔特·奥尔森

1
@StuartOlsen关键不是哪个标准定义了什么。关键是为什么定义它的人都这样定义。
user253751 2014年

Answers:


156

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

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

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

允许使用这种语法的决定是在1970年代做出的,此后就引起了很多混乱。


21
作为非C程序员,我发现此答案非常容易理解。+1
asteri 2014年

21
+1代表“允许使用这种语法的决定是在1970年代,从那以后就引起了很多混乱……”
NoSenseEtAl 2014年

8
这是正确的,但也可以使用语法传递该大小的数组void foo(int (*somearray)[20])。在这种情况下,在呼叫者站点上强制执行20。
v.oddou

14
-1作为C程序员,我发现此答案不正确。[]如pat的答案所示,在多维数组中不会被忽略。因此,包括数组语法是必要的。另外,即使在单维数组上,也没有什么可以阻止编译器发出警告。
user694733 2014年

7
通过“您的[]的内容”,我正在专门讨论“问题”中的代码。根本不需要这种语法怪异,可以通过使用指针语法来实现相同的目的,即,如果传递了指针,则要求参数是指针声明符。例如,在pat的示例中,void foo(int (*args)[20]);同样,严格来讲C没有多维数组。但它具有其元素可以是其他数组的数组。这不会改变任何东西。
MM

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;
}

第一维的大小将[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("%zd\n", sizeof(args));
                          ^
foo.c:3:14: note: declared here
void foo(int args[10][20])
             ^
1 warning generated.

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


非常有用-与之保持一致也是一维情况下怪异的原因。
jwg 2014年

1
这与一维情况相同。在C和C ++中看起来像2-D数组实际上是一个1-D数组,其中每个元素都是另一个1-D数组。在这种情况下,我们有一个包含10个元素的数组,每个元素都是“ 20个整数的数组”。如我的帖子所述,实际传递给函数的是指向的第一个元素的指针args。在这种情况下,args的第一个元素是“ 20个整数的数组”。指针包括类型信息;传递的是“指向20个整数的数组的指针”。
MM 2014年

9
是的,这就是int (*)[20]类型。“指向20个整数的数组的指针”。
2014年

33

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

这个问题已经patMatt广泛解释。编译器基本上是在忽略数组大小的第一维,实际上是在忽略传递的参数的大小。

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

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

参考资料

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

例如,假设您想要一个函数来重置一个10 ints 数组,将每个元素设置为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


2
@kietz,很抱歉您的建议编辑被拒绝,但是除非另有说明,否则我们会自动假定正在使用C ++ 11
2014年

的确如此,但是我们还应该根据您提供的链接指定是否有仅C ++ 11的解决方案。
2014年

@trlkly,我同意。我已经相应地编辑了答案。感谢您指出。
2014年

9

这是C的一项有趣功能,如果您这样倾斜,就可以有效地用脚射击自己。

我认为原因是C只是汇编语言之上的一步。删除了大小检查类似的安全功能,以实现最佳性能,如果程序员非常勤奋,这不是一件坏事。

同样,为函数参数分配大小有一个好处,就是当另一个程序员使用该函数时,他们有可能会注意到大小限制。仅使用指针不会将该信息传达给下一个程序员。


3
是。C旨在使程序员信任编译器。如果您如此公然地索引数组的末尾,则必须进行一些特殊且有意的操作。
约翰

7
14年前,我在C语言编程方面不遗余力。在我的所有教授中,一个词比其他词更给我留下了深刻的印象:“ C是由程序员编写的,是为程序员编写的。” 该语言非常强大。(为陈词滥调做准备)正如本伯叔叔告诉我们的那样:“能力越大,责任就越大”。
Andrew Falanga 2014年

6

首先,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语言增加了强大的功能。


7
您将运行时边界检查与编译时类型检查混为一谈。
Ben Voigt 2014年

@Ben Voigt:我只是在谈论边界检查,就像原来的问题一样。
user34814 2014年

2
@ user34814编译时边界检查在类型检查的范围内。几种高级语言提供了此功能。
Leushenko

5

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

void bar(int myArray[static 10])

如果您访问myArray [10],那么好的编译器应该给您警告。如果没有“ static”关键字,则10毫无意义。


1
如果您访问第11个元素并且该数组包含至少 10个元素,为什么编译器会发出警告?
nwellnhof 2014年

大概是因为编译器只能强制您拥有至少 10个元素。如果尝试访问第11个元素,则无法确定它是否存在(即使可能存在)。
Dylan Watson 2014年

2
我认为这不是对标准的正确理解。[static]允许编译器在调用 bar时发出警告int[5]。它没有规定你可以访问 bar。责任完全在呼叫方。
tab

3
error: expected primary-expression before 'static'从未见过这种语法。这不太可能是标准的C或C ++。
v.oddou

3
@ v.oddou,在C99中的6.7.5.2和6.7.5.3中指定。
塞缪尔·埃德温·沃德

5

这是C的众所周知的“功能”,因为C ++应该正确地编译C代码,所以传递给C ++。

问题来自多个方面:

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

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

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

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


也许C ++ 应该在20年前编写的C代码,但它肯定没有了,也没有很长一段时间(C ++ 98?C99至少,它没有被任何新的C ++标准的“固定”)。
海德2014年

@hyde对我来说听起来太苛刻了。用Stroustrup引用“除少数例外,C是C ++的子集。” (C ++ PL第4版,第1.2.1节)。虽然C ++和C都在进一步发展,并且存在最新C版本中的功能,但最新C ++版本中没有这些功能,但总体而言,我认为Stroustrup引用仍然有效。
mvw 2014年

@mvw在此千年中编写的大多数C代码,由于避免不兼容的功能而没有有意使C ++兼容,因此将使用C99 指定的初始化程序语法(struct MyStruct s = { .field1 = 1, .field2 = 2 };)来初始化结构,因为这是初始化结构的一种更为清晰的方法。结果,大多数当前的C代码将被标准C ++编译器拒绝,因为大多数C代码将初始化结构。
海德2014年

@mvw也许可以说,C ++应该与C兼容,因此,如果做出某些折衷,就有可能编写可以同时使用C和C ++编译器进行编译的代码。但是,这需要使用的一个子集两者 C和C ++,不只是子集的C ++。
海德2014年

@hyde您会惊讶于C ++可以编译多少C代码。几年前,整个Linux内核都是C ++可编译的(我不知道它是否仍然适用)。我通常在C ++编译器中编译C代码以获得高级警告检查,只有“生产”在C模式下编译才能获得最大的优化。
ZioByte 2014年

3

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

我发现C对数组类型的参数的处理是荒谬的,但这是努力采用一种即席语言的结果,该语言中的大部分不是特别明确定义或深思熟虑,并试图提出行为与现有实现对现有程序所做的操作一致的规范。从这种角度来看,许多C的怪癖是有道理的,特别是如果人们认为当他们中的许多发明了时,我们今天所知道的语言的大部分还不存在。据我了解,在C的前身BCPL中,编译器并没有很好地跟踪变量类型。声明int arr[5];等同于int anonymousAllocation[5],*arr = anonymousAllocation;; 一旦分配被搁置。编译器既不知道也不在乎arr是指针或数组。当以arr[x]或方式访问时*arr,无论如何声明,它都将被视为指针。


1

尚未解决的一件事是实际问题。

已经给出的答案说明,数组不能通过值传递给C或C ++中的函数。他们还解释说,声明为的参数将int[]被视为具有type 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


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.