为什么每次都要在C中指定数据类型?


67

从下面的代码片段中可以看到,我声明了一个char变量和一个int变量。编译代码时,它必须标识变量str和的数据类型i

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


10
%x%d%s都是格式说明符“告诉”printf()如何显示数据; 即是将位流显示为十六进制数字还是十进制整数或ASCII表示。数据就是数据就是数据。:-)程序员(使用printf)可以随意解释它。
TheCodeArtist 2013年

6
请参见Yu Hao的答案...问题在于printf和scanf是varargs函数,这意味着它们的参数没有静态类型。C是弱类型的……函数没有可以检查的运行时类型信息。格式用于此目的。
吉姆·巴尔特

我对格式说明符的回答如何?并且您认为stdio.h没有必要使用scanf()@mvp先生的建议吗?
Niko

4
虽然C语言的局限性,事实上,你可以在运行时平均创建格式字符串的printfscanf和朋友,你总是需要再次指定类型,如果格式字符串硬编码,一些编译器将至少验证您的类型匹配:gcc.gnu.org/onlinedocs/gcc/...
rakslice

1
尽管这一点是正确的,但没有真正的原因使编译器看不到“%?”。(其中%?是编译器扩展),然后使用typeof(next_arg_in_list)猜出什么'?' 应该替换为,加上使用“ 0x”之类的上下文来指定十六进制等的奖励……
Technosaurus

Answers:


120

因为像这样的可变参数函数scanf并没有可移植的方法printf来知道可变参数的类型,所以甚至不知道要传递多少个参数。

请参见C常见问题解答:如何发现函数实际使用了多少个参数?


这就是为什么必须至少有一个固定的自变量来确定变量自变量的数量,也许是类型的原因。这个参数(标准称为它parmN,请参见C11(ISO / IEC 9899: 201x§7.16变量参数))扮演着特殊的角色,并将被传递给宏va_start。换句话说,您无法在标准C语言中使用具有以下原型的函数:


21
+1是这里唯一能识别出问题所在的答案...缺少可变参数的静态类型。
吉姆·巴尔特

3
另外,如上面的注释中所述,在var类型和printf格式“ type”之间没有一一对应的关系。
MarkHu

@MarkHu是的,但这更像是结果而不是原因。无论如何,想一想,为什么格式说明printf用途%f输出double,怎么样float
Yu Hao

在我上面的评论之后添加的第二段是非常不幸的,完全是错误的。只需要第一个参数是因为在没有添加其他语言功能的情况下,没有其他方法可以在基于堆栈的实现中指定参数的起始地址。确定参数类型的方法有很多,包括全局变量,并且没有特殊的原因必须将类型信息放在第一个参数中……例如,在开始未知类型的参数之前,可以有5个已知类型的参数,而且您仍然必须使用va_arg所有这些。
Jim Balter 2014年

@JimBalter我没有在任何地方说第一个论点,对吗?parmN根据定义,是之前最右边的参数...,它不一定是第一个参数。它需要在标准C.
俞灏

28

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

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

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

为了说明这一点,printf编译时如下所示:

的原型printf是这样的:

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


总体上对机制进行了很​​好的解释,然后使用了更多技术性语言。

1
使用字符串作为格式的一个很好的用途是,该格式实际上可以来自各种来源:它没有在源代码中进行硬编码。您甚至可以要求用户提供自己的格式字符串,或者将其与gettext一起使用以更改模式顺序。
Max-P

3
@ Max-P,是的,这绝对是优势,但也是危险的填隙。:)当然,无论如何,您都必须编写一些包装程序,因为如果可以在命令行上提供格式字符串(可以这么说),则必须确保还提供了适当的参数。
Devolus

push...push call序列是实现一种可能的方法printf调用。实际的调用约定未由C标准指定(通常由平台的ABI指定)。例如,某些参数可能在寄存器中传递,并且它们可以以任何顺序传递。
基思·汤普森

@KeithThompson,我知道。这取决于实施和优化。但是,无论如何完成,都没有关联的类型信息,只有调用者指定的原始二进制值。
Devolus

14

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


1
@Kevin Panko感谢您的修改!就编辑而言,我仍然是菜鸟。
塔里克

+1表示printf不是语言功能或任何特殊功能,因此编译器没有义务对其进行优化
Sebastian 2014年

@Tarik如果printf不是C语言的一部分,那么它实际上在哪里实现?通过操作系统?
Tejas Chandrashekhar

@TejasChandrashekhar对您的问题的答案很晚,但是在这里是:组成语言的是关键字,语法和所产生的行为。printf只是部分用C语言实现的功能,最终会进行系统调用以将格式化的字节保存到文件中或显示在屏幕上。可在以下位置找到printf的源代码:code.woboq.org/linux/linux/arch/x86/boot/printf.c.html
Tarik

13

编译器可能是聪明的,但功能printf还是scanf很愚蠢-他们不知道什么是参数的类型,你传递的每一个电话。这就是为什么您需要通过%s%d每次通过。


您如何才能使这些功能更智能?C不支持反射
。– Tarik

1
C++您可以:类似cin >> counter; 或cout << str; 按预期工作。但是,C您无法做到这一点,这就是%d发明类似字符串格式的原因。
mvp

8
在C ++中,cin / cout的示例不合适。之所以起作用,是因为重载,因此编译器可以在编译时决定它将为调用提供哪个函数。这是与varargs完全不同的机制。对于用户而言,它看起来很相似,但从技术上来说却并非如此。
Devolus

7
这是因为C ++支持运算符重载,这最终意味着您有一个用于<<字符串的方法,还有一个用于<< << int的方法,依此类推...在像C这样的过程语言中,这等效于具有print_str,print_int ... 。
塔里克

1
@Timo从理论上讲,可以做任何事情,但是那将改变C的底层特性及其附带的灵活性。不再是C ...但是为什么要打扰呢?C ++已经存在。
塔里克

10

第一个参数是格式字符串。如果您要打印一个十进制数字,它可能看起来像:

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

等等,请参阅例如Wikipedia上的Format占位符,以了解可以包含哪些格式字符串。

这里也可以有多个参数:

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


8

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

没有。

您使用的是数十年前指定的语言。不要指望C带来现代设计美学,因为它不是现代语言。现代语言将倾向于在编译,解释或执行中牺牲少量效率,以提高可用性或清晰度。从计算机处理时间昂贵且供应极为有限的时代开始,它的设计便体现了这一点。

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


我的回答很奇怪。我从来没有主张过。
杰克·艾德利2014年

是的,很抱歉,我将其写到了错误的打开标签中。抱歉给您带来不便
塞巴斯蒂安

4

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

它与编译器无关,全部与为语法定义有关。scanf需要使用参数格式来scanf告知要为输入数据保留的大小。


4

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

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

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

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

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

被呼叫者无法区分案件。参数传递区域中只有一些位,我们不知道它们是表示字符数据的指针还是浮点数。

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

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

对演员0是必需的,因为没有上下文它被解释为一个空指针常量:编译器不知道,对于这样的说法预期的类型为指针类型。此外,这(void *) 0是不正确的,因为它将简单地作为void *类型而不是类型进行传递char *,尽管几乎可以肯定两者在二进制级别上是兼容的,所以它将在实践中起作用。此类exec函数的一个常见错误是:

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

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

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

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

但是,请注意,格式字符串可以不是文字字符串,并且可以在运行时计算,这对于此类类型检查方案是不可渗透的。虚构的例子:


2

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

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


1

printfscanfI / O函数的设计和定义是为了接收控制字符串和参数列表。

函数不知道传递给它的参数的类型,并且Compiler也无法将此信息传递给它。


4
-1这无法回答我所能回答的问题。

0

因为在printf中您没有指定数据类型,所以您在指定数据格式。这在任何语言中都是重要的区别,在C语言中则是双重重要的。

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

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

尽管如此,%s扫描程序仍然可以在更高级的编程环境中使用,该环境中存在字符串数据类型。因为是关于扫描,而不是键入。


1
不,每个格式说明符都指定所需的参数类型。%s需要一个类型的参数char*(必须是指向字符串的指针)。%d需要一个类型为的参数int%x%o要求和类型的参数unsigned int。依此类推。%s有点不同,因为它处理自变量指向的数据。哦,字符串的结尾不是用NULL;表示 那是一个空指针常量。它是由一个空表示字符'\0'
基思·汤普森

2
首先,您要滥用“ requires”一词。这意味着如果提供错误的数据类型,则会出现错误。您会收到警告,仅此而已。%s期望, char *但是如果您提供其他内容,您仍然可以编译。//不过,您对NULL的看法是正确的,我将对其进行更改。
艾萨克·拉比诺维奇

1
我并不是说“需要”一词暗示需要进行诊断。实际上,行为是不确定的,并且提供错误的类型是错误的。(它不需要诊断,因为通常这是不可能的;格式字符串不必是字符串文字。)通过提供"%s"or"%d"格式,程序员(即程序员)指定分别提供char*or或int,作为相应的参数。
基思·汤普森

“这意味着如果提供错误的数据类型,您将得到一个错误” –不,不是。这是错误的,因此,在大多数情况下,这是您的答案。
Jim Balter 2014年

@JimBalter告诉我我错了但没有告诉我为什么的评论不是很有用。
艾萨克·拉比诺维奇
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.