在C语言中使用数组参数被认为是一种不好的做法吗?

5
当声明一个访问连续内存值的函数时,我通常使用数组参数,例如:
f(int a[4]);

对于我的目的来说,它运作良好。然而,我最近阅读了Linus Torvalds的观点

因此,我想知道数组参数是否已被认为过时?更具体地说,

  • 编译器是否有任何情况可以利用此信息(数组大小)来检查越界访问,或者
  • 是否有任何情况可以通过这种技术带来一些优化机会?

无论如何,指向数组的指针又如何呢?

void f(int (*a)[4]);

请注意,此表单不容易出现“sizeof”错误。但在这种情况下效率如何?我知道GCC生成相同的代码(link)。是否总是如此?在这种情况下还有什么进一步的优化机会吗?

你还有什么其他建议可以将二维数组传递给函数?将函数参数定义为数组可以让编译器正确地对其进行索引,而将其定义为指针则无法提供足够的信息给编译器。请注意,外部维度的长度无论如何都会被忽略,因此对于定义字符串长度是没有用处的。 - Weather Vane
1
不传递数组会使像字符串这样的东西使用起来非常棘手... - Shawn
void f(int (*a)[4]);void g(int a[4]); 不是等价的。每个函数中只需使用 printf("%p\n", (void*)(a + 1)); - pmg
@WeatherVane 不错的观点(二维数组)。然而,恐怕这正是Linus所指的:sizeof运算符对于一个维度的工作方式如预期一样,但对于另一个维度则不然。因此,事实上,它可能会非常具有误导性。 - DaBler
如已注释,您无法有意义地在函数数组或指针参数上使用 sizeof,因此请避免使用它。 - Weather Vane
2个回答

9
如果你写代码
void f(int a[4]);

这意味着编译器将完全按照您编写的方式进行解释。

void f(int *a);

这就是为什么Linus有他的观点。 [4] 看起来像是定义数组的预期大小,但实际上不是这样。当你试图维护一个庞大而复杂的程序时,代码看起来意味着什么和实际意义之间的不匹配非常糟糕。
(一般来说,我建议人们不要假设Linus的观点是正确的。在这种情况下,我同意他的观点,但我不会那么生气地表达。)
自C99以来,有一种变化确实意味着它看起来意味着什么:
void f(int a[static 4]);

也就是说,所有调用f的函数都需要提供一个指向至少四个int数组的指针;如果他们没有这样做,程序的行为将是未定义的。原则上,这可以帮助优化器(例如,也许意味着f内部对a[i]的循环可以矢量化)。
您的替代方案是:
void f(int (*a)[4]);

将参数a赋予不同的类型(“指向4个整数数组的指针”而不是“指向整数的指针”)。该类型的数组表示法等效于:

void f(int a[][4]);

这样写可以立即清楚地表明当参数f是一个内部大小为4的二维数组时,声明才是适当的,否则不是。

sizeof问题是另一桩麻烦事。我的建议是尽可能避免在函数参数上使用sizeof。不要扭曲函数参数列表以使sizeof在函数内正确,这会使调用函数更难以正确执行,并且你可能会比实现它更频繁地调用函数。


我会遵循您的建议,不再使用指向数组的指针。然而,我希望找到一些情况,在这种构造方式除了代码文档之外还能带来其他的可能性。但是可能并没有更多需要说的了,所以我将接受这个答案。 - DaBler

0

除非它是sizeof或一元&运算符的操作数,或者是用于在声明中初始化字符数组的字符串字面量,否则类型为“N元素数组of T”的表达式将被转换(“衰减”)为类型为“指向T的指针”的表达式,并且表达式的值将是数组中第一个元素的地址。

当您将数组表达式作为参数传递给函数时:

int arr[100];
...
foo( arr );

函数实际接收的是数组第一个元素的指针,而不是数组的副本。其行为与您编写的代码完全相同:

foo( &arr[0] );

有一个规则,类型为T a[N]T a[]的函数参数会被“调整”为T *a,因此如果您的函数声明是

void foo( int a[100] )

它将被解释为你所写的一样

void foo( int *a )

这会带来一些重要的后果:

  • 数组在函数中被隐式地“按引用”传递,因此对数组内容的更改会反映在调用者中(与其他类型不同);

  • 您不能使用sizeof来确定传递的数组中有多少个元素,因为无法从指针中获取该信息。如果您的函数需要知道数组的物理大小以便正确使用它,则必须将该长度作为单独的参数传递1

在我的代码中,我不在函数参数列表中使用数组样式声明 - 函数接收的是指针,因此我使用指针样式声明。我可以看到使用数组样式声明的论点,主要是作为文档说明(此函数期望具有此大小的数组),但我认为强调参数的指针性是有价值的。

请注意,指向数组的指针也存在相同的问题 - 如果我调用

foo( &arr );

那么foo的原型需要是这样的

void foo( int (*a)[100] );

但这也是同样的原型,就像我调用它一样

void bar[10][100];

foo( bar );  

就像你无法知道参数a是指向单个int还是指向一系列int中的第一个,你也无法知道bar是指向单个100元素数组还是指向100元素数组序列中的第一个。


这就是为什么在 C99 之后 gets 函数被弃用,并在 C2011 中从标准库中删除的原因 - 没有办法告诉它目标缓冲区的大小,因此它会愉快地写入超出数组末尾的输入并覆盖其后面的任何内容。这就是为什么它成为了如此流行的恶意软件攻击方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接