關於printf錯用格式化字符串致使double和long double輸出錯誤的小隨筆

【題外話】服務器

之前用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的問題才共同致使的問題。app

 

【文章索引】ide

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

 

【1、奇怪的問題】網站

把出錯的一個提交的代碼精簡,而後就剩下以下的代碼:ui

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

在Linux下結果正常:this

結果在Windows下會出現以下圖的結果:spa

 

【2、格式化字符串的問題3d

接下來將上述程序的printf替換爲cout,發現沒有任何問題,判斷是printf那行出現了問題。code

查找相關資料(如相關連接1)發現,不論輸出float仍是double都應該使用printf("%f"),由於不論float仍是double都會做爲double類型輸出,確實之前沒有注意到這個問題。因此在第一節給出的那個程序將「%lf」改成「%f」就正確了。但相關連接1中並無說明%lf指的是什麼。component

不過若是嘗試在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,因此一切都同樣了吧)。

 

【3、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。

 

【相關連接】

  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
相關文章
相關標籤/搜索