【问题标题】:Why do I have to specify data type each time in C to printf() and scanf()?为什么每次在 C 中都要指定数据类型来 printf() 和 scanf()?
【发布时间】:2013-08-14 17:55:51
【问题描述】:

从下面的代码sn-p可以看出,我声明了一个char变量和一个int变量。编译代码时,必须识别变量stri的数据类型。

为什么我需要在扫描我的变量时通过将%s%d 指定为scanf 来再次告知它是字符串或整数变量?当我声明我的变量时,编译器还不够成熟,无法识别吗?

#include <stdio.h>

int main ()
{
  char str [80];
  int i;

  printf ("Enter your family name: ");
  scanf ("%s",str);  
  printf ("Enter your age: ");
  scanf ("%d",&i);

  return 0;
}

【问题讨论】:

  • %x, %d, %s 都是“告诉”printf()如何显示数据的格式说明符;即比特流是显示为十六进制数还是十进制整数或 ASCII 表示。数据就是数据就是数据。 :-) 程序员(使用 printf)可以随意解释它。
  • 见于浩的回答...问题是printf和scanf是varargs函数,也就是说它们的参数没有静态类型。并且 C 是弱类型的……没有函数可以检查的运行时类型信息;格式服务于这个目的。
  • 我对格式说明符的回答怎么样?你认为stdio.h 没有必要像@mvp 先生建议的那样使用scanf() 吗?
  • 虽然 C 语言的限制以及您可以在运行时创建格式字符串的事实意味着对于printfscanf 和朋友您将始终需要再次指定类型,如果格式字符串是硬编码的,一些编译器至少会为您验证类型是否匹配:gcc.gnu.org/onlinedocs/gcc/…
  • 这一点是有效的,编译器看不到“%?”并没有真正的原因。 (其中 %? 是编译器扩展)并使用 typeof(next_arg_in_list) 来猜测“?” 应该替换为,使用诸如“0x”之类的上下文来指定十六进制等...

标签: c compiler-construction printf scanf


【解决方案1】:

编译器可能很聪明,但函数printfscanf 很愚蠢——它们不知道你为每次调用传递的参数类型是什么。这就是为什么您每次都需要传递%s%d 的原因。

【讨论】:

  • 如何让这些功能变得智能? C 不支持反射。
  • C++ 你可以:像cin &gt;&gt; counter;或cout &lt;&lt; str;按预期工作。但是在C 中你不能这样做,这就是为什么发明了像%d 这样的字符串格式。
  • 在 C++ 中,cin/cout 的例子是不合适的。这个工作的原因是因为重载,所以编译器可以在编译时决定它将为调用提供哪个函数。这是与可变参数完全不同的机制。对于用户来说,它看起来很相似,但从技术上讲,它不是。
  • 那是因为 C++ 支持运算符重载,这最终意味着您有一个用于
  • @Timo 从理论上讲,任何事情都可以做,但这将改变 C 的低级性质和随之而来的灵活性。它不再是C了……但是为什么要麻烦呢? C++ 已经存在。
【解决方案2】:

scanf as prototype int scanf ( const char * format, ... ); 表示根据参数格式将给定数据存储到附加参数指向的位置。

它与编译器无关,都是为scanf定义的语法。需要参数格式让scanf知道为要输入的数据保留的大小。

【讨论】:

    【解决方案3】:

    因为像 scanfprintf 这样的变量参数函数没有可移植的方法来知道变量参数的类型,甚至不知道传递了多少个参数。

    参见 C 常见问题解答:How can I discover how many arguments a function was actually called with?


    这就是为什么必须至少有一个固定参数来确定可变参数的数量,也许还有类型。而这个参数(标准称之为parmN,参见C11(ISO/IEC 9899:201x) §7.16 变量参数)扮演了这个特殊的角色,并将被传递给宏va_start。换句话说,你不能在标准 C 中拥有这样一个原型的函数:

    void foo(...);
    

    【讨论】:

    • +1 是这里唯一能识别问题所在的答案...缺少可变参数的静态类型。
    • 另外,上面的cmets中也提到过,var类型和printf格式的“类型”并不是一一对应的。
    • @MarkHu 是的,但这更像是一个结果而不是一个原因。反正大家想一想,为什么printf的格式说明符用%f输出double,那float呢?
    • 第二段是在我上面的评论之后添加的,非常不幸并且完全错误。仅需要第一个参数是因为没有其他方法可以在基于堆栈的实现中指定参数的起始地址而不添加额外的语言功能。还有许多其他方法可以确定参数类型,包括全局变量,并且没有特别的原因类型信息必须在 first 参数中......例如,您可以有 5 个已知类型的参数在开始未知类型的参数之前,您仍然必须为所有参数使用va_arg
    • @JimBalter 我没有在任何地方说 first 参数,是吗? parmN,根据定义,是... 之前最右边的参数,它不必是first 参数。并且在标准 C 中是必需的。
    【解决方案4】:

    printf 不是intrinsic function。它本身不是 C 语言的一部分。编译器所做的只是生成代码来调用printf,传递任何参数。现在,由于 C 不提供 reflection 作为在运行时计算类型信息的机制,程序员必须显式提供所需的信息。

    【讨论】:

    • @Kevin Panko 感谢您的精彩编辑!就编辑而言,我仍然是菜鸟。
    • +1 提到 printf 不是语言功能或任何特殊方式,因此编译器没有义务对其进行优化
    • @Tarik 如果printf 不是C 语言的一部分,那么它在哪里实际实现?通过操作系统?
    • @TejasChandrashekhar 对您的问题的回答很晚,但这里是:构成语言的是关键字、语法和结果行为。 printf 只是一个部分在 C 中实现的函数,并最终执行系统调用以将格式化的字节放入文件或屏幕上。 printf 的源代码可以在这里找到:code.woboq.org/linux/linux/arch/x86/boot/printf.c.html
    【解决方案5】:

    第一个参数是格式字符串。如果您要打印十进制数,它可能如下所示:

    • "%d"(十进制数)
    • "%5d"(用空格填充到宽度 5 的十进制数)
    • "%05d"(用零填充到宽度 5 的十进制数)
    • "%+d"(十进制数字,始终带有符号)
    • "Value: %d\n"(数字前后的一些内容)

    等,例如查看Format placeholders on Wikipedia 以了解可以包含哪些格式字符串。

    这里也可以有多个参数:

    "%s - %d"(一个字符串,然后是一些内容,然后是一个数字)

    【讨论】:

      【解决方案6】:

      printfscanf 是 I/O 函数,它们的设计和定义方式是接收控制字符串和参数列表。

      函数不知道传递给它的参数的类型,编译器也无法将这些信息传递给它。

      【讨论】:

      • -1 据我所知,这并不能回答问题。
      【解决方案7】:

      编译器无法提供必要信息的原因很简单,因为这里没有涉及编译器。函数的原型没有指定类型,因为这些函数具有可变类型。所以实际的数据类型不是在编译时确定的,而是在运行时确定的。 然后该函数从堆栈中获取一个参数,一个接一个。这些值没有与之关联的任何类型信息,因此函数知道如何解释数据的唯一方法是使用调用方提供的信息,即格式字符串。

      函数本身不知道传入了哪些数据类型,也不知道传入的参数数量,所以printf无法自行决定。

      在 C++ 中,您可以使用运算符重载,但这是一种完全不同的机制。因为这里编译器会根据数据类型和可用的重载函数来选择合适的函数。

      为了说明这一点,printf,编译后看起来像这样:

       push value1
       ...
       push valueN
       push format_string
       call _printf
      

      printf的原型是这样的:

      int printf ( const char * format, ... );
      

      所以除了格式字符串中提供的内容外,没有携带任何类型信息。

      【讨论】:

      • 很好地解释了一般机制,然后是更多技术语言。
      • 使用字符串作为格式的一个很好的用途是格式实际上可以来自各种来源:它不是在源代码中硬编码的。您甚至可以要求用户提供他们自己的格式字符串或将其与 gettext 一起使用来更改模式顺序。
      • @Max-P,是的,这绝对是一个优势,但也是一个危险的坑。 :) 当然,无论如何,您都必须编写一些包装器,因为如果您可以在命令行上提供格式字符串(可以这么说),您必须确保还提供了适当的参数。
      • push ... push call 序列是实现printf 调用的一种可能方式。 C 标准没有指定实际的调用约定(通常由平台的 ABI 指定)。例如,一些参数可能在寄存器中传递,它们可以按任意顺序传递。
      • @KeithThompson,我知道。这取决于实现和优化。但是,无论如何完成,都没有关联的类型信息,只有调用者指定的原始二进制值。
      【解决方案8】:

      当我声明我的 变量?

      没有。

      您使用的是几十年前指定的语言。不要指望 C 具有现代设计美学,因为它不是现代语言。现代语言倾向于以少量的编译、解释或执行效率来换取可用性或清晰度的改进。 C 源于计算机处理时间昂贵且供应极为有限的时代,其设计反映了这一点。

      这也是为什么 C 和 C++ 在您真正、真正关心快速、高效或接近金属时仍然是首选语言的原因。

      【讨论】:

      • 这对我的回答来说是一个奇怪的评论。我从来没有声称过。
      • 是的,对不起,我把它写到了错误的打开标签中。很抱歉给您带来不便
      【解决方案9】:

      GCC(可能还有其他 C 编译器)至少在某些情况下会跟踪参数类型。但是语言不是这样设计的。

      printf 函数是一个接受可变参数的普通函数。变量参数需要某种运行时类型标识方案,但在 C 语言中,值不携带任何运行时类型信息。 (当然,C 程序员可以使用结构或位操作技巧创建运行时键入方案,但这些并没有集成到语言中。)

      当我们开发这样的函数时:

      void foo(int a, int b, ...);
      

      我们可以在第二个参数之后传递“任意”数量的附加参数,我们可以使用函数传递机制之外的某种协议来确定有多少以及它们的类型。

      例如,如果我们这样调用这个函数:

      foo(1, 2, 3.0);
      foo(1, 2, "abc");
      

      被调用者无法区分大小写。参数传递区只有一些位,我们不知道它们是代表字符数据的指针还是浮点数。

      传达此类信息的可能性很多。例如,在 POSIX 中,exec 系列函数使用具有相同类型 char * 的变量参数,并且使用空指针来指示列表的结尾:

      #include <stdarg.h>
      
      void my_exec(char *progname, ...)
      {
        va_list variable_args;
        va_start (variable_args, progname);
      
        for (;;) {
           char *arg = va_arg(variable_args, char *);
           if (arg == 0)
             break;
           /* process arg */
        }
      
        va_end(variable_args);
        /*...*/
      }
      

      如果调用者忘记传递空指针终止符,则行为将未定义,因为函数在消耗完所有参数后将继续调用va_arg。我们的my_exec 函数必须像这样调用:

      my_exec("foo", "bar", "xyzzy", (char *) 0);
      

      0 上的强制转换是必需的,因为没有上下文可以将其解释为空指针常量:编译器不知道该参数的预期类型是指针类型。此外,(void *) 0 不正确,因为它只是作为void * 类型而不是char * 传递,尽管两者几乎可以肯定在二进制级别兼容,因此它可以在实践中工作。这种类型的exec 函数的一个常见错误是:

      my_exec("foo", "bar", "xyzzy", NULL);
      

      编译器的 NULL 恰好被定义为 0 没有任何 (void *) 转换。

      另一种可能的方案是要求调用者传递一个数字,该数字指示有多少个参数。当然,这个数字可能不正确。

      printf 的情况下,格式字符串描述了参数列表。该函数对其进行解析并相应地提取参数。

      如开头所述,一些编译器,尤其是 GNU C 编译器,可以在编译时解析格式字符串,并针对参数的数量和类型执行静态类型检查。

      但是,请注意,格式字符串可以不是文字,并且可以在运行时计算 时间,它不受这种类型检查方案的影响。虚构示例:

      char *fmt_string = message_lookup(current_language, message_code);
      
      /* no type checking from gcc in this case: fmt_string could have
         four conversion specifiers, or ones not matching the types of
         arg1, arg2, arg3, without generating any diagnostic. */
      snprintf(buffer, sizeof buffer, fmt_string, arg1, arg2, arg3);
      

      【讨论】:

        【解决方案10】:

        这是因为这是告诉函数(如printfscanf)您传递的值类型的唯一方法。例如-

        int main()
        {
            int i=22;
            printf("%c",i);
            return 0;
        }
        

        此代码将打印字符而不是整数 22。因为您已告诉 printf 函数将变量视为 char。

        【讨论】:

          【解决方案11】:

          因为在 printf 中您没有指定数据类型,而是指定了数据格式。这在任何语言中都是一个重要的区别,在 C 语言中具有双重重要性。

          当您使用%s 扫描字符串时,您并不是在说“为我的字符串变量解析字符串输入”。你不能在 C 中这么说,因为 C 没有字符串类型。 C 与字符串变量最接近的东西是一个固定大小的字符数组,它恰好包含一个表示字符串的字符,字符串的结尾由一个空字符表示。所以你真正想说的是“这是一个保存字符串的数组,我保证它足够大,可以让你解析字符串输入。”

          原始?当然。 C 是 40 多年前发明的,当时一台典型的机器最多有 64K 的 RAM。在这样的环境中,保存 RAM 比复杂的字符串操作具有更高的优先级。

          不过,%s 扫描器仍然存在于更高级的编程环境中,其中存在字符串数据类型。因为它是关于扫描,而不是打字。

          【讨论】:

          • 不,每个格式说明符都指定了所需的参数类型。 %s 需要 char* 类型的参数(它必须是指向字符串的指针)。 %d 需要 int 类型的参数。 %x%o 需要 unsigned int 类型的参数。等等。 %s 有点不同,它处理参数指向的数据。哦,字符串的结尾不是由NULL 指示的;那是一个空的 pointer 常量。它由一个空字符表示,'\0'.
          • 首先,您误用了“需要”这个词。这意味着如果您提供错误的数据类型,您会收到错误消息。你得到一个警告,不再。 %s expects char * 但如果您提供其他内容,您仍然可以编译。 // 你对 NULL 的看法是对的,我会改变它。
          • 我并不是说“需要”这个词意味着需要进行诊断。事实上,行为是未定义的,提供错误的类型是错误的。 (它不需要诊断,因为这通常是不可能的;格式字符串不必是字符串文字。)通过提供"%s""%d" 格式,您,程序员,指定您将分别提供char*int 作为相应的参数。
          • “这意味着如果你提供了错误的数据类型,你会得到一个错误”——不,它没有。这是错误的,所以在很大程度上,这是你的答案。
          • @JimBalter 告诉我我错了却不告诉我为什么的评论不是很有用。
          猜你喜欢
          • 2019-07-24
          • 1970-01-01
          • 2018-12-10
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多