汉字在屏幕上的显示过程以及乱码的原因
一、計算機中的顯示原理
要想在計算機的顯示器上顯示文字,首先你得寫一個程序,這個程序的任務就是就是把文字的顯示信息發給顯卡,顯示信息包括在這個屏幕上的輸出位置、字的大小等等。然后顯卡就知道怎么顯示這個字符了。
屏幕上是如何顯示文字的原理是什么呢?
屏幕上其實有很多個小燈,小到肉眼看不見,當他們不亮時,屏幕就是黑色的,當他們亮了一部分,如果那一部分剛好是個文字的形狀,那么屏幕上就顯示文字了。這個原理就跟軍訓時人擺文字顯示字符一樣。如下圖,通過led燈的開和關顯示出了123。放到顯示器上,小燈會變得特變小,肉眼很難看到,當一部分紅色的小燈亮了,那一部分剛好擺成123的形狀,那么紅色的123這三個字符就在屏幕上顯示出來了。
如何讓顯示器得知道是那個燈亮那個燈滅,這就是顯卡的作用了,操作系統會根據文字的編碼,去字庫中找到要顯示的字符的點陣數據,點陣數據指明了哪個燈應該亮起,亮起的顏色是什么顏色。顯卡會結合點陣數據和其他顯示信息,進行計算(比如按照一定比例擴大等),然后發給顯示器控制顯示器的顯示!
注意:這一部分的具體細節我不確定,但是大概的思路應該是沒錯的。
通過以上得知,關鍵點在于文字的編碼,只要知道了文字的編碼,就能找到字庫的點陣數據。眾所周知,文字的編碼有很多種,而亂碼的根本原因是文件保存時采用的編碼和打開文件時用于解碼的編碼不一致,從而找到了錯誤的點陣數據,顯示了錯誤的輸出!。
二、從鍵盤輸入開始理解編碼的存在形式
以window系統為例,假設你剛剛打開了記事本。
1.你在鍵盤上按下了’a‘。
2.你的按下觸發了電路,鍵盤掃描到你a被按下,于是鍵盤形成了’a’的掃描碼,發送到了存在于鍵盤上的寄存器,同時給CPU發送了一個中斷信號,告訴CPU我這有活動了!
3.CPU根據鍵盤的中斷線路號檢測到是鍵盤發出的中斷信號,于是根據中斷號計算出鍵盤的中斷處理程序在內存中的地址,轉到鍵盤的中斷處理程序去執行。
4.鍵盤的中斷處理程序找到鍵盤的驅動程序代碼,轉到鍵盤的驅動程序執行。
5.鍵盤的驅動程序去讀取鍵盤上的保存掃描碼的寄存器,把‘a’的掃描碼讀到內存中。
6.驅動程序把掃描碼轉換成虛擬碼。為什么要轉換呢,因為不同的鍵盤由于廠家不同,型號不同、設計不同的原因,‘a’這個按鍵產生的掃描碼在不同的鍵盤上是不一樣的,為了統一管理,驅動程序得把不同鍵盤按下的‘a’轉換成統一的表示。比如把不同鍵盤按下的‘a’產生的掃描碼統一轉換成一個字節的0x41。驅動程序要進行轉換,那么驅動程序得知道這是哪種類型的鍵盤,不然沒有轉換的依據,原理是鍵盤的相關信息比如生產廠家、鍵盤型號等會保存在鍵盤上的一些只讀寄存器中,計算機通過這些只讀寄存器就知道這是哪種鍵盤。從而就知道該鍵盤的掃描碼對應的虛擬碼。
7.驅動程序把0x41交給操作系統上自帶的且在后臺默默運行的的IMM進程。
8.IMM進程把0x41交給系統當前使用的的輸入法編輯器。比如搜狗輸入法或者百度輸入法。系統上所有的輸入法,都由IMM管理。
9.輸入法收到了0x41,對0x41進行處理,霹靂巴拉一頓操作,首先查到0x41這個值對應的可能的文字,比如可能是‘啊’、‘阿’、‘吖‘…等。首先查詢系統當前的代碼頁是哪一個,也就是系統默認編碼,若你沒有修改系統的默認編碼,則查找的結果為GBK(相當于GB2312)編碼的。于是通過GBK的代碼頁這些可能的文字的GBK編碼找出來,通過操作系統從字庫中尋找這些可能文字的字庫數據,交給顯卡,顯卡把他們顯示出來。
10.顯卡把他們顯示出來后,屏幕上顯示了好多個文字讓你選擇,那么你通過鍵盤的左移右移回車等操作選中了一個字,這個鍵盤操作又產生掃描碼,最后還是輸入法接收到了你的鍵盤按鍵輸入情況,然后,輸入法就可以根據你的鍵盤輸入情況確定你選中了哪個文字,假設你選中了’啊’,于是輸入法把‘啊’這個字的GBK碼交給操作系統,操作系統把這個GBK碼放進指定內存中,這指定內存被稱為輸入緩沖區。
11.記事本可以掃描緩沖區有沒有內容,當檢測到了緩沖區有了內容后,記事本至少需要兩個操作,一是把‘啊’這個文字顯示到記事本的窗口里面,二是把"啊"的編碼放到自己的內存空間。
12.記事本接收到的是GBK編碼,這個編碼保存在了記事本的內存空間,并把“啊”輸出到了記事本的窗口中。這時你的輸入操作已經結束了(如果你不再進行輸入),接下來就是保存這個記事本的內容了,如果想要保存這個“啊”,你的應用程序就得向操作系統申請一個文件,把“啊”的編碼寫進文件中。如果你不作任何操作,硬盤上就會默認保存的是‘啊’的GBK編碼,如果你想保存的是’啊’的其他編碼,那也可以,轉換一下編碼格式,然后放進文件中保存。放進文件中保存,那c語言來說,有二進制方式的寫和文本文件方式的寫,該用什么方式呢?首先說明什么是文本文件,文本文件就是保存文本的文件,里邊都是一些字符(也就是文本)的編碼,解析出來后都是文字,你用utf-8格式保存的,里邊就是utf-8格式的字符的編碼,你用GBK格式保存的,里邊就是GBK的編碼,總之里邊保存的是文字信息。另一個就是二進制文件,一般來說我們編程很少用到。二進制文件里邊保存到不是文本,比如視頻文件、圖片文件、3D模型等。其實二進制文件和文本文件在文件中的保存形式都是0和1的二進制流,既然都是0和1的二進制流,為什么要區分他們呢,因為他們有點區別,比如文本文件以EOF(值為-1)作為文件結束標志,因為不管是什么編碼,都沒有哪個字符的編碼值是-1。而二進制流就不一樣了,里邊完全有可能有一段字節代表著-1,因此不能以-1作為文件的結束標志,一般來說二進制文件應該是通過比較文件長度來判斷結束標志的。在c語言中,我們通常是使用fwrite()和fread()函數來讀寫文件,那么我們并沒有指明以什么編碼方式來讀出或寫進文件啊,別忘了,兩個函數會是系統調用相關的,而系統默認的編碼格式就是GBK,因此這兩個函數都是按GBK來進行讀或取的。如果你想使用其他編碼比如UNICODE,就得使用其他讀寫的函數了,比如fgetwc()、fwscanf();這些函數會把GBK編碼轉換成UNICODE編碼再進行讀和寫。
13.記事本默認保存的編碼是ANSI,ANSI也叫多字節字符集,ANSI其實不是一種編碼方式,是所有使用不定長字節來表示字符的編碼格式的統稱,在簡體中文Windows操作系統上,ANSI指的是GBK編碼,在繁體中文Windows操作系統中,ANSI編碼代表Big5;在日文Windows操作系統中,ANSI 編碼代表 JIS 編碼。當然你用可以更改記事本的保存格式。Unicode同理,Unicode是一個字符集,不是一個編碼方式,在windows這邊,Unicode指的是UTF-16,在其他環境下,可能指的是UTF-8或UTF-32,比如linux上指的是utf-8.
|
|
|
|
|
三.縷一縷編程的過程
1.首先,現在我們簡化一下VS2013這個軟件,把VS2013看成是記事本(編輯器)+編譯器的結合體。它只有編寫文本進行保存和對文本文件進行編譯的功能。如果你使用的是中文操作系統的Windowsd的VS2013編寫源代碼,在你編寫完成后,運行之前或者按下CTRL+S,那么你的源代碼就會保存起來。跟記事本的保存一樣,那么它默認應該是使用GBK編碼格式來保存你的代碼源文件,那我不想按GBK來保存怎么辦呢?可以在文件->高級配置選項里修改源代碼的保存格式。
2.假設你的源代碼里有一個字符"你好",在你把你的源代碼保存了之后,硬盤上你的源代碼文件中存在著"你好,世界!"的GBK編碼:C4E3(你) BAC3(好) 。
3.你的打印文件里面有打印"你好"這個中文字符的語句,你想在屏幕上顯示"你好"
4.你點擊了運行按鈕,首先,編譯器的做的工作就是啟動它的編譯器對你的源代碼進行編譯,要進行編譯,首先得解析源代碼文件,要解析一個文件,得先知道它是什么編碼,否則解析要出錯啊,那么編譯器是按什么編碼格式來解析你的源文件呢,不用想就知道,那肯定是GBK編碼嘛,畢竟編輯器的默認編碼就是GBK的。那我要是把編輯器的編碼格式換了怎么辦呢,沒事,編譯器改成一樣的不就完事了。修改的方式為項目 -> 屬性 -> 配置屬性 -> c/c++ -> 命令行 -> 其他選項。在其他選項里輸入你所需要的編碼,比如utf-8。
一般這一行是空的,我們只需把“從父級或項目默認設置繼承”選上就好了。這樣它就會根據你項目的編碼格式主動更改相同的編碼進行解析。
4.等等,你好像記得有個地方也能修改項目的編碼屬性?就在項目 -> 屬性 -> 配置屬性 -> 常規 ->字符集那,有個使用多字節字符集和使用Unicode字符集?它默認也不是GBK啊,它默認是Unicode呢。這又是什么玩意?首先,在這里,多字節字符集=ANSI=GBK,Unicode=utf-16。這玩意是這樣的,它不是設置你寫的代碼的編碼格式,但是他能控制你使用的API的版本。什么意思呢?你寫的程序肯定有#include<“xxx”>的代碼,#號說明這是一個預編譯命令,c語言里可沒有#這個操作符。預編譯命令是給編譯器看的,編譯器檢測到了預編譯命令后,在鏈接的時候就會把#include<“xxx”>刪掉,把#include<“xxx”>原本的代碼復制過來放到這個地方。預編譯指令還有一個較為常見的就是#ifndef。完整意思就是if not define,字面上來理解就是如果沒有定義。而在vs上編程你經常#incluide<“xxx”>里邊的代碼里經常有和以下類似的代碼:
ifndef Unicode
typedef MessageBox MessageBoxA
#endif
typedef MessageBox MessageBoxW
翻譯如下:
如果沒有定義 Unicode
MessageBox 就是MessageBoxA
結束
MessageBox 就是 MessageBoxW
也就是說,默認情況下(也就是字符集是Unicode),你在代碼里使用MessageBox函數編譯器就把MessageBox替換為MessageBoxW,如果你在字符集里把"使用Unicode字符集"改成"使用多字節字符集",那么MessageBox就被編譯器替換為MessageBoxA!實際上,代碼庫里根本沒有MessageBox這個函數!MessageBoxA和MessageBoxW有什么區別呢,MessageBoxA是處理ANSI也就是GBK的,MessageBoxW是處理Unicode的,這兩個函數處理邏輯也是不一樣的,處理GBK的編碼,得先判斷內存中保存的數據是一個字節的還是兩個字節的,若是一個字節的(英文),就一次從內存中讀取一個字節,若是兩個字節的,就一次從內存中讀取兩個字節,兩個字節的最高位第八位一定是1,所以比較容易判斷。處理Unicode的比較簡單,主要是每次讀取的字節數數跟GBK的不一樣,且如果要在屏幕上顯示的話,還得經過轉碼,轉成GBK的。
5.在上邊我們提到,字符"你好",在硬盤中的源代碼文件中的存在形式是GBK編碼:C4E3(你) BAC3(好) ,那么想要正確的從內存中處理C4E3 BAC3(程序在運行時操作系統會把它從硬盤加載到內存),即把它按照一次讀兩個字節且顯示在屏幕上,你只能使用MessageBoxA這個函數,你可以直接使用MessageBoxA,也可以寫MessageBox,但是如果你寫MessageBox,你就得把字符集改成使用多字節字符集。如果你使用MessageBoxW對這兩個字符進行處理,那么MessageBoxW會認為C4E3 BAC3是Unicode字符,而當前系統的默認字符集是GBK,它會把C4E3 BAC3轉換成對應的GBK字符,然后通過操作系統進行輸出,本來C4E3 BAC3就是正確的了,再經過轉換,不亂碼才怪呢。要想使用MessageBoxW輸出正確的文字,那么你在源代碼里就應該這樣定義字符串:TEST(“你好”),或者:L"你好“,這樣編譯器在處理這個字符串時,就不會按照GBK的編碼把”你好“兩個字按照C4E3 BAC3放進編譯好的可執行文件了,而是會轉換成Unicode的,這樣MessageBoxW就能正確處理了。但是要明白,即使你加了TEXT,中文的”你好"的存在形式是這樣的:
源代碼(GBK)–>可執行文件(Unicode)
即在保存成源代碼的時候,還是默認按照GBK來保存,編輯器可不管你什么TEXT不TEXT的,因此編譯器還是按照GBK來讀,但是編譯器看到了帶TEXT的字符串,它就會把TEXT中的字符串從GBK編碼轉換成Unicode編碼的了,編譯器進行處理也就是編譯后形成的可執行文件里面的“你好”數據就是Unicode編碼的。
總之,得明白了在中文字符在源文件中的編碼和在可執行文件中的編碼,以及源文件是誰按什么編碼保存的,誰按什么編碼解析讀取的。
5.如何找到亂碼源頭。
一是檢查保存格式,二是檢查編譯格式,三檢查系統編碼格式,這三個一定要整齊畫一。四是檢查函數有沒有用對,比如在mfc編程中帶W的和帶A的。
以上前三步默認情況都是不會錯的,除非被更改過。
還有一種字符叫寬字符,比如wchar,wspring等。定義寬字符時效果也同加了TEXT,編譯軟件會改成Unicode的。或者說初始化寬字符時字符得加TEXT或L。比如wchar_r ch1 = L‘A’,普通的char數組也可以保存漢字,只是兩個字節表示一個漢字(gbk編碼,其他編碼可能是三個字節),打印輸出時,格式化方式得是兩個%c連在一起,既%c%c,表示從該char數組中去取兩個字符作為一個漢字進行輸出。
這在其他語言上也是行得通的,比如Python解釋器默認是按utf-8來對源代碼進行解碼,當你使用記事本寫python代碼,且文件按ansi格式保存,執行的時候那就是亂碼了。
注意,大部分的編碼格式都兼容ascll,所以即便是編碼不匹配,英文還是能正常被解析。
6.關于燙屯錕斤拷,諾諾諾
6.1 燙
”燙“出現的原因是你試圖輸出一個未經初始化的字符數組?;蛘哒f棧內存未經初始化。
從上圖我們可以看到,text字符數組我們只初始化了前兩個字節,后面的字節是未經初始化的,那么按照慣例,未經初始化的變量應該默認為NULL才對,也就是不會打印出來,但是vs的編譯器在Debug模式下,會把未初始化的棧內存賦值為CC,而不是我們以為的NULL了,0xCC按照GBK編碼讀出來就是”燙“
printf()函數首先會讀到第一個字節,也就是C4,算出最高位是1,于是明白這不是一個英文字符,于是繼續讀下一個字節,讀到了E3,于是把C4E3當成一個字符來輸出,也就是“你”。繼續讀下一個字節,讀到的是CC,最高位還是1,那就再讀一位,兩個字節CCCC當成一個字節就是“燙”,然后繼續…,后面5個字符怎么亂套了我也不是很清楚。
6.2 屯
“屯”出現的原因是堆未經過初始化,你卻想把堆中的內容打印出來。
我們申請內存都是在堆區開辟的,內存分為幾塊區域,代碼區,放代碼,順序執行,數據區,放你定義的數據。棧區,當使用棧的時候在這塊內存區域生成一個棧,函數調用會把數據壓到棧,定義一個棧也很簡單,首先在內存中找一個起始地址,記錄在寄存器上,然后再使用一個寄存器記錄棧頂的位置,幾個字節的數據進來,棧頂位置變大幾個字節,數據出去,棧頂位置變小,一個棧就搭建好了。之后是堆,你在代碼中動態申請的內存,都會在堆這一塊內存區域的某個位置給你申請。
原理跟“燙”差不多,只是編譯器默認把堆區的內容初始化為0xcd.
解決辦法很簡單,養成初始化的好習慣就好了,初始化為0就可以了。
上圖可以看出,我只把第三個字節初始化為0,后面幾個字節并沒有經過初始化,編譯器還是默認給他們賦值為0xCD,但是當printf函數讀到了第三個字節0,它就以為這個字符串已經結束了,也就不再輸出后面的“屯”了。但是我們平常編程中還是要把申請的內存完全初始化比較穩妥。
6.3 琨斤拷
錕斤拷是不同編碼的字符轉換中轉換失敗導致的,當你想把某種編碼通過編碼轉換工具轉換成目前廣泛流行的utf-8編碼,然后編碼轉換工具就會根據原編碼在對照表中查找(或者通過計算)對應的utf-8編碼,但是編碼轉換工具找不到啊,它發現這兩種編碼根本不能轉換,那怎么辦呢,那就統一給他們一個默認編碼吧,這個默認編碼是0xEFBFBD(三個字節),于是內存中就會出現大量的EFBFBDEFBFBDEFBFBDEFBFBD…
然后我們按照GBK的來讀,我們知道,GBK是按照一個字節或者兩個字節來讀的,上面的字符明顯不是ascll,于是printf函數會兩個字節兩個字節的讀。首先讀前兩個字節EFBF(琨),再往下讀兩個字節BDEF(斤),再往下讀兩個字節BFBD(拷),再往下兩個字節EFBF(琨)…
上圖中有一部分沒有被初始化到,所以就“燙”起來了!
6.4 諾
“諾”好像是bom的原因,目前不是很理解。
以上純屬個人理解,如有錯誤,希望能指出!
參考鏈接:
使用char字符實現漢字處理
VS工程屬性“字符集”和源文件“高級保存選項”字符集區別
printf,wprintf與setlocale,char與wchar_t區別
總結
以上是生活随笔為你收集整理的汉字在屏幕上的显示过程以及乱码的原因的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于文件系统
- 下一篇: cpu与外设工作原理