请解释编程是如何工作的

7
#include<stdio.h>
int f();

int main()
{

    f(1);
    f(1,2);
    f(1,2,3);
}

f(int i,int j,int k)
{

    printf("%d %d %d",i,j,k);

}

它正常运行(没有任何错误)...您能解释一下它是如何执行的吗? f(1)和f(1,2)如何与f(int,int,int)相关联?


2
你正在做黑魔法般的 C 语言操作 ;-)你用哪个编译器来编译这个程序? - Vanya
看起来你已经将C# 4.0带到了C语言中。 - this. __curious_geek
这个程序不工作。如果在你的情况下它“运行良好”,那么你可能对“运行良好”的定义有一个相当非正统的想法。 - AnT stands with Russia
@AndreyT 定义“它不起作用”。它可以运行,而且没有崩溃。因此,可以说“它起作用”,尽管它可能没有太多用处... - ShinTakezou
我已经在Dev-Cpp、Visual Studio等平台上尝试了同样的程序。 - Kedar Joshi
显示剩余2条评论
6个回答

14

你对"error"这个术语的定义与我不同 :-) 当你调用你的f函数前两次时会打印出什么?我的输出是:

1 -1216175936 134513787
1  2          134513787
1  2          3

对于我的三个函数调用。

你看到的是C语言早期人们在函数调用中随意而自由的一个遗留问题。

所有发生的事情就是你正在调用一个函数f,它从堆栈中打印出三个值(即使你只给了一个或两个值)。当你没有提供足够的值时,你的程序很可能会使用已有的值,这通常会导致在读取时出现数据问题,并在写入时导致灾难性故障。

这是完全可编译的,但非常不明智的C代码。我指的是“未定义行为”的字面意思(特别是指C99:“如果表示所调用函数的表达式具有不包括原型的类型,则如果参数的数量不等于参数的数量,则行为未定义”)。

你应该提供完整的函数原型,例如:

void f(int,int,int);

为确保编译器能够检测到这个问题,在可变参数函数中使用省略号 (...)。


顺便说一下,通常在背后发生的是调用函数从以下堆栈开始:

12345678
11111111

并将两个值推入堆栈(例如),以便它最终看起来像:

12345678
11111111
2
1

当被调用的函数使用栈上的前三个值时(因为它需要这些值),它发现栈中有1211111111

它执行所需的操作,然后返回,调用函数会清除掉那两个值(这被称为调用者清理策略)。如果有人试图使用被调用者清理策略进行此操作,那么将会遇到麻烦 :-) 尽管在C语言中这相当少见,因为这样做会使可变参数函数(如printf)变得有些困难。


是的,它仍然“valid”,但setjmplongjmp也是如此——这并不一定是一个好主意 :-) 无论如何,我添加了一点关于为什么它很可能有效的内容。 - paxdiablo
2
它能编译通过并不代表它的正确性。在C/C++中,*((int*)NULL) = 37;是完全有效的代码,但并不正确,会导致未定义的行为。声明一个函数int f()是标准的,并声明了一个返回int类型和接受未知类型和数量参数的函数,但未知并不意味着你可以随意调用它。你只是告诉编译器停止打扰你,因为你知道你在做什么。 - David Rodríguez - dribeas
没问题@ShinTakezou,当你的评论发出时,我可能正在编辑。这种情况偶尔会发生。 - paxdiablo
@dribeas,你的目的很清楚,但这与OP所要求的不一样(在另一个评论中已经解释了)。假设代码是 `#include<stdio.h> int f();int main() {f(1); f(2,2); f(3,2,3);}int f(int i,int j,int k) { if ( i == 1 ) printf("%d\n", i); if ( i == 2 ) printf("%d %d\n", i, j); if ( i == 3 ) printf("%d %d %d\n", i, j, k); }` . 现在,你可以专注于它为什么能够工作。OP的示例代码只是“马虎”,但他表达了自己的观点。 - ShinTakezou
@dribeas 此外,“如何将 f(1) 和 f(1,2) 链接到 f(int,int,int)” 部分让我想到他具有一些 C++ 背景并考虑了重载:f(1) 和 f(1,2) 的“签名”不对应于现有函数,因此他期望出现错误(在编译时)。 - ShinTakezou
显示剩余7条评论

4
这个声明:
int f();

这段代码告诉编译器:“f是一个接受固定数量参数并返回int的函数”。然后你尝试用一个、两个和三个参数来调用它 - C编译器在概念上是单遍的(预处理后),所以此时编译器没有可用的信息与你争论。

你实际上实现的f()接受三个int参数,所以只提供一个或两个参数的调用会导致未定义的行为 - 这是一种错误,意味着编译器不需要给出错误消息,并且运行程序时可能发生任何事情


从问题的角度来看,我们仍然可以说该程序“运行正常”,并且不包含错误(错误和未定义的行为属于不同的“错误”领域)。 - ShinTakezou
3
没有任何“未定义行为”的程序能够正常工作,即使这个未定义行为产生了正确的结果 :-) - paxdiablo
这个程序“工作”是因为它按照用户的要求展示问题。它的未定义行为是故意放置的(否则问题根本不存在),所以它“工作”得很好。用户的问题是为什么,因为他很可能期望在没有编译器错误的情况下无法调用f这种方式。 - ShinTakezou
@ShinTakezou:“一只坏掉的时钟(模拟)每天会两次显示正确时间。”如果你只在时钟提供正确时间时看它,那么它是否功能正常?导致未定义行为的代码仍然可能偶尔提供正确结果。 - David Rodríguez - dribeas
1
用户没有指定正确的结果。他并没有说“我希望输出1 2 3”。我们可以假设正确的预期结果是编译器错误或崩溃(他说“它运行良好,没有错误”)。相反,程序打印出了一些内容并正确退出。所以,它是有效的,他想知道为什么没有收到编译器错误或崩溃。 - ShinTakezou

3
int f();

在C语言中,这声明了一个可以接受可变数量参数的函数,也就是在C++中等价于以下内容:

int f(...);

为了检查这个问题,使用以下内容代替int f();
int f(void);

这将导致编译器发出警告。

请注意:这里还涉及到C链接器的一个怪异之处... C链接器不会在调用函数时验证传递的参数,并且只是链接到具有相同名称的第一个公共符号。因此,在main中使用f()是允许的,因为声明了int f()。但是,在调用点上,链接器会在链接时间绑定函数f(int, int, int)。希望这有些意义(如果没有,请告诉我)


6
不,"int f();"并没有声明一个带可变数量参数的函数。它声明了一个接受固定但未指定数量参数的函数。 - caf
1
我的字典中有一个固定但未指定的变量...可能会因人而异。 - Sandeep Datta
1
对于正在使用的调用约定,被调用者完全不知道堆栈上实际存在多少参数。可变参数的东西存在于语法中(允许编译器进行检查),并且在编译时生成正确的代码;但是由于在运行时被调用者不知道真正的参数数量,除非你将其作为(第一个)参数传递,否则即使使用可变参数也可以产生相同类型的“未定义行为”。 - ShinTakezou
1
@SDX2000,我可能没有表达清楚(这是我的错,不是你的)。我的意思是:使用可变参数,函数可以处理任意数量的参数(是的,必须告诉它有多少个参数,可以使用“格式字符串”或哨兵)。对于非可变参数,函数被定义为具有N个参数,并且它只能获取这些N个参数(当然,放置任何不可移植的堆栈技巧除外)。 - paxdiablo
1
或者换句话说 - 声明了一个函数 f();,编译器在调用该函数时可以自由使用被调用者清理堆栈的调用约定,例如 x86 上的 stdcall。但对于真正的可变参数函数,则不行。 - caf
显示剩余6条评论

1

它可以正常运行,因为int f()意味着其他答案已经解释过的:它表示参数数量未指定。这意味着您可以使用您想要的参数数量来调用它(也可以超过3个),而编译器不会报错。

它之所以在“内部”工作的原因是因为参数被推入堆栈,然后在f函数中“从”堆栈中访问。如果您传递0个参数,则函数的、j、k“对应”于函数PoV中的垃圾值。尽管如此,您仍然可以访问它们的值。如果您传递1个参数,则其中一个i,j,k访问该值,其他值则获取垃圾值。以此类推。

请注意,如果参数以其他方式传递,相同的推理也适用,但无论如何这些约定都在使用中。这些约定的另一个重要方面是被调用者不负责调整堆栈;这由知道实际推送了多少参数的调用者负责。如果不是这样,f的定义可能会暗示它需要“调整”堆栈以“释放”三个整数,这将导致某种崩溃。

你所编写的代码对于当前标准来说是可以的(在gcc编译时即使加了-std=c99 -pedantic,也没有警告;有一个警告,但只是关于定义前缺少int的问题),尽管许多人认为这是"过时特性"并且很讨厌。当然,在示例代码中你的用法并没有展示出任何有用性,相反使用原型更能帮助消除错误!(但我仍然更喜欢C而不是Ada)

补充

更加 "有用" 的用法不会引起 "未定义行为" 问题,可以是:

#include<stdio.h>
int f();

int main()
{

    f(1);
    f(2,2);
    f(3,2,3);
}

int f(int i,int j,int k)
{
  if ( i == 1 ) printf("%d\n", i);
  if ( i == 2 ) printf("%d %d\n", i, j);
  if ( i == 3 ) printf("%d %d %d\n", i, j, k);
}

2
我严重反对说OP所写的代码是"fine"。这段代码很容易崩溃 - 例如,在像"stdcall"这样的"被调用者调整堆栈"的调用约定下。如果省略f(1);f(1, 2);这两个函数调用,那么这段代码就没问题了。 - caf
遗憾的是,类似Pascal的调用约定已经不再使用(我不会说从不使用,但在C语言中它们几乎不被使用)。请参见其他评论,我已经放置了一些代码,以便人们可以专注于回答他的问题,而不是关注他编写的快速导致“未定义行为”的代码,但仍然展示了真正问题的要点。 - ShinTakezou
@caf 为了让我的话更加清晰,最近我写了一些用于编程竞赛的代码。我很难说这是好的 C 代码;但这不是编程竞赛的重点,因此不值得专注于它:从编程竞赛的角度来看,它是“好”的代码。为了展示用户的问题,代码很好并且可以正常工作(即没有编译时错误或崩溃);正如在评论中已经写过的那样,“f(1)和f(1,2)链接到f(int,int,int)”部分让我想他认为应该会出现错误(顺便说一下,对于 C++ std 来说,它会给出编译时错误,这可能对 OP 更合理)。 - ShinTakezou
1
当然,但我觉得指出它仅仅是因为他实现的偶然而运行(只要它没有崩溃),而不是由语言设计,这也很重要。顺便说一下,Win32 API使用被调用者调整堆栈的调用约定... - caf
@caf C标准函数并没有这个功能,因此据我所知,他可以使用这个“特性”而确信它不会崩溃。另一方面,如果要说一个函数期望不同的调用约定,则必须添加一个特殊的“属性”,我相信在这种情况下编译器会发出警告/错误。当然,重要的是要说出来,作为一个附注。但是,在这个主题上有更多的内容比为什么通常可以这样做! - ShinTakezou
显示剩余2条评论

0

当你使用g++编译器编译相同的程序时,会看到以下错误 -

g++ program.c
program.c: In function `int main()':
program.c:2: error: too many arguments to function `int f()'
program.c:6: error: at this point in file
program.c:2: error: too many arguments to function `int f()'
program.c:7: error: at this point in file
program.c:2: error: too many arguments to function `int f()'
program.c:8: error: at this point in file
program.c: At global scope:
program.c:12: error: ISO C++ forbids declaration of `f' with no type

使用带有 -std=c99 选项的 gcc 只会产生警告

使用与 g++ 默认设置相同的标准编译同一程序,会出现以下信息:

gcc program.c -std=c++98
cc1: warning: command line option "-std=c++98" is valid for C++/ObjC++ but not for C

我的答案是C编译器遵循的标准与C++遵循的标准不同,前者没有后者那么严格。


0
在C语言中,声明至少要声明返回类型。所以
int f();

声明一个返回类型为int的函数。这个声明不包含函数所需参数的任何信息。函数的定义为

f(int i,int j,int k)
{

    printf("%d %d %d",i,j,k);
}

现在已知该函数需要三个int参数。如果您使用与定义不同的参数调用函数,则不会得到编译时错误,而是运行时错误(或者如果您不喜欢错误的负面含义:“未定义的行为”)。C编译器标准并不强制捕获这些不一致之处。

为了防止这些错误,您应该使用适当的函数原型,例如

f(int,int,int);           //in your case
f(void);                  //if you have no parameters

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