【題外話】
以前用HUSTOJ給學校搭建Online Judge,所有的評測都是在Linux下進行的。后來為了好往學校服務器上部署,所以大家重新做了一套Online Judge,Web和Judge都是基於Windows和.NET平台的。這兩天將學校Online Judge中以前在Linux下(GCC 4.6.3)評測的提交全部在Windows上(GCC 4.7.2 MinGW)重測一遍,結果莫名其妙發現很多以前通過的題目現在出現了結果錯誤的問題,其共同結果都是結果為0,查看源代碼發現其都是使用printf("%ld")輸出的double。原本以為是GCC的Bug,后來查找資料才發現實際上是對C語言了解不夠充分加上MinGW的問題才共同導致的問題。
【文章索引】
把出錯的一個提交的代碼精簡,然后就剩下如下的代碼:
#include <cstdio> using namespace std; int main() { double n; scanf("%lf",&n); printf("%.0lf\n",n); return 0; }
在Linux下結果正常:

結果在Windows下會出現如下圖的結果:

接下來將上述程序的printf替換為cout,發現沒有任何問題,判斷是printf那行出現了問題。
查找相關資料(如相關鏈接1)發現,不論輸出float還是double都應該使用printf("%f"),因為不論float還是double都會作為double類型輸出,確實以前沒有注意到這個問題。所以在第一節給出的那個程序將“%lf”改為“%f”就正確了。但相關鏈接1中並沒有說明%lf指的是什么。
不過如果嘗試在GCC上編譯如下的代碼卻會給出如下圖的警告:
#include <cstdio> using namespace std; int main() { long double n = 1.22222222; printf("%f", n); printf("%lf", n); return 0; }

也就是說,對於GCC而言,在printf中使用“%f”和“%lf”實際上都表示的是double類型,而要表示long double,則應該使用“%Lf”(注意大小寫),而使用MSVC編譯編譯時並沒有發生這些問題(也可能是因為MSVC認為double = long double,所以一切都一樣了吧)。
雖然上一節找出了第一節程序的問題,可是為什么會出現這樣的問題呢。
繼續查找發現了相關鏈接2和相關鏈接3,發現在這兩個問題的回答中都提到了MinGW在Windows上運行是需要MSVC的運行時的。以前確實也沒注意到這點,於是去MinGW的官方網站,確實發現了如下兩段:
MinGW provides a complete Open Source programming tool set which is suitable for the development of native MS-Windows applications, and which do not depend on any 3rd-party C-Runtime DLLs. (It does depend on a number of DLLs provided by Microsoft themselves, as components of the operating system; most notable among these is MSVCRT.DLL, the Microsoft C runtime library. Additionally, threaded applications must ship with a freely distributable thread support DLL, provided as part of MinGW itself).
MinGW compilers provide access to the functionality of the Microsoft C runtime and some language-specific runtimes. MinGW, being Minimalist, does not, and never will, attempt to provide a POSIX runtime environment for POSIX application deployment on MS-Windows. If you want POSIX application deployment on this platform, please consider Cygwin instead.
果然,MinGW雖然不需要任何第三方的運行庫,但是需要微軟的運行庫,其中包括了MSVCRT.DLL以及其他的微軟C語言運行庫。所以GCC編譯后的程序還是運行在MSVC運行庫上的程序。同時又由於32位的MSVC並不支持更高精度的double類型(在32位的MSVC中long double與double的精度均為8位,見相關鏈接4),而GCC在32位的long double是12字節,64位更是16字節,所以就出現了不兼容的問題。
所以我們可以做這樣一個實驗,將long double類型存儲的數據按字節輸出、同時按不同方式輸出其結果,代碼如下:
1 #include <cstdio> 2 using namespace std; 3 4 void print_bytes(const char* name, long double &n) 5 { 6 char* p = (char*)&n; 7 8 printf("%s [%ld-%ld]\n", name, p, p + sizeof(long double)); 9 10 for (int i = 0; i < sizeof(long double); i++) 11 { 12 printf("0x%02X ", (*p & 0xFF)); 13 p++; 14 } 15 16 printf("\n"); 17 } 18 19 int main() 20 { 21 long double n1 = 0; 22 long double n2 = 0; 23 24 print_bytes("inited_n1", n1); 25 print_bytes("inited_n2", n2); 26 printf("\n"); 27 28 scanf("%lf", &n1); 29 scanf("%Lf", &n2); 30 31 print_bytes("inputed_n1", n1); 32 print_bytes("inputed_n2", n2); 33 printf("\n"); 34 35 printf("type \t\t n1 \t\t\t\t n2\n"); 36 printf("%%f \t\t "); 37 printf("%f \t\t\t ", n1); 38 printf("%f\n", n2); 39 40 printf("%%lf \t\t "); 41 printf("%lf \t\t\t ", n1); 42 printf("%lf\n", n2); 43 44 printf("%%Lf \t\t "); 45 printf("%Lf \t\t\t ", n1); 46 printf("%Lf\n", n2); 47 48 return 0; 49 }
分別將這個代碼在32位機器上用MSVC和GCC MinGW編譯,以及在32位Linux下用GCC編譯,可以得到如下的結果(從上到下分別為32位Windows下用MSVC編譯、32位Windows下用GCC MinGW編譯、32位Linux下用GCC編譯):


可以發現,MSVC下long double為8字節,而GCC編譯后的程序不論在Linux下還是在Windows下都為12字節。不過仔細看可以發現,雖然GCC生成的程序占用了12字節,但其只用到了前10字節(后2字節不論怎樣賦值其內容都不會發生改變),也就是說GCC的long double實際上是10字節(80bit)的。
除此之外,還可以發現,當使用scanf("%lf")時,不論變量是什么類型的,都是按8字節存儲的(即按double類型存儲的),而使用scanf("%Lf"),則是按10字節存儲的(即按long double類型存儲的)。由於在MSVC下double = long double,所以不論怎么混用,結果都是正確的。而在Linux下,我們發現,當存儲的long double為真正的long double時(使用scanf("%Lf")),只能使用%Lf輸出結果,而long double內存儲的內容為double時,只能使用輸出double的格式化字符串輸出。
所以猜想在GCC MinGW下,可能就像在Linux下存儲的double而強制輸出long double那樣會輸出為0一樣,存儲的內容為double,而MSVC將其認定為long double輸出,所以最終結果為0。
【相關鏈接】
- 為什么printf()用%f輸出double型,而scanf卻用%lf呢?:http://book.51cto.com/art/200901/106880.htm
- printf and long double:http://stackoverflow.com/questions/4089174/printf-and-long-double
- gcc: printf and long double leads to wrong output:http://stackoverflow.com/questions/7134547/gcc-printf-and-long-double-leads-to-wrong-output-c-type-conversion-messes-u
- Long Double:http://msdn.microsoft.com/en-us/library/9cx8xs15.aspx
