与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函数声明语法是否仍然不安全?如果是这样,怎么办?使用新语法是否会否定已经存在的原型? At least one person显然声称,在以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)
中的main()
替换为f(1, 2)
,则gcc
会在该行上发出“参数过多”错误,并特别注明“此处声明”第 3 行,而不是第2行。在clang
中,这是警告,不是错误,并且没有包含说明声明行的包含注释。
如果将f(1)
中的main()
替换为f("hello world")
,则gcc
会在该行上发出整数转换警告,并带有一条注释,指出第3行并显示“ expected'int ',但参数的类型为'char *'”。注意,clang
给出了类似的错误。
如果将f(1)
中的main()
替换为f("hello", "world")
,则会依次给出上述结果。
我的问题是: 假设已经提供了函数原型 ,K&R语法是否比使用内联类型关键字的样式安全?我的研究表明,答案是“不,一点儿”,但是对于较旧类型声明的绝大多数否定的观点,几乎是一致的看法,使我想知道是否有什么我可以忽略的。我有什么要忽略的吗?
答案 0 :(得分:2)
我的研究表明,如果函数如上定义,则后续调用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,该调用具有未定义的行为:
如果表示被调用函数的表达式的类型不包含原型,则…如果参数数量不等于参数数量,则行为是不确定的。
根据C 2018 6.5.2.2 2,给定int foo(int a);
,该调用违反了约束:
如果表示被调用函数的表达式的类型包括原型,则参数的数量应与参数的数量一致。
假设适当的前向声明已经到位(在本例中为int foo(int);),那么K&R函数声明语法是否仍然不安全?
如果一个函数同时具有一个声明和一个原型(如在您的前向声明中一样)和一个没有原型的定义(使用旧的K&R语法),则标识符的结果类型就是原型版本的标识符。可以将使用参数列表声明的函数的类型与使用K&R语法声明的函数的类型合并。 First C 2018 6.7.6.3 15告诉我们两种类型兼容:
要使两个函数类型兼容,两者都应指定兼容的返回类型。 …如果一种类型具有参数类型列表,而另一种类型由包含(可能为空)标识符列表的函数定义指定,则两者应在参数数量上达成一致,并且每个原型参数的类型均应与参数类型兼容。类型是从默认参数提升应用到相应标识符的类型而得出的。…
然后C 2018 6.2.7 3告诉我们它们可以合并:
复合类型可以由两种兼容的类型构造;它是与两种类型都兼容并且满足以下条件的一种类型:
...
-如果只有一种类型是带有参数类型列表的函数类型(函数原型),则复合类型是带有参数类型列表的函数原型。
...
C 2018 6.2.7 4告诉我们标识符采用复合类型:
对于在内部可见或外部链接可见的范围内声明的标识符,如果该先前声明指定了内部或外部链接,则后面声明处的标识符类型将变为复合类型。
因此,如果您同时拥有int foo(int a);
和int foo() int a; {}
,则foo
的类型为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.
{
}
答案 1 :(得分:1)
问题中的代码不是很成问题;处理int
几乎没有问题。棘手的地方是这样的函数:
int another(int c, int s, double f);
int another(c, s, f)
short s;
float f;
char c;
{
return f * (s + c); // Nonsense - but it compiles cleanly enough
}
请注意,该原型不是
int another(char c, short s, float f);
有趣的是,除非您在编译选项中添加-pedantic
(或-Wpedantic
),否则GCC会接受两个原型。这是已记录的GCC扩展名-§6.38 Prototypes and Old-Style Function Definitions。相比之下,clang
抱怨(作为警告,如果未指定-Werror
选项,即使没有-Wall
或-Wextra
等,它也会默认抱怨):
$ clang -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -c kr19.c
kr19.c:7:10: error: promoted type 'int' of K&R function parameter is not compatible with the
parameter type 'char' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
char c;
^
kr19.c:2:18: note: previous declaration is here
int another(char c, short s, float f);
^
kr19.c:5:11: error: promoted type 'int' of K&R function parameter is not compatible with the
parameter type 'short' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
short s;
^
kr19.c:2:27: note: previous declaration is here
int another(char c, short s, float f);
^
kr19.c:6:11: error: promoted type 'double' of K&R function parameter is not compatible with the
parameter type 'float' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
float f;
^
kr19.c:2:36: note: previous declaration is here
int another(char c, short s, float f);
^
3 errors generated.
$
只要您认识到较短类型的差异,并且原型与升级的类型匹配,您实际上就不会遇到用K&R表示法定义函数并使用原型表示法声明它们的麻烦。
但是,差异并没有明显的好处-如果您已在标头中正确编写了原型,为什么不使用该原型声明作为函数定义的基础?
我仍在研究具有80年代和90年代初期的一些K&R函数定义的代码库。大多数此类函数的确在标头中具有原型,而原型中具有提升的类型。我正在积极清理它,以将所有函数定义都转换为原型符号,确保在定义(非{static
)个函数或调用任何函数之前,函数范围内始终存在一个原型。本地约定是在文件顶部冗余声明static
函数;也可以。
在我自己的代码中,我从不使用K&R表示法-甚至没有无参数的函数也不用(我总是对它们使用function(void)
)。
由于在标准(C11 §6.11 Future directions中已将其明确标记为“过时”,因此我强烈建议不要在任何现代C代码中使用K&R表示法:
使用带空括号的函数声明符(不是原型格式的参数类型声明符)是过时的功能。
使用带有单独的参数标识符和声明列表(不是原型格式的参数类型和标识符声明器)的函数定义是过时的功能。
答案 2 :(得分:0)
完全没有区别。您可以根据需要定义它们。它们是可交换的。但是,当然没有人会建议使用史前符号。
我的研究表明,如果函数的定义如上, 后续调用foo(8,6,7,5,3,0,9)将导致未定义 行为;而使用后一种语法foo(8,6,7,5,3,0,9) 实际上是无效的。
您的研究是错误的。如果语言标准允许不使用原型就使用功能,则
https://onlinegdb.com/SkKF4DAtE
int main()
{
printf("%d\n", foo(2,3,4,5,6,7,8,9));
printf("%d\n", foo1(2,3,4,5,6,7,8,9));
return 0;
}
int foo(a)
int a;
{
return a*a;
}
int foo1(int a)
{
return a*a;
}
答案 3 :(得分:0)
请注意,我专门指的是函数声明 语法,而不是使用非原型函数。
您似乎在玩语言时有些松懈。我认为您的意思是您在谈论选择功能 definition 的效果。函数定义提供函数声明,这些声明可能(ANSI样式)或可能不(K&R样式)包括原型。不属于定义的函数声明(正向声明)也可以提供原型,也可以不提供原型。重要的是要了解,行为是否会根据范围内的原型有所不同。
关于前一种语法如何不创建函数已经做了很多事情 原型。我的研究表明,如果函数定义为 以上,后续调用foo(8,6,7,5,3,0,9)会导致 不确定的行为;
是。这是由语言限制引起的(因此,不仅行为未定义,而且还必须诊断违规)。在C11中,相关文本为paragraph 6.5.2.2/2:
如果表示被调用函数的表达式的类型为 包括原型,论据的数量应与 参数数量。每个参数的类型应使其 值可以分配给具有非限定版本的对象 相应参数的类型。
请注意,这与函数调用参数与作用域内原型的协议有关,无论原型是否实际与函数的定义匹配。 (但是其他规则要求相同函数类型的所有声明(包括其定义)必须兼容。)
而使用后一种语法foo(8,6,7,7,5,3, 0,9)实际上是无效的。
是?您似乎在区分“无效”和行为未定义之间没有区别。我认为任何定义都“无效”的代码肯定具有未定义的行为。无论如何,在这种情况下的行为也是显式未定义的,因为如果存在范围内的原型,则无论函数定义的形式如何,均适用6.5.2.2/2(上述)。如果没有范围内的原型,则适用6.5.2.2/6段(在C11中)。它部分说:
如果表示被调用函数的表达式的类型为 不包括原型[...]如果参数数量不包括 等于参数数量,行为是不确定的。
编译器可能无法识别或诊断这种错误,并且您可能在某些特定的实现上对其进行了测试,而没有观察到任何不良影响,但是该行为绝对是未定义的,并且在某些情况下,在某些实现上,情况会中断。 / p>
我的问题是:假设函数原型已经存在 所提供的K&R语法是否比内联样式更安全 输入关键字?
如果函数f()
的原型相同,则在定义函数和调用函数的范围内(无论如何,都是好的做法),并且如果f()
的K&R样式定义指定了范围如果返回类型和参数类型与原型相同,则一切正常。函数定义中作用域中的多个兼容声明被组合以形成该函数的复合类型,该类型将恰好与原型所声明的类型相同。语义完全就像该定义直接使用ANSI语法一样。在section 6.2.7和标准的相关部分中对此进行了解决。
但是,维护与词法不匹配其原型的功能定义会带来额外的维护负担。另外,由于参数声明列表中的参数类型与原型不匹配而导致出现错误的可能性也增加了,因此从某种意义上讲,这种方法不如简单地始终使用ANSI样式安全。
但是,如果在其K&R样式定义的范围内没有f()
的原型,并且其任何参数的类型为float
或小于{{1}的整数类型},那么有更多的出错机会。尽管声明了参数类型,但函数实现将期望参数已接受默认参数提升,并且如果实际上尚未进行参数化,则行为未定义。可以编写一个预期需要的原型,以供在调用int
的情况下使用,但是要弄清楚这一点太容易了。从这个意义上说,您建议的组合确实不太安全。
答案 4 :(得分:-1)
我的问题是:假设已经提供了函数原型,那么K&R语法是否比使用内联类型关键字的样式安全?
完全是错误。有关详细信息,请参见Default argument promotions in C function calls。
这也是未定义的行为。根据{{3}}:
如果函数定义的类型与表示被调用函数的表达式所指向的类型(表达式的类型)不兼容,则行为是不确定的。
您应该从不将原型与按旧K&R样式定义的功能混合使用。
以K&R风格编写的函数希望其参数在传递时经过6.5.2.2 Function calls, paragraph 9 of the C11 standard。
使用原型调用函数的代码不会提升该函数的参数。
所以传递给函数的参数不是函数期望得到的。
故事的结尾。
不要这样做。