一直大概清楚c语言里可变参数的原理(参考C标准库参考指南(上)),所以我在使用printf的时候,都会让格式说明和参数类型严格对应,比如printf("%llu %d", 8llu, 5);。这当然没问题,但有时候发现没对应的时候,不理解printf的实际输出:
unsigned long long m = 17179869187; //0x400000003 printf("%d %d", m, 9); //3 9 我本以为是3 4 printf("%llu %d", 8, 5, 6); //8 5 我本以为是0x500000008 6
难道printf会根据格式说明字符调整参数类型,printf("%d %d", m, 9);实际被编译器转换为了printf("%d %d", (int)m, (int)9);?
于是我就想看看printf到底是如何实现的,但没找到,于是先看看va_start等宏是怎么实现的,这倒是找到了(MinGWx86_64-w64-mingw32includevadefs.h),看完反而理解了printf的输出结果了。以下代码经过修改:
#if defined(__GNUC__) typedef __gnuc_va_list va_list; #elif defined(_MSC_VER) typedef char * va_list; #elif !defined(__WIDL__) #error VARARGS not implemented for this compiler #endif #ifdef __cplusplus #define _ADDRESSOF(v) (&reinterpret_cast(v)) #else #define _ADDRESSOF(v) (&(v)) #endif #if defined (__GNUC__) #define va_start(v,l) __builtin_va_start(v,l) #define va_arg(v,l) __builtin_va_arg(v,l) #define va_end(v) __builtin_va_end(v) #elif defined(_MSC_VER) #if defined(_M_IX86) #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) #define va_start(v,l) ((v) = (va_list)_ADDRESSOF(l) + _INTSIZEOF(l)) #define va_arg(v,l) (*(l*)(((v) += _INTSIZEOF(l)) - _INTSIZEOF(l))) #define va_end(v) ((v) = (va_list)0) #elif defined(_M_AMD64) #define _PTRSIZEOF(n) ((sizeof(n) + sizeof(void*) - 1) & ~(sizeof(void*) - 1)) #define _ISSTRUCT(t) ((sizeof(t) > sizeof(void*)) || (sizeof(t) & (sizeof(t) - 1)) != 0) #define va_start(v,l) ((v) = (va_list)_ADDRESSOF(l) + _PTRSIZEOF(l)) #define va_arg(v,t) _ISSTRUCT(t) ? (**(t**)(((v) += sizeof(void*)) - sizeof(void*))) : ( *(t *)(((v) += sizeof(void*)) - sizeof(void*))) #define va_end(v) ((v) = (va_list)0) #else #error VARARGS not implemented for this TARGET #endif #endif
没太理解c++版的_ADDRESSOF为何这么实现,好像和c版本的结果一样吧,这不是重点,不管了。
gnuc直接调用的内部实现,看微软的实现应该也一样的。
x86下_INTSIZEOF的作用是向sizeof(int)也就是4字节对齐,也就是说传入一个char,它也是占4个字节的,下一个参数的地址要加4。
x64下_PTRSIZEOF的作用类似,向指针的大小,也就是8个字节对齐。
因为现在一般都是64位机器了,就能理解原因了:
unsigned long long m = 17179869187; //0x400000003 // m和9在参数传递时都占8字节, 它们在栈上的数据应该是:3 4 9 0, // 第2个参数在强制转换成int时转成了3, 第3个参数被解释成了9 printf("%d %d", m, 9); // 它们在栈上的数据应该是:8 0 5 0 6 0 printf("%llu %d", 8, 5, 6);
验证如下:
void show(const char *s, ...) { int *p = (int*)((long long)&s + 8); //将p指向第2个参数的地址 printf("%d %d %d %dn", *p, *(p+1), *(p+2), *(p+3)); //3 4 9 0 } int main() { unsigned long long m = 17179869187; //0x400000003 show("", m, 9); return 0; }
另外用-m32选项编译成32位程序进行测试也符合预期:
printf("sizeof(int)=%d, sizeof(void*)=%dn", sizeof(int), sizeof(long long)); // 4 8 printf("%d %d %lld %dn", '1', '2', 3, 4, 5); //49 50 17179869187 5
突然想起上面参考里的一段话:
这个机制基于如下(但不仅限于)假设:
一个可变参数表在内存中占据了一个连续的字符数组;
后继的参数占据着字符数组更高位的后继元素;
2^N字节对齐;
参数表列里有一个指示参数数目的参数或有一个指示参数表列结束的标志。
原来如此。
其实看汇编应该更容易理解,奈何当年就随便学了学,早不记得了。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)