
1. 项目概述为什么在VS2012的VC里读写Excel的Unicode是个“老大难”如果你正在用Visual Studio 2012VS2012配合VC也就是我们常说的Visual C做开发并且项目里需要处理Excel文件特别是文件内容里包含了中文、日文或者任何非ASCII字符那你大概率已经踩过或者即将踩进一个经典的“坑”乱码。这个项目标题——“在VS2012中实现VC对Excel文件的Unicode读写操作”——精准地戳中了一个在特定技术栈下非常具体且棘手的痛点。它不是一个泛泛的“读写Excel”教程而是聚焦于“Unicode”这个字符编码标准在“VS2012”和“VC”这个略显“复古”但依然广泛存在的开发环境组合下如何打通与Excel文件交互的“任督二脉”。为什么说它是个“老大难”问题因为这里涉及了多个技术层面的历史包袱和兼容性交织。VS2012发布于2012年其VC编译器对C11标准的支持是部分的它默认的字符集设置、库文件链接方式都与后续版本如VS2015及以后有显著差异。而Excel文件尤其是.xls和.xlsx格式内部对文本的存储本质上都是UnicodeUTF-16 LE是主流。当我们用VC去操作时如果还停留在使用char和std::string表示“窄字符”的旧思维或者错误地链接了非Unicode版本的库那么从Excel读出来的中文会变成一堆问号“???”或者乱码“锟斤拷”写进去的数据也会面目全非。这个问题的本质是让VC程序的内存字符串表示宽字符wchar_t和std::wstring与Excel文件内部的Unicode存储格式正确、无损地对接起来。所以这个项目的核心价值在于它提供了一套在VS2012这个特定环境下从工程配置、编码意识到具体代码实现的全链路解决方案。它适合那些维护遗留项目、或因为某些依赖限制必须使用VS2012的C开发者也适合任何想深入理解Windows平台下字符编码、COM技术以及Office自动化原理的程序员。通过搞定这个问题你不仅能完成手头的Excel数据处理任务更能透彻理解从“窄字节”到“宽字节”的演进以及如何在老环境中实践正确的Unicode编程范式。2. 环境准备与核心思路拆解在动手写代码之前正确的环境配置和清晰的实现思路是成功的一半。在VS2012的VC项目中处理Excel我们通常不推荐直接从二进制层面解析.xlsx文件那太复杂了而是利用微软提供的自动化接口。这里主要有两条技术路径我们的方案选择需要基于它们的特点和VS2012的环境来权衡。2.1 技术路径选择OLE Automation vs. 第三方库1. OLE AutomationCOM技术这是最“原生”的Windows方案。Excel作为一个COM服务器暴露出一系列接口如_Application_Workbook_WorksheetRange。我们的VC程序作为客户端通过COM技术调用这些接口来操作Excel。这种方式功能最强大、最直接能使用Excel几乎全部对象模型。优点功能完整无需额外依赖库与Excel版本绑定通过类型库学习一次可通用于其他Office组件。缺点需要熟悉COM编程代码相对繁琐进程间调用如果Excel作为独立进程启动可能有性能开销且对Unicode的支持需要特别注意接口方法是否接受BSTR类型。2. 第三方库如libxlsxwriter, OpenXLSX这些是纯C/C库直接生成或解析Excel文件格式不依赖安装Excel程序。优点部署简单性能好不依赖Office环境。缺点功能可能不如COM接口全面特别是对于复杂的格式、图表对于读写都需要支持且功能要求高的场景库的选择和集成复杂度不低。我们的选择与理由 对于标题中强调的“Unicode读写操作”并且考虑到VS2012的环境我们选择OLE AutomationCOM方案。原因如下功能完整性确保读写操作能覆盖所有单元格内容、格式等兼容性好。环境一致性VS2012对COM的支持非常成熟通过“导入类型库”可以方便地生成智能指针包装类简化编程。Unicode支持直接COM接口中广泛使用BSTRBasic String类型它本质上就是一个指向宽字符wchar_t字符串的指针原生支持Unicode。只要我们正确设置项目属性并使用宽字符版本的类型库就能实现Unicode字符串的传递。注意有些古老的教程可能会提到使用_variant_t包装char*来传递字符串这在非Unicode项目设置下会导致编码问题。我们的核心思路是全程使用宽字符。2.2 VS2012项目关键配置详解正确的项目配置是避免后续各种编译和运行时错误的基石。请严格按照以下步骤操作创建项目新建一个Visual C的“Win32控制台应用程序”或“MFC应用程序”根据你的UI需求。在应用设置中建议取消“预编译头”以减少复杂度但这不是必须的。设置字符集最关键的一步打开项目“属性页”。进入“配置属性” - “常规”。找到“字符集”选项将其从“使用多字节字符集”改为**“使用Unicode字符集”**。这步做了什么它定义了项目级的宏_UNICODE和UNICODE。这会改变一系列CRTC运行时库和Windows API函数的版本。例如_tprintf会根据这个设置被编译为printf多字节或wprintf宽字符。我们的目标是让所有字符串处理都基于wchar_t。导入Excel类型库Type Library在“解决方案资源管理器”中右键点击你的项目选择“添加” - “类”。在弹出的对话框中选择左侧的“Visual C” - “MFC”然后在中间选择“TypeLib中的MFC类”。点击“添加”。在“可用类型库”列表中找到并选择“Microsoft Excel XX.X Object Library”。这里的“XX.X”对应你电脑上安装的Excel版本号例如15.0对应Excel 201316.0对应Excel 2016/2019/365。VS2012可能只能识别较老的版本但高版本库通常向下兼容。在“接口”列表中选择你需要的主要类至少应包括_Application_Workbook_WorksheetRange点击“完成”。这会在你的项目中生成一系列以“C”开头如CApplicationCWorkbook的包装类它们封装了COM接口的智能指针并提供了更易用的方法。设置编译依赖由于使用了MFC的包装类即使你是控制台程序这些类也依赖于MFC的COM支持你需要确保链接了必要的库。在项目属性中“配置属性” - “链接器” - “输入” - “附加依赖项”中通常需要添加mfc110.libmfcs110.lib具体版本号110对应VS2012等。更简单的方法是在创建项目时如果选择了MFC这些会自动配置如果是控制台程序你可能需要在stdafx.h如果用了预编译头或代码开头显式#include afxdisp.h并且确保项目设置中“MFC的使用”为“在共享DLL中使用MFC”。调试设置可选但重要如果你在调试时遇到“CoInitialize未调用”的错误请确保你的main或WinMain函数开头调用了CoInitialize(NULL);或AfxOleInit();后者在MFC项目中更常用。并在程序退出前调用对应的CoUninitialize();。3. 核心代码实现从创建到读写的完整流程配置好环境后我们进入核心的代码实现环节。我将用一个完整的示例演示如何创建Excel应用、写入Unicode字符串、读取它们并保存。3.1 初始化与Excel实例创建首先我们需要初始化COM库并启动或连接一个Excel应用程序实例。#include afxdisp.h // MFC自动化核心头文件 #include iostream int main() { // 初始化COM库对于控制台程序使用CoInitialize // 如果是在MFC程序中通常在InitInstance里调用AfxOleInit()更合适 CoInitialize(NULL); try { // 创建Excel Application对象 CApplication excelApp; if (!excelApp.CreateDispatch(_T(Excel.Application))) { std::wcerr L无法启动Excel应用程序。请确保Excel已安装。 std::endl; CoUninitialize(); return -1; } // 使Excel应用程序可见调试时方便查看 excelApp.put_Visible(TRUE); // 禁用警告提示如覆盖保存 excelApp.put_DisplayAlerts(FALSE); // 添加一个新的工作簿 CWorkbooks workbooks excelApp.get_Workbooks(); CWorkbook workbook workbooks.Add(); CWorksheets worksheets workbook.get_Worksheets(); CWorksheet worksheet worksheets.get_Item(COleVariant((short)1)); // 获取第一个工作表 // ... 后续的读写操作将在这里进行 ... // 保存工作簿到指定路径 CString strFilePath _T(C:\\Test\\Unicode_Test.xlsx); workbook.SaveAs(COleVariant(strFilePath), COleVariant((long)-4143), // xlWorkbookDefault COleVariant(), COleVariant(), COleVariant(), COleVariant(), COleVariant((long)0), COleVariant(), COleVariant(), COleVariant(), COleVariant()); // 关闭工作簿并退出Excel workbook.Close(COleVariant((BOOL)FALSE), COleVariant(), COleVariant()); excelApp.Quit(); } catch (COleException* e) { TCHAR szError[256]; e-GetErrorMessage(szError, 256); std::wcerr LOLE异常: szError std::endl; e-Delete(); } catch (COleDispatchException* e) { std::wcerr L自动化异常: e-m_strDescription.GetString() std::endl; e-Delete(); } catch (...) { std::wcerr L发生未知异常。 std::endl; } CoUninitialize(); return 0; }关键点解析CApplicationCWorkbook等是由“导入类型库”步骤生成的包装类。_T(“Excel.Application”)是一个宏在Unicode项目下会扩展为L”Excel.Application”确保传递给COM的是宽字符串。CreateDispatch尝试创建或获取一个Excel的COM服务器实例。COleVariant是MFC中用于包装VARIANT数据类型的类COM接口的许多参数需要它。我们用它将shortlongBOOLCString等类型安全地转换为VARIANT。3.2 写入Unicode内容到单元格现在我们向工作表的特定单元格写入包含中文、英文混合的Unicode字符串。// 接上面的代码在获取worksheet之后 // 假设我们要向A1和B1单元格写入数据 CRange rangeA1 worksheet.get_Range(COleVariant(_T(A1)), COleVariant(_T(A1))); CRange rangeB1 worksheet.get_Range(COleVariant(_T(B1)), COleVariant(_T(B1))); // 准备要写入的Unicode字符串 CString strData1 _T(Hello, 世界); // 中英文混合 CString strData2 _T(测试数据αβγΔΘ); // 包含希腊字母 // 写入单元格 rangeA1.put_Value2(COleVariant(strData1)); // 使用Value2属性它直接处理值不涉及格式转换更推荐 rangeB1.put_Value2(COleVariant(strData2)); // 也可以批量写入一个区域 CRange rangeA2_B3 worksheet.get_Range(COleVariant(_T(A2)), COleVariant(_T(B3))); // 构造一个二维SAFEARRAY安全数组来一次性写入多行多列数据 // 这是一个稍微高级但效率更高的操作 SAFEARRAY* psa SafeArrayCreateVector(VT_VARIANT, 0, 4); // 创建包含4个VARIANT的一维数组 VARIANT* pVarArray; SafeArrayAccessData(psa, (void**)pVarArray); // 填充数据 pVarArray[0] COleVariant(_T(张三)).Detach(); // Detach取出底层的VARIANT pVarArray[1] COleVariant(_T(工程师)).Detach(); pVarArray[2] COleVariant(_T(李四)).Detach(); pVarArray[3] COleVariant(_T(设计师)).Detach(); SafeArrayUnaccessData(psa); // 将一维数组转换为适合Range的二维数组2行2列 SAFEARRAY* psa2D SafeArrayCreate(VT_VARIANT, 2, {2, 2} /* 维度 */); // 这里省略了详细的数组元素搬运代码通常我们会直接对目标Range的每个单元格赋值代码更清晰。 // 对于简单批量操作可以用循环 CString arrData[2][2] {{_T(张三), _T(工程师)}, {_T(李四), _T(设计师)}}; for(int i0; i2; i){ for(int j0; j2; j){ CRange cell worksheet.get_Range( COleVariant( CString().Format(_T(%c%d), LAj, i2) ), COleVariant( CString().Format(_T(%c%d), LAj, i2) ) ); cell.put_Value2(COleVariant(arrData[i][j])); } }为什么用Value2而不是ValueValue属性在读写时可能会涉及一些额外的格式转换例如货币、日期而Value2属性直接获取或设置单元格的原始值不进行任何转换。对于字符串尤其是Unicode字符串和数字使用Value2更直接、更安全避免了潜在的转换错误。这是处理数据时一个重要的经验技巧。3.3 从单元格读取Unicode内容读取操作是写入的逆过程关键在于正确处理返回的VARIANT类型。// 接续上面的代码假设我们要读取刚才写入的A1和B1单元格 rangeA1 worksheet.get_Range(COleVariant(_T(A1)), COleVariant(_T(A1))); rangeB1 worksheet.get_Range(COleVariant(_T(B1)), COleVariant(_T(B1))); COleVariant varValueA1 rangeA1.get_Value2(); COleVariant varValueB1 rangeB1.get_Value2(); // 检查并转换VARIANT类型 if (varValueA1.vt VT_BSTR) { // VT_BSTR 表示返回的是一个BSTR宽字符串 CString strReadA1 varValueA1.bstrVal; // COleVariant 可以自动转换到CString std::wcout L单元格A1的内容: (LPCTSTR)strReadA1 std::endl; } else if (varValueA1.vt VT_R8) { // 可能是数字 double dVal varValueA1.dblVal; std::wcout L单元格A1的内容(数字): dVal std::endl; } else { std::wcout L单元格A1的内容类型未知或为空。 std::endl; } // 更通用的处理方式使用COleVariant的ChangeType方法尝试转换为字符串 COleVariant varStrA1 varValueA1; varStrA1.ChangeType(VT_BSTR); if (varStrA1.vt VT_BSTR) { std::wcout L转换后A1内容: varStrA1.bstrVal std::endl; } // 对于B1单元格同样处理 if (varValueB1.vt VT_BSTR) { std::wcout L单元格B1的内容: varValueB1.bstrVal std::endl; }读取时的核心要点类型判断一定要检查VARIANT的vt类型标签字段。单元格可能包含字符串VT_BSTR、数字VT_R8、布尔值VT_BOOL、错误VT_ERROR或为空VT_EMPTY。BSTR的生命周期COleVariant类在其析构时会自动释放其包含的BSTR资源所以我们通常不需要手动调用SysFreeString。但如果你直接操作VARIANT务必注意这一点。空单元格空单元格的vt可能是VT_EMPTY。直接访问其bstrVal会导致错误。4. 高级话题与性能优化当基本读写满足后我们可能会面临更复杂的需求比如处理大量数据、设置格式或处理更复杂的文件结构。4.1 批量读写与数组操作对于成千上万个单元格的读写逐个单元格操作get_Range-put_Value2速度极慢。最高效的方式是使用数组VARIANT的SAFEARRAY进行批量操作。批量写入示例// 假设我们要将一块10行 x 5列的数据写入到Sheet1的A1:E10 CRange targetRange worksheet.get_Range(COleVariant(_T(A1)), COleVariant(_T(E10))); // 1. 在内存中构建一个二维VARIANT数组 (SAFEARRAY) SAFEARRAYBOUND rgsabound[2]; rgsabound[0].cElements 10; // 10行 rgsabound[0].lLbound 0; rgsabound[1].cElements 5; // 5列 rgsabound[1].lLbound 0; SAFEARRAY* psa SafeArrayCreate(VT_VARIANT, 2, rgsabound); // 2. 填充数据到SAFEARRAY long indices[2]; for (long row 0; row 10; row) { indices[0] row; for (long col 0; col 5; col) { indices[1] col; CString strValue; strValue.Format(_T(R%dC%d), row1, col1); COleVariant varValue(strValue); // 将VARIANT放入数组指定位置。注意SafeArrayPutElement会复制VARIANT。 // 我们需要传递varValue的底层VARIANT并确保其内容如BSTR被正确复制。 // 一种方法是使用VariantCopy。但更简单的方式是利用COleVariant的Detach/Attach不过要小心内存管理。 // 这里演示一种相对安全的方式创建新的VARIANT并复制。 VARIANT vtItem; VariantInit(vtItem); VariantCopy(vtItem, (VARIANT)varValue); // 复制 SafeArrayPutElement(psa, indices, vtItem); // 注意vtItem的内容如BSTR已被复制到SAFEARRAY中此处的vtItem需要清理吗 // 实际上SafeArrayPutElement内部会复制VARIANT的内容所以我们本地的vtItem可以销毁。 VariantClear(vtItem); // 清理本地副本 } } // 3. 将整个数组一次性赋值给Range COleVariant varArray; varArray.vt VT_ARRAY | VT_VARIANT; varArray.parray psa; targetRange.put_Value2(varArray); // 使用Value2 // 4. 清理SAFEARRAY (非常重要) // COleVariant varArray在析构时会调用VariantClear进而清理parray。 // 但因为我们把psa直接赋给了varArray.parrayvarArray析构时会负责清理。 // 所以这里我们不应该再手动SafeArrayDestroy(psa)否则会导致双重释放。 // 正确做法是在将psa交给varArray后就不要再手动管理psa的生命周期。批量读取示例// 读取A1:E10区域的数据到数组 CRange sourceRange worksheet.get_Range(COleVariant(_T(A1)), COleVariant(_T(E10))); COleVariant varData sourceRange.get_Value2(); // 获取整个区域的值 if (varData.vt (VT_ARRAY | VT_VARIANT)) { SAFEARRAY* psaRead varData.parray; long lBoundRow, uBoundRow, lBoundCol, uBoundCol; SafeArrayGetLBound(psaRead, 1, lBoundRow); // 第一维行的下界 SafeArrayGetUBound(psaRead, 1, uBoundRow); // 第一维的上界 SafeArrayGetLBound(psaRead, 2, lBoundCol); // 第二维列的下界 SafeArrayGetUBound(psaRead, 2, uBoundCol); // 第二维的上界 long indices[2]; for (long row lBoundRow; row uBoundRow; row) { indices[0] row; for (long col lBoundCol; col uBoundCol; col) { indices[1] col; VARIANT vtCell; VariantInit(vtCell); SafeArrayGetElement(psaRead, indices, vtCell); // 获取元素 // 处理vtCell... if (vtCell.vt VT_BSTR) { std::wcout vtCell.bstrVal L\t; } else if (vtCell.vt VT_R8) { std::wcout vtCell.dblVal L\t; } VariantClear(vtCell); // 清理获取到的元素VARIANT } std::wcout std::endl; } } // varData析构时会自动清理psaRead批量操作的注意事项性能飞跃对于大数据量数组操作比循环操作单个单元格快几个数量级。内存管理是难点SAFEARRAY和VARIANT的内存管理非常容易出错导致内存泄漏或崩溃。务必理清每个VARIANT和BSTR的所有权。使用COleVariant和MFC包装类能在很大程度上简化管理但在直接操作SAFEARRAY时仍需谨慎。索引从1开始注意通过Excel对象模型获取的Range的Value2属性返回的数组其索引默认是从1开始的LBound 1而不是编程中常见的0。这与VBA中数组的行为一致。但在某些情况下尤其是通过_Worksheet的Range直接获取的数组也可能是基于1的。最佳实践是总是使用SafeArrayGetLBound和SafeArrayGetUBound来获取实际的边界而不是假设。4.2 文件格式与编码的深层考量标题强调“Unicode读写”我们还需要关注文件保存时的格式。.xls vs .xlsx.xlsExcel 97-2003格式对Unicode的支持是完整的但文件是二进制格式。.xlsxExcel 2007是基于XML的开放格式OOXML内部文本默认使用UTF-8或UTF-16。我们的COM接口调用SaveAs时通过文件扩展名.xlsx和FileFormat参数如xlOpenXMLWorkbook 值51来指定格式。无论哪种格式只要通过COM接口正确传递了BSTRExcel都会妥善处理Unicode存储。SaveAs方法的FileFormat参数在之前的保存代码中我们使用了COleVariant((long)-4143)这是常量xlWorkbookDefault的值它会让Excel根据文件扩展名决定默认格式。更明确的写法是使用预定义的常量// 保存为Excel 2007 .xlsx 格式 const long xlOpenXMLWorkbook 51; workbook.SaveAs(COleVariant(strFilePath), COleVariant(xlOpenXMLWorkbook), ...); // 保存为Excel 97-2003 .xls 格式 const long xlExcel8 56; workbook.SaveAs(COleVariant(strFilePath), COleVariant(xlExcel8), ...);使用明确的格式常量可以提高代码的可读性和可维护性。CSV/TXT导出如果你需要将数据导出为纯文本文件如CSV并保持Unicode需要在保存后或通过另存为操作并指定正确的编码。Excel的“另存为”对话框可以选择编码如UTF-8。通过自动化你可以调用Workbook.SaveAs并设置FileFormat为xlCSVUTF8值62 Excel 2016或先保存为Unicode文本xlUnicodeText 值42。但注意不同Excel版本对这些常量的支持可能不同需要测试。5. 避坑指南与疑难杂症排查在实际开发中你几乎一定会遇到下面这些问题。这里是我踩过坑后总结的排查清单。5.1 编译与链接错误“未找到标识符”或“CApplication等类未声明”原因没有成功导入Excel类型库或者生成的头文件没有被包含到当前编译单元。解决检查“类视图”中是否有CApplication等类。确保在stdafx.h或源文件开头#include了生成的头文件通常叫excel.h或类似名称可以在“解决方案资源管理器”的“头文件”筛选器下找到。“无法解析的外部符号”链接错误原因项目没有链接必要的MFC库。解决对于控制台项目在项目属性 - 常规 - 项目默认值中将“MFC的使用”改为“在共享DLL中使用MFC”。或者手动在链接器输入中添加mfc110.libmfcs110.lib等。确保#include afxdisp.h。“CoInitialize未调用”运行时错误原因COM库未在调用COM接口前初始化。解决在main或线程入口函数开始处调用CoInitialize(NULL);或AfxOleInit();MFC程序。并在程序结束前调用CoUninitialize();。5.2 运行时错误与乱码问题“自动化服务器不能创建对象”或“类未注册”原因电脑上没有安装Excel或者安装的Excel版本与代码中ProgID”Excel.Application”不匹配/损坏。解决确保安装了Excel。可以尝试在“运行”中输入excel.exe看能否启动。对于版本问题可以尝试使用版本独立的ProgID”Excel.Application”它通常指向最新安装的版本。也可以尝试具体版本如”Excel.Application.16″。写入或读取的内容是乱码原因A项目字符集设置错误。这是最常见的原因。排查检查项目属性 - 常规 - 字符集必须是“使用Unicode字符集”。原因B字符串字面量没有使用_T()或L前缀。排查确保所有硬编码的字符串如”A1″ 都用_T(“A1”)包裹。对于直接赋值给CString或BSTR的字符串使用L前缀如CString str L”中文”;。原因C使用了错误的接口或属性。例如某些旧的教程可能使用接受char*的参数或者使用了Value属性而非Value2。排查坚持使用由类型库生成的最新包装类的方法并使用Value2属性进行数据读写。原因D从其他窄字符源如std::stringchar[]转换到宽字符时编码转换错误。解决如果源数据是窄字符如从ANSI文件读取你需要知道它的原始编码如GBK然后使用MultiByteToWideChar函数配合正确的代码页进行转换。切勿直接强制类型转换。程序退出后Excel进程仍在后台运行原因Excel COM对象没有被正确释放。解决确保按照关闭工作簿-Quit应用-释放智能指针通常自动的顺序操作。即使发生异常也要在catch块中确保执行退出逻辑。可以使用try-catch包裹在catch后也调用Quit。更优雅的做法是使用RAII思想将Excel应用对象的创建和销毁封装在一个类中。批量操作时程序变慢或内存占用高原因频繁的COM跨进程调用、或大型SAFEARRAY内存管理不当。优化使用数组批量操作如前所述这是最大的性能提升点。禁用屏幕更新和事件在开始大批量操作前设置excelApp.put_ScreenUpdating(FALSE);和excelApp.put_EnableEvents(FALSE);。操作完成后恢复。手动计算模式如果操作涉及大量公式设置excelApp.put_Calculation(xlCalculationManual); 完成后设置为xlCalculationAutomatic。及时释放对象循环中创建的CRange等对象在不再需要时尽早将其赋值为空或让其离开作用域以释放COM引用。5.3 调试技巧使用.GetLastError()或异常信息MFC的COleDispatchException会包含错误描述m_strDescription。捕获并打印它。逐步执行并观察本地变量在调试器中将COleVariant变量添加到监视窗口展开查看其vt类型和实际值如bstrVal这是判断数据是否正确传递的最直接方法。先录制宏再对照代码在Excel中打开“开发者工具”录制一个你想要完成的操作如写入某些文字到单元格。停止录制后查看生成的VBA代码。VBA的语法和对象模型与通过COM自动化调用非常相似可以为你提供清晰的参考。例如VBA中的Range(“A1”).Value2 “测试”对应到C就是range.put_Value2(COleVariant(L”测试”));。在我自己的项目中最深刻的教训就是对字符集设置的忽视。曾经在一个大型遗留项目迁移时因为一个子项目库的字符集设置是“多字节”而主程序是“Unicode”导致链接时没有报错但运行时字符串传递彻底混乱排查了整整两天。所以现在我的第一条规则就是在VC项目中统一使用Unicode字符集这是现代Windows开发的基石。对于必须与老旧窄字符接口交互的部分明确地在边界进行编码转换而不是混用。