1. 程式人生 > >【程式】給C++的cout和fstream新增Unicode支援,使其能向螢幕或檔案輸入/輸出wchar_t字串

【程式】給C++的cout和fstream新增Unicode支援,使其能向螢幕或檔案輸入/輸出wchar_t字串

【程式】

#include <fstream>
#include <iostream>
#include <Windows.h>

#define RDBUF_LEN 200

using namespace std;

ostream &operator << (ostream &os, const wchar_t *wstr)
{
	if (os == cout)
		WriteConsoleW(GetStdHandle(STD_OUTPUT_HANDLE), wstr, wcslen(wstr), NULL, NULL); // 輸出到螢幕上
	else
	{
		// 此時wstr是UTF-16編碼
		// 將其轉換為UTF-8編碼後寫入檔案
		int n = WideCharToMultiByte(CP_UTF8, NULL, wstr, -1, NULL, NULL, NULL, NULL);
		char *p = new char[n];
		WideCharToMultiByte(CP_UTF8, NULL, wstr, -1, p, n, NULL, NULL);
		os.write(p, n - 1);
		delete[] p;
	}
	return os;
}

ostream &operator << (ostream &os, wstring &ws)
{
	os << ws.c_str();
	return os;
}

istream &operator >> (istream &is, wstring &ws)
{
	int n = 0;
	wchar_t *wp;
	if (is == cin)
	{
		/* 從控制檯中輸入一行字串 */
		bool complete = false;
		DWORD dwRead; // 實際讀到的字元個數
		HANDLE hInput = GetStdHandle(STD_INPUT_HANDLE);
		wp = NULL;
		do
		{
			/* 分次迴圈讀取, 直到遇到換行符 */
			wp = (wchar_t *)realloc(wp, (n + RDBUF_LEN + 2) * sizeof(wchar_t)); // 分配記憶體時始終為\r\n預留空間
			ReadConsoleW(hInput, wp + n, RDBUF_LEN + 2, &dwRead, NULL);
			if (wp[n + dwRead - 2] == '\r')
			{
				wp[n + dwRead - 2] = '\0'; // 把\r換成\0, 並結束讀取
				complete = true;

				// 注意: 當本次只讀到\n一個字元時, wp[n + dwRead - 2]指向上一次讀到的那一段的最後一個字元, 且一定是\r
				// 例如: [abc][de\r][\n  ], 此時RDBUF_LEN=1, 第二次讀的時候n=3, dwRead=3, wp[3+3-2]=wp[4]是e而不是\r, 所以繼續
				// 第三次讀的時候n=6, dwRead=1, wp[6+1-2]=wp[5]恰好是\r, 因此將\r替換成\0並退出迴圈
			}
			else
				n += dwRead;
		} while (!complete);
		ws = wp;
		free(wp);
	}
	else
	{
		/* 從檔案中讀取一行字串 */
		streampos start_pos = is.tellg(); // 記錄開始讀取的位置
		char ch;
		while (ch = is.get(), !is.eof() && ch != '\r' && ch != '\n')
			n++; // 尋找換行符或檔案結束符出現的位置
		is.seekg(start_pos); // 回到開始位置

		// 讀取這一部分內容
		char *p = new char[n + 1];
		is.read(p, n);
		p[n] = '\0';

		// 使檔案位置指標跳過換行符
		while (ch = is.get(), !is.eof() && (ch == '\r' || ch == '\n'));
		if (!is.eof())
			is.seekg(-1, ios::cur);

		// 轉換成UTF-16編碼
		n = MultiByteToWideChar(CP_UTF8, NULL, p, -1, NULL, NULL);
		wp = new wchar_t[n];
		MultiByteToWideChar(CP_UTF8, NULL, p, -1, wp, n);
		delete[] p;
		ws = wp;
		delete[] wp;
	}
	return is;
}

int main(void)
{
	// 從控制檯中輸入Unicode字串
	wstring wstr;
	cout << L"請輸入一段文字: ";
	cin >> wstr;

	// 開啟檔案並寫入Unicode字串
	ofstream file("file.txt");
	if (!file.is_open())
	{
		cout << L"檔案開啟失敗" << endl;
		return 0;
	}
	file << L"1234" << endl;
	file << L"簡體中文abc" << endl;
	file << L"¿Cómo estás?" << endl;
	file << L"使用者輸入: " << wstr << endl;
	cout << L"已成功寫入" << file.tellp() << L"位元組" << endl;
	file.close();

	// 開啟檔案並從檔案中讀取Unicode字串
	ifstream ifile("file.txt");
	cout << endl << endl << endl << L"**********************檔案中的內容*********************" << endl;
	while (!ifile.eof())
	{
		ifile >> wstr;
		cout << wstr << endl; // 顯示
	}
	ifile.close();
	return 0;
}

【執行結果】

【輸出的檔案】

【背景知識】
char字元陣列能存放任意編碼的字串
wchar_t在Windows系統下一般用來存放UTF-16編碼的字串,Linux下一般是UTF-32字串
在C/C++中,"xxx"型的字串的編碼與原始檔的編碼格式一致,而L"xxx"則統一為UTF-16(Windows) / UTF-32(Linux)編碼。
在Visual Studio中預設建立的原始檔的編碼格式是ANSI編碼,所以char xxx[] = "xxx"的編碼一般都是ANSI。
ANSI的編碼格式並不固定,它只是一個編碼對映。在簡體中文Windows作業系統下,ANSI指向的是GB2312編碼。由於英文版系統下ANSI並不指向GB2312編碼,所以開啟相關的文字檔案就會顯示為亂碼,而開啟UTF-8編碼的文字檔案就不會亂碼。
Windows API函式都分為ANSI和Unicode版本。例如SetWindowTextA接受的是char字元陣列,因為char字元陣列一般都是ANSI編碼,而SetWindowTextW接受的是wchar_t字元陣列(預設UTF-16編碼)
UTF-8、UTF-16和UTF-32是Unicode的三種實現方式。Unicode負責給每個字元編號,而這三種實現方式則按不同的方式儲存其編號。

C/C++中自帶的printf, cout等庫函式都不能正確輸出Unicode的字串,因為它們在輸出前都會將Unicode轉換成ANSI,導致字元資訊丟失,並且這個過程無法阻止。要想輸出Unicode字串只有呼叫Windows的API函式:WriteConsoleW。對於Unicode字串的輸入也有同樣的問題。
本文就是把這個功能封裝到了C++的cout裡面。