16

这个问题是关于可变参数函数,以及它们的最后一个命名参数,在省略号之前:

void f(Type paramN, ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

我正在阅读 C 标准,发现va_start宏有以下限制:

参数 parmN 是函数定义中可变参数列表中最右边参数的标识符(就在 , ... 之前的那个)。如果参数 parmN 使用寄存器存储类、函数或数组类型或与应用默认参数提升后产生的类型不兼容的类型声明,则行为未定义。

我想知道为什么以下代码的行为未定义

void f(int paramN[], ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

并且不是未定义的以下

void f(int *paramN, ...) {
  va_list ap;
  va_start(ap, paramN);
  va_end(ap);
}

这些宏旨在通过纯 C 代码实现。但是纯 C 代码无法确定paramN是声明为数组还是指针。在这两种情况下,参数的类型都被调整为指针。函数类型参数也是如此。

我想知道:这种限制的理由是什么?当这些参数调整在内部就位时,某些编译器是否在实现这一点时遇到问题?(同样的未定义行为也适用于 C++ - 所以我的问题也是关于 C++ 的)。

4

5 回答 5

6

对寄存器参数或函数参数的限制可能类似于:

  • 不允许使用register存储类获取变量的地址。
  • 函数指针有时与指向对象的指针完全不同。例如,它们可能比指向对象的指针大(您不能可靠地将函数指针转换为对象指针并再次转换回来),因此向函数指针的地址添加一些固定数字可能无法让您到达下一个参数. 如果va_start()和/或va_arg()通过在地址上添加一些固定数量来实现,paramN并且函数指针大于对象指针,则计算最终会导致对象va_arg()返回的地址错误。这似乎不是实现这些宏的好方法,但可能有平台具有(甚至需要)这种类型的实现。

我想不出阻止允许数组参数的问题是什么,但 PJ Plauger 在他的《标准 C 库》一书中这样说:

对 中定义的宏施加的一些限制<stdarg.h>似乎是不必要的严厉。对于某些实现,它们是。然而,每一个都是为了满足至少一个严肃的 C 实现的需要而引入的。

而且我想很少有人比 Plauger 更了解 C 库的来龙去脉。我希望有人可以用一个实际的例子来回答这个特定的问题;我认为这将是一个有趣的琐事。

新信息:


“国际标准的基本原理 - 编程语言 - C”这样说va_start()

to的parmN参数va_start旨在帮助实现者va_start完全在 C 中编写符合宏的定义,即使使用 C89 之前的编译器(例如,通过获取参数的地址)。对参数声明的限制parmN源于允许这种实现的意图,因为如果参数的声明不满足这些限制,则将 & 运算符应用于参数名称可能不会产生预期的结果。

并不是说这有助于我限制数组参数。

于 2009-08-30T11:00:16.183 回答
3

它不是未定义的。请记住,当参数声明为int paramN[]时,实际参数类型仍将衰减为int* paramN立即(这在 C++ 中可见,例如,如果您应用typeidparamN)。

我必须承认,我不确定规范中的这个位是什么,考虑到你首先不能有函数或数组类型的参数(因为它们会指针衰减)。

于 2009-08-30T08:58:41.873 回答
2

我从 Dinkumware找到了另一个相关的引用。

最后一个参数不能有寄存器存储类,并且它必须有一个不被翻译器改变的类型。它不能有:

* an array type
* a function type
* type float
* any integer type that changes when promoted
* a reference type [C++ only]

显然,问题恰恰在于参数的传递方式与声明方式不同。有趣的是,他们还禁止浮动和做空,尽管标准应该支持这些。

作为一种假设,可能是某些编译器在sizeof正确处理此类参数时存在问题。例如,可能是这样,因为

int f(int x[10])
{
        return sizeof(x);
}

一些(错误的)编译器将返回10*sizeof(int),从而破坏va_start实现。

于 2009-08-30T17:09:53.530 回答
1

C++11 says:

[n3290: 13.1/3]: [..] Parameter declarations that differ only in a pointer * versus an array [] are equivalent. That is, the array declaration is adjusted to become a pointer declaration. [..]

and C99 too:

[C99: 6.7.5.3/7]: A declaration of a parameter as ‘‘array of type’’ shall be adjusted to ‘‘qualified pointer to type’’, where the type qualifiers (if any) are those specified within the [ and ] of the array type derivation. [..]

And you said:

But pure C code cannot find out whether or not paramN was declared as an array or as a pointer. In both cases, the type of the parameter is adjusted to be a pointer.

Right, so there's no difference between the two pieces of code you showed us. Both have paramN declared as a pointer; there is actually no array type there at all.

So why would there be a difference between the two when it comes to the UB?

The passage you quoted...

The parameter parmN is the identifier of the rightmost parameter in the variable parameter list in the function definition (the one just before the , ...). If the parameter parmN is declared with the register storage class, with a function or array type, or with a type that is not compatible with the type that results after application of the default argument promotions, the behavior is undefined.

...applies to neither, as would be expected.

于 2011-11-12T23:15:18.370 回答
1

我只能猜测该register限制是为了简化库/编译器的实现——它消除了他们担心的特殊情况。

但我对数组/函数限制一无所知。如果它仅在 C++ 标准中,我会冒险猜测存在一些模糊的模板匹配场景,其中类型参数和类型参数之间的T[]差异T*会产生差异,正确处理会变得复杂va_start等等。但是由于这个子句出现在 C 标准中也是如此,显然这种解释被排除在外。

我的结论是:对标准的疏忽。可能的情况:一些准标准的 C 编译器以不同的方式实现了类型T[]T*不同的参数,并且 C 标准委员会中该编译器的发言人在标准中添加了上述限制;该编译器后来变得过时,但没有人认为这些限制足以令人信服地更新标准。

于 2009-08-30T10:04:01.227 回答