为什么 C 和 C++ 编译器允许函数签名中的数组长度,而它们从未强制执行?
Why do C and C++ compilers allow array lengths in function signatures when they're never enforced?
这是我在学习期间的发现:
#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代码。
问题产生于几个方面:
- 数组名应该完全等同于指针。
- C应该是快速的,最初被开发为一种"高级汇编器"(特别设计用于编写第一个"可移植操作系统":Unix),因此不应该插入"隐藏"代码;
- 访问静态数组或动态数组(堆栈中或已分配)的机器码实际上是不同的。
- 由于被调用的函数无法知道作为参数传递的数组的"类型",因此所有内容都应该是指针并被视为指针。
你可以说数组在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
这样的函数的可移植实现。
允许编译器能够检查传递的数组的大小是否与期望的相同。如果不是这样,编译器可能会警告一个问题。
- 在 C++ Consol 中的数组中执行用户命令时出现问题
- 将C++数组强制转换为矢量
- C# 是否有办法将双精度数组强制转换为类似于C++转换为 char* 的字符串?
- 为什么泄漏内存比在动态数组上执行 delete[] 慢
- 使用 for 循环反转数组不执行任何操作
- 如何将基类指针数组强制转换为派生类
- 将无符号字符数组强制转换为long-long
- C++:传递二维数组以执行函数
- C++ 将二进制数组强制转换为类
- 在 c++ 中将多维数组强制转换为其超类的多维数组
- 如何在每个块的元素多于线程的数组上执行并行扫描
- 将字节数组强制转换为浮点
- c++将数组强制转换为可变大小的向量
- 不能将数组强制转换为指针
- 将数组数组强制转换为指针数组
- 使用数组元素执行计算
- 将char数组强制转换为结构时出现Segfault
- 将指针数组强制转换为其他对象
- 将数组强制转换为变量
- 将整数数组强制转换为空指针-phread_create