這兩天将學校Online Judge中以前在Linux下評測的送出全部在Windows上重測一遍,結果莫名其妙發現很多以前通過的題目現在出現了結果錯誤的問題,其共同結果都是結果為0,檢視源代碼發現其都是使用printf("%ld")輸出的double。原本以為是GCC的Bug,後來查找資料才發現實際上是對C語言了解不夠充分加上MinGW的問題才共同導緻的問題。
【題外話】
以前用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的問題才共同導緻的問題。
【文章索引】
- 奇怪的問題
- 格式化字元串的問題
- 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,是以一切都一樣了吧)。
【三、MinGW的問題】
雖然上一節找出了第一節程式的問題,可是為什麼會出現這樣的問題呢。
繼續查找發現了相關連結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
如果您覺得本文對您有所幫助,不妨點選下方的“推薦”按鈕來支援我! 本文及文章中代碼均基于“署名-非商業性使用-相同方式共享 3.0”,文章歡迎轉載,但請您務必注明文章的作者和出處連結,如有疑問請私信我聯系! |