函数声明与定义 C

13

我有一段简单的代码,其中我的函数是在main函数之前声明的,就像这样:

int function1();
int function2();

int main() {
   /* ... */
   function1(x,y);
   function2(x,y);
   /* .... */
}

int function1(int x, float y) { /* ... */ }
int function2(int x, float y) { /* ... */ }

在我的主函数之后,我有一些函数的定义:

如果我像这样在主函数之前声明函数,有什么区别吗?

int function1(int x, float y);
int function2(int x, float y);

int main() {
   /* ... */
   function1(x,y);
   function2(x,y);
   /* .... */
}

int function1(int x, float y) { /* ... */ }
int function2(int x, float y) { /* ... */ }

你的前两行声明了一个不同于后面声明的函数签名(没有参数)。因此,前两行是不必要的。函数声明描述了名称、参数数量和类型以及返回类型。两个函数可以有相同的名称但不同的参数。它们不能仅在返回类型上有所不同。 - BryanT
10
@BryanT 这是不正确的(尽管在 C++ 中是正确的)。在 C 语言中,函数声明中的空括号意味着它可以接受任意数量、任意类型的参数。如果你明确想要零个参数,使用 (void);可以参考 main 函数的标准签名之一:int main(void) { ... } - user4520
我同意。我一开始也在考虑 C++。但是按照将要定义的方式明确定义它们不是更好吗? - BryanT
3
我先说一句,这个问题完全值得5个赞,而最佳答案的75分也非常出色,我真的希望SO的评分能够影响我们的职业生涯,像这样的答案确实很重要。但不幸的是,并非所有的消息都是好的。如果SO要影响我们的职业生涯,那么你应该被淘汰或者被扣除负分,因为你没有使用“-Wall -Wextra”,编译器会告诉你其中的区别。 - Alec Teal
被接受的答案对于其含义仍然是不正确的。虽然它谈到了使用默认参数提升和不提供原型的一般情况,但它未能注意到第一个程序特别之处,因为它将float作为参数,所以总是导致未定义行为,因为float不是可以通过默认参数提升得到的结果。 - Antti Haapala -- Слава Україні
4个回答

18
是的,它们是不同的。
在第一个示例中,您只告诉编译器函数的名称和返回类型,没有提及其预期的参数。
在第二个示例中,您在调用函数之前向编译器提供了完整的函数签名,包括返回类型和预期的参数。
第二种形式几乎普遍更好,因为它可以帮助编译器在调用函数时更好地警告您参数类型或数量错误。
另外请注意,在C语言中,“int function()”是一个可以接受任何参数的函数,而不是没有参数的函数。要实现没有参数的函数,需要使用显式的“void”,即“int function(void)” 。这通常会使从C++转到C的人犯错。
另请参见:为什么没有参数的函数(与实际函数定义相比)能够编译? 为了演示为什么第一种过时的形式在现代C语言中不好,以下程序将使用“gcc -Wall -ansi -pedantic”或“gcc -Wall -std=c11”编译而不发出警告。
#include<stdio.h>
int foo();

int main(int argc, char**argv)
{
  printf("%d\n", foo(100));
  printf("%d\n", foo(100,"bar"));
  printf("%d\n", foo(100,'a', NULL));
  return 0;
}

int foo(int x, int y)
{
  return 10;
}

更新: M&M提醒我,我的示例在函数中使用了int而不是float。 我认为我们都可以同意声明int function1()是不好的形式,但我的说法这个声明接受任何参数并不完全正确。请参见Vlad的答案,了解相关规范部分的说明。


您还应该提到OP的第一个情况是未定义行为,即使编译器没有诊断它(编译器没有这个要求)。 - M.M
当然第二个更好,因为第一个仍然具有未定义的行为,这意味着在我的编译器上,它无法使用默认设置进行编译。 - Antti Haapala -- Слава Україні

14

是的,它们不同;第二个是正确的,第一个整体上是错误的。它如此错误以至于GCC 5.2.1拒绝完全编译它。它在你那里能工作只是一种偶然

/* this coupled with */
int function1();

int main() {
    /* this */
    function1(x, y);
}

/* and this one leads to undefined behaviour */
int function1(int x, float y) {
    /* ... */
}

在上面的代码中,声明 int function1(); 没有指定参数类型(它没有原型),这被认为是 C11(以及 C89、C99)标准中一种过时的特性。如果调用了这种类型的函数,则会对参数进行默认参数提升:将 float 提升为 double,但 int 则按原样传递。
由于您实际的函数期望参数为 (int, float),而不是 (int, double),这将导致未定义行为。即使您的函数期望一个 (int, double)y 是一个整数,或者说您使用 function1(0, 0); 而不是 function(0, 0.0); 来调用它,您的程序仍然会有未定义的行为。幸运的是,GCC 5.2.1 注意到了 function1 的声明和定义存在冲突:
% gcc test.c
test.c:9:5: error: conflicting types for ‘function1’
 int function1(int x, float y) {
     ^
test.c:9:1: note: an argument type that has a default promotion can’t 
    match an empty parameter name list declaration
 int function1(int x, float y) {
 ^
test.c:1:5: note: previous declaration of ‘function1’ was here
 int function1();
     ^
test.c:12:5: error: conflicting types for ‘function2’
 int function2(int x, float y) {
     ^
test.c:12:1: note: an argument type that has a default promotion can’t 
    match an empty parameter name list declaration
 int function2(int x, float y) {
 ^
test.c:2:5: note: previous declaration of ‘function2’ was here
 int function2();
     ^

当编译器退出并显示错误代码时,我的tcc却可以愉快地编译它,没有任何诊断信息,也没有其他问题。它只是生成了错误的代码。当然,如果你将声明放在头文件中,并且定义放在不包含该声明的不同编译单元中,情况也是一样的。


现在,如果编译器没有检测到这种情况,在运行时任何事情都可能发生,这是未定义的行为所期望的。例如,假设参数是通过堆栈传递的情况下;在32位处理器上,intfloat可以放在4个字节中,而double可能需要8个字节;即使是float,函数调用也会将x作为inty作为double推入堆栈——总共调用者会推入12个字节,而被调用者只期望8个字节。在另一种情况下,假设您使用两个整数调用该函数。调用代码将把它们加载到整数寄存器中,但调用者希望在浮点寄存器中得到一个双精度值。浮点寄存器可能包含陷阱值,当访问时会导致程序崩溃。

最糟糕的是,您的程序现在可能无法像预期的那样运行,从而包含了一个Heisenbug,这可能会在您使用更新版本的编译器重新编译代码或将其移植到另一个平台时引起问题。


除非你费尽心思地告诉编译器,否则它不会警告你关于“错误”的形式。请参见我的答案中的示例程序。 - Brian McFarland
@Antti Haapala 第一个代码片段没有问题。 - Vlad from Moscow
3
@VladfromMoscow 这段代码存在未定义行为,根据我的书籍来看它是错误的。 - Antti Haapala -- Слава Україні
1
@AnttiHaapala,您在写第二个参数。我认为两个参数都是uint类型:) 如果第二个参数是float类型,但参数会被提升为double,则确实存在未定义的行为。 - Vlad from Moscow
1
@BrianMcFarland 在你的回答中,你将 float y 改为了 int y - M.M
显示剩余2条评论

14

区别在于,如果像第二段代码片段中那样存在函数原型,则编译器会检查参数的数量和类型是否与参数列表一致。如果发现不一致,编译器可以在编译时发出错误。

如果没有函数原型,如第一段代码片段中,则编译器对每个参数执行默认参数提升,包括整数提升和将float类型的表达式转换为double类型。如果在这些操作之后,晋升后的参数的数量和类型与参数列表不一致,则行为是未定义的。编译器可能无法发出错误,因为函数定义可能在其他编译单元中。

以下是C标准(6.5.2.2函数调用)中的相关引用:

 

2 如果表示所调用函数的表达式具有包括原型的类型,则参数的数量必须与参数的数量相符。每个参数的类型都应该这样,其值可以被分配给相应参数的未限定版本的对象的类型。

 

6 如果表示所调用函数的表达式没有包括原型,则对每个参数执行整数提升,并将具有float类型的参数晋升为double类型。这些称为默认参数提升。如果参数的数量不等于参数的数量,则行为是未定义的。如果函数的类型包括省略号的原型或者促进后的参数的类型与参数的类型不兼容,则行为是未定义的。如果函数的类型没有包括原型,并且晋升后的参数的类型与晋升后的参数的类型不兼容,则行为是未定义的,除非以下情况:

 

—一个晋升类型是有符号整数类型,另一个晋升类型是相应的无符号整数类型,并且值在两种类型中都可以表示;

 

—两种类型都是指向限定或未限定版本的类型的指针。

就你的代码片段而言,如果第二个参数的类型为double,那么该代码将是良好形式的。 然而,由于第二个参数的类型为float,但相应的实参将被提升为double类型,因此第一个代码片段具有未定义的行为。


2
在第一种情况下,main() 对每个参数执行整数提升和floatdouble的提升。这些被称为"默认参数提升"。因此,您可能会错误地调用函数,通过传递一个int和一个double,而函数期望一个int和一个float
请参阅C函数调用中的默认参数提升和答案以获取更多详细信息。

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