如何将 std::string 的实例转换为小写

992

我想将一个std::string转换为小写。虽然我知道有函数tolower(),但是以往使用这个函数时遇到问题,而且即使使用它来处理std::string也并不理想,因为它需要迭代每个字符。

是否有其他的方法可以保证100%的成功率呢?


51
如果不通过遍历列表,你还能用什么其他方法将列表中的每个元素转换为另一种形式?如果需要对字符串中的每个字符应用某个函数,那么必须遍历该字符串。无法绕过这一点。 - user21037
29
为什么这个问题会导致评分下降?我不介意遍历我的字符串,但我想知道除了tolower()、toupper()等函数外是否还有其他函数。 - Konrad
16
如果它们可能已经是小写字母,但肯定是A-Z或a-z,您可以将其与0x20 OR而不是加上去。这是一种聪明到可能是愚蠢的优化,几乎从来不值得这样做... - Steve Jessop
6
我不知道为什么它会被踩……当然,这个问题表述有点奇怪(因为你确实必须以某种方式迭代每个项目),但这是一个有效的问题。 - warren
6
注意:tolower()并非始终有效。小写/大写操作仅适用于字符,而std::string本质上是一个字节数组,而非字符数组。对于ASCII字符串,普通的tolower()函数很好用,但它无法正确地将Latin-1或UTF-8字符串转换为小写形式。你必须了解字符串的编码,并且可能需要在转换为小写字符之前进行解码。 - Constantin
显示剩余9条评论
32个回答

1109

内容摘自不那么常见的问题

#include <algorithm>
#include <cctype>
#include <string>

std::string data = "Abc";
std::transform(data.begin(), data.end(), data.begin(),
    [](unsigned char c){ return std::tolower(c); });

你真的不能逃避迭代每个字符。否则,就没有办法知道该字符是否是小写或大写。

如果你真的讨厌tolower(),这里有一种只针对ASCII字符的专用替代方法,但我不建议你使用:

char asciitolower(char in) {
    if (in <= 'Z' && in >= 'A')
        return in - ('Z' - 'z');
    return in;
}

std::transform(data.begin(), data.end(), data.begin(), asciitolower);

请注意,tolower() 只能进行单个字节字符的替换,这在许多脚本中并不适合,特别是如果使用像UTF-8这样的多字节编码。


30
虽然这些算法已经有点老了,但它们并没有改变太多。@Stefan Mai: 调用STL算法会产生什么样的“很多开销”呢?这些函数相当精简(即简单的for循环),通常都会内联优化,因为在同一编译单元中很少有相同模板参数调用同一函数的情况。 - eq-
353
每当你假设字符是ASCII时,上帝就会杀死一只小猫咪。 :( - Rag
15
你的第一个例子可能存在未定义行为(将char传递给::tolower(int))。你需要确保不要传递负值。 - juanchopanza
48
使用 ::tolower 可能会导致崩溃,对于非 ASCII 输入是未定义行为。 - Cheers and hth. - Alf
9
在使用 tolower 之前需要加上 ::,以表示它在最外层的命名空间中。如果您在另一个命名空间中使用此代码,则可能会有一个不同(可能不相关)的 tolower 定义被优先选择而没有 ::。 - Charles Ofria
显示剩余36条评论

372
Boost提供了一个字符串算法来实现这个功能:

to_lower

#include <boost/algorithm/string.hpp>

std::string str = "HELLO, WORLD!";
boost::algorithm::to_lower(str); // modifies str

对于非原地操作,可以使用to_lower_copy函数

#include <boost/algorithm/string.hpp>

const std::string str = "HELLO, WORLD!";
const std::string lower_str = boost::algorithm::to_lower_copy(str);

25
不支持非ASCII-7字符。 - DevSolar
3
这很慢,可以看一下这个基准测试:godbolt.org/z/neM5jsva1 - prehistoricpenguin
3
@prehistoricpenguin 你说慢?慢是因为你的代码实现中有一个错误,而不是直接调用 Boost 库。如果代码很关键,比如被频繁调用并成为瓶颈,那么考虑慢速问题可能是值得的。 - Mayou36
我相信Boost不是C++标准库的解决方案,对吗? - ZoomIn
3
不是这样的。这是你在这个网站上每个C++问题都会看到的那些极其不幸的答案之一...因为添加整个库来做一些非常简单的事情显然是最受欢迎的方法! - Logix
显示剩余3条评论

340

简述:

使用ICU库如果不使用,你的转换程序可能会在一些你甚至没有意识到的情况下默默失败。


首先,你需要回答一个问题:你的std::string的编码是什么?是ISO-8859-1吗?或者是ISO-8859-8?还是Windows Codepage 1252?无论你使用什么来将大写字母转换为小写字母,它是否知道这一点?(或者对于超过0x7f的字符是否失败?)
如果你正在使用UTF-8(在8位编码中唯一明智的选择)作为容器的std::string,如果你认为你仍然掌控着事情,那么你已经欺骗了自己。你正在将多字节字符序列存储在不知道多字节概念的容器中,大多数你可以执行的操作也是如此!即使像.substr()这样简单的操作也可能导致无效的(子)字符串,因为你在多字节序列的中间进行了分割。
一旦您尝试像std::toupper( 'ß' )std::tolower( 'Σ' )这样的操作,无论在任何编码中,都会遇到麻烦。因为1)标准只能一次处理一个字符,所以它无法正确地将ß转换为SS。2)标准只能一次处理一个字符,所以它无法确定Σ是单词中间(应该使用σ)还是在末尾(应该使用ς)。另一个例子是std::tolower('I'),其结果应根据区域设置而异,在大多数情况下期望得到i,但在土耳其,ı(LATIN SMALL LETTER DOTLESS I)是正确的答案(在UTF-8编码中占用多个字节)。
所以,任何逐个字符或更糟糕的逐个字节进行的大小写转换都是有缺陷的设计。这包括目前存在的所有std::变体。
此外,标准库所能做的事情取决于在运行软件的机器上支持哪些语言环境...如果您的目标语言环境在客户机上不受支持,该怎么办?
因此,您真正需要的是一种字符串类,它能够正确处理所有这些问题,并且不是任何std::basic_string<>变体。
(C++11注意:std::u16string和std::u32string更好,但仍然不完美。C++20引入了std::u8string,但所有这些只是指定编码。在许多其他方面,它们仍然对Unicode机制无知,如规范化、排序等)
虽然Boost在API方面看起来不错,但Boost.Locale基本上是ICU的包装器。如果使用ICU支持编译Boost... 如果没有,Boost.Locale仅限于为标准库编译的区域设置支持。
相信我,有时候让Boost与ICU一起编译真的很痛苦。(Windows没有包含ICU的预编译二进制文件,因此您必须将它们与应用程序一起提供,会带来一整个新的问题...)
因此,我个人建议直接从源头获得完整的Unicode支持,并直接使用ICU库:
#include <unicode/unistr.h>
#include <unicode/ustream.h>
#include <unicode/locid.h>

#include <iostream>

int main()
{
    /*                          "Odysseus" */
    char const * someString = u8"ΟΔΥΣΣΕΥΣ";
    icu::UnicodeString someUString( someString, "UTF-8" );
    // Setting the locale explicitly here for completeness.
    // Usually you would use the user-specified system locale,
    // which *does* make a difference (see ı vs. i above).
    std::cout << someUString.toLower( "el_GR" ) << "\n";
    std::cout << someUString.toUpper( "el_GR" ) << "\n";
    return 0;
}

编译(以此示例中的G++为例):

g++ -Wall example.cpp -licuuc -licuio

这将给出:
ὀδυσσεύς

注意单词中间的Σ<->σ转换,以及单词结尾的Σ<->ς转换。没有基于<algorithm>的解决方案可以为您提供这种转换。

32
这是一般情况下的正确答案。标准规定除了“ASCII”之外的任何处理都没有提供,这只是虚假和欺骗。它会让你以为你可以处理UTF-16,但实际上不行。正如这个答案所说,如果你不进行自己的unicode处理,就无法获取UTF-16字符串的正确字符长度(而不是字节长度)。如果你需要处理真正的文本,请使用ICU。谢谢,@DevSolar - lmat - Reinstate Monica
ICU在Ubuntu/Windows上是否默认可用,还是需要单独安装?另外,这个答案怎么样:https://dev59.com/03RC5IYBdhLWcg3wYf8p#35075839? - Shital Shah
icu::UnicodeString::length() 技术上也会欺骗你(尽管不太频繁),因为它报告的是 16 位代码单元的数量,而不是代码点的数量。;-) - masaers
2
@DevSolar 同意!在文本中,长度的概念相当无意义(我们可以将连字添加到违规列表中)。话虽如此,由于人们习惯于制表符和控制字符占用一个长度单位,因此代码点将是更直观的度量标准。哦,还有感谢您给出正确答案,很遗憾看到它排名如此之低 :-( - masaers
实际上,std::string 不知道它包含的是多字节字符编码的文本,这是一种特性而不是错误。这是唯一明智的做法,这也是为什么几乎每个人都这样做的原因。然而,从过去到现在,没有适当的标准 API 来处理除基本文本以外的任何内容确实是一个问题。即使在托管环境中,它也必须是可选的,因为它相当沉重,并且有许多情况下它并不需要。 - Deduplicator
显示剩余6条评论

39

使用 C++11 的范围 for 循环,可以写出更简洁的代码:

#include <iostream>       // std::cout
#include <string>         // std::string
#include <locale>         // std::locale, std::tolower

int main ()
{
  std::locale loc;
  std::string str="Test String.\n";

 for(auto elem : str)
    std::cout << std::tolower(elem,loc);
}

10
然而,在法语机器上,该程序无法转换法语语言中允许的非ASCII字符。例如,字符串'Test String123. É Ï\n'将被转换为:'test string123. É Ï\n',尽管字符É Ï及其小写形式'é'和'ï'在法语中是允许的。看起来此帖子中其他留言没有提供解决方案。 - incises
2
我认为你需要为此设置适当的区域设置。 - user1095108
1
@incises,然后有人发布了一个关于ICU的答案,这肯定是正确的方法。比大多数其他解决方案更容易理解区域设置。 - Alexis Wilke
如果可能的话,我个人更倾向于不使用外部库。 - kayleeFrye_onDeck

33

使用基于范围的for循环和引用变量的另一种方法

string test = "Hello World";
for(auto& c : test)
{
   c = tolower(c);
}

cout<<test<<endl;

1
我猜它对UTF-8不起作用,是吗? - Rodrigo

32

一个可工作的示例? - Velkan

27

这是对Stefan Mai回答的跟进:如果您想将转换结果放在另一个字符串中,您需要在调用std::transform之前预先分配其存储空间。由于STL会将转换后的字符存储到目标迭代器(在循环的每次迭代中将其递增),目标字符串不会自动调整大小,您会面临内存覆盖的风险。

#include <string>
#include <algorithm>
#include <iostream>

int main (int argc, char* argv[])
{
  std::string sourceString = "Abc";
  std::string destinationString;

  // Allocate the destination space
  destinationString.resize(sourceString.size());

  // Convert the source string to lower case
  // storing the result in destination string
  std::transform(sourceString.begin(),
                 sourceString.end(),
                 destinationString.begin(),
                 ::tolower);

  // Output the result of the conversion
  std::cout << sourceString
            << " -> "
            << destinationString
            << std::endl;
}

2
这并没有将Ä调整为ä。 - Purefan
1
这里也可以使用后插入迭代器,而不是手动调整大小。 - chili

9

将字符串转换为小写的最简单方法是如下所示,无需担心std命名空间:

1:带/不带空格的字符串

#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
int main(){
    string str;
    getline(cin,str);
//------------function to convert string into lowercase---------------
    transform(str.begin(), str.end(), str.begin(), ::tolower);
//--------------------------------------------------------------------
    cout<<str;
    return 0;
}

2:不带空格的字符串

#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
int main(){
    string str;
    cin>>str;
//------------function to convert string into lowercase---------------
    transform(str.begin(), str.end(), str.begin(), ::tolower);
//--------------------------------------------------------------------
    cout<<str;
    return 0;
}

1
这是完全错误的:如果您查看文档,就会发现 std::tolower 无法处理 char,它只支持 unsigned char。因此,如果 str 包含 0x00-0x7F 之外的字符,则此代码将产生未定义行为。 - Dmitry Grigoryev
这也是错误的,因为在全局命名空间中使用以 str 开头的标识符是严格保留的。 - Roflcopter4

7

我编写了这个简单的帮助函数:

#include <locale> // tolower

string to_lower(string s) {        
    for(char &c : s)
        c = tolower(c);
    return s;
}

使用方法:

string s = "TEST";
cout << to_lower("HELLO WORLD"); // output: "hello word"
cout << to_lower(s); // won't change the original variable.

7

我自己写的模板函数,用于执行大小写转换。

#include <string>
#include <algorithm>

//
//  Lowercases string
//
template <typename T>
std::basic_string<T> lowercase(const std::basic_string<T>& s)
{
    std::basic_string<T> s2 = s;
    std::transform(s2.begin(), s2.end(), s2.begin(),
        [](const T v){ return static_cast<T>(std::tolower(v)); });
    return s2;
}

//
// Uppercases string
//
template <typename T>
std::basic_string<T> uppercase(const std::basic_string<T>& s)
{
    std::basic_string<T> s2 = s;
    std::transform(s2.begin(), s2.end(), s2.begin(),
        [](const T v){ return static_cast<T>(std::toupper(v)); });
    return s2;
}

2
这正是我所需要的。我只是使用了支持UTF-16的宽字符towlower函数。 - Juv
需要使用 ::tolower 和 ::toupper,而不是 tolower 和 toupper。 - Andrea Giudiceandrea

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