c-在参数声明中指定数组的大小有什么意义

  • 本文关键字:数组 参数 声明 arrays c pointers
  • 更新时间 :
  • 英文 :

#include <stdio.h>
int a[] = {1,2};
void test(int in[3]){
//
}

int main() {
test(a); 
return 0;
}

在上述代码中,int in[3]int *in相同。数字3实际上没有任何作用,它甚至不是正确的大小,但即使如此,编译器也不会抱怨。那么,这种语法在C中被接受是有原因的,还是我缺少了一个功能?

当数组参数声明包含常量大小时,它唯一可以作为读者的文档,通过向他们指示函数所需的数组大小。对于常量表达式n,编译器将数组声明(如int in[n])转换为int *in,之后编译器没有差异,因此n的值不会影响任何内容。

最初在C中,函数参数是由初始函数声明之后的声明列表指定的,例如:

int f(a, b, c)
int a;
float b;
int c[3];
{
… function body
}

我猜想,在这些声明中允许数组大小,只是因为它们使用了与其他声明相同的语法。编写排除大小的编译器代码和文档要比简单地允许它们发生但忽略它们更困难。当在函数原型(int f(int a, float b, int c[3]))中声明参数类型时,我猜想也应用了同样的推理。

但是:

  • 如果声明包含static,如int in[static n]中所示,则在调用函数时,根据C 2018 6.7.6.3 7,相应的参数必须至少指向n元素。编译器可能会将其用于优化
  • 如果数组大小不是常数,则在调用函数时编译器可能会对其进行求值。例如,如果函数声明为void test(int in[printf("Hi")]),那么当调用函数时,GCC 10.2和Apple Clang 11.0都会打印"嗨"。(然而,我不清楚C标准是否需要进行此评估。)
  • 此调整仅针对实际数组参数,而不针对其中的数组。例如,在参数声明int x[3][4]中,x的类型被调整为int (*)[4]。4仍然是大小的一部分,并且对x的指针算术有影响
  • 当参数被声明为数组时,元素类型必须是完整的。相反,声明为指针的参数不需要指向完整的类型。例如,如果struct foo尚未完全定义,但struct foo *x未完全定义,则struct foo x[3]会生成诊断消息

如果我们在函数定义中指定数组的大小,它可以用于使用静态分析工具检查错误。以下代码使用了cppcheck工具。

#include <stdio.h>
void test(int in[3])
{
in[3] = 4;
}

输出为:

Cppcheck 2.2
[test.cpp:4]: (error) Array 'in[3]' accessed at index 3, which is out of bounds.
Done!

但是,如果你不给出任何大小,你就不会从cppcheck中得到任何错误。

#include <stdio.h>
void test(int in[])
{
in[3] = 4;
}

输出为:

Cppcheck 2.2
Done!

但是,一般来说,在函数定义中不需要指定数组的大小。使用sizeof运算符,我们无法在另一个函数中找到数组的大小,因为只复制指针的值。因此,sizeof运算符的输入将是int*类型,而不是int[]类型(在函数test()内部)。因此,数组大小的值不会影响代码。请参阅以下代码:

#include <stdio.h>
int a[] = {1, 2, 3, 4, 5, 6, 7, 8};
void test(int in[8]) // Same as void test(int *arr)
{
unsigned int n = sizeof(in) / sizeof(in[0]); // sizeof(int*)/sizeof(int)
printf("Array size inside test() is %dn", n);
}
int main()
{
unsigned int n = sizeof(a) / sizeof(a[0]);  //sizeof(int[])/sizeof(int)
printf("Array size inside main() is %dn", n);
test(a);
return 0;
}

输出为:

Array size inside main() is 8
Array size inside test() is 2

因此,我们需要将数组的大小与另一个变量一起传递。

在C中,指向一个结构的指针和指向相同数据结构的数组的指针之间没有区别。要获得下一个的起始地址,只需用数据的大小来增加指针,由于不可能只根据指针本身来确定大小,因此作为程序员,必须提供这一点。

让我们尝试修改程序

#include <stdio.h>
void test(int in[3]){
printf("%d %d,%d,%dn",in[0],in[1],in[2],in[3]); // !Sic bug intentional 
}
int main() {
int a[] = {1,2};
int b[] = {3,4};
test(a); 
test(b); 
return 0;
}

运行它:

$ gcc pointer_size.c  -o a.out  && ./a.out 
1 2,3,4
3 4,-1420617472,-1719256057

在这种情况下,数组是背靠背放置的,因此从a读取索引2和3将产生从b读取的数据,当我们从b读取太多时,将读取这些地址上的任何内容。

即使到目前为止,这也是安全漏洞的常见来源。

就C语言和编译器而言,是否指定大小无关紧要,因为数组无论如何都会调整为指向第一个元素的指针。

然而,声明大小可以提高通过编译器以外的外部工具进行静态分析的能力。例如,静态分析器可以很容易地判断出这是一个数组越界错误:

void test(int in[3]){
in[3] = 0;
}

但它不知道这是否是一个错误:

void test(int* in){
in[3] = 0;
}

与此相关的是,不同数组大小之间不存在的类型安全性实际上可以通过使用指针传递数组来解决。因为指向数组的指针不会衰减,并且会挑剔是否获得正确的大小。示例:

void test(int (*in)[3]){
int* ptr = *in;
ptr[3] = 0;
}
int foo[10];
test(&foo);  // compiler error
int bar[3];
test(&bar);  // ok

然而,这种技巧使代码更难阅读和理解。

最新更新