acmilan作者
11年3个月前修改于 11年3个月前IP:四川
789660
3楼
四、关于字符串、字符集那些事

和现代Windows操作系统一样,COM也使用UTF-16 LE字符集的16位wchar_t字符串(文档中称之为LPCOLESTR或LPCWSTR)。C/C++对wchar_t字符串也有支持,比如C的wcslen、wcscpy等函数、C++的wstring字符串对象等,WinAPI也有lstrlenW、lstrcpyW等函数可以处理它们。

由于字符串都是基于wchar_t的,因此COM和WinAPI之间可以正常交换字符串。所以在Win32程序中使用COM并不需要进行转码。但是对于C/C++的控制台输入输出函数(如scanf、printf)来说,使用wchar_t却容易出问题。由于它们是基于char的,wchar_t需要在内部转换成char才能正常输出。输出不正常的主要原因是VC运行库默认总是使用"C"这种区域设定(即将Unicode的00-FF段直接对应到单字节,大于FF的直接丢弃)进行转码。解决这个问题通常有三个方法:

第一个方法是使用setlocale(LC_ALL, ".OCP");和locale::global(locale(".OCP"));重置一下区域设定。但是C语言中对于宽字符的输入输出,需要使用特殊的函数,其中许多带有下划线(如_getws_s),不太美观。并且不支持UTF-8(setlocale(LC_ALL, ".65001")返回NULL)。

第二个方法是不使用VC运行时的转码机制,改用WinAPI转码。Windows有两个WinAPI可以实现wchar_t和char之间的转码:MultiByteToWideChar和WideCharToMultiByte。它们不受VC运行库的限制,但是使用较为麻烦,这里不考虑。

第三个方法是使用ATL为我们定义了两个伪函数CW2A和CA2W。它们其实是转码中间层,我们调用的是它的构造函数。由于它们实际上也是调用的WinAPI转码,因此同样不用受VC运行库的限制。CW2A的作用是将wchar_t字符串(Wide)转换为临时char字符串(ANSI),而CA2W则是将char字符串(ANSI)转换为临时wchar_t字符串(Wide)。使用它们要包含atlbase.h。

CW2A和CA2W的使用方法很简单:

1.将char字符串传入CA2W,可得到临时wchar_t字符串(用于COM调用)
wcscpy(my_wide_string, CA2W("我是普通char字符串")); // 使用C式wchar_t[]字符串保存
wstring my_wide_string2 = CA2W("我是普通char字符串"); // 使用C++式wstring字符串保存
wstring my_wide_string3 = CA2W("我是普通char字符串", 950); // 使用C++式wstring字符串保存,char使用Big5字符集(950)
hr = pISpVoice->Speak(CA2W("我是普通char字符串"), SPF_DEFAULT, NULL); // 在COM中直接使用

2.将wchar_t字符串传入CW2A,可得到临时char字符串(用于C/C++的输入输出)
strcpy(my_ansi_string, CW2A(L"我是wchar_t字符串")); // 使用C式char[]字符串保存
string my_ansi_string2 = CW2A(L"我是wchar_t字符串"); // 使用C++式string字符串保存
string my_ansi_string3 = CW2A(L"我是wchar_t字符串", 65001); // 使用C++式string字符串保存,char使用UTF-8字符集(65001)
printf("字符串:%s\n", (char*)CW2A(L"我是wchar_t字符串", 1)); // 输出字符串,OEM代码页,这里要手动转换为(char *)

CW2A和CA2W有个可选参数,可指定代码页,其中有一些系统指定的特殊代码页:
0 当前ANSI代码页CP_ACP(记事本编码)(简体中文936,繁体中文950,英文1252)(默认值)
1 当前OEM代码页CP_OEMCP(命令提示符编码)(简体中文936,繁体中文950,英文437)
65000 UTF-7代码页CP_UTF7(不常用)
65001 UTF-8代码页CP_UTF8(常用国际编码)

有些文章可能提到可以用wchar_t *或char *变量来接收传过来的字符串,这是错误的。C++中通过直接调用构造函数创建的对象,生命期仅限本条语句。CA2W和CW2A对于128个字符以内的字符串是分配在栈上的,即使析构了字符串有时还可以从栈中读出。但是对于特别长的字符串则会分配在堆上,用char *或wchar_t *接收的只是地址而已,CW2A或CA2W的析构函数已经调用,为字符串所分配的空间已释放,再访问它肯定会崩溃。

在这里我们实现一个可以交互式阅读文本的程序,在命令提示符(OEM代码页)输入语句,即可让Windows的语音引擎为我们读出来。

首先我们要使用gets_s读入字符串(使用起来比gets差不多,但能正确判断缓冲区大小):
char strinput[200] = "";
gets_s(strinput); // 从控制台读入字符串

然后我们将pISpVoice->Speak第一个参数L"电脑在说话!"替换为CA2W(strinput, 1)即可:
hr = pISpVoice->Speak(CA2W(strinput, 1), SPF_DEFAULT, NULL);
ValidateHR(hr);

为了能够重复输入,我们将上述两个过程套进while(true)循环里,并判断是否输入了exit,若有则退出:
if (strcmpi(strinput, "exit") == 0) break; // 如果输入了exit则退出

最终代码如下。现在,你可以让系统读出你输入的任意字符串了。[s::lol]
#include <stdio.h>
#include <stdlib.h>
                                                                                     
#include <windows.h> // 使用COM组件需要包含windows.h
#include <sapi.h> // 使用ISpVoice语音组件需要包含sapi.h
#include <atlbase.h> // 使用CComPtr<t>和CA2W需要包含atlbase.h
                                                                                     
#define ValidateHR(hr) \
    if (FAILED(hr)) { \
        printf("HRESULT错误:%p,在%s第%d行\n", hr, __FILE__, __LINE__); \
        exit(1); \
    }
                                                                                     
                                                                                     
int main()
{
                                                                                     
    HRESULT hr = CoInitialize(0); // 初始化COM环境,参数保留,必须为0
    ValidateHR(hr);
                                                                                     
    {
        // 建立ISpVoice接口实例(组件ID为CLSID_SpVoice)
        CComPtr<ispvoice> pISpVoice;
        hr = pISpVoice.CoCreateInstance(CLSID_SpVoice); // 省略了后两个参数:NULL, CLSCTX_ALL
        ValidateHR(hr);
                                                                                     
        while (true)
        {
            char strinput[200] = "";
            gets_s(strinput); // 从控制台读入字符串(OEM代码页)
                                                                                     
            if (strcmpi(strinput, "exit") == 0) // 如果输入了exit则退出
                break;
                                                                                     
            // 调用ISpVoice的Speak成员函数(使用CA2W转码)
            hr = pISpVoice->Speak(CA2W(strinput, 1), SPF_DEFAULT, NULL);
            ValidateHR(hr);
        }
                                                                                     
    } // pISpVoice在作用域边界会自动释放
                                                                                         
    CoUninitialize(); // 卸载COM环境
    return 0;
}</ispvoice></t></atlbase.h></sapi.h></windows.h></stdlib.h></stdio.h>
评论
文号 / 789660

千古风流
名片发私信
学术分 4
总主题 466 帖总回复 2942 楼拥有证书:进士 学者 笔友
注册于 2009/05/30 21:22:42最后登录 2019/01/31 17:16:38
主体类型:个人
所属领域:无
认证方式:邮箱
IP归属地:0

个人简介

暂未填写
笔记
{{note.content}}
{{n.user.username}}
{{fromNow(n.toc)}} {{n.status === noteStatus.disabled ? "已屏蔽" : ""}} {{n.status === noteStatus.unknown ? "正在审核" : ""}} {{n.status === noteStatus.deleted ? '已删除' : ''}}
  • 编辑
  • 删除
  • {{n.status === 'disabled' ? "解除屏蔽" : "屏蔽" }}
我也是有底线的