为什么gets(stdin)返回一个整数?以及其他错误

3

我刚开始接触C编程(虽然我有Java的经验)。读了一些教程后,我决定开始在 Coderbyte 上解决编码挑战。

我尝试的第一个挑战是这个

挑战

编写函数 FirstFactorial(num),将传递给它的参数 num 返回其阶乘。例如:如果 num = 4,则您的程序应返回 (4 * 3 * 2 * 1) = 24。对于测试用例,范围将介于1和18之间,输入始终为整数。

示例测试用例

输入:4
输出:24

输入:8
输出:40320

我的解决方案:

#include <stdio.h>

void FirstFactorial(int num[]) {

  int i = num -1;

  for(i ; i > 0; i--) {
    num = num * i;
    printf("%d",i);
  }

  printf("\t %d", num);
}

int main(void) {

  // disable stdout buffering
  setvbuf(stdout, NULL, _IONBF, 0);

  // keep this function call here
  FirstFactorial(gets(stdin));
  return 0;

}

输入参数的值为:8

错误信息:

main.c: In function 'FirstFactorial':
main.c:5:11: warning: initialization makes integer from pointer without a cast [-Wint-conversion]
   int i = num -1;
           ^~~
main.c:8:15: error: invalid operands to binary * (have 'int *' and 'int')
     num = num * i;
               ^
main.c: In function 'main':
main.c:23:18: warning: passing argument 1 of 'FirstFactorial' makes pointer from integer without a cast [-Wint-conversion]
   FirstFactorial(8);
                  ^
main.c:3:6: note: expected 'int *' but argument is of type 'int'
 void FirstFactorial(int num[]) {
      ^~~~~~~~~~~~~~

exit status 1

看起来有一些问题,我有几个问题:

  1. I've never heard of gets(stdin). I looked up gets(), and the glibc documentation says the function returns a char*. How can I pass it to a function that takes an int?

  2. It looks like

    int i = num -1;
    

    is initializing i as 4 and not 7. Why?

  3. The for loop seems to be decrementing i correctly (i = 7, 6, 5, 4, 3, 2, 1). But this statement:

    num = num * i;
    

    is generating an error. What is wrong with it? It looks like a normal multiplication.


9
甚至在阅读问题的其余部分之前,首先忘记gets(),因为在当前版本的C中,它已经不存在了,这是有充分理由的。使用gets()无法编写正确/安全的代码。 - user2371524
6
永远不要使用 gets 函数。它很危险,容易发生缓冲区溢出,因此已被从 C 规范中移除。相反,使用例如 fgets 函数。 - Some programmer dude
8
请弃用你目前的教材或老师,因为它所教授的知识已经过时。请查看 为什么gets函数如此危险以至于不应使用? - Lundin
3
使用 scanf("&d", &var); 代替读取整数。 - HolyBlackCat
6
真的需要尽快获取一本入门书。你的代码毫无意义,看起来你只是猜测 C 语言的工作方式,这是任何语言中都非常不好的编程方式。 - Some programmer dude
显示剩余7条评论
3个回答

6
为了方便未来的访客:这是 Coderbytes 对语言的可怕滥用。 gets(stdin) 一开始就不应该起作用:类型不匹配。
实际发生的事情是,Coderbytes 盲目地找到并替换了第一个 gets(stdin) 实例,用你提供的文本字符串替换之后再将代码发送给编译器。这甚至不是预处理器宏,而是源代码的盲目替换。
因此,虽然在现实中你永远不应该这样做,在 Coderbytes 上这是必要的恶行:似乎这是唯一支持将输入放入程序的方法。 来源

另外,如果你想找点乐子,尝试清除其他所有内容并将此代码放入Coderbytes:

int main(){
    printf("%s", "This is a literal string containing gets(stdin) along with other words");
}

你会发现替换甚至发生在字符串文字中!

这是问题#1的正确答案 - gets(stdin)之所以“有效”是因为Coderbyte捣鬼。其他答案忽略了这一点。然而,对于它们对问题#2和#3的回答,这些答案仍然很有价值。 - MultiplyByZer0

4
忽略了 gets 是危险的,并且已经根据 为什么gets函数如此危险,不应该使用? 被完全从C语言中删除,以下是您问题的答案:
  1. 我从未使用过 gets(stdin)。我在C库参考文献中检查过它。它看起来像会返回一个字符(是的,用户输入)。为什么我可以将其作为整数传递给函数?
没有人曾经使用过 gets(stdin),因为它期望参数是指向存储结果的字符缓冲区的指针,而不是 stdin。与 fgets 不同,gets 只能从 stdin 读取,并且因此默认设置为 stdin - 您无法更改它。
您不能将其传递给期望 int[] 的函数。由于从 gets 返回的 char*int[] 不兼容,因此您的编译器必须在此处给出诊断消息。如果您的编译器没有给出这样的消息,则它是有问题的,不应使用。
gcc编译器在此处给出了消息,而不是您引用的那个消息。这似乎表明您正在 gnu90(“垃圾模式”)中运行gcc,这不建议初学者使用。请参见本答案底部,了解您应该如何运行它。
  1. 看起来 int i = num -1; 将 i 初始化为 4 而不是 7。我不明白为什么?
那一行不是有效的C代码。在这种情况下,num 是一个数组,由于它是函数参数,因此会调整为类型 int*。因此,num - 1 给出指针算术运算,这不是您想要的。结果的类型是 int*。您不能将具有结果类型 int* 的表达式分配给 int。同样,您的编译器必须给出诊断消息,并且它正确地做到了这一点:
warning: initialization makes integer from pointer without a cast

如果尽管收到以上信息,它仍然生成可执行文件,则该程序的行为是未定义的,因为它不是有效的C语言,那么任何事情都可能发生。
但是看起来语句num = num * i;没有起作用。
出于与上述原因相同的原因,num被声明为数组,因此您无法以任何明智的方式对其进行算术运算。
总的来说,“猜测试错”的方式不能编程,编程不是这样工作的。您必须确切地知道每一行代码的作用。我强烈建议您将编译器警告提高到最高级别,并确保在运行程序之前没有警告:
gcc -std=c11 -pedantic-errors -Wall -Wextra

谢谢您的解释。现在看来我有很多东西要学习。我现在使用 int 而不是数组,并替换了 gets 函数。现在它可以工作了!但我还需要更多地了解指针和数组。 - Limon

0

正如之前提到的,你不应该使用gets()函数,因为它极易发生缓冲区溢出(你一定需要了解这个问题);正如其他人所提到的,一个更好的替代方案是fgets。之所以它更好,是因为它只会写入指定长度的字节数到缓冲区中;如果它没有停止,那么它将继续向缓冲区之外的内存中写入数据,这是不好的。这会导致许多安全问题和崩溃。

处理代码中的其他问题如下:

在C语言中,类型比Java中更加灵活。变量只是内存中的一个位置;你可以将该内存位置解释为不同的类型,并且C语言允许你很容易地进行类型转换,但如果你不小心就会被坑到。

在C语言中,数组始终是指针或内存地址,所以对于“int num []”,“num”是指针,而不是整数,就像“int * num”。这就是为什么当您尝试对“num”和int变量进行算术运算时,编译器会给出警告。这意味着从gets或fgets获得的内容是一个字符串,而不是整数,因此将gets的原始输出传递到FirstFactorial将会给您返回垃圾。您需要做的是获取该字符串表示的整数。
gets返回的内容可以用作int,因为它是char*,即地址,可以被“解释”为整数(因为它实际上是一组保存指向内存地址的数字的字节)。由于它们都只是位字符串,编译器可以这样解释它们,但会警告您可能没有语义上想要告诉它要做什么。

字符串实际上是内存中的数字,“代表”字符的字形(就像ASCII一样:http://ee.hawaii.edu/~tep/EE160/Book/chap4/subsection2.1.1.1.html)你所做的就像这样FirstFactorial('4'),你想要的是FirstFactorial(4)。将字符串“4”转换为4的最佳方式是使用strtol(这里有示例:https://www.techonthenet.com/c_language/standard_library_functions/stdlib_h/strtol.php),但是atoi稍微容易一些,以下是一个语义上执行您代码功能但更安全且编译更好的示例:

#include <stdio.h>

void FirstFactorial(int num) {

    int i = num -1;

    for(i ; i > 0; i--) {
        num = num * i;
    //        printf("%d",i);
    }

   printf("\t %d", num);
}

#define BUFFER_LENGTH 60
int main(void) {

   char str[BUFFER_LENGTH];

   // disable stdout buffering
   if( fgets (str, BUFFER_LENGTH, stdin)!=NULL ) {
       int num = atoi(str);
       FirstFactorial( num );
   }
   // keep this function call here
   return 0;

} 

请注意 #define,这是一个预处理器宏,有助于在缓冲区大小和传递给fgets的长度之间保持60的一致性。如果您决定更改缓冲区的大小,则可能会更改缓冲区大小(例如为40),但忘记更改传递给fgets的长度;这将导致fgets将最多写入60个字节到仅有40个字节的缓冲区中,这意味着您可能会覆盖其他内存的20个字节,这又是不好的。
获取一本现代的C语言书籍会非常有帮助,同时找一个导师或者教练来帮助您了解其中的差异,可以节省时间并避免一些容易出现的问题,从而提高代码质量。C语言需要一些指导才能掌握最佳实践,了解为什么要这样做或者不要这样做也很重要。

1
请不要教初学者使用 atoi。请改用 strtol,因为它完全具有相同的功能但更加安全。 - Lundin
2
返回的内容可以用作int,因为它是一个char,而char是一个地址,可以被“解释”为整数。这是不正确的,char*不能隐式转换为整数。这违反了简单赋值的约束(参数按照赋值规则复制到参数中),因此不是有效的C语言。如果gcc在这种情况下输出二进制文件,那么它的行为就无法预测了。这是未定义的行为。"因为它们实际上只是位字符串,编译器可以这样解释它们" 不,C编译器不会这样做。GNU屎编译器可能会这样做。 - Lundin
有时候,当我回答问题时,从评论中学到的比提问更多。感谢您的反馈! - Halcyon
谢谢您提供的参考资料,我会查看它们。看起来C语言比Java难得多。我认为我需要从头开始学习并忘记Java。 - Limon

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