[C++] cout、wcout無法正常輸出中文字符問題的深入調查(2):VC2005的crt源碼分析


作者:zyl910

  前面測試了各種編譯器的執行結果,但為什么它們的執行結果是那樣呢?這需要仔細分析。VC2005的測試結果比較典型,而且調試跟蹤比較方便,於是本篇對VC2005的crt源碼進行分析。


一、須知

  開發工具是VC2005,平台為32位的x86,編譯模式為Debug,使用MBCS字符集。


二、cout輸出窄字符串

2.1 已初始化locale

  “已初始化locale”是指——在輸出前執行了初始化locale,即執行了下列語句——

    // init.
    locale::global(locale(""));
    wcout.imbue(locale(""));

 

  現在開始進行分析。
  “cout << psa”表示使用cout輸出窄字符串。按F11單步跟蹤,它依次進入了下列函數——
operator<<:[C++庫] 流輸出運算符。
basic_streambuf<char>::sputn:[C++庫] 輸出字符串(公開方法)。
basic_streambuf<char>::xsputn:[C++庫] 輸出字符串(內部實現)。循環對源串中的每一個char調用overflow。【注意#1】gbk編碼的漢字是2個字節,會調用overflow 2次。
basic_filebuf<char>::overflow:[C++庫] 數據溢出,即向文件寫入一個字符。【注意#2】因為現在是char版,無需轉換編碼,直接調用_Fputc。
_Fputc<char>:[C++庫]向文件寫入一個char。
fputc:[C庫] 向文件寫入一個char。
_flsbuf:[C庫] 刷新緩沖區並輸出char。
_write:[C庫] 向文件寫數據。
_write_nolock:[C庫] 向文件寫數據(不加鎖版)。【注意#3】條件判斷存在漏洞,導致漢字的首字節無法輸出。返回-1。

  此時的調用棧——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0012fb50, unsigned int cnt=0x00000001)  行170 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0012fb50, unsigned int cnt=0x00000001)  行74 + 0x11 字節 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x10311d20)  行189 + 0x11 字節 C
  msvcr80d.dll!fputc(int ch=0xffffffba, _iobuf * str=0x10311d20)  行52 + 0x4b 字節 C
  msvcp80d.dll!std::_Fputc<char>(char _Byte=0xba, _iobuf * _File=0x10311d20)  行81 + 0xf 字節 C++
  msvcp80d.dll!std::basic_filebuf<char,std::char_traits<char> >::overflow(int _Meta=0x000000ba)  行261 + 0x1c 字節 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::xsputn(const char * _Ptr=0x0041774d, int _Count=0x00000007)  行379 + 0x1a 字節 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::sputn(const char * _Ptr=0x0041774c, int _Count=0x00000008)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<std::char_traits<char> >(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...}, const char * _Val=0x0041774c)  行768 + 0x3e 字節 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a58)  行45 + 0x12 字節 C++

  發現_write_nolock函數存在Bug,代碼摘錄——

// C:\VS2005\VC\crt\src\write.c, 160 line:
        /* don't need double conversion if it's ANSI mode C locale */
        if (toConsole && !(isCLocale && (tmode == __IOINFO_TM_ANSI))) {
            UINT consoleCP = GetConsoleCP();
            char mboutbuf[MB_LEN_MAX];
            wchar_t tmpchar;
            int size = 0;
            int written = 0;
            char *pch;

            for (pch = (char *)buf; (unsigned)(pch - (char *)buf) < cnt; ) {
                BOOL bCR;

                if (tmode == __IOINFO_TM_ANSI) {
                    bCR = *pch == LF;
                    /*
                     * Here we need to do double convert. i.e. convert from
                     * multibyte to unicode and then from unicode to multibyte in
                     * Console codepage.
                     */
                    if (!isleadbyte(*pch)) {
                        if (mbtowc(&tmpchar, pch, 1) == -1) {
                            break;
                        }
                    } else if ((cnt - (pch - (char*)buf)) > 1) {
                        if (mbtowc(&tmpchar, pch, 2) == -1) {
                            break;
                        }
                        /*
                         * Increment pch to accomodate DBCS character.
                         */
                        ++pch;
                    } else {
                        break;
                    }
                    ++pch;
                } else if (tmode == __IOINFO_TM_UTF8 || tmode == __IOINFO_TM_UTF16LE) {
                    /*
                     * Note that bCR set above is not valid in case of UNICODE
                     * stream. We need to set it using unicode character.
                     */
                    tmpchar = *(wchar_t *)pch;
                    bCR = tmpchar == LF;
                    pch += 2;
                }

                if (tmode == __IOINFO_TM_ANSI)
                {
                    if( (size = WideCharToMultiByte(consoleCP,
                                                    0,
                                                    &tmpchar,
                                                    1,
                                                    mboutbuf,
                                                    sizeof(mboutbuf),
                                                    NULL,
                                                    NULL)) == 0) {
                        break;
                    } else {
                        if ( WriteFile( (HANDLE)_osfhnd(fh),
                                        mboutbuf,
                                        size,
                                        (LPDWORD)&written,
                                        NULL) ) {
                            charcount += written;
                            if (written < size)
                                break;
                        } else {
                            dosretval = GetLastError();
                            break;
                        }
                    }

                    if (bCR) {
                        size = 1;
                        mboutbuf[0] = CR;
                        if (WriteFile((HANDLE)_osfhnd(fh),
                                      mboutbuf,
                                      size,
                                      (LPDWORD)&written,
                                      NULL) ) {
                            if (written < size)
                                break;
                            lfcount ++;
                            charcount++;
                        } else {
                            dosretval = GetLastError();
                            break;
                        }
                    }
                }
                else if ( tmode == __IOINFO_TM_UTF8 || tmode == __IOINFO_TM_UTF16LE)
...


// C:\VS2005\VC\crt\src\write.c, 443 line:
        if (charcount == 0) {
                /* If nothing was written, first check if an o.s. error,
                   otherwise we return -1 and set errno to ENOSPC,
                   unless a device and first char was CTRL-Z */
                if (dosretval != 0) {
                        /* o.s. error happened, map error */
                        if (dosretval == ERROR_ACCESS_DENIED) {
                            /* wrong read/write mode should return EBADF, not
                               EACCES */
                                errno = EBADF;
                                _doserrno = dosretval;
                        }
                        else
                                _dosmaperr(dosretval);
                        return -1;
                }
...

 

  _write_nolock函數的主要處理流程是——
循環處理源串中的每一個char
{
 調用mbtowc將當前char轉換為寬字符。利用isleadbyte函數判斷當前char是不是多字節字符的首字節,再判斷是否能湊夠2個字節進行轉換。
 調用WideCharToMultiByte將寬字符轉為窄字符串。
 調用WriteFile將窄字符串寫入文件。
}

  問題就是出在“調用mbtowc將當前char轉換為寬字符”這一步——
因為先前在basic_streambuf<char>::xsputn函數中,就已經將源串分解為各個char了。gbk編碼的漢字是2個字節,所以會先將漢字的首字節傳遞到_write_nolock函數。
因現在是首字節,所以“if (!isleadbyte(*pch))”判斷為假。因現在只有一個字節,“else if ((cnt - (pch - (char*)buf)) > 1)”判斷也為假。最終到else分支,執行break跳出循環。
跳出循環后,因為沒有輸出字符,於是進入“if (charcount == 0)”分支。因dosretval變量未初始化,所以該變量為非0值的可能性很高,於是進入了“if (dosretval != 0)”分支。最終執行“return -1”返回-1。

  函數返回時——
_write_nolock:【注意#3】條件判斷存在漏洞,導致漢字的首字節無法輸出。返回-1。
_write:返回_write_nolock的返回值,即返回-1。
_flsbuf:因_flsbuf的返回值(-1)與字符數不同(sizeof(TCHAR)),返回EOF(-1)。
fputc:返回_flsbuf的返回值,即返回EOF(-1)。
_Fputc<char>:因“fputc的返回值(EOF)與EOF不相等”的結果為假((fputc(_Byte, _File) != EOF)),返回false。
basic_filebuf<char>::overflow:因_Fputc返回false,返回_Traits::eof(),即EOF(-1)。
basic_streambuf<char>::xsputn:【注意#4】因overflow返回EOF(-1),跳出循環,返回實際輸出的字符數。
basic_streambuf<char>::sputn:返回xsputn的返回值,即返回實際輸出的字符數。
operator<<:【注意#5】因實際輸出的字符數與源串字符數不同,設置流標記為bad。

  這就是“已初始化locale時,cout無法輸出中文窄字符串”的原因。


2.2 未初始化locale

  “未初始化locale”是指——在輸出前沒有初始化locale,即將相關語句注釋了——

    // init.
    //locale::global(locale(""));
    //wcout.imbue(locale(""));

 

  “cout << psa”仍會執行到_write_nolock函數。此時的調用棧——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0012fb98, unsigned int cnt=0x00000001)  行268 + 0x5 字節 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0012fb98, unsigned int cnt=0x00000001)  行74 + 0x11 字節 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x10311d20)  行189 + 0x11 字節 C
  msvcr80d.dll!fputc(int ch=0xffffffba, _iobuf * str=0x10311d20)  行52 + 0x4b 字節 C
  msvcp80d.dll!std::_Fputc<char>(char _Byte=0xba, _iobuf * _File=0x10311d20)  行81 + 0xf 字節 C++
  msvcp80d.dll!std::basic_filebuf<char,std::char_traits<char> >::overflow(int _Meta=0x000000ba)  行261 + 0x1c 字節 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::xsputn(const char * _Ptr=0x0041774d, int _Count=0x00000007)  行379 + 0x1a 字節 C++
  msvcp80d.dll!std::basic_streambuf<char,std::char_traits<char> >::sputn(const char * _Ptr=0x0041774c, int _Count=0x00000008)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<std::char_traits<char> >(std::basic_ostream<char,std::char_traits<char> > & _Ostr={...}, const char * _Val=0x0041774c)  行768 + 0x3e 字節 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行45 + 0x12 字節 C++

  在_write_nolock函數中,因為現在使用的是C默認locale(未初始化locale),所以執行的語句不同。代碼摘錄——

// C:\VS2005\VC\crt\src\write.c, 160 line:
        /* don't need double conversion if it's ANSI mode C locale */
        if (toConsole && !(isCLocale && (tmode == __IOINFO_TM_ANSI))) {
...
// C:\VS2005\VC\crt\src\write.c, 268 line:
        } else if ( _osfile(fh) & FTEXT ) {
            /* text mode, translate LF's to CR/LF's on output */

            dosretval = 0;          /* no OS error yet */

            if(tmode == __IOINFO_TM_ANSI) {
                char ch;                    /* current character */
                char *p = NULL, *q = NULL;  /* pointers into buf and lfbuf resp. */
                char lfbuf[BUF_SIZE];
                p = (char *)buf;        /* start at beginning of buffer */
                while ( (unsigned)(p - (char *)buf) < cnt ) {
                    q = lfbuf;      /* start at beginning of lfbuf */

                    /* fill the lf buf, except maybe last char */
                    while ( q - lfbuf < sizeof(lfbuf) - 1 &&
                            (unsigned)(p - (char *)buf) < cnt ) {
                        ch = *p++;
                        if ( ch == LF ) {
                            ++lfcount;
                            *q++ = CR;
                        }
                        *q++ = ch;
                    }

                    /* write the lf buf and update total */
                    if ( WriteFile( (HANDLE)_osfhnd(fh),
                                lfbuf,
                                (int)(q - lfbuf),
                                (LPDWORD)&written,
                                NULL) )
                    {
                        charcount += written;
                        if (written < q - lfbuf)
                            break;
                    }
                    else {
                        dosretval = GetLastError();
                        break;
                    }
                }

 

  因現在isCLocale為真,於是轉到“else if ( _osfile(fh) & FTEXT )”分支。簡單做了一下換行符處理后,便調用WriteFile寫數據。操作成功。

  這就是“未初始化locale,cout能正常輸出中文窄字符串”的原因。


2.3 其他測試

  修改了一下項目配置,改為Unicode字符集。進行調試,發現程序運行效果完全相同。這是因為_write_nolock是msvcr80d.dll中已經編譯好代碼,本項目的編譯參數不會影響msvcr80d.dll的執行效果。
  再修改項目配置,改為靜態鏈接。進行調試,發現程序運行效果完全相同。原理同上。
  

三、wcout輸出寬字符串

3.1 已初始化locale

  “wcout << psw”表示使用cout輸出窄字符串。按F11單步跟蹤,它依次進入了下列函數——
operator<<:[C++庫] 流輸出運算符。
basic_streambuf<wchar_t>::sputn:[C++庫] 輸出字符串(公開方法)。
basic_streambuf<wchar_t>::xsputn:[C++庫] 輸出字符串(內部實現)。循環對源串中的每一個wchar_t調用overflow。【注意#1】漢字一般是1個wchar_t,會調用overflow 1次。
basic_filebuf<wchar_t>::overflow:[C++庫] 數據溢出,即向文件寫入一個字符。【注意#2】因為現在是wchar_t版,需要進行編碼轉換。
codecvt<wchar_t,char,int>::out:[C++庫] 將wchar_t串轉為char串(公開方法)。
codecvt<wchar_t,char,int>::do_out:[C++庫] 將wchar_t串轉為串(內部實現)。
_Wcrtomb:[C庫] 調用WideCharToMultiByte將wchar_t字符轉換為多字節串。

  此時的調用棧——
> msvcp80d.dll!_Wcrtomb(char * s=0x0018fc18, wchar_t wchar=L'漢', int * pst=0x6ad750ec, const _Cvtvec * ploc=0x00264cf0)  行111 C
  msvcp80d.dll!std::codecvt<wchar_t,char,int>::do_out(int & _State=0, const wchar_t * _First1=0x0018fc38, const wchar_t * _Last1=0x0018fc3a, const wchar_t * & _Mid1=0x0018fc38, char * _First2=0x0018fc18, char * _Last2=0x0018fc20, char * & _Mid2=0x0018fc18)  行1000 + 0x1f 字節 C++
  msvcp80d.dll!std::codecvt<wchar_t,char,int>::out(int & _State=0, const wchar_t * _First1=0x0018fc38, const wchar_t * _Last1=0x0018fc3a, const wchar_t * & _Mid1=0x0018fc38, char * _First2=0x0018fc18, char * _Last2=0x0018fc20, char * & _Mid2=0x0018fc18)  行897 C++
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=27721)  行273 + 0x90 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x004187e2, int _Count=5)  行379 + 0x1a 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x004187e0, int _Count=6)  行170 C++
  tcharall_cpp_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x004187e0)  行853 + 0x3e 字節 C++
   wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字節 C++

  在_Wcrtomb函數中,它會調用WideCharToMultiByte這個Windows API進行編碼轉換。
  編碼轉換成功后,又會回到overflow函數。它會調用fwrite輸出轉換后的char串,依次進入了下列函數——
fwrite:[C庫] 向文件寫入數據。
_fwrite_nolock:[C庫] 向文件寫入數據(不加鎖版)。【注意#3】循環對數據的每一個char調用_flsbuf。
_flsbuf(int ch, _iobuf* str) // [C庫] 刷新緩沖區並輸出char。
_write(int fh, const void* buf, unsigned int cnt) // [C庫] 向文件寫數據。
_write_nolock(int fh, const void* buf, unsigned int cnt) // [C庫] 向文件寫數據(不加鎖版)。

  此時的調用棧——
> msvcr80d.dll!_write_nolock(int fh=0x00000001, const void * buf=0x0018fae0, unsigned int cnt=0x00000001)  行470 C
  msvcr80d.dll!_write(int fh=0x00000001, const void * buf=0x0018fae0, unsigned int cnt=0x00000001)  行74 + 0x11 字節 C
  msvcr80d.dll!_flsbuf(int ch=0xffffffba, _iobuf * str=0x67cc1d20)  行189 + 0x11 字節 C
  msvcr80d.dll!_fwrite_nolock(const void * buffer=0x0018fc18, unsigned int size=0x00000001, unsigned int num=0x00000002, _iobuf * stream=0x67cc1d20)  行194 + 0xd 字節 C
  msvcr80d.dll!fwrite(const void * buffer=0x0018fc18, unsigned int size=0x00000001, unsigned int count=0x00000002, _iobuf * stream=0x67cc1d20)  行83 + 0x15 字節 C
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=0x6c49)  行280 + 0x59 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x004187e2, int _Count=0x00000005)  行379 + 0x1a 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x004187e0, int _Count=0x00000006)  行170 C++
  tcharall_cpp_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x004187e0)  行853 + 0x3e 字節 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字節 C++

  在_write_nolock函數中,又遇到了同樣的問題——
因為先前在_fwrite_nolock函數中,就已經將源串分解為各個char了。gbk編碼的漢字是2個字節,所以會先將漢字的首字節傳遞到_write_nolock函數。
因現在是首字節,所以“if (!isleadbyte(*pch))”判斷為假。因現在只有一個字節,“else if ((cnt - (pch - (char*)buf)) > 1)”判斷也為假。最終到else分支,執行break跳出循環。
跳出循環后,因為沒有輸出字符,於是進入“if (charcount == 0)”分支。因dosretval變量未初始化,所以該變量為非0值的可能性很高,於是進入了“if (dosretval != 0)”分支。最終執行“return -1”返回-1。

  函數返回時——
_write_nolock:【注意#4】條件判斷存在漏洞,導致漢字的首字節無法輸出。返回-1。
_write:返回_write_nolock的返回值,即返回-1。
_flsbuf:因_flsbuf的返回值(-1)與字符數不同(sizeof(TCHAR)),返回EOF(-1)。
_fwrite_nolock:因_flsbuf返回EOF(-1),跳出循環,返回實際輸出的字符數(0)。
fwrite:返回_fwrite_nolock的返回值,即返回0。
basic_filebuf<wchar_t>::overflow:因fwrite的返回值(0)與編碼轉換后的字符數不同,返回_Traits::eof(),即WEOF(-1)。
basic_streambuf<wchar_t>::xsputn:【注意#5】因overflow返回WEOF(-1),跳出循環,返回實際輸出的字符數。
basic_streambuf<wchar_t>::sputn:返回xsputn的返回值,即返回實際輸出的字符數。
operator<<:【注意#6】因實際輸出的字符數與源字符數不同,設置流標記為bad。

  這就是“已初始化locale時,cout無法輸出中文窄字符串”的原因。雖然basic_filebuf<wchar_t>::overflow能正常的將寬字符轉為窄字符串,但_write_nolock的Bug造成了無法輸出。


3.2 未初始化locale

  未初始化locale時,“wcout << psw”的執行路徑與先前不同,依次進入了下列函數——
operator<<:[C++庫] 流輸出運算符。
basic_streambuf<wchar_t>::sputn:[C++庫] 輸出字符串(公開方法)。
basic_streambuf<wchar_t>::xsputn:[C++庫] 輸出字符串(內部實現)。循環對源串中的每一個wchar_t調用overflow。【注意#1】漢字一般是1個wchar_t,會調用overflow 1次。
basic_filebuf<wchar_t>::overflow:[C++庫] 數據溢出,即向文件寫入一個字符。【注意#2】因為現在是“未初始化locale”,不做編碼轉換,直接調用_Fputc<wchar_t>。
_Fputc<wchar_t>:[C++庫] 輸出 wchar_t。
fputwc:[C庫] 輸出 wchar_t(公開方法)。
_fputwc_nolock:[C庫] 輸出 wchar_t(內部實現)。【注意#3】因為現在是wchar_t版,需要進行編碼轉換。
wctomb_s:[C庫] (緩沖安全版)將寬字符轉為多字節字符(公開方法)。
_wctomb_s_l:[C庫] (緩沖安全版)將寬字符轉為多字節字符(內部實現)。

  此時的調用棧——
> msvcr80d.dll!_wctomb_s_l(int * pRetValue=0x0012fbac, char * dst=0x0012fba0, unsigned int sizeInBytes=0x00000005, wchar_t wchar=L'漢', localeinfo_struct * plocinfo=0x00000000)  行81 C++
  msvcr80d.dll!wctomb_s(int * pRetValue=0x0012fbac, char * dst=0x0012fba0, unsigned int sizeInBytes=0x00000005, wchar_t wchar=L'漢')  行145 + 0x18 字節 C++
  msvcr80d.dll!_fputwc_nolock(wchar_t ch=L'漢', _iobuf * str=0x10311d20)  行133 + 0x14 字節 C
  msvcr80d.dll!fputwc(wchar_t ch=L'漢', _iobuf * str=0x10311d20)  行60 + 0xe 字節 C
  msvcp80d.dll!std::_Fputc<wchar_t>(wchar_t _Wchar=L'漢', _iobuf * _File=0x10311d20)  行86 + 0xf 字節 C++
  msvcp80d.dll!std::basic_filebuf<wchar_t,std::char_traits<wchar_t> >::overflow(unsigned short _Meta=0x6c49)  行261 + 0x1c 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::xsputn(const wchar_t * _Ptr=0x0041773e, int _Count=0x00000005)  行379 + 0x1a 字節 C++
  msvcp80d.dll!std::basic_streambuf<wchar_t,std::char_traits<wchar_t> >::sputn(const wchar_t * _Ptr=0x0041773c, int _Count=0x00000006)  行170 C++
  wchar_crtbug_2005.exe!std::operator<<<wchar_t,std::char_traits<wchar_t> >(std::basic_ostream<wchar_t,std::char_traits<wchar_t> > & _Ostr={...}, const wchar_t * _Val=0x0041773c)  行853 + 0x3e 字節 C++
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行46 + 0x12 字節 C++

  在_wctomb_s_l函數中,因為現在使用的是C默認locale(未初始化locale),對於編碼大於255的字符會報錯。代碼摘錄——

// C:\VS8_2005\VC\crt\src\wctomb.c, 79 line:
    if ( _loc_update.GetLocaleT()->locinfo->lc_handle[LC_CTYPE] == _CLOCALEHANDLE )
    {
        if ( wchar > 255 )  /* validate high byte */
        {
            if (dst != NULL && sizeInBytes > 0)
            {
                memset(dst, 0, sizeInBytes);
            }
            errno = EILSEQ;
            return errno;
        }

 

  函數返回時——
_wctomb_s_l:【注意#4】因現在是C地區,而漢字的unicode碼>255,於是返回EILSEQ。
wctomb_s:同_wctomb_s_l,返回EILSEQ。
_fputwc_nolock:因wctomb_s的返回值非0,返回WEOF(-1)。
fputwc:返回WEOF(-1)。
_Fputc<wchar_t>:判斷條件為“return (::fputwc(_Wchar, _File) != WEOF);”,返回false。
basic_filebuf<wchar_t>::overflow:因_Fputc返回false,返回WEOF(-1)。
basic_streambuf<wchar_t>::xsputn:【注意#5】因overflow返回WEOF(-1),跳出循環,返回實際輸出的字符數。
basic_streambuf<wchar_t>::sputn:返回xsputn的返回值,即返回實際輸出的字符數。
operator<<:【注意#6】因實際輸出的字符數與源字符數不同,設置流標記為bad。

  這就是“未初始化locale時,cout無法輸出中文窄字符串”的原因。主要因為C默認locale不支持編碼大於255的字符。


四、printf輸出窄字符串

4.1 已初始化locale

  “printf("\t%s\n", psa)”表示使用printf輸出窄字符串。按F11單步跟蹤,它依次進入了下列函數——
printf:[C庫] 帶格式輸出。
_output_l:[C庫] 根據locale信息進行帶格式輸出。對格式字符串進行解析,根據“%s”提取窄字符串,然后調用write_string輸出窄字符串。
write_string:[C庫] 寫窄字符串。循環對源串中的每一個字符調用write_char。
write_char:[C庫] 寫窄字符。

  此時的調用棧——
> msvcr80d.dll!write_char(char ch=0xd7, _iobuf * f=0x10311d20, int * pnumwritten=0x0012fba8)  行2442 C++
  msvcr80d.dll!write_string(char * string=0x0041774f, int len=0x00000004, _iobuf * f=0x10311d20, int * pnumwritten=0x0012fba8)  行2570 + 0x19 字節 C++
  msvcr80d.dll!_output_l(_iobuf * stream=0x10311d20, const char * format=0x00417823, localeinfo_struct * plocinfo=0x00000000, char * argptr=0x0012fe54)  行2260 + 0x18 字節 C++
  msvcr80d.dll!printf(const char * format=0x00417820, ...)  行63 + 0x18 字節 C
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行50 + 0x13 字節 C++

  write_char函數的源碼如下——

// C:\VS8_2005\VC\crt\src\output.c, 2428 line:
LOCAL(void) write_char (
    _TCHAR ch,
    FILE *f,
    int *pnumwritten
    )
{
    if ( (f->_flag & _IOSTRG) && f->_base == NULL)
    {
        ++(*pnumwritten);
        return;
    }
#ifdef _UNICODE
    if (_putwc_nolock(ch, f) == WEOF)
#else  /* _UNICODE */
    if (_putc_nolock(ch, f) == EOF)
#endif  /* _UNICODE */
        *pnumwritten = -1;
    else
        ++(*pnumwritten);
}

 

  可見,因現在采用的是MBCS字符集,它是調用_putc_nolock函數來輸出字符的。
  在VS2005中,_putc_nolock函數無法按F11單步跟蹤進去。而且“C:\VS8_2005\VC\crt\src”目錄下也找不到_putc_nolock函數的源碼。
  雖然無法看見_putc_nolock函數的源碼,但根據測試結果可以知道,它能正常的處理窄字符串。


4.2 未初始化locale

  未初始化locale時,“printf("\t%s\n", psa)”的執行路徑與先前相同,最終調用_putc_nolock逐個逐個的輸出窄字符。


五、printf輸出寬字符串

5.1 已初始化locale

  “printf("\t%ls\n", psw)”表示使用printf輸出寬字符串。按F11單步跟蹤,它依次進入了下列函數——
printf:[C庫] 帶格式輸出。
_output_l:[C庫] 根據locale信息進行帶格式輸出。對格式字符串進行解析,根據“%ls”提取寬字符串,隨后調用wctomb_s進行編碼轉換。
wctomb_s:[C庫] (緩沖安全版)將寬字符轉為多字節字符(公開方法)。
_wctomb_s_l:[C庫] (緩沖安全版)將寬字符轉為多字節字符(內部實現)。

  此時的調用棧——
> msvcr80d.dll!_wctomb_s_l(int * pRetValue=0x0012fb2c, char * dst=0x0012fb24, unsigned int sizeInBytes=0x00000006, wchar_t wchar=L'W', localeinfo_struct * plocinfo=0x00000000)  行115 C++
  msvcr80d.dll!wctomb_s(int * pRetValue=0x0012fb2c, char * dst=0x0012fb24, unsigned int sizeInBytes=0x00000006, wchar_t wchar=L'W')  行145 + 0x18 字節 C++
  msvcr80d.dll!_output_l(_iobuf * stream=0x10311d20, const char * format=0x0041781c, localeinfo_struct * plocinfo=0x00000000, char * argptr=0x0012fe54)  行2252 + 0x2d 字節 C++
  msvcr80d.dll!printf(const char * format=0x00417818, ...)  行63 + 0x18 字節 C
  wchar_crtbug_2005.exe!main(int argc=0x00000001, char * * argv=0x003b6a00)  行51 + 0x13 字節 C++

  在_wctomb_s_l函數中,因為現在已初始化locale,所以它能能正確的將寬字符串轉為窄字符串。
  編碼轉換成功后,又會回到_output_l函數。它會調用write_string輸出轉換后的窄字符串,依次進入了下列函數——
write_string:[C庫] 寫窄字符串。循環對源串中的每一個字符調用write_char。
write_char:[C庫] 寫窄字符。調用_putc_nolock函數正常的輸出窄字符串。


5.2 未初始化locale

  未初始化locale時,“printf("\t%ls\n", psw)”的執行路徑與先前大致相同,也調用_wctomb_s_l進行編碼轉換。
  在_wctomb_s_l函數中,因為現在使用的是C默認locale(未初始化locale),對於編碼大於255的字符會報錯,於是造成可寬字符串不能輸出。


六、總結

  總結一下不能輸出時的原因——
已初始化locale時,cout無法輸出中文窄字符串:因為_write_nolock函數中的條件判斷存在漏洞,導致漢字的首字節無法輸出。
已初始化locale時,wcout無法輸出中文寬字符串:因為_write_nolock函數中的條件判斷存在漏洞,導致漢字的首字節無法輸出。
未初始化locale時,wcout無法輸出中文寬字符串:因為在C默認locale時的_wctomb_s_l函數不支持編碼大於255的字符。
未初始化locale時,printf無法輸出中文寬字符串:因為在C默認locale時的_wctomb_s_l函數不支持編碼大於255的字符。

  其中前2條是bug,而后2條是C標准中規定的。

 

參考資料——
《ISO/IEC 9899:1999》(C99). ISO/IEC,1999. www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf
《C++ International Standard - ISO IEC 14882 Second edition 2003》(C++03). ISO/IEC,2003-10-15.
《C++標准程序庫—自修教程與參考手冊》. Nicolai M.Josuttis 著,侯捷、孟岩 譯. 華中科技大學出版社,2002-09.
《[C] 跨平台使用TCHAR——讓Linux等平台也支持tchar.h,解決跨平台時的格式控制字符問題,多國語言的同時顯示》. http://www.cnblogs.com/zyl910/archive/2013/01/17/tcharall.html
《[C++] cout、wcout無法正常輸出中文字符問題的深入調查(1):各種編譯器測試》. http://www.cnblogs.com/zyl910/archive/2013/01/20/wchar_crtbug_01.html

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM