首页 > 解决方案 > 显式前向声明函数后的预标准 C 函数头语法

问题描述

我想了解与 ANSI 引入的显式函数原型结合使用时的预标准“K&R 风格”函数声明语法的行为信息。具体来说,语法如下:

int foo(a)
    int a;
{
    /* ... */
}

而不是这样:

int foo(int a) {
    /* ... */
}

请注意,我专门指的是函数声明语法,而不是非原型函数的使用。

关于前一种语法如何不创建函数原型的问题已经做了很多。我的研究表明,如果函数如上定义,后续调用foo(8, 6, 7, 5, 3, 0, 9)将导致未定义的行为;而对于后一种语法,foo(8, 6, 7, 5, 3, 0, 9)实际上是无效的。这是有道理的,但我首先明确地前向声明了我的所有函数。如果编译器不得不依赖从定义生成的原型,我已经认为这是我的代码中的一个缺陷;所以我确保使用编译器警告来通知我,如果我未能前向声明一个函数。

假设正确的前向声明已经到位(在本例中int foo(int);为 ),K&R 函数声明语法是否仍然不安全?如果是这样,怎么做?新语法的使用是否否定了已经存在的原型? 至少有一个人显然声称在以 K&R 风格定义函数之前前向声明函数实际上是非法的,但我已经做到了,它编译和运行都很好。

考虑以下代码:

/*  1 */ #include <stdio.h>
/*  2 */ void f(int); /*** <- PROTOTYPE IS RIGHT HERE ****/
/*  3 */ void f(a)
/*  4 */     int a;
/*  5 */ {
/*  6 */     printf("YOUR LUCKY NUMBER IS %d\n", a);
/*  7 */ }
/*  8 */ 
/*  9 */ int main(argc, argv)
/* 10 */ int argc;
/* 11 */ char **argv;
/* 12 */ {
/* 13 */    f(1);
/* 14 */    return 0;
/* 15 */ }

当逐字给出此代码时,gcc -Wall两者clang -Weverything都不会发出警告并生成运行时打印YOUR LUCKY NUMBER IS 1后跟换行符的程序。

如果将f(1)inmain()替换为f(1, 2),则会gcc在该行发出“参数过多”错误,其中“declared here”注释特别指示第3行,而不是第 2 行。在clang中,这是警告,而不是错误,并且没有注释指示包括声明行。

如果f(1)inmain()替换为f("hello world"),则gcc在该行上发出整数转换警告,并附注指示第 3 行并显示“预期为 'int' 但参数为 'char *' 类型”。clang给出了类似的错误,没有注释。

如果将f(1)inmain()替换为f("hello", "world"),则按顺序给出上述结果。

我的问题是:假设已经提供了函数原型,那么 K&R 语法是否比具有内联类型关键字的样式更不安全?我的研究给出的答案是,“不,一点也不”,但是对于旧式类型声明的压倒性负面、显然几乎一致的意见让我想知道是否有我忽略的东西。有什么我忽略的吗?

标签: clanguage-lawyerfunction-prototypes

解决方案


我的研究表明,如果函数如上定义,后续调用 foo(8, 6, 7, 5, 3, 0, 9) 将导致未定义的行为;而对于后一种语法, foo(8, 6, 7, 5, 3, 0, 9) 实际上是无效的。

这是对的。鉴于int foo(a) int a; {},根据 C 2018 6.5.2.2 6,调用具有未定义的行为:

如果表示被调用函数的表达式的类型不包含原型,... 如果参数的数量不等于参数的数量,则行为未定义。

并且,int foo(int a);根据 C 2018 6.5.2.2 2,该调用违反了约束:

如果表示被调用函数的表达式具有包含原型的类型,则参数的数量应与参数的数量一致。

假设有适当的前向声明(在本例中为 int foo(int);),K&R 函数声明语法是否仍然不安全?

如果一个函数有一个带有原型的声明,就像在你的前向声明中一样,还有一个没有原型的定义(使用旧的 K&R 语法),那么标识符的结果类型就是原型版本的类型。使用参数列表声明的函数类型可以与使用 K&R 语法声明的函数类型合并。首先 C 2018 6.7.6.3 15 告诉我们这两种类型是兼容的:

对于要兼容的两种函数类型,两者都应指定兼容的返回类型。…如果一种类型有参数类型列表,而另一种类型由包含(可能为空)标识符列表的函数定义指定,则两者在参数数量上应一致,并且每个原型参数的类型应与将默认参数提升应用到相应标识符的类型所产生的类型。...</p>

然后 C 2018 6.2.7 3 告诉我们它们可以合并:

复合类型可以由两种兼容的类型构成;它是与这两种类型都兼容并满足以下条件的类型:

…</p>

— 如果只有一种类型是带有参数类型列表的函数类型(函数原型),则复合类型是带有参数类型列表的函数原型。

…</p>

C 2018 6.2.7 4 告诉我们标识符采用复合类型:

对于在一个标识符的先前声明可见的范围内声明的具有内部或外部链接的标识符,如果先前的声明指定了内部或外部链接,则后面声明的标识符的类型将成为复合类型。

因此,如果您同时拥有int foo(int a);and int foo() int a; {}foo则拥有 type int foo(int a)

这意味着如果每个函数都使用原型声明,那么在调用它们的语义方面,没有原型定义它们与使用原型定义它们一样安全。(我不评论样式或其他样式可能或多或少容易受到错误编辑或与函数调用的实际语义无关的其他方面引起的错误的影响)。

但是请注意,原型中的类型必须与默认参数提升后的 K&R 样式定义中的类型匹配。例如,这些类型是兼容的:

void foo(int a);
void foo(a)
char a; // Promotion changes char to int.
{
}

void bar(double a);
void bar(a)
float a; // Promotion changes float to double.
{
}

这些类型不是:

void foo(char a);
void foo(a)
char a; // char is promoted to int, which is not compatible with char.
{
}

void bar(float a);
void bar(a)
float a; // float is promoted to double, which is not compatible with float.
{
}

推荐阅读