C++中如何按区域设置逐字符比较C风格宽字符串的wchar_t
问题描述
我手里有两个非std::wstring的C风格空终止宽字符串,想通过迭代器遍历,按指定区域设置(_locale_t)逐字符比较来实现排序。因为必须遵循区域规则,所以不能直接用==、>、<这些标准运算符。目前只找到能比较整串的_wcsicmp_l函数,直接传wchar_t指针的话会被当成字符串比,不是单个字符。我写了段示例代码,想问问这个实现合不合理、有没有性能坑,还有没有更省事的写法,最终目标是实现包含字符和数字的自然排序。
示例代码:
inline bool compareChars(const wchar_t& lhs, const wchar_t& rhs) { //必须加终止符\0,不然会从当前字符一直比较到字符串结尾,不是只比单个字符 wchar_t str_lhs[2]{ lhs, L'\0' }, str_rhs[2]{ rhs, L'\0' }; _locale_t loc = _create_locale(LC_ALL, "cs-CZ.utf8"); bool res = _wcsicoll_l(&str_lhs[0], &str_rhs[0], loc) < 0; _free_locale(loc); return res; } int main() { const wchar_t* firstStr = L"Test", * secondStr = L"Test2"; const wchar_t* it1 = firstStr; const wchar_t* it2 = secondStr; const wchar_t* itEnd1 = it1; while (*itEnd1 != '\0')itEnd1++; const wchar_t* itEnd2 = it2; while (*itEnd2 != '\0')itEnd2++; for (; it1 != itEnd1; ++it1) //遍历第一个字符串 for (; it2 != itEnd2; ++it2) //遍历第二个字符串 { int res = compareChars(*it1, *it2); //把字符当字符串的一部分比较 } return 0; }
问题解答
一、现有实现的合理性
你的写法逻辑上能跑通:把单个宽字符包装成带终止符的字符串,再用_wcsicoll_l按指定区域规则比较,确实能得到符合区域设置的单字符比较结果,但有几个明显的问题:
- 每次比较都重复创建销毁区域对象:
_create_locale和_free_locale是开销不小的操作,完全没必要每次比字符都搞一遍。 - 嵌套循环逻辑错了:现在的两层for循环会把第一个字符串的每个字符和第二个字符串的每个字符全比一遍,这根本不是字符串逐位比较的逻辑——正确的应该是同步遍历两个字符串,同一位置的字符比,直到出现差异或者其中一个字符串结束。
二、性能问题
现有实现的性能坑特别严重:
- 区域对象反复创建销毁:
_create_locale需要加载区域相关的数据,频繁调用会带来大量不必要的系统开销,要是你用这个逻辑排序大量字符串,性能会崩得很明显。 - 冗余内存操作:每次比较都要创建两个长度为2的数组并初始化,虽然单次开销不大,但架不住次数多,纯纯浪费。
- 循环逻辑完全错误:本来应该是O(n+m)的逐位比较,现在变成了O(n*m)的全量比较,做了超多无用功,完全不符合字符串排序的需求。
三、更简便高效的实现方式
1. 先优化单字符比较的基础逻辑
把区域对象的创建移到比较函数外面,复用同一个对象,避免反复创建销毁:
// 可以用静态变量存区域对象,程序运行期间只创建一次 static _locale_t cs_locale = _create_locale(LC_ALL, "cs-CZ.utf8"); inline bool compareChars(const wchar_t lhs, const wchar_t rhs) { wchar_t str_lhs[2] = { lhs, L'\0' }; wchar_t str_rhs[2] = { rhs, L'\0' }; // 直接用提前创建好的区域对象 return _wcsicoll_l(str_lhs, str_rhs, cs_locale) < 0; } // 记得程序退出前释放区域对象 void cleanupLocale() { if (cs_locale) _free_locale(cs_locale); }
2. 修正字符串逐位比较的逻辑
要实现字符串按区域排序,得同步遍历两个字符串的对应位置,直到找到差异或其中一个结束:
bool compareStrings(const wchar_t* str1, const wchar_t* str2) { while (*str1 != L'\0' && *str2 != L'\0') { // 这里还是要把单个字符包装成带终止符的字符串,不然_wcsicoll_l会一直往后比 wchar_t s1[2] = { *str1, L'\0' }; wchar_t s2[2] = { *str2, L'\0' }; int cmp = _wcsicoll_l(s1, s2, cs_locale); if (cmp != 0) return cmp < 0; ++str1; ++str2; } // 短字符串排在前面 return *str1 == L'\0'; }
3. 单字符区域比较的更优选择
其实Windows有个更合适的函数CompareStringEx,可以直接按区域设置比较指定长度的字符串,单个字符就传长度1,不用额外包装:
#include <winnls.h> bool compareChars(const wchar_t lhs, const wchar_t rhs) { // 用捷克语区域设置 int result = CompareStringEx( L"cs-CZ", NORM_IGNORECASE, // 忽略大小写,和_wcsicoll_l的行为一致 &lhs, 1, &rhs, 1, nullptr, nullptr, 0 ); return result == CSTR_LESS_THAN; }
这个函数直接支持单字符的区域规则比较,比包装成字符串再调用_wcsicoll_l高效得多。
四、实现包含字符与数字的自然排序
自然排序的核心是把连续的数字序列当成整体数值比较,比如"Test123"要排在"Test45"前面,因为123比45大。实现思路大概是这样:
- 同步遍历两个字符串,跳过前面相同的非数字字符。
- 遇到数字时,分别提取两个字符串里的连续数字序列,转成数值。
- 比较两个数值的大小,不一样就直接返回结果;一样的话继续遍历后面的字符。
- 如果其中一个字符串先结束,短的那个排在前面。
结合区域比较的自然排序示例代码:
#include <winnls.h> #include <cwctype> // 判断是否为宽字符数字 bool isDigit(wchar_t c) { return iswdigit(c) != 0; } // 提取连续数字序列并转成数值 unsigned long long extractNumber(const wchar_t*& str) { unsigned long long num = 0; while (isDigit(*str)) { num = num * 10 + (*str - L'0'); ++str; } return num; } bool naturalCompare(const wchar_t* str1, const wchar_t* str2) { const wchar_t* s1 = str1; const wchar_t* s2 = str2; while (*s1 != L'\0' && *s2 != L'\0') { if (isDigit(*s1) && isDigit(*s2)) { unsigned long long num1 = extractNumber(s1); unsigned long long num2 = extractNumber(s2); if (num1 != num2) return num1 < num2; } else { // 用区域规则比较非数字字符 int cmp = CompareStringEx( L"cs-CZ", NORM_IGNORECASE, s1, 1, s2, 1, nullptr, nullptr, 0 ); if (cmp != CSTR_EQUAL) return cmp == CSTR_LESS_THAN; ++s1; ++s2; } } // 处理其中一个字符串先结束的情况 return *s1 == L'\0'; }
内容的提问来源于stack exchange,提问作者Michal Hadraba
相关产品推荐
相关产品推荐

