68

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

为什么我需要在扫描我的变量时通过指定%sor%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;
}
4

11 回答 11

122

因为变量参数函数没有可移植的方法,比如scanf知道printf变量参数的类型,甚至不知道传递了多少参数。

请参阅 C 常见问题解答:如何发现函数实际调用的参数数量?


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

void foo(...);
于 2013-08-13T08:05:53.433 回答
28

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

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

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

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

 push value1
 ...
 push valueN
 push format_string
 call _printf

的原型printf是这样的:

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

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

于 2013-08-13T08:23:55.777 回答
14

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

于 2013-08-13T08:06:24.060 回答
13

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

于 2013-08-13T08:00:15.163 回答
10

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

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

等,请参阅例如Wikipedia 上的 Format placeholders以了解格式字符串可以包含的内容。

这里也可以有多个参数:

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

于 2013-08-13T08:06:45.657 回答
8

当我声明我的变量时,编译器还不够成熟以识别它吗?

不。

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

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

于 2013-08-13T09:41:24.477 回答
4

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 *type 而不是传递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);
于 2013-08-13T18:55:36.213 回答
4

scanf正如原型int scanf ( const char * format, ... );所说,根据参数格式将给定数据存储到附加参数指向的位置。

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

于 2013-08-13T08:05:18.453 回答
2

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

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

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

于 2013-08-14T06:05:25.360 回答
1

printf并且scanf是以接收控制字符串和参数列表的方式设计和定义的 I/O 函数。

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

于 2013-08-13T08:07:38.223 回答
0

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

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

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

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

于 2013-08-15T22:34:30.937 回答