关于C#:printf,具有无与伦比的格式和参数


printf with unmatched format and parameters

我在试着理解printf函数。

我知道在我读到这个函数后,C编译器会自动将所有小于int的参数强制转换为chars和int的缩写。我还知道long long int(8字节)并没有按原样进行强制转换和推送到堆栈中。

所以我写了这个简单的C代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>

int main()
{
    long long int a = 0x4444444443434343LL;
    // note that 0x44444444 is 4 times 0x44 which is D in ascii.
    // and 0x43434343 is 4 times 0x43 which is C in ascii.

    printf("%c %c
"
, a);

    return 0;
}

它创建一个大小为8字节的变量并将其推送到堆栈中。我还知道printf循环遍历格式字符串,当它看到%c时,指针将递增4(因为它知道char已转换为int-下面的示例)

比如:char c=(char)va_arg(list,int)->

(*(int*)((指针+=sizeof(int))-sizeof(int)))

如您所见,当指针指向时,它将得到4个字节,并将其递增4

我的问题是:

在我的逻辑中,它应该打印在小endian机器c d上。这不是发生的事情,我问为什么?我相信你们中的一些人比我更了解实现,这就是为什么我问他的问题。

编辑:实际结果是C,后面跟着一些垃圾字符。

我知道有些人可能会说它的未定义行为——它实际上取决于实现,我只想知道实现的逻辑。


您的逻辑可以解释早期C编译器在70和80年代的行为。较新的ABI使用各种方法将参数传递给函数,包括变量参数函数。您必须研究您的系统ABI以了解在您的案例中参数是如何传递的,从具有显式未定义行为的构造中推断并没有帮助。

顺便说一下,短于int的类型不是铸造或铸造的,而是提升为int的。注意,当传递给变量参数函数时,float值转换为double。非整数类型和大于int的整数类型根据abi传递,这意味着它们可以在常规寄存器甚至特殊寄存器中传递,而不一定在堆栈上。

printf依赖于中定义的宏来隐藏这些实现细节,因此可以针对具有不同ABI和不同标准类型大小的架构以可移植的方式编写。


正如评论所揭示的那样,这里存在一个根本性的误解。

according to the format string here the compiler should know that 4 bytes were pushed, convert 4 bytes to char and print it...

但问题是,没有规则规定C对所有内容都使用单字节寻址堆栈。

不同的处理器体系结构可以——也可以——使用各种技术向函数传递参数。一些参数可以在常规堆栈上传递,但其他参数可以在寄存器中传递,或者通过其他技术传递。不同类型的参数可以在不同类型的寄存器(32对64位、整数对浮点等)中传递。

显然,C编译器必须知道如何为其编译的平台正确传递参数。显然,像printf这样的变量函数必须仔细编写,以根据使用它的平台正确获取变量参数。但是像%d这样的格式说明符并不是简单地表示"从堆栈中弹出4个字节,并将它们视为int"。同样,%c并不意味着"弹出4个字节,并将生成的整数作为字符打印"。当printf遇到格式说明符%c%d时,需要安排获取int类型的下一个参数,无论需要什么。事实上,如果调用代码实际传递的下一个参数不是int类型——例如,与这里一样,下一个参数实际上是long long int类型——一般来说,无法知道会发生什么。

具体地说,当printf刚刚看到%d%c说明符时,它在内部所做的相当于调用

1
va_arg(argp, int)

字面意思是"获取int类型的下一个参数"。然后由va_arg的作者(以及中声明的其他函数和宏)来确切了解在这个特定平台上获取int类型的下一个参数需要什么。

很明显,可以知道在特定平台上实际会发生什么。(很明显,va_arg的作者必须知道!)但是你不会基于C语言本身,或者通过猜测你认为应该发生什么来解决这个问题。您将不得不阅读abi——应用程序二进制接口——它指定了平台上函数调用约定的详细信息。这些细节很难找到,因为很少有程序员真正关心它们。

我说"为了正确获取变量参数,必须仔细编写printf",但实际上我有点误会了,因为正如我后来所说,"这实际上取决于va_arg的作者,以确切知道它需要什么。"您是对的,可以编写一个相当可移植的printf实现。在C FAQ列表中有一个例子。

如果您想了解更多关于函数调用约定的信息,另一个有趣的主题是外部函数接口或FFI。(例如,还有另一个库libffi可以帮助您——方便携带!--执行一些与操作函数参数有关的更奇特的任务。)


有太多的notestypes

c将11个整数类型signed charchar……,unsigned long long指定为不同的类型。除了char必须与signed charunsigned char匹配外,这些编码可以实现为10种不同的编码,也可以实现为2种编码(全部使用64位有符号或无符号)。标准库为这11个库中的每一个都有一个printf()说明符。(由于Sub-int促销活动,还有其他问题)。

到目前为止还没有真正的问题。

然而,C有许多其他类型的printf()说明符:

1
2
3
4
5
6
7
8
9
ju           uintmax_t
jd           intmax_t
zu           size_t
td           ptrdiff_t
PRIdLEASTN   int_leastN_t  where N is 8, 16, 32, 64
PRIuLEASTN   uint_leastN_t
PRIdN        intN_
PRIuN        uintN_t
Many others

一般来说,这些附加类型可以与上述11种类型不同,也可以与之兼容。

每当代码在printf()中使用这些其他类型时,就会出现不同/兼容的问题,并阻止许多编译器检测/提供最佳建议的匹配打印说明符。

1存在各种条件/限制。