天天看點

關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆

這兩天将學校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的問題才共同導緻的問題。

【文章索引】

  1. 奇怪的問題
  2. 格式化字元串的問題
  3. MinGW的問題

【一、奇怪的問題】

把出錯的一個送出的代碼精簡,然後就剩下如下的代碼:

#include <cstdio>
using namespace std;
int main()
{
    double n;
    scanf("%lf",&n);
    printf("%.0lf\n",n);
    return 0;
}      

在Linux下結果正常:

關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆

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

關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆

【二、格式化字元串的問題】

接下來将上述程式的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;
}      
關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆

也就是說,對于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編譯):

關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆
關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆
關于printf錯用格式化字元串導緻double和long double輸出錯誤的小随筆

可以發現,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。

【相關連結】

  1. 為什麼printf()用%f輸出double型,而scanf卻用%lf呢?:http://book.51cto.com/art/200901/106880.htm
  2. printf and long double:http://stackoverflow.com/questions/4089174/printf-and-long-double
  3. 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
  4. Long Double:http://msdn.microsoft.com/en-us/library/9cx8xs15.aspx

如果您覺得本文對您有所幫助,不妨點選下方的“推薦”按鈕來支援我!

本文及文章中代碼均基于“署名-非商業性使用-相同方式共享 3.0”,文章歡迎轉載,但請您務必注明文章的作者和出處連結,如有疑問請私信我聯系!

繼續閱讀