1. 程式人生 > >VS下使用多字符集編碼和Unicode字符集編碼的總結

VS下使用多字符集編碼和Unicode字符集編碼的總結

編寫MFC程式的時候,總遇到字符集轉換的問題,這裡總結一下,方便大家使用。
在多位元組字符集編碼下,設定如下環境:
多字符集編碼
這時CString與char陣列是可以互相轉換的,而如果改成“使用Unicode字符集”,設定如下:
Unicode字符集
原來的程式碼就會報很多錯誤,諸如:
error C2664: “Cxxxxx::ConvertStringtoBtye”: 不能將引數 1 從“wchar_t *”轉換為“char *”
這是因為使用Unicode編碼,編譯器已經把CString視為寬字元處理了,就不能再自動轉為char *了。那麼程式碼中用到的地方都得處理下,簡單的如使用字串常量,加上_T巨集就可以解決問題,如:

AfxMessageBox("編碼型別不正確!");

改為

AfxMessageBox(_T("編碼型別不正確!"));
strTest.Replace(" ", "");

改為

strTest.Replace(_T(" "), _T(""));

而對於一些函式,MFC為了相容老版本,定義了兩種型別,一種以W結尾的寬字元函式,另一種以A結尾的多字元函式。例如SQLGetInstalledDrivers函式,如果是多字符集就呼叫SQLGetInstalledDriversA,如果是Unicode字符集,就呼叫SQLGetInstalledDriversW:
SQLGetInstalledDrivers函式版本


對於這種函式,使用時我們也得進行區分,為了相容性,也使用UNICODE分別處理:

WORD cbBufMax = 2000;
WORD cbBufOut;

#ifdef  UNICODE
	char szBuf[2001];
	char *pszBuf = szBuf;

	// Get the names of the installed drivers ("odbcinst.h" has to be included )
	if(!SQLGetInstalledDrivers(szBuf, cbBufMax, & cbBufOut))
	{
		m_sExcelDriver = "";
	}
#else
	wchar_t szBuf[2001];
	wchar_t *pszBuf = szBuf;

	// Get the names of the installed drivers ("odbcinst.h" has to be included )
	if (!SQLGetInstalledDrivers(szBuf, cbBufMax, &cbBufOut))
	{
		m_sExcelDriver = _T("");
	}
#endif

對於一些操作字元和字串的庫函式,也是有區別的:

#ifdef  UNICODE
	// Search for the driver...
	do
	{
		if (wcsstr(pszBuf, _T("Excel")) != 0)
		{
			// Found !
			m_sExcelDriver = CString(pszBuf);
			break;
		}
		pszBuf = wcschr(pszBuf, '\0') + 1;
	} while (pszBuf[1] != '\0');
#else
	// Search for the driver...
	do
	{
		if( strstr( pszBuf, "Excel" ) != 0 )
		{
		// Found !
			m_sExcelDriver = CString( pszBuf );
			break;
		}
		pszBuf = strchr( pszBuf, '\0' ) + 1;
	}
	while( pszBuf[1] != '\0' );

為了有更好的相容性,應該選擇兩個版本通用的函式,比如字串轉長整形,最好使用_tcstol函式來代替使用跟字符集相關的strtol或wcstol函式,類似的還有_ttoi、_ttof之類的轉換函式。

在Unicode字符集下寫檔案的時候,對於長度操作要注意,一個寬字元是要寫兩個長度的:

	CFile fileSave;
	CString strGetData(_T("寫入測試"));
	CString strPath(_T("test.txt")); 

	if (!fileSave.Open(strPath, CFile::modeCreate |CFile::modeNoTruncate | CFile::modeWrite))
	{
		return;
	}

	wchar_t wch = 0xFEFF;
	fileSave.Write(&wch, sizeof(wchar_t));

	fileSave.Write(strGetData.LockBuffer(), wcslen(strGetData)*2);
	strGetData.UnlockBuffer();
	fileSave.Close();

使用Unicode字符集還有一大問題,就是CString與char之間的相互轉換,以下函式就我總結的轉換函式,char轉CString函式:

CString Char2CString(char *pChar)
{
	int charLen = strlen(pChar); // 計算pChar所指向的字串大小
	int len = MultiByteToWideChar(CP_ACP, 0, pChar, charLen, NULL, 0); // 計算多位元組字元的大小
	wchar_t *pWChar = new wchar_t[len + 1]; // 為寬位元組字元數申請空間 
	MultiByteToWideChar(CP_ACP, 0, pChar, charLen, pWChar, len); // 多位元組編碼轉換成寬位元組編碼
	pWChar[len] = '\0';  

	// 將wchar_t陣列轉換為CString
	CString str;
	str.Append(pWChar);
	delete[] pWChar;

	return str;
}

CString轉char*函式:

char* CString2Char(CString str)
{
	DWORD dwCount = str.GetLength();   
	int len = WideCharToMultiByte(CP_ACP, 0, str, dwCount, NULL, 0, NULL, NULL); // 獲取寬位元組字元的大小
	char *pChar = new char[len + 1];

	WideCharToMultiByte(CP_ACP, 0, str, dwCount, (char *)pChar, len, NULL, NULL); // 寬位元組編碼轉換成多位元組編碼 
	pChar[len] = '\0'; // 注意以'\0'結束
	return pChar;
}

從程式碼可以看出,這裡CString轉char*需要new一個記憶體空間,使用後得記得delete掉才行。如果不習慣釋放空間,那CString轉char時最好不要開闢空間,萬一忘記delete就造成記憶體洩露了,可以寫一個改進版的轉換函式:

static int CStrintToCharWithoutNew(CString str, char *buf)
{
	int len = WideCharToMultiByte(CP_ACP, 0, str, -1, NULL, 0, NULL, NULL);
	if (len > 0)
		WideCharToMultiByte(CP_ACP, 0, str, -1, buf, len, NULL, NULL);
	buf [len] = '\0'; // 注意以'\0'結束
	return len;
}

這樣使用後就不需要再記得delete了,前提是陣列得定義的足夠大:

char recvData[200];
CString testdata = _T("測試轉換");
len = CStrintToCharWithoutNew(testdata, recvData);

上面的方法雖然基礎,但顯得麻煩了些,char*轉CString只需要使用A2T()或A2W()巨集即可:

char cBuf[] = "hello 世界";
CString str = _T("");

USES_CONVERSION;
str = A2T(cBuf);
// str = A2W(cBuf);

如果專案沒包含標頭檔案#include <atlconv.h>需要自己加上,USES_CONVERSION巨集一定要放在使用前,否則會報錯:

error C2065: “_lpa”: 未宣告的識別符號

類似的CString轉char只需使用T2A或W2A巨集即可。對於網上說的char轉CString使用Format方法,如下:

char cBuf[] = "hello 世界";
CString str = _T("");
str.Format(_T("%s"), cBuf);

經測試,在Unicode編碼下是不行的,cBuf陣列就不是寬字元,改為下面寫法就可以了:

str.Format(_T("%s"), _T("hello 世界"));

有時函式的引數是指標型別LPTSTR(多字元下是char ,Unicode下實際是wchar_t),那麼如何把CString轉為LPTSTR呢,下面兩種方法都可以:

CString str("hello 世界"); 
LPTSTR lp = (LPTSTR)(LPCTSTR)str;
// LPTSTR lp = str.GetBuffer(); 

上面的程式碼在多字符集和Unicode字符集下都可以使用的,不過使用GetBuffer()時,使用完記得呼叫ReleaseBuffer()釋放記憶體。這裡有個細節,細心的讀者可能會發現,CString建立的時候沒有加_T巨集:

CString str(_T("hello 世界"));

這裡CString的建構函式自動為我們處理了,所以不用擔心編碼問題的。說到這裡,您一定對_T巨集感興趣了,這個巨集到底做了什麼呢?在tchar.h檔案中可以看到對它的定義,摘錄下來如下:

#define _T(x)       __T(x)
#define _TEXT(x)    __T(x)

#define __T(x)      L ## x           // 編碼為 Unicode
#define __T(x)      x                // 編碼為 多字元

其實它根據不同的編碼環境來轉換字元的,在Unicode下,會把字元前面加個L代表寬字元,所以下面的定義只能在Unicode下使用:

wchar_t wBuf[] = _T("hello 世界");

就等同於:

wchar_t wBuf[] = L"hello 世界";

L為寬字元的定義巨集,除錯時可以發現寬字元變數的值是帶L的。而在多字符集下面就報錯了,因為轉義成了:

wchar_t wBuf[] = "hello 世界;

有時候會見到WCHAR和TCHAR巨集,不用慌,其實他們也是wchar_t的變體,類似很多巨集定義都可以在WinNT.h檔案中找到。說到TCHAR,有必要說明一下,是MFC為了統一字符集操作而定義的型別,它跟_T巨集類似,在不同的字符集下有不同的定義:

typedef wchar_t TCHAR;   // 編碼為 Unicode
typedef char TCHAR;      // 編碼為 多字元

那麼對於剛才的問題就有了解決方案,定義陣列時如下定義:

TCHAR tBuf[] = _T("hello 世界");

這樣就不會因為編譯時選擇不同的字符集而造成編譯出錯了。這下好了,按照這些標準寫的程式碼,無論怎麼選字符集都不會報錯,本以為萬事大吉了,可是世事難料啊,專案要呼叫兩個別人寫的庫,一個是Unicode字符集的,而另一個是多字符集編譯的,怎麼選都是報錯啊。
比如這裡選通用的Unicode字符集,分析一下多字符集庫報錯的原因,標頭檔案的函式使用了LPCTSTR型別引數,而這個型別也是隨不同的字符集而代表不同型別的,在WinNT.h檔案有定義:

typedef LPCWSTR PCTSTR, LPCTSTR;
typedef __nullterminated CONST WCHAR *LPCWSTR, *PCWSTR;

分析可見在Unicode下實際上是WCHAR 型別,而那個庫原本在多字符集條件下編譯時應該轉為LPCSTR(實際上是char)型別的:

typedef __nullterminated CONST CHAR *LPCSTR, *PCSTR;

所以對於對外介面來說,寫成通用字符集的引數是很不好的,人家編譯時選擇的字符集不一定和你一樣,很容易造成連線錯誤的,直接寫成char*型別才是正道啊。