工作中遇到C++处理字符串的总结
1.引言
由于这段时间遇到项目上的wstring,并且之前听说C++对字符串的处理非常弱,所以想总结下C++处理字符串的方式
会有以下的部分
- C风格字符串
- 标准库中std::string和std::wstring
- 标准库中的ostringstream类
- getline函数
- C++17中的std::string_view
- C++20中的char8_t
- C++20中的split_view
- boost中的split
2.C风格字符串
这些字符串存储为字符数组,并以空字符’\0’结束。这是C++从C语言继承的字符串类型。例如:
#include <iostream>
using namespace std;
int main () {
char s [] = "GeeksforGeeks";
cout << s << endl;
return 0;
}
输出:GeeksforGeeks
3.std::string和std::wstring
std::string
:它是基于char
类型的,通常用于处理ASCII字符,这包括英文字符。然而,它也可以用于处理UTF-8编码的字符串,UTF-8编码可以表示任何Unicode字符。std::wstring
:它是基于wchar_t
类型的,通常用于处理宽字符或Unicode字符。在Windows中,wchar_t
通常是16位的,可以用于表示UTF-16编码的字符串。在其他平台上,wchar_t
可能是32位的,可以用于表示UTF-32编码的字符串。因此,如果你需要处理包含中文、韩文等非ASCII字符的字符串,使用std::wstring
可能会更方便。
然而,请注意,这并不意味着你不能在std::string
中存储中文或韩文等字符。只要你使用正确的编码(如UTF-8),并且你的代码能够正确处理这种编码,那么你就可以在std::string
中存储任何Unicode字符。
另外一种解释:
std::string
:std::string用于表示标准ASCII和UTF-8字符串。std::string是一个基于char的模板化的基本字符串,它可以存储标准ASCII字符集(0-255)。当你需要处理包含ASCII字符或UTF-8编码的字符串时,可以使用std::string。
std::wstring
:std::wstring用于表示宽字符/Unicode(UTF-16)字符串。std::wstring是一个基于wchar_t的模板化的基本字符串,它可以存储宽字符。当你需要处理包含宽字符或Unicode字符的字符串时,可以使用std::wstring。
对于这两种类型的字符串,可以执行许多相同的操作,如分配新值、比较两个字符串、查找子字符串、替换子字符串等。然而,由于它们存储的字符类型不同,因此在某些情况下,你可能需要使用特定于类型的函数或方法。例如,当你需要将一个数字转换为字符串时,你可以使用std::to_string函数(对于std::string)或者std::to_wstring函数(对于std::wstring)
科普一下
在C++中,区分ASCII和UTF-8字符串以及UTF-8和UTF-16字符串主要取决于你的代码如何处理这些字符串。
ASCII和UTF-8:ASCII是一个7位字符集,包含128个字符。UTF-8是一种变长的编码方式,它可以使用1到4个字节来表示一个字符⁶。在UTF-8中,ASCII字符被编码为单个字节,这意味着任何ASCII文件都是有效的UTF-8文件。因此,如果一个字符串只包含ASCII字符,那么你无法仅通过查看该字符串就确定它是否是UTF-8编码的。然而,如果一个字符串包含非ASCII字符(即,字节值大于127的字符),并且这个字符串是有效的UTF-8编码的,那么你可以确定这个字符串是UTF-8编码的。
UTF-8和UTF-16:这两种都是Unicode的编码方式,但它们使用不同数量的字节来表示字符。UTF-8使用1到4个字节来表示一个字符,而UTF-16则使用2或4个字节。因此,如果你看到一个字符串中有2或4个字节的字符,那么这可能是一个UTF-16编码的字符串。然而,请注意,确定一个字符串的编码方式通常需要更多的上下文信息。
至于
char
和wchar_t
:
- char:
char
是C++中基本的字符/字节类型,通常用于处理ASCII字符。- wchar_t:
wchar_t
有时用于处理宽字符,如中文。在Windows中,wchar_t
通常用于表示UTF-16编码的字符串。
对了不要理解错了,string处理UTF-8编码字符串指的是每一个字符,比如一个UTF-8编码的字符可能只需要1个字节(例如ASCII字符),也可能需要2、3或4个字节(例如某些汉字)。因此,当我们说string存储的是UTF-8编码的字符串时,我们是指std::string
存储的是一系列的字节,这些字节按照UTF-8的规则解码后可以得到一系列的字符。每个字符可能需要1到4个字节,具体取决于该字符的Unicode代码点,所以,如果你有一个包含10万个汉字的字符串,并且每个汉字都使用UTF-8编码并需要3个字节,那么你将需要300,000个字节来存储这个字符串。这远远小于string的最大大小,因此你应该可以在string中存储这个字符串
好了,现在讲讲实际咋用吧
构造函数:用于创建一个新的字符串对象。例如:
std::string str1; // 创建一个空的字符串
std::wstring wstr1; // 创建一个空的宽字符串
赋值操作:用于给字符串赋值。例如:
std::string str2 = "Hello, world!";
std::wstring wstr2 = L"你好,世界!";
append:用于在字符串的末尾添加字符。例如:
str2.append(" How are you?");
wstr2.append(L" 你好吗?");
replace:用于替换字符串中的一部分。例如:
str2.replace(0, 5, "Hi");
wstr2.replace(0, 2, L"嗨");
find:用于查找子字符串。例如:
size_t pos = str2.find("world");
size_t wpos = wstr2.find(L"世界");
substr:用于获取子字符串。例如:
std::string sub = str2.substr(0, 2);
std::wstring wsub = wstr2.substr(0, 1);
length/size:用于获取字符串的长度。例如:
size_t len = str2.length();
size_t wlen = wstr2.length();
4.标准库中的ostringstream类
先说它有什么用吧,它就是你给他仍各种数据类型,他都给你转为字符串,比如整数、浮点数、布尔值等,然后它会自动将这些数据转换为字符串,并将它们按照你投入的顺序连接起来
那它为啥会被发明出来捏?
1.因为之前我们要对各种类型转为字符串的话,其实都需要自己手动进行类型转换,比较麻烦,所以用ostringstream就简单很多
// int转字符串
int num = 123;
std::string str = std::to_string(num);
// double转字符串
double num = 123.456;
std::string str = std::to_string(num);
// bool转字符串 ,用条件运算符?:
bool b = true;
std::string str = b ? "true" : "false";
//如果有了ostringstream时
int num = 123;
double d = 123.456;
bool b = true;
std::ostringstream oss;
oss << num << ' ' << d << ' ' << std::boolalpha << b;
std::cout << "The string is: " << oss.str() << std::endl; // The string is: 123 123.456 true
2.像之前你如果用+来连接字符串,可能会导致大量的内存和复制操作,而用它的话就不会
// 之前+来连接字符串
std::string str;
for (int i = 0; i < 10000; ++i) {
str += "abc";
}
每次循环都会创建一个新的字符串对象来存储str + "abc"
的结果,然后将这个新的字符串对象赋值给str
。这可能会导致大量的内存分配和复制操作
比之下,ostringstream
提供了一种更高效的方式来连接字符串。当你向ostringstream
中插入数据时,这些数据会被直接添加到ostringstream
内部的缓冲区中,而不需要创建新的字符串对象。因此,使用ostringstream
可以避免大量的内存分配和复制操作
std::ostringstream oss;
for (int i = 0; i < 10000; ++i) {
oss << "abc";
}
std::string str = oss.str();
现在知道它是为什么来的后,那咱们就得讲讲啥时候用它,啥时候用string
std::ostringstream
是一个流类,它提供了一种方便、高效的方式来将各种类型的数据转换为字符串,并将这些字符串连接在一起。当你需要将非字符串类型(如整数、浮点数等)转换为字符串,并在转换过程中进行格式化,然后将转换结果拼接在一起时,使用ostringstream
可能会更方便。std::string
是一个字符串类,它提供了许多操作来分配、比较和修改字符串。当你需要处理包含ASCII字符或UTF-8编码的字符串时,使用std::string
可能会更方便。
4.1 三种C风格流输入和输出
现在写到这里时,发现ostringstream竟然还是成套的,ostringstream、istringstream、stringstream三种,分别是执行C风格字符串的输出,输入,输入和输出
std::ostringstream oss;
oss << "Hello, " << "World!";
std::cout << oss.str() << std::endl; // 输出:Hello, World!
std::string s = "Hello World!";
std::istringstream iss(s);
std::string word;
while (iss >> word) {
std::cout << word << std::endl; // 输出:Hello\nWorld! 这是两行
}
std::stringstream ss;
ss << "Hello, World!";
std::string s = ss.str();
std::cout << s << std::endl; // 输出:Hello, World!
ss.str(""); // 清空ss
ss << "Goodbye, World!";
s = ss.str();
std::cout << s << std::endl; // 输出:Goodbye, World!
//以逗号为分割,一个个字符串,std::getline()函数可以接受一个可选的第三个参数,用于指定分隔符
std::stringstream ss(sText);
std::string idone;
while(std::getline(ss, idone, ","))
{
jAppList.append(idone);
}
std::ostringstream
,std::istringstream
和std::stringstream
这三个类是C++标准库中的字符串流类,它们提供了一种方便的方式来处理字符串。相比于传统的字符串操作,它们有以下几个优点:
- 格式化输出:你可以像使用
std::cout
那样使用字符串流,这使得格式化输出变得非常简单。例如,你可以轻松地将各种类型的数据(如整数、浮点数等)转换为字符串。 - 灵活的输入操作:你可以像使用
std::cin
那样使用字符串流来读取数据。这使得你可以方便地从字符串中提取各种类型的数据。 - 字符串解析和拼接:使用字符串流,你可以方便地解析和拼接字符串。例如,你可以使用
std::getline()
函数来分割字符串,或者使用<<
运算符来拼接字符串。
4.2 传统字符串操作
上面说的传统字符串操作指的是
字符串拼接:使用+
或+=
运算符将两个字符串连接在一起。
std::string str1 = "Hello, ";
std::string str2 = "World!";
std::string str3 = str1 + str2; // str3现在是"Hello, World!"
子字符串:使用substr()
函数获取字符串的一部分。
std::string str = "Hello, World!";
std::string sub = str.substr(0, 5); // sub现在是"Hello"
查找:使用find()
函数查找子字符串的位置。
std::string str = "Hello, World!";
size_t pos = str.find("World"); // pos现在是7
替换:使用replace()
函数替换字符串的一部分。
std::string str = "Hello, World!";
str.replace(0, 5, "Goodbye"); // str现在是"Goodbye, World!"
4.3 C风格流操作
使用std::ostringstream
,std::istringstream
和std::stringstream
来替换传统字符串操作:
字符串拼接:使用std::ostringstream
进行字符串拼接。
#include <sstream>
#include <iostream>
int main() {
std::ostringstream oss;
oss << "Hello, " << "World!";
std::cout << oss.str() << std::endl; // 输出:Hello, World!
return 0;
}
子字符串:使用std::istringstream
进行字符串分割。
#include <sstream>
#include <iostream>
int main() {
std::string s = "Hello, World!";
std::istringstream iss(s);
std::string word;
while (iss >> word) {
std::cout << word << std::endl; // 输出:Hello,\nWorld!
}
return 0;
}
查找和替换:使用std::stringstream
进行查找和替换操作。
#include <sstream>
#include <iostream>
int main() {
std::string s = "Hello, World!";
std::stringstream ss(s);
std::string word;
while (ss >> word) {
if (word == "World!") {
word = "Universe!";
}
std::cout << word << ' ';
}
std::cout << std::endl; // 输出:Hello, Universe!
return 0;
}
在C++中,当使用std::istringstream
或std::stringstream
的提取运算符(>>
)从流中读取字符串时,空格(包括空格、制表符和换行符)被视为默认的分隔符。这意味着提取运算符会读取并存储从当前位置开始到下一个空格之前的所有字符,然后跳过该空格,继续从下一个位置开始读取。
所以在子字符串的例子中,字符串"Hello, World!“被插入到std::istringstream
对象中。然后,当执行iss >> word
时,它首先读取并存储"Hello,”(遇到空格停止),然后跳过空格,再读取并存储"World!"。这就是为什么会看到"Hello,"和"World!"被分开打印出来
显示的是
Hello,
World!
4.4 举例说明
上面我写的有段代码是这样的
std::stringstream ss(sText);
std::string idone;
while(std::getline(ss, idone, ","))
{
jAppList.append(idone);
}
如果用传统字符串来操作的话是这样的
std::string sText = "your,string,here";
size_t pos = 0;
std::string token;
while ((pos = sText.find(",")) != std::string::npos) {
token = sText.substr(0, pos);
jAppList.append(token);
sText.erase(0, pos + 1);
}
jAppList.append(sText);
这种方法的一个潜在缺点是,它会修改原始字符串sText
。如果你需要保留原始字符串,你可能需要先复制一份。此外,这种方法可能在处理大型字符串时效率较低,因为每次删除操作都可能涉及到内存移动。相比之下,使用std::stringstream
和std::getline
可以避免这些问题
当你使用std::stringstream
时,你实际上是在创建一个新的字符串流,而不是直接操作原始字符串。这意味着原始字符串保持不变。
然后,你可以使用std::getline
从这个流中读取数据。每次调用std::getline
时,它都会读取流中的下一个标记(在这个例子中,标记是由逗号分隔的)。这个过程不会修改流中的数据,也不需要移动内存。
因此,使用std::stringstream
和std::getline
可以避免修改原始字符串和频繁的内存移动,这可能会在处理大型字符串时提高效率。
Json::Value jAppList(Json::arrayValue);
定义了一个Json数组jAppList
。在代码中,每次从std::stringstream
对象读取一个逗号分隔的部分,都会将其添加到这个Json数组中。
在这种情况下,添加元素到Json数组通常不会导致内存重新分配。这是因为Json库通常会预先分配一定数量的元素空间,当数组需要增长时,它会按照一定的策略(例如,每次翻倍)来增加容量。因此,虽然添加元素可能偶尔会导致内存重新分配,但这种情况并不频繁。
总结下来就是这样的:
- 避免了不必要的字符串复制:使用
std::stringstream
创建了一个新的字符串流,而不是复制整个原始字符串。虽然这个流在创建时会分配一些内存,但这通常比复制整个字符串所需的内存要少。 - 避免了频繁的内存重新分配:每次调用
std::getline
时,都会从流中读取一个逗号分隔的部分,并将其存储在一个临时字符串中。这个过程不会导致额外的内存分配,因为每次读取的部分都会覆盖临时字符串中的旧内容。 - 高效地添加元素到Json数组:将每个分割后的部分添加到一个Json数组中。添加元素到Json数组通常不会导致内存重新分配,因为Json库通常会预先分配一定数量的元素空间,并且在数组需要增长时,它会按照一定的策略(例如,每次翻倍)来增加容量。
- 点赞
- 收藏
- 关注作者
评论(0)