为啥 C 和 C++ 编译器在从未强制执行时允许函数签名中的数组长度?
Posted
技术标签:
【中文标题】为啥 C 和 C++ 编译器在从未强制执行时允许函数签名中的数组长度?【英文标题】:Why do C and C++ compilers allow array lengths in function signatures when they're never enforced?为什么 C 和 C++ 编译器在从未强制执行时允许函数签名中的数组长度? 【发布时间】:2014-05-05 19:47:04 【问题描述】:这是我在学习期间发现的:
#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 允许你将一个参数声明为数组类型,而它的行为却完全像一个指针。 @Brian:我不确定这是支持还是反对这种行为的论据,但如果参数类型是数组类型的typedef
,它也适用。因此,参数类型中的“指向指针的衰减”不仅仅是用*
替换[]
的语法糖,它实际上是通过类型系统。这对某些标准类型(例如 va_list
)具有实际影响,可能使用数组或非数组类型定义。
@songyuanyao 您可以使用指针:int dis(char (*a)[1])
在 C(和 C++)中完成一些不完全不同的事情。然后,传递一个指向数组的指针:dis(&b)
。如果你愿意使用 C++ 中不存在的 C 特性,你也可以说 void foo(int data[static 256])
和 int bar(double matrix[*][*])
之类的东西,但这完全是另一回事。
@StuartOlsen 关键不是哪个标准定义了什么。关键是为什么定义它的人这样定义它。
【参考方案1】:
这是将数组传递给函数的语法的一个怪癖。
实际上,在 C 中不可能传递数组。如果您编写的语法看起来应该传递数组,实际发生的情况是传递了指向数组第一个元素的指针。
由于指针不包含任何长度信息,所以函数形参列表中你[]
的内容实际上被忽略了。
允许这种语法的决定是在 1970 年代做出的,从那时起就引起了很多混乱......
【讨论】:
作为非 C 程序员,我觉得这个答案很容易理解。 +1 +1 表示“允许这种语法的决定是在 1970 年代做出的,从那时起就引起了很多混乱......” 这是对的,但也可以使用void foo(int (*somearray)[20])
语法传递一个大小的数组。在这种情况下,调用者站点上强制执行 20。
-1 作为 C 程序员,我发现这个答案不正确。 []
在多维数组中不会被忽略,如 pat 的答案所示。所以包括数组语法是必要的。此外,即使在单维数组上,也没有什么能阻止编译器发出警告。
通过“你的[]的内容”,我是在专门谈论问题中的代码。这种语法怪癖根本没有必要,同样的事情可以通过使用指针语法来实现,即如果传递了一个指针,那么要求参数是一个指针声明符。例如。在 pat 的示例中,void foo(int (*args)[20]);
另外,严格来说,C 没有多维数组;但它有数组,其元素可以是其他数组。这不会改变任何事情。【参考方案2】:
第一个维度的长度被忽略,但附加维度的长度是必要的,以允许编译器正确计算偏移量。在下面的示例中,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.
在这里,编译器警告说它只会给出数组衰减到的指针的大小,而不是数组本身的大小。
【讨论】:
非常有用 - 与此一致也可能是一维情况下怪癖的原因。 这与一维案例的想法相同。看起来像 C 和 C++ 中的二维数组实际上是一维数组,其中的每个元素都是另一个一维数组。在这种情况下,我们有一个包含 10 个元素的数组,其中每个元素都是“20 个整数的数组”。正如我的帖子中所描述的,实际上传递给函数的是指向args
的第一个元素的指针。在这种情况下,args 的第一个元素是“20 个整数的数组”。指针包括类型信息;传递的是“指向 20 个整数数组的指针”。
是的,这就是int (*)[20]
类型; “指向 20 个整数数组的指针”。
@pat 你说我们只能省略第一个维度而不能省略其他维度那么为什么这段代码运行时没有任何错误或警告代码链接:ide.geeksforgeeks.org/WMoKbsYhB8 请解释。我错过了什么吗?
int (*p)[]
的类型是一个指向长度不确定的一维数组的指针。 *p
的大小未定义,因此您不能直接索引p
(即使索引为0
!)。对p
唯一能做的就是将其取消引用为*p
,然后将其索引为(*p)[i]
。这不会保留原始数组的二维结构。【参考方案3】:
这个问题以及如何在 C++ 中解决它
by pat 和 Matt 已对问题进行了广泛的解释。编译器基本上忽略了数组大小的第一个维度,有效地忽略了传递参数的大小。
另一方面,在 C++ 中,您可以通过两种方式轻松克服此限制:
使用参考文献 使用std::array
(C++11 起)
参考文献
如果您的函数只是尝试读取或修改现有数组(而不是复制它),您可以轻松使用引用。
例如,假设您想要一个函数来重置一个包含十个int
s 的数组,并将每个元素设置为0
。您可以使用以下函数签名轻松做到这一点:
void reset(int (&array)[10]) ...
不仅会work just fine,还会enforce the dimension of the array。
你也可以利用模板来制作上面的代码generic:
template<class Type, std::size_t N>
void reset(Type (&array)[N]) ...
最后你可以利用const
的正确性。让我们考虑一个打印 10 个元素的数组的函数:
void show(const int (&array)[10]) ...
通过应用const
限定符,我们是preventing possible modifications。
数组的标准库类
如果您像我一样认为上述语法既丑陋又不必要,我们可以把它扔进罐子里,改用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
。
【讨论】:
@kietz,很抱歉您的建议修改被拒绝了,但我们automatically assume C++11 is being used,除非另有说明。 这是真的,但我们还应该根据您提供的链接指定是否有任何解决方案仅适用于 C++11。 @trlkly,我同意。我已经相应地编辑了答案。感谢您指出。【参考方案4】:这是 C 的一个有趣功能,如果您愿意,它可以让您有效地击中自己的脚。 我认为原因是 C 只是比汇编语言高出一步。 大小检查和类似的安全功能已被删除,以实现最佳性能,如果程序员非常勤奋,这并不是一件坏事。 此外,将 size 分配给函数参数的好处是,当其他程序员使用该函数时,他们可能会注意到大小限制。仅使用 指针 并不能将该信息传达给下一个程序员。
【讨论】:
是的。 C 旨在信任程序员而不是编译器。如果你如此明目张胆地对数组的末尾进行索引,那么你一定是在做一些特别而有目的的事情。 我在 14 年前开始使用 C 语言进行编程。在我的教授所说的所有内容中,最让我印象深刻的一句话是:“C 是由程序员编写的,是为程序员编写的。”语言非常强大。 (准备陈词滥调)正如本叔叔教导我们的那样,“权力越大,责任越大。”【参考方案5】:首先,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 语言增加了强大的功能。
【讨论】:
您将运行时边界检查与编译时类型检查混淆了。 @Ben Voigt:我只是在谈论边界检查,就像原来的问题一样。 @user34814 编译时边界检查在类型检查的范围内。几种高级语言提供此功能。【参考方案6】:告诉编译器 myArray 指向一个至少有 10 个整数的数组:
void bar(int myArray[static 10])
如果您访问 myArray [10],一个好的编译器应该会给您一个警告。如果没有“static”关键字,10 将毫无意义。
【讨论】:
如果您访问第 11 个元素并且数组包含 至少 10 个元素,为什么编译器会发出警告? 这大概是因为编译器只能强制你有至少 10 个元素。如果您尝试访问第 11 个元素,则无法确定它存在(即使它可能存在)。 我认为这不是对标准的正确解读。[static]
允许编译器在您使用int[5]
调用 bar
时发出警告。它并没有规定您可以访问 within bar
的内容。责任完全在调用方。
error: expected primary-expression before 'static'
从未见过这种语法。这不太可能是标准的 C 或 C++。
@v.oddou,它在 C99、6.7.5.2 和 6.7.5.3 中指定。【参考方案7】:
这是 C 的一个众所周知的“特性”,被传递给 C++,因为 C++ 应该正确编译 C 代码。
问题来自几个方面:
-
数组名应该完全等同于指针。
C 应该是快的,最初被开发为一种“高级汇编程序”(专门用于编写第一个“便携式操作系统”:Unix),所以它不是应该插入“隐藏”代码;因此,运行时范围检查是“禁止的”。
为访问静态数组或动态数组(在堆栈中或已分配)而生成的机器代码实际上是不同的。
由于被调用函数无法知道作为参数传递的数组的“种类”,因此所有内容都应该是指针并被视为指针。
你可以说 C 并不真正支持数组(这不是真的,正如我之前所说的,但它是一个很好的近似值);数组实际上被视为指向数据块的指针,并使用指针算法进行访问。 由于 C 没有任何形式的 RTTI 您必须在函数原型中声明数组元素的大小(以支持指针运算)。这对于多维数组来说甚至更“真实”。
无论如何以上都不再是真的了:p
大多数现代 C/C++ 编译器确实支持边界检查,但标准要求它默认关闭(为了向后兼容)。例如,最近的 gcc 版本使用“-O3 -Wall -Wextra”进行编译时范围检查,使用“-fbounds-checking”进行完整的运行时范围检查。
【讨论】:
也许 C++ 应该在 20 年前编译 C 代码,但它肯定 不是,而且很长一段时间都没有(C ++98?至少是 C99,尚未被任何较新的 C++ 标准“修复”)。 @hyde 这对我来说听起来有点太苛刻了。引用 Stroustrup 的话:“除了少数例外,C 是 C++ 的一个子集。” (C++ PL 第 4 版,第 1.2.1 节)。虽然 C++ 和 C 都在进一步发展,并且存在最新 C 版本中没有的最新 C++ 版本中的功能,但总的来说,我认为 Stroustrup 引用仍然有效。 @mvw 大多数在这个千年中编写的 C 代码,并没有通过避免不兼容的特性来故意保持 C++ 兼容,将使用 C99 designated initializers 语法 (struct MyStruct s = .field1 = 1, .field2 = 2 ;
)初始化结构,因为它是初始化结构的更清晰的方法。结果,大多数当前的 C 代码将被标准 C++ 编译器拒绝,因为大多数 C 代码将初始化结构。
@mvw 或许可以说,C++ 应该与 C 兼容,因此,如果做出某些妥协,就可以编写可以用 C 和 C++ 编译器编译的代码。但这需要使用 两个 C 和 C++ 的子集,而不仅仅是 C++ 的子集。
@hyde 你会惊讶于有多少 C 代码是 C++ 可编译的。几年前,整个 Linux 内核都是 C++ 可编译的(我不知道它是否仍然适用)。我经常在 C++ 编译器中编译 C 代码以获得更好的警告检查,只有“生产”在 C 模式下编译以挤压最大的优化。【参考方案8】:
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
访问时,无论它是如何声明的,它都将被视为一个指针。
【讨论】:
【参考方案9】:尚未回答的一件事是实际问题。
已经给出的答案解释了数组不能按值传递给 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
这样的函数的可移植实现。
【讨论】:
【参考方案10】:允许编译器检查传递的数组大小是否与预期相同。如果不是这种情况,编译器可能会警告问题。
【讨论】:
以上是关于为啥 C 和 C++ 编译器在从未强制执行时允许函数签名中的数组长度?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 Microsoft 的 C/C++ 编译器允许使用逗号分隔表达式的 if 语句? [复制]