我在试着理解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和不同标准类型大小的架构以可移植的方式编写。
- 晋升和种姓有什么区别?每个系统都实现自己的以满足其调用约定?
- 强制转换是指可以用来强制将值显式转换为另一种类型的语法,如(double)1。C标准为表达式中要提升为int或unsigned int的小整数类型定义了一组规则,并为二进制表达式(如1.1 + 1中要转换为公共类型的值定义了另一组规则。这些提升和转化是含蓄的。一些编码风格的指南坚持避免这种隐式转换,并要求在任何地方都进行显式转换,代价是降低可读性和潜在的可移植性问题。
正如评论所揭示的那样,这里存在一个根本性的误解。
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说明符时,它在内部所做的相当于调用
字面意思是"获取int类型的下一个参数"。然后由va_arg的作者(以及中声明的其他函数和宏)来确切了解在这个特定平台上获取int类型的下一个参数需要什么。
很明显,可以知道在特定平台上实际会发生什么。(很明显,va_arg的作者必须知道!)但是你不会基于C语言本身,或者通过猜测你认为应该发生什么来解决这个问题。您将不得不阅读abi——应用程序二进制接口——它指定了平台上函数调用约定的详细信息。这些细节很难找到,因为很少有程序员真正关心它们。
我说"为了正确获取变量参数,必须仔细编写printf",但实际上我有点误会了,因为正如我后来所说,"这实际上取决于va_arg的作者,以确切知道它需要什么。"您是对的,可以编写一个相当可移植的printf实现。在C FAQ列表中有一个例子。
如果您想了解更多关于函数调用约定的信息,另一个有趣的主题是外部函数接口或FFI。(例如,还有另一个库libffi可以帮助您——方便携带!--执行一些与操作函数参数有关的更奇特的任务。)
- 因此,"问题"是编译器可能决定以不同于堆栈的方式传递参数,并且为了完全理解在极端情况下会发生什么,我必须了解指定函数调用约定的系统ABI?如果C将对所有内容使用一个单字节寻址堆栈,int为4字节,long long int为8字节,那么我的假设是正确的?-我知道很多情况
- @??????????如果你的例子是long long int a = 0x0000004400000043LL; ,我可能会同意你的观点。(我可以。但是的,这是很多假设!)
- 为什么要这样?字符A=0x00000044与字符B=0x44444444完全相同。
- @??????????你为什么认为它们完全一样?谁说当你把一个int打印成一个char的时候,你把低8位丢掉了?你知道,有很多方法可以处理8位以上的字符。
- 但是我们正在讨论的是大小为1字节的ASCII字符,也就是最小有效字节将首先被存储的小endian机器,然后对字符的转换将取第一个字节,在您的情况下,也在我的0x44和0x43中,请解释一下您自己,我可能会说些废话。
- @??????????但我不是在说ASCII字符。我说的是C字符,它可能超过8位,可能使用ASCII以外的字符集,比如Unicode。我能想象printf("%c", 0x01B5)打印?(然后是EDOCX1的问题(8),但那是一个思想上的问题。)
- @??????????:是否有系统按照您描述的方式工作?当然。你应该依赖它吗?不,不能保证明天还是那样,或者在其他稍微不同的情况下。
- @如果我们谈论的字符大小大于1字节(例如Unicode),那么我同意,0x44444444不等于0x00000044。"char c='ab';"btw"这是什么意思?
- @在这里的注释之后,我明白了每个系统都是不同的,并且每个编译器都可以按照他想要的方式实现东西——所以我当然不能依赖于这个。我只是想知道,如果所有的环境都按我所认为的方式设置——int是4字节,long long int是8字节,编译器传递参数的方式只是堆栈——我的假设是否正确(显然也是ASCII字符)
- @??????????如果它能让你安心,我会这样说:"是的,你是对的。它将完全按照您的建议工作,假设您使用的机器与您所想到的机器一样,完全按照您的建议工作。"(但正如您现在发现的,有真正的机器工作方式非常不同。)
- 至于char c = 'ab',请看这个问题。
- @史蒂文萨米特,我可以写一篇完全的废话,你的答案仍然有效。谢谢你的回答。
有太多的notestypes
c将11个整数类型signed char、char……,unsigned long long指定为不同的类型。除了char必须与signed char或unsigned 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存在各种条件/限制。