知無涯之回車換行的故事

jopen 9年前發布 | 6K 次閱讀 換行


知無涯之回車換行的故事

不知各位有沒有過這樣的經歷:

  • 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 FeedReturn,從名字可以看出,這分別對應著前面提到的兩個操作。然而,通常一個回車操作不能夠在一個字符打印的時間內完成,所以可以利用Carriage移動的時間,去完成另外一個完全獨立的操作Line Feed,這也是通常Carriage Return會被放在Line Feed前面的原因。你可以想象,如果在在Carriage和紙移動的過程中按下了其它的字符鍵,打印的內容將變得十分混亂。所以在Carriage ReturnLine 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語言會嘗試去“理解”這些回車與換行,它會知道LFCRLF都可能是EOL,所以不管文件中是LF還是CRLF,被讀進內存時都會變成LF。而Binary Mode下,C語言不會做任何的“理解”,所以這些字符在文件中什么樣,讀到內存中依然那樣。

接下來是寫操作的區別:

知無涯之回車換行的故事

Text Mode下,內存中的每一個LF寫入文件中時都會變為CRLF,當然,如果不幸內存中為CRLF,以此種模式寫入到文件中時就會變成CRCRLF注意:這里不是CRLF。原因我想大概是如果你認為內存中的數據是文本,那么它一定是以LF作為EOLCR也一定是你有意而為之,是個有意義的字符,所以它并不會處理。)。而Binary Mode下,內存中的內容會被原封不動的寫到文件中。

所以為了保證一致性,一定需要注意配套使用讀和寫,即讀和寫采用同一種模式打開文件

Linux和Mac OSX平臺

因為Linux和Mac OSX平臺與C語言對待EOL的方式完全一致,所以Text ModeBinary Mode在這些平臺下沒有任何區別,可以參考fopenman page。實際上,所有遵循POSIX的平臺都忽略了b這個參數。

雖說在這些平臺上處理EOL非常簡單,但是如果你的程序需要移植到其它非POSIX平臺上時,請務必正確對待b參數。

更多資料

如果還有興趣,可以看看下面這些有趣的資料:

結尾

這樣一個小小的EOL便如此復雜,給人們帶來了極大的困擾,但就如我在知無涯之C++ typename的起源與用法最 后討論過的一樣,這個決定是經歷過無數決斷、波折與妥協才有了現在的結果。你可以選擇保守,為向后兼容而作出妥協,那么你得面對不斷累加的“不完美”,甚 至“丑陋”的設計;你也可大膽嘗試,破舊立新,犧牲向后兼容換取進步,那你也許得忍受人們的“唾罵”,或許還需承擔被人們拋棄的風險。如何在這之間作出選 擇,沒有明確的答案,恐怕一切就只有靠自己去判斷了吧。

(全文完)

feihu

2014.12.17 于 Shenzhen

來自:http://feihu.me/blog/2014/end-of-line/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!