知無涯之回車換行的故事
不知各位有沒有過這樣的經歷:
- Linux上創建的文件在Windows上打開時,結果所有內容會擠成一行。而Windows上創建的文件在Linux上打開時,每一行的結尾又多了一個奇怪字符
^M
。 - 在安裝Windows版的git時,安裝向導在某一步會提示你選擇”Configuring the line ending conversions”,里面提到了Windows-style和unix-style的line endings,為什么會有這些呢?
- 調用C語言的API
fopen
時,會有text mode和binary mode,這兩者有什么區別?
其實這一切都和我們常說的回車換行有關,但你有沒有很奇怪,什么是回車?直接用換行不就好了,為什么要分開兩個詞?我們使用的鍵盤上的鍵明明起得是換行的作用,為什么叫回車?千萬別被繞暈了,本文將和大家討論有關回車換行的一段有趣的歷史,隨后將回答這些問題。
目錄
- 歷史
- 打字機
- 分歧出現
- 混亂的狀況
- 統一
- Text Mode VS Binary Mode
- Windows平臺
- Linux和Mac OSX平臺
- 更多資料
- 結尾
歷史
我們通常所說的回車換行其實只相當于一個概念,即一行結束,開始下一行,英文叫做End-of-Line
,簡寫為EOL
。你也可以將這理解為一個邏輯上的換行,但為了與回車換行中的換行區分開來,我們后面還是稱呼它為EOL
。
打字機
回車換行嚴格說起來是兩個獨立的概念,即回車和換行,它們的出現要追溯到計算機出現之前,那時有一種電傳打字機:Teletype Model 33 ASR,如下圖:
在打字機上,有一個部件叫Carriage
,它是打字頭,相當于打字機的光標。每輸入一個字符,Carriage
就前進一格。當輸滿一行后,想要切換到下一行時,需要Carriage
在兩個方向上的運動:水平和豎直。水平方向上需要將Carriage
移到一行的起始位置,豎直方向上需要紙張向上移動一行,此時也就是相當于Carriage
下移了一行。(這在很多影視作品里面可以看到,打字者們打完一行之后,通常會用手撥動一個滑塊,然后聽到“咔”的一聲,接著輸入下一行。只是在這款打字機中不再需要人為的去撥動。)而這兩個動作分別對應著:
- Carriage Return(CR),也即回車,它在ASCII表中的值為0x0D,可以用轉義符
\r
表示 - Line Feed(LF),也即換行,它在ASCII表中的值為0x0A,可以用轉義符
\n
表示
因為打字機是機械的結構,所以雖然從邏輯上只表示為EOF
,但從設計上它需要分為兩個獨立的操作,這也正是我們習慣連起來說回車換行的原因。可以參照下圖看看其鍵盤的布局:
鍵盤的右方有一個Line Feed
和Return
,從名字可以看出,這分別對應著前面提到的兩個操作。然而,通常一個回車操作不能夠在一個字符打印的時間內完成,所以可以利用Carriage
移動的時間,去完成另外一個完全獨立的操作Line Feed
,這也是通常Carriage Return
會被放在Line Feed
前面的原因。你可以想象,如果在在Carriage
和紙移動的過程中按下了其它的字符鍵,打印的內容將變得十分混亂。所以在Carriage Return
和Line Feed
之后,有時會有1~3個NUL字符(即相當于匯編語言中的空指令,僅起占位作用),以等待前兩個操作的完成。所以實際上打字機的EOL
為:EOL = CR + LF + 1~3NUL
。
分歧出現
等到早期的計算機發明時,很自然的這兩個概念被拿了過來。但是由于那時的存儲設備非常昂貴,一些人認為在每行的結尾加兩個字符用于換行,實在是極大的浪費,于是各個廠商在這一點上便出現了分歧。
由于一些早期的微型計算機還沒有用于隱藏底層硬件細節的設備驅動,所以它們直接沿用了打字機的慣例,使用不帶NUL的CRLF
作為一個EOL
。而CP/M為了和這些微型計算機使用同一個終端,也采用了這種設計。所以它的克隆MS-DOS也同樣使用CRLF
,由于Windows又是基于MS-DOS,為保持兼容性,所以就導致了如今的Windows是采用CRLF
作為EOL
,即\r\n
(或0x0D 0x0A
)。
而Multics在被設計之時就非常認真的考慮了這一問題,設計者們覺得只需一個字符便完全足夠來表示EOL
,這樣更加合理。那么選擇CR
還是LF
呢?本來由于那時的鍵盤上都有一個Return
鍵,所以可能更好的選擇是CR
。但當時考慮到CR
可以用來重寫一行,以完成如粗體和
刪除線
等效果,所以他們選擇了稍稍難以理解的LF
。然后自己設計了一個設備驅動程序來將LF
轉換為各種打字機所需要的EOL
,這個方案非常完美,當然除了LF
稍微奇怪一些。隨后一脈相承的Unix和Linux們都繼承了這個選擇,于是你在這些操作系統上可以發現每一行的結尾是一個LF
,即\n
(或0x0A
)。
Mac系統的選擇就更加復雜一些。Apple在設計Mac OS時,他們采用了一個最容易理解的選擇:CR
,即\r
(或0x0D
)。但這只維持到Mac OS 9,后一個版本的Mac OSX基于Mach-BSD內核,所以此后版本的Mac OSX在每行的結尾存儲了與Linux一樣的LF
,即\n
(或0x0A
)。
混亂的狀況
還有很多其它的操作系統采用更加不同的方案,這也導致了混亂的產生,文章開始提出的幾個問題便由該混亂引起。因為Linux和Mac OSX上使用的是LF
,而Windows上使用的是CRLF
,那么Linux和Mac OSX上創建的文件在Windows上打開時,由于每一行的結尾只有一個LF
,但Windows只認識CRLF
,所以便不會有邏輯上的換行處理,故所有的文字被擠到了一行。反過來,如果Windows上的文件在Linux和Mac OSX上打開時,僅需LF
便可換行,那么每一行的結尾便多了一個CR
,對應的ASCII碼為^M
。
而git的安裝向導會特意有一個這樣的提醒頁面也出于此,因為一個項目可能有多個開發者,每個開發者可能使用的是不同的系統,那么開發者checkout代碼時,如果不做換行符的轉換,有可能就會出現只有一行或者行尾多了^M
的情況。當然,如果你有一個可以識別多種EOL
的現代文本編輯器,那么不做轉換也無妨(notepad不行)。
如果出現了上面的轉換問題時,也別著急,可以對文件進行轉換。那在我們寫程序時如何正確的處理這些問題?像隱藏硬件細節的驅動程序一樣,我們可寄希望于高級語言。
統一
為了避免在這些不同的實現中掙扎,高級語言給我們帶來了福音,它們各自使用了統一的方式來處理EOL
。在C語言中,你一定知道在字符串中如果要增加一個換行符的話,直接用\n
即可,比如:
1 printf("This is the first line! \nThis is a new line!");
上面的輸出將是:
This is the first line!
This is a new line!
為什么C語言選擇了\n
而不是\r
?這絕非偶然。熟悉C語言歷史的朋友可能知道當初C語言是Dennis Ritchie為開發Unix而設計,所以它沿用了Unix上EOL
的慣例便很容易理解了。而我們知道Unix使用的LF
的ASCII碼為0x0A
,轉義符為\n
,因此C語言中也使用\n
作為換行。
Text Mode VS Binary Mode
但是,千萬別簡單的認為上面的\n
最終寫到文件中就一定是其ASCII碼0x0A
,或者文件中的0x0A
被讀到內存中就是其轉義符\n
。這取決于你打開文件的方式。在C語言中,在對文件進行讀取操作之前,都需要先打開文件,可以使用下面的函數:
1 #inlcude 2 FILE *fopen(const char *path, const char *mode);
注意看第二個參數mode
,它是一個字符指針,通常可以為讀(r),寫(w),追加(a)或者讀寫(r+, w+, a+),僅指定這些參數時,文件將被當成是文本文件來操作,即Text Mode
,而如果在這些參數之外再指定一個額外的b
時,文件便會被當成是二進制文件,即Binary Mode
。這兩種模式的區別在哪里呢?這里稍稍有些復雜,因為它們在不同的平臺上表現不同。
Windows平臺
對于Windows平臺,因為其使用CRLF
來表示EOL
,故對于Text Mode
需要做一定的轉換才能夠與C語言保持一致。接下來的兩個圖可以給出最為直觀的描述。
先看二者對于讀操作的區別:
Text Mode
下,C語言會嘗試去“理解”這些回車與換行,它會知道LF
和CRLF
都可能是EOL
,所以不管文件中是LF
還是CRLF
,被讀進內存時都會變成LF
。而Binary Mode
下,C語言不會做任何的“理解”,所以這些字符在文件中什么樣,讀到內存中依然那樣。
接下來是寫操作的區別:
Text Mode
下,內存中的每一個LF
寫入文件中時都會變為CRLF
,當然,如果不幸內存中為CRLF
,以此種模式寫入到文件中時就會變成CRCRLF
(注意:這里不是CRLF
。原因我想大概是如果你認為內存中的數據是文本,那么它一定是以LF
作為EOL
,CR
也一定是你有意而為之,是個有意義的字符,所以它并不會處理。)。而Binary Mode
下,內存中的內容會被原封不動的寫到文件中。
所以為了保證一致性,一定需要注意配套使用讀和寫,即讀和寫采用同一種模式打開文件。
Linux和Mac OSX平臺
因為Linux和Mac OSX平臺與C語言對待EOL
的方式完全一致,所以Text Mode
和Binary Mode
在這些平臺下沒有任何區別,可以參考fopen
的man page。實際上,所有遵循POSIX的平臺都忽略了b
這個參數。
雖說在這些平臺上處理EOL
非常簡單,但是如果你的程序需要移植到其它非POSIX平臺上時,請務必正確對待b
參數。
更多資料
如果還有興趣,可以看看下面這些有趣的資料:
- 阮一峰的《回車與換行》
- The End of Line Puzzle,也即上面那篇文章的出處
- 關于What is the ASCII code for newline character?的一個回答
- 維基百科上關于Newline的解釋
- 從網絡的角度講述了End-of-Line的故事
- 打字機的一段視頻,需梯子
結尾
這樣一個小小的EOL
便如此復雜,給人們帶來了極大的困擾,但就如我在知無涯之C++ typename的起源與用法最 后討論過的一樣,這個決定是經歷過無數決斷、波折與妥協才有了現在的結果。你可以選擇保守,為向后兼容而作出妥協,那么你得面對不斷累加的“不完美”,甚 至“丑陋”的設計;你也可大膽嘗試,破舊立新,犧牲向后兼容換取進步,那你也許得忍受人們的“唾罵”,或許還需承擔被人們拋棄的風險。如何在這之間作出選 擇,沒有明確的答案,恐怕一切就只有靠自己去判斷了吧。
(全文完)
feihu
2014.12.17 于 Shenzhen
來自:http://feihu.me/blog/2014/end-of-line/