为什么在函数声明中需要返回类型和参数类型?

huangapple go评论64阅读模式
英文:

Why do we need return type and parameter type in funtion declarations?

问题

我被告知我们在没有读取定义的情况下需要它们,以便编译器能够继续前进。

我们以某种方式需要它们,以确保程序能够正常工作,避免函数之间的冲突。

请解释。

英文:

I've been told we somehow need them so the compiler can continue onwards without having read the definition yet.

Somehow we need them in order for the program to work properly, to avoid conflicts between functions.

Please explain.

答案1

得分: 3

如果你要进行分离编译,意味着函数的调用与定义可能在不同的源文件中,可能在不同的时间编译,也许相隔数月或数年,你需要决定如何协调传递给函数的信息以及函数返回的信息。

首先让我们讨论返回值。函数可以声明为返回intdoublechar *或者几乎任何C的其他类型。但是对于给定的ABI,不同的值以不同的方式返回。整数和指针可能会返回到通用处理器寄存器中。浮点数值可能会返回到浮点寄存器中。结构体可能会返回到特定的内存区域中。无论如何,被调用的函数将会返回值,但是调用函数(单独编译)将要对返回的值进行处理,因此它必须知道如何生成正确的代码来处理返回值。在C的分离编译模型下,它唯一的了解方式就是函数声明,它指示了返回类型。如果调用代码认为函数会返回一个int,但是实际上被调用的函数返回了一个double,它就无法工作,因为被调用的函数会将其返回值放在返回类型为double的地方,而调用代码会从返回类型为int的地方取值,结果会得到不确定的垃圾值。

现在让我们来谈谈传递给函数的参数。同样,根据参数的类型,参数传递背后的机制可以采取多种形式。再次强调,不匹配是容易发生的,但可能导致严重问题。只要编写调用代码的程序员始终能够始终传递正确数量和正确类型的参数,我们就不需要函数原型,而事实上,C的最初几年就是这样的。然而,在当今,这种风险通常被认为是不可接受的,函数原型被认为是强制性的——由语言标准、编译器和大多数C程序员来确定。 (你可能仍然会找到一些抱怨原型是新潮或不必要的人,但他们基本上可以忽略不计。)

关于"可变参数"函数(如printf)还有一个问题。因为它们不接受固定数量的固定类型参数,所以它们无法具有预先指定这些参数类型的原型。函数原型中的省略号("...")表示变量参数,这些参数(a)无法被编译器强制执行,但它们(b)确实会执行默认参数提升,以提供更多的规则性(基本上是将小类型如charint提升为int,将float提升为double)。"可变参数"函数也通常被认为是老派的,并且如果不是完全按照printf的模式(这意味着编译器可以查看格式字符串(如果是常量)并为程序员提供双重检查参数的服务),则可能是相当危险的,不建议用于新设计。

这些要求从C语言发明以来一直在不断发展,并且(如评论中所讨论的)仍在发生变化。如果你有关于什么是合法的、什么是不合法的,什么有效、什么无效的具体问题,你可能想明确地提问,但答案可能取决于我们正在谈论的C语言版本。

我们可以通过查看标准的sqrt函数来对大多数问题有一个很好的了解。假设你写了下面的代码:

double x = sqrt(144);

曾经,这是错误的。在某个时期,如果没有在作用域中的函数声明,sqrt会被假定为返回int,这意味着这个调用是行不通的,因为sqrt实际上返回的是一个double。(编译器会生成代码来获取一个int并将其转换为x所需的double,但是这是毫无意义的,因为sqrt实际上并没有返回int供转换。)但是,还有第二个问题:sqrt接受一个double类型的参数,而这段代码没有传递,所以sqrt甚至都没有收到要计算平方根的正确值。

所以,曾经,你绝对需要这样写:

extern double sqrt();

(你可能从<math.h>中得到了这个声明)来告诉编译器sqrt返回double,并且你需要这样调用:

double x = sqrt(144.);

或者

double x = sqrt((double)144);

来传递double类型的值。

今天,如果你在没有函数原型的情况下调用sqrt(也就是说,在作用域中没有声明),编译器更有可能抱怨说没有原型在作用域中——也就是说,它不会静默地假定sqrt返回int。如果作用域中有一个声明,它将是显式指定了sqrt接受一个double类型参数的原型声明:

extern double sqrt(double);

所以,今天,这段代码是可以的——编译器知道在将其传递给sqrt之前将int144隐式转换为double

如果你做了一些真的错误的事情,比如调用`sqrt(44, 55

英文:

If you're going to have separate compilation, meaning that the call versus the definition of a function might be in separate source files, compiled at different times, perhaps months or years apart, you need to decide how information passed to and returned from the function is going to be coordinated.

Let's cover the return value first. Functions can be declared to return int, or double, or char *, or just about any of C's other types. (There are one or two exceptions, types which it's impossible to return, but they needn't concern us here.) But for any given ABI, different values get returned in different ways. Integers and pointers might get returned in a general-purpose processor register. Floating-point values might get returned in a floating-point register. Structures might get returned in some specially-designated memory area. In any case, the called function is going to do the returning, but the calling function (compiled separately) is going to do something with the returned value, so it has to know how to emit the right code to do that. And the only way for it to know (again, under C's separate-compilation model) is the function declaration that indicated the return type. If the calling code thought the function was going to return an int, but the called function actually returned a double, it just wouldn't work, because the called function would place its return value in the spot where return values of type double go, and the calling code would fetch a value from the place where return values of type int go, and it would get indeterminate garbage instead.

Now let's talk about the arguments passed to the function. Again, the mechanisms behind argument passing can take several forms, depending on the type(s) of the argument(s). Again, mismatches are easy, but can cause serious problems. If the programmers writing calling code could be relied on to always pass the correct number of arguments, and of the correct types, we wouldn't need function prototypes — and indeed, that's how C was for the first few years of its life. These days, however, that risk is generally considered as unacceptable, and function prototypes are considered mandatory — by the language standard, by compilers, and by most C programmers. (You might still find a few holdouts, grumbling that prototypes are newfangled or unnecessary, but they're basically an ignorable minority.)

There's a remaining wrinkle concerning "varargs" functions such as printf. Since they don't accept a fixed number of fixed-type arguments, they can't have a prototype that specifies the types of those arguments in advance. An ellipsis ("...") in a function prototype indicates variable arguments that (a) can't be enforced by the compiler but that (b) do have the default argument promotions performed on them, to provide a little more regularity (basically, small types like char and int promoted to int, and float promoted to double). Varargs functions, too, are generally consider old-school and risky if not downright dangerous, and are not recommended for new designs, unless perhaps if they follow the pattern of printf, meaning that the compiler can peek at the format string (if constant) and do the programmer the favor of double-checking the arguments.

The requirements here have been evolving ever since C was invented, and (as discussed in the comments) are still changing. If you have specific questions about what's legal and what's not, what works and what won't, you might want to ask them explicitly, but the answer may depend on which version of C we're talking about.

We can get a good picture of most of the issues just by looking at the standard sqrt function. Suppose you say

double x = sqrt(144);

Once upon a time, that was doubly wrong. Once upon a time, without a function declaration in scope, sqrt would be assumed to return int, meaning that this call wouldn't work, because sqrt actually returns a double. (The compiler would emit code to fetch an int and convert it to the double required by x, but this would be meaningless since there's no int actually returned by sqrt to convert.) But, there's a second problem: sqrt accepts a double argument, which this code doesn't pass, so sqrt wouldn't even receive the correct value to take the square root of.

So, once upon a time, you absolutely needed

extern double sqrt();

(which you probably got from &lt;math.h&gt;) to tell the compiler that sqrt returned a double, and it was your responsibility to call

double x = sqrt(144.);

or

double x = sqrt((double)144);

to cause a double value to be passed.

Today, if you call sqrt out of a clear sky (that is, without a declaration in scope), the compiler is more likely to complain that there's no prototype in scope — that is, it will not quietly assume that sqrt returns int. And if there is a declaration in scope, it will be the prototype declaration

extern double sqrt(double);

which explicitly says that sqrt expects one argument of type double. So, today, the code

double x = sqrt(144);

works fine — the compiler knows to implicitly convert the int value 144 to double before passing it to sqrt.

If you did something really wrong, like calling sqrt(44, 55), in the old days you'd get no complaints (and a very wrong answer), while today you'll get an error saying you've passed too many arguments.

This is probably a longer answer than you were looking for, but in closing, there are two final points to make:

  1. No one would claim that any of this makes perfect sense, or is the way you'd design things from scratch, today. We're living (as ever) with a number of imperfect compromises between modernity and backwards compatibility.
  2. The "guarantees" apparently promised by function prototypes — namely, that automatic argument conversions will be performed if possible, and that compile-time errors will be emitted for gross mismatches — are not absolute, and still depend on a certain amount of programmer care, namely to ensure that the prototypes (upon which everything depends) are actually correct. This generally means putting them in .h files, included both by the caller and the defining source file. See also this question exploring how those vital conventions might be enforced.

答案2

得分: 0

因为C编译器按照从上到下的顺序编译代码。如果你要在主函数中使用任何函数,有两种方法可以做到:

  1. 你可以先编写函数,然后在主函数中使用它(编译器按照从上到下的顺序编译代码,所以它会知道有一个函数)。
  2. 就像你所做的那样,你可以使用参数来告诉编译器:“嘿,这里有一个函数,我会稍后使用它”。
英文:

Because C Compilers compile code row from top to bottom. If you gonna use any function in your main function you have two ways to do that.

  1. You can code your function first then use main(Compiler code row from top to bottom so it will figure out there is a function)
  2. Like you do you can use parameters to say "Hey there is a function that i made i will use that later" to Compiler.

答案3

得分: 0

  • Declaration without parameters. COMPILES.

    • 无参数声明。编译通过。
  • Declaration without parameters and either without type. COMPILES BUT WARNING.

    • 无参数声明以及无类型声明。编译通过但会有警告。
  • Call BEFORE definition (and no declaration). Compile error. ERROR.

    • 在定义之前调用(没有声明)。编译错误。错误。

Compiles perfectly (as @Vlad from Moscow pointed, neither parameters are needed in declaration):

  • 完美编译(正如莫斯科的@Vlad所指出的,在声明中无需参数):
#include <stdio.h>

int sum ();     // 无参数声明(如果也省略int类型,将会编译但有警告)

int main (void)
{
    int numa = 2, numb = 3;

    printf("Sum %d + %d = %d\n", numa, numb, sum(numa, numb));

    return 0;
}

int sum (int numa, int numb)
{
    return numa + numb;
}

output:

./ftype_def_dec
Sum 2 + 3 = 5

Type int of function omitted (compiled but with warning):

  • 函数的int类型被省略(编译但有警告):
gcc -o ftype_def_dec ftype_def_dec.c
ftype_def_dec.c:3:1: warning: type specifier missing, defaults to 'int' [-Wimplicit-int]
sum ();
^
1 warning generated.

Does not compile (if you omit declaration in a function called in main() BEFORE it's DEFINITION [every detail [types & what/how it does explicited]]):

  • 不会编译(如果在主函数main()中调用的函数在其定义之前没有声明[每个细节[类型及如何实现明确]]):
#include <stdio.h>

// int sum ();

int main (void)
{
    int numa = 2, numb = 3;

    printf("Sum %d + %d = %d\n", numa, numb, sum(numa, numb));

    return 0;
}

int sum(int numa, int numb)
{
    return numa + numb;
}
gcc -o ftype_def_dec ftype_def_dec.c
ftype_def_dec.c:9:44: error: implicit declaration of function 'sum' is invalid in C99 [-Werror,-Wimplicit-function-declaration]
printf("Sum %d + %d = %d\n", numa, numb, sum(numa, numb));
                                          ^
1 error generated.
英文:

Example:

  • Declaration without parameters. COMPILES.
  • Declaration without parameters and either without type. COMPILES BUT WARNING.
  • Call BEFORE definition (and no declaration). Compile error. ERROR.

Compiles perfectly (as @Vlad from Moscow pointed, neither parameters are needed in declaration):

  1 #include &lt;stdio.h&gt;
  2 
  3 int sum ();     // no parameters declaration (if also omit type int, will compile but with warning
  4 
  5 int main (void)
  6 {
  7         int numa = 2, numb = 3;
  8 
  9         printf (&quot;Sum %d + %d = %d\n&quot;, numa, numb, sum ( numa, numb ));
 10 
 11         return 0;
 12 }
 13 
 14 int sum ( int numa, int numb )
 15 {
 16         return numa + numb;
 17 }

output:

./ftype_def_dec                    
Sum 2 + 3 = 5

type int of function ommited (compiled but with warning):

gcc -o ftype_def_dec ftype_def_dec.c
ftype_def_dec.c:3:1: warning: type specifier missing, defaults to &#39;int&#39; [-Wimplicit-int]
sum ();
^
1 warning generated.

Does not compile (if you omit declaration in a function called in main() BEFORE it's DEFINITION [every detail [types & what/how it does explicited]]):

  1 #include &lt;stdio.h&gt;
  2 
  3 // int sum ();
  4 
  5 int main (void)
  6 {
  7         int numa = 2, numb = 3;
  8 
  9         printf (&quot;Sum %d + %d = %d\n&quot;, numa, numb, sum ( numa, numb ));
 10 
 11         return 0;
 12 }
 13 
 14 int sum ( int numa, int numb )
 15 {
 16         return numa + numb;
 17 }

gcc -o ftype_def_dec ftype_def_dec.c
ftype_def_dec.c:9:44: error: implicit declaration of function &#39;sum&#39; is invalid in C99 [-Werror,-Wimplicit-function-declaration]
        printf (&quot;Sum %d + %d = %d\n&quot;, numa, numb, sum ( numa, numb ));
                                                  ^
1 error generated.

答案4

得分: 0

因为编译器必须知道如何调用和使用您的函数(即它接受哪些参数以及它的返回类型是什么(尽管前者可以省略)),在其他调用函数中(比如在main函数中)。

正如另一个答案所提到的,编译器会从程序顶部向底部编译您的程序。因此,当它在程序中的某一行达到您使用函数的地方时,如果您之前没有声明过它,编译器将不知道如何解析和编译那个语句,或者无法检测它是否包含错误。

举个简单的例子:

void square(int*);

int main(void) {
    
    int number = 4;
    
    number = square(number); // 错误
    // 正确的用法应该是:
    square(&number);
    return 0;
}

void square(int *num) {
    if (!num)
        return;
    *num *= *num;
}

然而,如果在main函数之前没有声明square,编译器将不知道如何处理main函数中对square的调用,也无法确定两次调用中的哪一个是错误的。

换句话说,如果在调用函数之前没有声明该函数,square就是一个未声明的标识符

就像您不能做这样的事情:

int main(void) {
    int x = 2;
    y = x + 5; // 错误:未声明的标识符'y'
    int y;
    return 0;
}

...因为编译器在第一次使用y时根本不知道y是什么。

英文:

Because the compiler has to be able to know how your function can be called and used (i.e. what parameters it accepts and what its return type is (although the former can be omitted)) in other calling functions (such as in main).

As mentioned by another answer, the compiler will compile your program from top to bottom. Thus, when it reaches a given line in your program where you use your function, if you had not previously declared it, it would not know how to parse and compile that statement, and/or be able to detect if it consists of an error.

Take a simple example:

void square(int*);

int main(void) {
    
    int number = 4;
    
    number = square(number); // ERROR
    // correct usage would be:
    square(&amp;number);
    return 0;
}

void square(int *num) {
    if (!num)
        return;
    *num *= *num;
}

However, if you had not declared square before main, the compiler would have no way of knowing what do with your call to square inside main, or which of the two calls was an error.

In other words, square is an undeclared identifier if you have not declared the function before calling it.

Just like you cannot do something like:

int main(void) {
    int x = 2;
    y = x + 5; // ERROR: undeclared identifier &#39;y&#39;
    int y;
    return 0;
}

... because the compiler has no idea what y is by the time it reaches its first usage.

huangapple
  • 本文由 发表于 2023年2月14日 04:24:20
  • 转载请务必保留本文链接:https://go.coder-hub.com/75440830.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定