C++中的不区分大小写字符串比较

372

在不将字符串转换为所有大写或所有小写的情况下,在C++中进行不区分大小写的字符串比较的最佳方法是什么?

请指出这些方法是否支持Unicode,并且它们的可移植性如何。


@Adam:虽然这种变体在可用性方面很好,但在性能方面很差,因为它创建了不必要的副本。我可能会忽略某些东西,但我认为最好(非Unicode)的方法是使用std::stricmp。否则,请阅读Herb的观点 - Konrad Rudolph
在 C 语言中,通常需要强制将整个字符串转换为大写字母,然后进行比较 - 或者自己编写比较函数 :P - Michael Dorgan
一个稍后的问题有一个更简单的答案:strcasecmp(至少适用于BSD和POSIX编译器)https://dev59.com/mWDVa4cB1Zd3GeqPailc - Móż
@Mσᶎ 这个问题也有答案,但需要注意的是 strcasecmp 不是标准库的一部分,且至少有一个常用编译器中缺失。 - Mark Ransom
30个回答

329

Boost 包含了一个方便的算法:

#include <boost/algorithm/string.hpp>
// Or, for fewer header dependencies:
//#include <boost/algorithm/string/predicate.hpp>

std::string str1 = "hello, world!";
std::string str2 = "HELLO, WORLD!";

if (boost::iequals(str1, str2))
{
    // Strings are identical
}

16
这个是否支持UTF-8?我认为不支持。 - vladr
20
不行,因为UTF-8允许相同的字符串因为重音、组合、双向文本等问题而被编码为不同的二进制代码。 - vy32
12
@vy32 那是完全错误的!UTF-8组合是互相排斥的。它必须始终使用最短可能的表示,如果没有这样做,它就是一个格式错误的UTF-8序列或代码点,必须小心处理。 - Wiz
57
@Wiz,你忽略了 Unicode 字符串规范化的问题。ñ 可以表示为一个组合的 ˜ 和 n,或者使用一个 ñ 字符。在执行比较之前,你需要使用 Unicode 字符串规范化。请查阅 Unicode 技术报告#15:http://unicode.org/reports/tr15/。 - vy32
14
因为"ß"转换成大写字母是"SS": http://www.fileformat.info/info/unicode/char/df/index.htm。 - Mooing Duck
显示剩余10条评论

134
使用boost的问题在于你必须链接并依赖于boost。在某些情况下(例如android)并不容易。
而且使用char_traits意味着所有比较都是不区分大小写的,这通常不是你想要的。
这应该足够了。它应该相当高效。不过它不处理Unicode或其他任何东西。
bool iequals(const string& a, const string& b)
{
    unsigned int sz = a.size();
    if (b.size() != sz)
        return false;
    for (unsigned int i = 0; i < sz; ++i)
        if (tolower(a[i]) != tolower(b[i]))
            return false;
    return true;
}

更新:附加 C++14 版本 (#include <algorithm>):

bool iequals(const string& a, const string& b)
{
    return std::equal(a.begin(), a.end(),
                      b.begin(), b.end(),
                      [](char a, char b) {
                          return tolower(a) == tolower(b);
                      });
}

更新:使用 std::ranges 的 C++20 版本:
#include <ranges>
#include <algorithm>
#include <string_view>

bool iequals(std::string_view lhs, std::string_view rhs) {
    auto to_lower{ std::ranges::views::transform(static_cast<int(*)(int)>(std::tolower)) };
    return std::ranges::equal(lhs | to_lower, rhs | to_lower);
}

33
Boost字符串库实际上是一个仅包含头文件的库,所以不需要链接任何东西。此外,您可以使用Boost的“bcp”工具将字符串头文件复制到您的源代码目录中,这样您就不需要要求完整的Boost库。 - Gretchen
11
了解一个简单且不依赖于boost库的版本很不错。 - Deqing
2
@Anna Boost的文本库需要构建和链接。它使用IBM ICU。 - Behrouz.M
6
直接调用std::tolower函数会出现问题,需要对char进行static_cast强制转换为unsigned char类型后再调用。 - Evg
2
@Timmmm 我已经为这个答案添加了一个 C++20 版本,因为我认为这里是最合适的地方,并且与此线程中的其他答案相比,我觉得它最接近你的其他解决方案。 - Ben Cottrell
显示剩余10条评论

123

利用标准的char_traits。记住,std::string实际上是std::basic_string<char>的typedef,或者更明确地说,是std::basic_string<char,std::char_traits<char> >char_traits类型描述了字符比较、复制、转换等操作。您所需做的就是用自定义的char_traitsbasic_string typedef出一个新的字符串,并提供该字符串以不区分大小写方式进行比较。

struct ci_char_traits : public char_traits<char> {
    static bool eq(char c1, char c2) { return toupper(c1) == toupper(c2); }
    static bool ne(char c1, char c2) { return toupper(c1) != toupper(c2); }
    static bool lt(char c1, char c2) { return toupper(c1) <  toupper(c2); }
    static int compare(const char* s1, const char* s2, size_t n) {
        while( n-- != 0 ) {
            if( toupper(*s1) < toupper(*s2) ) return -1;
            if( toupper(*s1) > toupper(*s2) ) return 1;
            ++s1; ++s2;
        }
        return 0;
    }
    static const char* find(const char* s, int n, char a) {
        while( n-- > 0 && toupper(*s) != toupper(a) ) {
            ++s;
        }
        return s;
    }
};

typedef std::basic_string<char, ci_char_traits> ci_string;

详细信息请参见本周大师第29期


15
据我所知,根据我的实验,这会使得你的新字符串类型与std::string不兼容。 - Zan Lynx
11
当然会影响,但这是为了它自己的好。不区分大小写的字符串是另一种东西:typedef std::basic_string<char, ci_char_traits<char> > istring,而不是typedef std::basic_string<char, std::char_traits<char> > string - Andreas Spindler
295
你只需要... - Tim MB
35
任何在这种琐碎情况下强制产生如此疯狂结果的语言结构都应该被毫不犹豫地放弃。 - Erik Aronesty
5
@DaveKennedy 我认为Erik建议放弃人类语言,因为那些是迫使这种疯狂的语言结构。 :-) - srm
显示剩余10条评论

65
如果您使用的是POSIX系统,您可以使用strcasecmp。虽然此函数不是C标准的一部分,也不可用于Windows。只要区域设置为POSIX,它就会对8位字符执行不区分大小写的比较。如果区域设置不是POSIX,则结果未定义(因此可能进行本地化比较,也可能不进行)。没有等效的宽字符。
如果失败,许多历史C库实现都具有stricmp()和strnicmp()函数。在Windows上,Visual C++将所有这些重命名为带下划线的前缀,因为它们不是ANSI标准的一部分,因此在该系统上它们被称为_stricmp或_strnicmp。某些库还可能具有等效的宽字符或多字节函数(通常命名为wcsicmp、mbcsicmp等)。
C和C++都很少关注国际化问题,因此除了使用第三方库外,没有好的解决方案。如果需要C/C++的强大库,请查看IBM ICU(Unicode国际组件)。ICU适用于Windows和Unix系统。

57

你是在谈论一个简单的不区分大小写的比较还是一个完整的规范化Unicode比较?

一个简单的比较无法找到可能相同但不是二进制相等的字符串。

例如:

U212B (ANGSTROM SIGN)
U0041 (LATIN CAPITAL LETTER A) + U030A (COMBINING RING ABOVE)
U00C5 (LATIN CAPITAL LETTER A WITH RING ABOVE).

这些都是等效的,但它们也具有不同的二进制表示。

话虽如此,Unicode规范化应该作为必读内容,特别是如果你计划支持韩文、泰文和其他亚洲语言。

此外,IBM基本上专利了大多数优化的Unicode算法,并将它们公开提供。他们还维护一个实现:IBM ICU


晚点了,我知道...“都是等价的”的说法可能不完全正确,尽管我对给定的情况不熟悉 - 但是,德语中的'Umlaut'可以通过将aou与分音符相结合或直接使用字母äöü来创建 - 但是两个点的距离(略微)不同(直接字符更窄)... - Aconcagua

34

我对非 Unicode 版本的第一个想法是做类似这样的事情:

bool caseInsensitiveStringCompare(const string& str1, const string& str2) {
    if (str1.size() != str2.size()) {
        return false;
    }
    for (string::const_iterator c1 = str1.begin(), c2 = str2.begin(); c1 != str1.end(); ++c1, ++c2) {
        if (tolower(static_cast<unsigned char>(*c1)) != tolower(static_cast<unsigned char>(*c2))) {
            return false;
        }
    }
    return true;
}

3
std::tolower 不应直接作用于 char,需要使用 static_cast 转换为 unsigned char参考链接 - Evg
1
@Evg,那么if (tolower(static_cast<unsigned char>(*c1)) != tolower(static_cast<unsigned char>(*c2))可以吗? - Shadow2531
1
是的,这应该是正确的方式。 - Evg

34

在字符串的情况下,boost::iequals不支持UTF-8编码。

您可以使用boost::locale

comparator<char,collator_base::secondary> cmpr;
cout << (cmpr(str1, str2) ? "str1 < str2" : "str1 >= str2") << endl;
  • 主要 -- 忽略重音和字符大小写,仅比较基本字母。例如,"facade" 和 "Façade" 是相同的。
  • 次要 -- 忽略字符大小写但考虑重音。"facade" 和 "façade" 是不同的,但 "Façade" 和 "façade" 是相同的。
  • 第三级 -- 考虑大小写和重音:"Façade" 和 "façade" 是不同的。忽略标点符号。
  • 第四级 -- 考虑所有大小写、重音和标点符号。单词在Unicode表示方面必须完全相同。
  • 完全相同 -- 与第四级相同,但也比较字符编码点。

28

您可以在Unix上使用strcasecmp或在Windows上使用stricmp

到目前为止,还有一件事情没有提到,如果您正在使用stl字符串与这些方法,最好先比较两个字符串的长度,因为在字符串类中已经提供了这个信息。如果您要比较的两个字符串首先长度就不相同,则这样做可以避免进行昂贵的字符串比较操作。


由于确定字符串的长度需要迭代字符串中的每个字符并将其与0进行比较,因此与直接比较字符串有多大的区别呢?我猜在两个字符串不匹配的情况下,您可以获得更好的内存局部性,但是如果匹配,则运行时间可能近2倍。 - uliwitness
6
C++11规定std::string::length的复杂度必须是常量:http://www.cplusplus.com/reference/string/string/length/ - bradtgmurray
2
这是一个有趣的小事实,但在这里没有什么影响。strcasecmp()和stricmp()都接受未装饰的C字符串,因此没有涉及std::string。 - uliwitness
4
如果将 "a" 与 "ab" 进行比较,这些方法将返回 -1。尽管长度不同,但 "a" 在 "ab" 之前。因此,如果调用方关心顺序,仅比较长度是不可行的。 - Nathan

16

我正在尝试从所有帖子中汇编一个好的答案,因此请帮忙编辑:

这是一种实现方法,虽然它会改变字符串,不支持Unicode,但它是可移植的,这是一个优点:

我正在尝试从所有帖子中汇编一个好的答案,因此请帮忙编辑:

这是一种实现方法,虽然它会改变字符串,不支持Unicode,但它是可移植的,这是一个优点:

bool caseInsensitiveStringCompare( const std::string& str1, const std::string& str2 ) {
    std::string str1Cpy( str1 );
    std::string str2Cpy( str2 );
    std::transform( str1Cpy.begin(), str1Cpy.end(), str1Cpy.begin(), ::tolower );
    std::transform( str2Cpy.begin(), str2Cpy.end(), str2Cpy.begin(), ::tolower );
    return ( str1Cpy == str2Cpy );
}

据我所读,这比stricmp()更具可移植性,因为stricmp()实际上不是标准库的一部分,而是由大多数编译器供应商实现的。

要获得真正的Unicode友好实现,似乎必须离开标准库。一个好的第三方库是IBM ICU(国际Unicode组件)

此外,boost::iequals提供了一个相当不错的实用程序来进行此类比较。


请问,::tolower是什么意思?为什么可以使用tolower而不是tolower()?双冒号'::'是什么意思?谢谢。 - VextoR
20
这不是一个非常高效的解决方案 - 你需要复制两个字符串并对它们进行转换,即使第一个字符不同。 - Timmmm
3
如果你无论如何都要复制,为什么不使用值传递而不是引用传递呢? - celticminstrel
1
问题明确要求在比较之前不要“转换”整个字符串。 - Sandburg
1
std::tolower 不应直接在 char 上调用,需要使用 static_cast 转换为 unsigned char - Evg

16
查看std::lexicographical_compare
// lexicographical_compare example
#include <iostream>     // std::cout, std::boolalpha
#include <algorithm>    // std::lexicographical_compare
#include <cctype>       // std::tolower

// a case-insensitive comparison function:
bool mycomp(char c1, char c2) {
    return std::tolower(c1) < std::tolower(c2);
}

int main() {
    std::string foo = "Apple";
    std::string bar = "apartment";
    
    std::cout << std::boolalpha;
    
    std::cout << "Comparing foo and bar lexicographically (foo<bar):\n";
    
    std::cout << "Using default comparison (operator<): ";
    std::cout << std::lexicographical_compare(foo.begin(), foo.end(), bar.begin(), bar.end());
    std::cout << '\n';
    
    std::cout << "Using custom comparison (mycomp): ";
    std::cout << std::lexicographical_compare(foo.begin(), foo.end(), bar.begin(), bar.end(), mycomp);
    std::cout << '\n';
    
    return 0;
}

演示


1
这个方法有潜在的不安全和不可移植性。std::tolower 只适用于字符是 ASCII 编码的情况。对于 std::string 则没有这样的保证 - 所以很容易出现未定义行为。 - plasmacel
1
@plasmacel 然后使用能够处理其他编码方式的函数。 - Brian Rodriguez
std::lexicographical_compare看起来很有前途,直到我们遇到了mycomp。 :-( - Robin Davies
这个算法适用于任何元素类型。如果你想要支持Unicode,那么请使用支持Unicode的字符串以及支持Unicode的迭代器/比较器。换句话说,问题出在std::string而不是std::lexicographical_compare上。 - Brian Rodriguez
这个算法适用于任何元素类型。如果你想要支持Unicode,那么请使用支持Unicode的字符串以及支持Unicode的迭代器/比较器。换句话说,问题出在std::string而不是std::lexicographical_compare - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接