從呼叫printf 到顯示器上看到字串

2021-06-27 21:29:40 字數 3594 閱讀 3177

看如下最簡單的c程式:

int main(int argc, char** argv)

本文就是力圖描述這個程式的執行過程,具體來說,就是從呼叫printf(),到「abc」三個字元顯示到顯示器上,到底是乙個什麼樣的過程。

使用strace跟蹤執行上面的程式,可以發現,最終導致呼叫了 write(1, "abc", 3)。也就是最終的效果就是向終端裝置寫入三個位元組。

現在我們把簡單的abc換成中文試試。

int main(int argc, char** argv)

同樣使用strace跟蹤執行,發現最終呼叫的是: write(1, "\344\270\255\346\226\207", 6)。為便於檢視,換成16進製表示,就是向終端裝置寫入 e4 b8 ad e6 96 87 共6個位元組。進一步說,其實就是「中文」這兩個字的utf-8編碼。之所以是utf-8編碼,是因為這個c原始檔本身就是使用utf-8編碼的。下面我們使用gbk對同樣內容的源**進行編譯執行,再用strace跟蹤,會發現最終呼叫的是: write(1, "\326\320\316\304", 4),換成16進製制,就是向終端寫入 d6 d0 ce c4 共4個位元組,而這正是「中文」兩字的gbk編碼。

由此可見,printf(「字串")輸出最終的結果就是把字串的編碼寫入終端裝置,而如何編碼取決於原始檔的儲存編碼方式。

實際上,

編碼是由編譯器完成的。printf只是把給他的引數當成乙個位元組陣列而已,其本身不了解也不需要字元編碼的概念。

這顯然會導致一些問題,比如utf-8編碼的原始檔編譯生產的執行檔案,只能輸出字串的utf-8編碼,一旦執行在非utf-8的終端上,則無法正確顯示(或者需要使用呼叫iconv工具 進行轉碼)。顯然這種「編譯時就確定字元編碼」的方式非常死板。那麼如何改進呢,為此printf()提供了乙個 %ls 指示符,與之相對應的則是wchar_t型別的字串,也叫做寬字元。每乙個wchar_t型別的字元在記憶體中占用4個位元組,其內容是該字元的unicode編碼(注意不是utf-8),而且其編碼方式時固定的,不會因為原始檔的存放編碼改變而改變。為了與傳統的char進行區別,宣告常量時使用 l"字元」的格式。下面是寬字元版本的原始檔。

int main(int argc, char** argv)

通過%ls,printf就會了解到後面的指標指向的是乙個寬字串,而不是多位元組字串(簡單的位元組陣列),這樣在最終呼叫write寫入終端前,就會把這個寬字串進行相應的字元編碼,然後輸出。通過strace 除錯執行,我們發現最終會呼叫 write(1, "-n", 2),顯然這是不正確的。原因就在於呼叫write前的字元編碼有問題。我們先來看看寬字串「中文」的記憶體。為便於gdb除錯,稍微修改一下源程式:

int main(int argc, char** argv)

用gdb除錯,檢視str指向的記憶體:

(gdb) p str

$2 = l"中文"

(gdb) x /12xb &str

0xbffff684: 0x2d 0x4e 0x00 0x00 0x87 0x65 0x00 0x00

0xbffff68c: 0x00 0x00 0x00 0x00

可見每個字元占用4個位元組,內容為unicode**點,這個常量字串的記憶體結構當然是編譯時就已經確定的。那麼為什麼最後會導致編碼為 "-n"了呢。原因在於printf("%ls")對寬字串編碼時,依據的是程式執行時的locale,而我們在程式中沒有明確設定locale,那麼就會採用預設的 c locale,也就是會把寬字元轉換為對應的ascii碼,這種轉換其實無法進行,所以只是簡單的「不轉換」,這樣當遇到第三個位元組0x00時,就認為字串結束了,而前面的兩個位元組 2d 4e 其實正是 -n 兩個字元的ascii碼。

雖然轉碼不成功,但是至少是在執行時進行了轉碼。下面,我們稍微改動程式,新增設定locale的功能。

int main(int argc, char** argv)

這樣,我們就可以在執行時為程式提供locale的值,繼續使用strace跟蹤, strace ./a.out zh_cn.utf-8,我們發現最終會呼叫 write(1, "\344\270\255\346\226\207", 6),這次把寬字串按照utf-8進行了編碼,然後呼叫write寫入終端了,由於當前終端也是採用的utf-8編碼,所以能正確顯示出「中文」兩字。那麼如果終端編碼為gbk的呢?沒問題,只要執行程式時,提供zh_cn.gbk這個引數就可以了。如下:

./a.out zh_cn.gbk

繼續跟蹤發現最終呼叫 write(1, "\326\320\316\304", 4),可見,寫入的是"中文"的gbk編碼。

做個小結:(1)printf("%s")或printf("")都是把字串當成位元組陣列直接呼叫write寫出,不涉及字元編碼。

(2)printf("%ls")把寬字串根據執行時的locale進行編碼後,呼叫write寫出。

(3)通過setlocale()來設定字元編碼規則。

這裡有四個地方涉及到字元編碼:(1)原始檔(2)字串的記憶體表示(3)printf內部的轉碼(4)終端本身

只要(3)中的轉碼能夠正確進行,並且(3)和(4)採用相同的字元編碼,那麼應用程式部分就做夠了正確顯示字串的準備了,至於最終是否能在顯示器上正確顯示出字串,還要實際作業系統和實際裝置的支援,也就是第二階段。

系統呼叫write(1, 位元組陣列,長度)會最終呼叫終端裝置的_write()函式,該函式再呼叫底層硬體(如顯示卡)驅動控制顯示器顯示。我們先來看看簡單字串abc的情況,因為無論採用任何型別的終端裝置都可以正確顯示它們。

終端按照底層裝置,可以分成三大型別。一是底層輸出裝置就是文字模式的vga顯示卡顯示器;二是底層輸出裝置是圖形模式的vga顯示裝置;三是偽終端裝置,底層輸出裝置是其他gui系統中的視窗。上圖分別針對這三種終端粗略**了顯示過程。對於字元模式vga,顯示卡韌體裡的字元發生器從來都不支援中文;對於圖形模式顯示卡,理論上只要有中文字形位圖那麼就可以顯示,但是實際上終端軟體本身並不支援unicode編碼,也就無法顯示中文,非官方有一些軟體如zhcon一定程度上支援中文,但是遠不完善;對於第三種,幾乎所有的gui視窗都支援中文輸出,是目前最完美的終端型別。

本文在linux平台討論printf(),但是printf()是c標準庫函式,理論上所有平台通用。但是實際上不同平台的printf()還有有不少差異的,特別是在引入wchar_t之後。例如就wchar_t本身來說,gcc編譯時大小為4位元組,而vc++編譯時卻是2位元組大小;又如wprintf()函式,在vc++中不支援%ls而是採用%s來表示寬字串。

字元編碼問題是計算機世界裡的最基礎最重要的乙個主題,在unicode仍未完全統一全世界的軟體之前,各種編碼轉換讓字串處理更加複雜。亂碼問題始終困擾著廣大的程式設計師,深入理解編碼原理與轉換細節是解決亂碼問題的不二選擇,堅持使用unicode是減少各種麻煩的良好習慣。

實現雙屏顯示 從主顯示器分離所有輔助顯示裝置

devmode 結構中您需要設定為零將 dmfields 中呼叫changedisplaysetting 之前下列標誌的 devmode 條目 dm pelswidth dm pelsheight dm bitsperpel dm position dm displayfrequency dm di...

LG顯示器公司從創立以來首次裁員

9 月 30 日訊息,據國外 報道,週六,蘋果 商 lg 顯示器公司 lg display 發言人證實,該公司將通過自願退休的方式裁員,這是該公司成立以來第一次裁員。lg 顯示器公司計畫從 10 月份開始接受該公司生產部門員工自願退休的申請,該部門的員工數量約佔該公司員工總數的 65 該發言人表示,...

多台主機同時接到一台顯示器上

前段時間,為了除錯高畫質,本來想買一台高畫質電視。無奈 偏高,囊中羞澀。選來選去最終選了一款led 的23.5的顯示器,帶兩個高畫質介面,乙個vga介面。當不用除錯高畫質時,可以插到電腦上用 我的電腦還是 以前的顯示卡,不支援高畫質介面 真是一舉兩得。sigma wince接高畫質,除錯很順利。只是...