04.C++字符串
04.C++ 字符串
字符串/字符数组的工作原理
- 字符串实际上是字符数组
- C++ 中有一种数据类型叫做 char,是 Character 的缩写,占用一个字节的内存。它很有用因为它能把指针转换为 char 型指针,所以你可以用字节来做指针运算。它对于分配内存缓冲区也很有用,比如分配 1024 个 char 即 1KB 空间。它对字符串和文本也很有用,因为 C++ 对待字符的默认方式是通过 ascii 字符进行文本编码。我们在 C++ 中处理字符是一个字符一个字节。A
ascii
可以扩展比如 UTF-8、UTF-16、UTF-32,我们有 wide string(宽字符串)等。我们有两个字节的字符、三个字节、四个字节的等等。
- C++ 中有一种数据类型叫做 char,是 Character 的缩写,占用一个字节的内存。它很有用因为它能把指针转换为 char 型指针,所以你可以用字节来做指针运算。它对于分配内存缓冲区也很有用,比如分配 1024 个 char 即 1KB 空间。它对字符串和文本也很有用,因为 C++ 对待字符的默认方式是通过 ascii 字符进行文本编码。我们在 C++ 中处理字符是一个字符一个字节。A
- C++ 中默认的双引号就是一个字符数组
const char
,并且末尾会补\0
(空终止符),而 cout 会输出直到\0
就终止。
C 风格字符串
C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。
- C 语言没有内置的字符串类型
- 它通过以 null 字符 (‘
\0
’) 结尾的字符数组表示字符串 const char*
是指向字符常量的指针,通常用于引用字符串字面量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
char site[6] = {'R', 'U', 'N', 'O', 'O', 'B'};
std::cout << site << std::endl; // 没有\0结尾,字符串不知道何时结束,RUNOOB��_
char site1[7] = {'R', 'U', 'N', 'O', 'O', 'B'};
std::cout << site1 << std::endl; // 自动加了\0,RUNOOB
// 未显式地指定null字符。在某些情况下,编译器会在字符数组的末尾自动添加null字符,尤其是当数组初始化时未完全填满元素的时候。由于在这个声明中你已经声明了7个字符空间,但只显式地初始化了6个,编译器将会把最后一个元素默认初始化为 '0',使其成为一个合法的C字符串。
char site2[7] = {'R', 'U', 'N', 'O', 'O', 'B', '\0'};
std::cout << site2 << std::endl; // 手动加了\0结尾,RUNOOB
// 这个声明显式地在字符数组的末尾包含了null字符 '0'。这意味着数组 site 是一个合法的C字符串
//字符数组 = 字符串
char str1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
//自动加入\0
char str2[] = "Hello";
const char* name = "hacket"; //c风格字符串
// char* name = "hacket"; //报错,因为C++中默认的双引号就是一个字符数组const char*
不需要把 null 字符放在字符串常量的末尾。C++ 编译器会在初始化数组时,自动把 \0 放在字符串的末尾。
更多见:[[C语言字符串]]
字符串操作
函数 | 描述 |
---|---|
strcpy(s1, s2); | 复制字符串 s2 到字符串 s1。 |
strcat(s1, s2); | 连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号 |
strlen(s1); | 返回字符串 s1 的长度。 |
strcmp(s1, s2); | 如果 s1 和 s2 相同,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0 |
strchr(s1, ch); | 返回指向字符串 s1 中字符 ch 的第一次出现的位置的指针。 |
strstr(s1, s2); | 返回指向字符串 s1 中字符串 s2 的第一次出现的位置的指针。 |
说明:
strcmp
: 两个字符串自左向右逐个字符相比(按 ASCII 值大小相比较)
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <cstring>
using namespace std;
int main ()
{
char str1[13] = "runoob";
char str2[13] = "google";
char str3[13];
int len ;
// 复制 str1 到 str3
strcpy( str3, str1);
cout << "strcpy( str3, str1) : " << str3 << endl; // strcpy( str3, str1) : runoob
// 连接 str1 和 str2
strcat( str1, str2);
cout << "strcat( str1, str2): " << str1 << endl; // strcat( str1, str2): runoobgoogle
// 连接后,str1 的总长度
len = strlen(str1);
cout << "strlen(str1) : " << len << endl; // strlen(str1) : 12
return 0;
}
C++ 中的字符串
std::string
介绍
- C++ 标准库里有个类叫
string
,实际上还有一个模板类basic_string
。std::string
本质上就是这个basic_string
的char
作为模板参数的模板类实例。叫模板特化(template specialization
),就是把char
作为模板类basic string
的模板参数,意味着char
就是每个字符背后的的数据类型。 - 在 C++ 中使用字符串时你应该使用
std::string
。 string
有个接受参数为char *
或者const char*
指针的构造函数。在 C++ 中用双引号
来定义字符串一个或者多个单词时,它其实就是const char 数组
,而不是 char 数组。std::string
本质上它就是一个 char 数组,一个 char 的数组和一些内置函数
std::string
使用
string 构造函数
构造函数原型:
string();
// 创建一个空的字符串例如: string str;string(const char* s);
// 使用字符串 s 初始化string(const string& str);
// 使用一个 string 对象初始化另一个 string 对象string(int n, char c);
// 使用 n 个字符 c 初始化
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void test_string_constructor()
{
string s1; // 创建空字符串,调用无参构造函数
cout << "str1 = " << s1 << endl; // str1 =
const char *str = "hello world";
string s2(str); // 把c_string转换成了string
cout << "str2 = " << s2 << endl; // str2 = hello world
string s3(s2); // 调用拷贝构造函数
cout << "str3 = " << s3 << endl; // str3 = hello world
string s4(10, 'a');
cout << "str4 = " << s4 << endl; // str4 = aaaaaaaaaa
// std::string s1; // 默认构造
// std::string s2("hello"); // 有参构造
// std::string s3(s2); // 拷贝构造
// std::string s4(10, 'a'); // 10个a
// std::string s5 = "hello"; // 拷贝构造
// std::string s6 = s2; // 拷贝构造
// std::string s7 = s4; // 拷贝构造
// std::string s8 = std::move(s4); // 移动构造
// std::string s9 = std::move(s5); // 移动构造
}
string 赋值操作
赋值的函数原型:
string& operator=(const char* s);
//char* 类型字符串 赋值给当前的字符串string& operator=(const string &s);
//把字符串 s 赋给当前的字符串string& operator=(char c);
//字符赋值给当前的字符串string& assign(const char *s);
//把字符串 s 赋给当前的字符串string& assign(const char *s, int n);
//把字符串 s 的前 n 个字符赋给当前的字符串string& assign(const string &s);
//把字符串 s 赋给当前字符串string& assign(int n, char c);
//用 n 个字符 c 赋给当前字符串
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 赋值
void test_string_assign()
{
string str1;
str1 = "hello world";
cout << "str1 = " << str1 << endl;
string str2;
str2 = str1;
cout << "str2 = " << str2 << endl;
string str3;
str3 = 'a';
cout << "str3 = " << str3 << endl;
string str4;
str4.assign("hello c++");
cout << "str4 = " << str4 << endl;
string str5;
str5.assign("hello c++", 5);
cout << "str5 = " << str5 << endl;
string str6;
str6.assign(str5);
cout << "str6 = " << str6 << endl;
string str7;
str7.assign(5, 'x');
cout << "str7 = " << str7 << endl;
}
string 字符串拼接
函数原型:
string& operator+=(const char* str);
//重载 +=操作符string& operator+=(const char c);
//重载 +=操作符string& operator+=(const string& str);
//重载 +=操作符string& append(const char *s);
//把字符串 s 连接到当前字符串结尾string& append(const char *s, int n);
//把字符串 s 的前 n 个字符连接到当前字符串结尾string& append(const string &s);
//同 operator+=(const string& str)string& append(const string &s, int pos, int n);
//字符串 s 中从 pos 开始的 n 个字符连接到字符串结尾
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 字符串拼接
void test_string_append()
{
string str1 = "I";
str1 += " love game";
cout << "str1 = " << str1 << endl; // str1 = I love game
str1 += ':';
cout << "str1 = " << str1 << endl; // str1 = I love game:
string str2 = "LOL DNF";
str1 += str2;
cout << "str1 = " << str1 << endl; // str1 = I love game:LOL DNF
string str3 = "I";
str3.append(" love ");
str3.append("game abcde", 4);
// str3.append(str2);
str3.append(str2, 4, 3); // 从下标4位置开始 ,截取3个字符,拼接到字符串末尾
cout << "str3 = " << str3 << endl; // str3 = I love game DNF
// string ss = "hello" + "world"; // error,不能直接相加,需要用append或者+=,2个c_string不能相加,只能和string相加,或者用append;为什么不能直接相加呢?因为c_string是一个字符数组,没有重载+运算符,所以不能相加
string ss2 = string("hello") + "world"; // ok,string可以和c_string相加
string ss3 = "hello" + string("world"); // ok,string可以和c_string相加
string ss4 = string("hello") + string("world"); // ok,string可以和string相加
}
追加字符串注意:
1
std::string name3 = "hacket3" + "googd"; // 编译错误: “+”: 不能添加两个指针
原因是在将两个 const char数组
相加,因为,双引号里包含的内容是 const char 数组,它不是真正的 string;它不是字符串,你不能将两个指针或者两个数组加在一起,它不是这么工作的。
解决:
1
2
3
4
5
6
7
8
9
10
11
// 方式1:把它们分成多行
std::stri·ng name4 = "hacket4";
name4 += "good";
std::cout << "name4=" << name4 << std::endl;
// 这样做是在将一个指针加到了字符串name4上了,然后+=这个操作符在string类中被重载了,所以可以支持这么操作。
// 方式2:显式地调用string构造函数将其中一个传入string构造函数中,相当于你在创建一个字符串,然后附加这个给他。
std::string name5 = "hacket5" + std::string("googd");
std::cout << "name5=" << name5 << std::endl;
bool contains = name.find("ha") != std::string::npos;//用find去判断是否包含字符“ha”
string 查找和替换
函数原型:
int find(const string& str, int pos = 0) const;
//查找 str 第一次出现位置,从 pos 开始查找int find(const char* s, int pos = 0) const;
//查找 s 第一次出现位置,从 pos 开始查找int find(const char* s, int pos, int n) const;
//从 pos 位置查找 s 的前 n 个字符第一次位置int find(const char c, int pos = 0) const;
//查找字符 c 第一次出现位置int rfind(const string& str, int pos = npos) const;
//查找 str 最后一次位置,从 pos 开始查找int rfind(const char* s, int pos = npos) const;
//查找 s 最后一次出现位置,从 pos 开始查找int rfind(const char* s, int pos, int n) const;
//从 pos 查找 s 的前 n 个字符最后一次位置int rfind(const char c, int pos = 0) const;
//查找字符 c 最后一次出现位置string& replace(int pos, int n, const string& str);
//替换从 pos 开始 n 个字符为字符串 strstring& replace(int pos, int n,const char* s);
//替换从 pos 开始的 n 个字符为字符串 s
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//查找和替换
void test01()
{
//查找
string str1 = "abcdefgde";
int pos = str1.find("de");
if (pos == -1)
{
cout << "未找到" << endl;
}
else
{
cout << "pos = " << pos << endl;
}
pos = str1.rfind("de");
cout << "pos = " << pos << endl;
}
void test02()
{
//替换
string str1 = "abcdefgde";
str1.replace(1, 3, "1111");
cout << "str1 = " << str1 << endl;
}
find
查找是从左往后,rfind
从右往左find
找到字符串后返回查找的第一个字符位置,找不到返回 -1replace
在替换时,要指定从哪个位置起,多少个字符,替换成什么样的字符串
string 字符串比较
功能描述: 字符串之间的比较
比较方式: 字符串比较是按字符的 ASCII 码进行对比
- = 返回 0
- 返回 1
- 返回 -1
函数原型:
int compare(const string &s) const;
//与字符串 s 比较int compare(const char *s) const;
//与字符串 s 比较
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//字符串比较
void test01()
{
string s1 = "hello";
string s2 = "aello";
int ret = s1.compare(s2);
if (ret == 0) {
cout << "s1 等于 s2" << endl;
}
else if (ret > 0)
{
cout << "s1 大于 s2" << endl;
}
else
{
cout << "s1 小于 s2" << endl;
}
}
string 字符存取
string 中单个字符存取方式有两种
char& operator[](int n);
// 通过[]
方式取字符char& at(int n);
// 通过at
方法获取字符
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void test01()
{
string str = "hello world";
for (int i = 0; i < str.size(); i++)
{
cout << str[i] << " ";
}
cout << endl;
for (int i = 0; i < str.size(); i++)
{
cout << str.at(i) << " ";
}
cout << endl;
//字符修改
str[0] = 'x';
str.at(1) = 'x';
cout << str << endl;
}
string 插入和删除
功能描述:
- 对 string 字符串进行插入和删除字符操作
函数原型:
string& insert(int pos, const char* s);
//插入字符串string& insert(int pos, const string& str);
//插入字符串string& insert(int pos, int n, char c);
//在指定位置插入 n 个字符 cstring& erase(int pos, int n = npos);
//删除从 Pos 开始的 n 个字符
示例:
1
2
3
4
5
6
7
8
9
10
//字符串插入和删除
void test01()
{
string str = "hello";
str.insert(1, "111");
cout << str << endl;
str.erase(1, 3); //从1号位置开始3个字符
cout << str << endl;
}
string 子串
功能描述:
- 从字符串中获取想要的子串
函数原型:
string substr(int pos = 0, int n = npos) const;
//返回由 pos 开始的 n 个字符组成的字符串
示例:
1
2
3
4
5
6
7
8
9
10
11
12
//子串
void test01()
{
string str = "abcdefg";
string subStr = str.substr(1, 3);
cout << "subStr = " << subStr << endl;
string email = "hello@sina.com";
int pos = email.find("@");
string username = email.substr(0, pos);
cout << "username: " << username << endl;
}
std::string
其他方法
C++ 标准库提供了string类类型,支持上述所有的操作,另外还增加了其他更多的功能。
c_char()
得到 C 风格的字符串size()
字符串大小empty()
判空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
string str1 = "Hello";
string str2 = "World";
string str3("天之道");
string str4(str3);
cout<< str1 << str2 <<endl;
// str1拼接str2 组合新的string
string str5 = str1 + str2;
// 在str1后拼接str2 str1改变
str1.append(str2);
//获得c 风格字符串
const char *s1 = str1.c_str();
//字符串长度
str1.size();
//长度是否为0
str1.empty();
//......等等
std::string
字符串原理
std::string
对象在内部通常由三个主要部分构成:一个指向动态分配存储区域的指针、一个长度计数、一个容量计数。- C++11 后,
std::string
实现了小字符串优化 (SSO
),较短的字符串可以直接存储在对象本身,避免动态内存分配。
std::string
和 const char*
std::string
和 const char*
都可以用来表示字符串
std::string
- 类型安全:
std::string
是 C++ 标准库中的一个类,提供了一系列用于字符串处理的成员函数和操作符。 - 动态内存管理:
std::string
会自动管理内存,动态调整大小来适应字符串内容的变化,无需用户手动分配和释放内存。 - 方法和操作: 它提供了大量用于字符串处理的方法,如
append
、insert
、find
、substr
等,还重载了运算符如 + 和 == 以便字符串连接和比较。 - 异常安全:
std::string
遵循 C++ 的异常处理模型,如果在内存分配等操作中发生错误,它会抛出异常。 - 鲁棒性: 出于安全和便利的考虑,
std::string
检查越界错误,并可以存储任何包含null
字符在内的数据。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <string>
int main() {
// 创建字符串
std::string str = "Hello, World!";
// 连接字符串
str += " I'm a C++ string.";
// 访问单个字符
char ch = str[7]; // 'W'
// 获取C风格的字符串
const char* cstr = str.c_str();
// 查找子字符串的位置
std::size_t pos = str.find("World");
// 截取子字符串
std::string sub = str.substr(pos, 5); // "World"
// 输出字符串
std::cout << str << std::endl; // "Hello, World! I'm a C++ string."
std::cout << sub << std::endl; // "World"
return 0;
}
std::string
和 char*
char *
是一个指针std::string
是一个类,内部封装了char *
,是一个char *
型的容器
std::string
和 const char*
- C 风格字符串:
const char*
是指向字符数组的指针,这个字符数组以null
字符(\0
)结尾。 - 不进行内存管理: 使用
const char*
时,必须手动处理内存管理,这包括为字符串分配内存、释放内存以及处理字符串的生命周期。 - 标准库函数: C 提供了一套标准库函数(如
strcpy
,strcat
,strlen
等),用于const char*
类型的字符串操作。 - 性能:
const char*
可以在不涉及动态内存分配的情况下使用,这可能在某些性能敏感的应用中有优势。 - 兼容性: C 字符串与多数 C 库和操作系统的 API 兼容性更好。
在使用 const char*
的情况下,你不能直接更改指向的内容,如果字符串字面量常常是 const char*
类型,尝试修改它们会导致未定义行为,通常是运行时错误。
如果你正在用 C++ 编程,推荐使用 std::string
。虽然性能略低于裸指针和字符数组,但提供的安全性和便利性通常要重要得多。在需要与 C API 交互时,std::string
提供了 .c_str()
成员函数,可以返回一个 const char*
指针,指向兼容 C 风格的字符串。
示例:在 C 中操作字符串,主要是通过字符数组和指针进行的。下面的例子在 C++ 中也同样适用,因为 C++ 保持了与 C 的向后兼容性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include <cstring>
int main() {
// 创建C风格的字符串
const char* cstr = "Hello, World!";
// 复制字符串
char cpy[50];
strcpy(cpy, cstr);
// 连接字符串
strcat(cpy, " I'm a C string.");
// 访问单个字符
char ch = cpy[7]; // 'W'
// 比较字符串
if (strcmp(cstr, cpy) != 0) {
std::cout << "Strings are not the same." << std::endl;
}
// 字符串长度
std::size_t len = strlen(cstr);
// 查找字符
const char* found = strchr(cstr, 'W');
// 输出字符串
std::cout << cpy << std::endl; // "Hello, World! I'm a C string."
// 如果found不为NULL,输出找到的字符和之后的内容
if (found) {
std::cout << found << std::endl; // "World!"
}
return 0;
}
std::string
和 const char*
的转换
__ 从 const char_ 到 std::string:_*
将 const char*
类型转换为 std::string
是隐式的。当一个 const char* 值传递给需要 std::string 的函数时,std::string 类的构造函数会被调用来创建一个新的 std::string 对象。
1
2
3
const char* cstr = "Hello, World";
std::string str = cstr; // 隐式转换
// 在此操作中,std::string 的构造函数会复制 const char* 指向的内容,直到遇到 null 字符。
从 std:: string 到 const char *
: 相反地,从 std::string
到 const char*
的转换不是隐式的,需要显式调用 std::string
的 .c_str()
或 .data()
成员函数。
1
2
std::string str = "Hello, World";
const char* cstr = str.c_str(); // 显式转换
.c_str()
函数返回 const char*
指针,指向 std::string
内部数据的 null-terminated c-style
字符串。这是安全的,只要原始的 std::string
对象没有被修改或销毁。
在实际使用中,通常在将 std::string
传递给只接受 const char*
参数的 C 风格函数时需要这样做。但请注意,如果 std::string
在调用 .c_str()
后被改变或者被销毁,返回的 const char*
指针可能会指向无效的内存。
字符串高效使用方法
- 避免不必要的字符串复制,尤其在函数参数传递时使用引用。
- 尽量使用
std::string
的成员函数而不是<cstring>
中的函数,这样可以保证类型安全和异常安全。 - 使用
reserve
函数预先分配足够空间,以减少重新分配带来的性能损失。 - 利用 C++11 引入的移动语义,比如使用
std::move
在字符串之间转移所有权,而不是复制。 - 使用字符串时,要权衡性能与易用性。
std::string
提供易用性,但在性能敏感的上下文中,const char*
和C风格字符串
可能以减小内存占用和避免动态分配为代价。使用任何字符串表示形式时,都要注意安全性,避免缓冲区溢出、野指针和内存泄漏等问题。
C++ 字符串字面量
- 字符串字面量就是双引号中的内容。
- 字符串字面量是存储在内存的只读部分的,不可对只读内存进行写操作。
C++11
以后,默认为const char*
,否则会报错。
1
2
3
4
5
const char* name6 = "hacket"; //Ok!
name6[2] = 'a'; //ERROR!const不可修改
//如果你真的想要修改这个字符串,你只需要把类型定义为一个数组而不是指针
char name7[] = "hacket"; //Ok!
name7[2] = 'a'; //ok
- 从
C++11
开始,有些编译器比如Clang
,实际上只允许你编译const char*
, 如果你想从一个字符串字面量
编译 char,你必须手动将他转换成char*
1
2
3
```cpp
char* name = (char*)"hacket"; //Ok!
name[2] = 'a'; //OK
- 别的一些字符串
基本上,
char
是一个字节的字符,char16_t
是两个字节的 16 个比特的字符(utf16),char32_t
是 32 比特 4 字节的字符(utf32),const char
就是 utf8. 那么wchar_t
也是两个字节,和 char16_t 的区别是什么呢?事实上宽字符的大小,实际上是由编译器决定的,可能是一个字节也可能是两个字节也可能是 4 个字节,实际应用中通常不是 2 个就是 4 个(Windows 是 2 个字节,Linux 是 4 个字节),所以这是一个变动的值。如果要两个字节就用 char16_t,它总是 16 个比特的。
string_literals
string_literals
中定义了很多方便的东西,这里字符串字面量末尾加 s
,可以看到实际上是一个操作符函数,它返回标准字符串对象(std::string
)。
1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
std::string name0 = "hbh"s + " hello";
std::wstring name0 = L"hbh"s + L" hello";
}
string_literals
也可以忽略转义字符,字符串前面加个 R
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>
int main()
{
using namespace std::string_literals;
const char* example =R"(line1
line2
line3
line4)"
std::cin.get();
}
C++ 中字符串更快
- 内存分配建议:能分配在栈上就别分配到堆上,因为把内存分配到堆上会降低程序的速度
- gcc 的 string 默认大小是 32 个字节,字符串小于等于 15 直接保存在栈上,超过之后才会使用 new 分配
- string 的常用优化:SSO(短字符串优化)、COW(写时复制技术优化)
- C++17 中的
std::string_view
为何优化字符串?
std::string
和它的很多函数都喜欢分配在堆上,这实际上并不理想 。- 一般处理字符串时,比如使用
substr
切割字符串时,这个函数会自己处理完原字符串后创建出一个全新的字符串,它可以变换并有自己的内存(new
,堆上创建)。 - 在数据传递中减少拷贝是提高性能的最常用办法。在
C
中指针是完成这一目的的标准数据结构,而在 C++ 中引入了安全性更高的引用类型。所以在 C++ 中若传递的数据仅仅可读,const string&
成了C++
天然的方式。但这并非完美,从实践上来看,它至少有以下几方面问题:
字符串字面值、字符数组、字符串指针的传递依然要数据拷贝 这三类低级数据类型与 string 类型不同,传入时编译器要做隐式转换,即需要拷贝这些数据生成 string 临时对象。const string&
指向的实际上是这个临时对象。通常字符串字面值较小,性能损失可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,影响程序性能。
substr O(n) 复杂度 substr 是个常用的函数,好在 std::string
提供了这个函数,美中不足的时每次都要返回一个新生成的子串,很容易引起性能热点。实际上我们本意不是要改变原字符串,为什么不在原字符串基础上返回呢?
C++17 std::string_view
优化字符串
std::string_view
是 C++ 17 标准中新加入的类,正如其名,它提供一个字符串的视图,即可以通过这个类以各种方法 “ 观测 “ 字符串,但不允许修改字符串。由于它只读的特性,它并不真正持有这个字符串的拷贝,而是与相对应的字符串共享这一空间。即——构造时不发生字符串的复制。同时,你也可以自由的移动这个视图,移动视图并不会移动原定的字符串。
通过调用 std::string_view
构造器可将字符串转换为 std::string_view
对象。string
可隐式转换为 std::string_view
。
std::string_view
是只读的轻量对象,它对所指向的字符串没有所有权。std::string_view
通常用于函数参数类型,可用来取代const char*
和const string&
。std::string_view
代替const string&
,可以避免不必要的内存分配。std::string_view
的成员函数即对外接口与std::string
相类似,但只包含读取字符串内容的部分。std::string_view::substr()
的返回值类型是std::string_view
,不产生新的字符串,不会进行内存分配。std::string::substr()
的返回值类型是std::string
,产生新的字符串,会进行内存分配。std::string_view
字面量的后缀是 sv。(std::string
字面量的后缀是 s)
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "stdafx.h"
#include "StringOpt.h"
using namespace std;
// 一种调试在heap上分配内存的方法,自己写一个new的方法,然后设置断点或者打出log,就可以知道每次分配了多少内存,以及分配了几次
static uint32_t s_AllocCount = 0;
void* operator new(size_t size)
{
s_AllocCount++;
std::cout << "new, malloc size=" << size << " bytes\n";
return malloc(size);
}
#define STRING_view 0
#if STRING_view
void PrintName(std::string_view name)
{
std::cout << name << std::endl;
}
#else
void PrintName(const std::string& name)
{
cout << name << endl;
}
#endif
void StringOpt::testStringOpt()
{
//string name = "abc hacket,good luck for you 123456!";
const std::string name = "Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs";
// string name = "abc hacket!";
#if STRING_view
std::string_view firstName(name.c_str(), 3);
std::string_view lastName(name.c_str() + 4, 9);
#else
string firstName = name.substr(0, 3);
string lastName = name.substr(4, 9);
#endif
PrintName(name);
PrintName(firstName);
PrintName(lastName);
std::cout << s_AllocCount << " allocations" << std::endl;
cin.get();
}
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//无#define STRING_view 1
Allocating 8 bytes
Allocating 80 bytes
Allocating 8 bytes
Allocating 8 bytes
Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgsgsgsgsgsgsgsdgsgsgnj
Yan
Chernosaf
4 allocations
//有#define STRING_view 1
Allocating 8 bytes
Allocating 64 bytes
Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs
Yan
Chernosaf
2 allocations
使用 std::string
每进行一次 使用 std::string_view
减少了内存在堆上的分配;
进一步优化:使用 C 风格字符串:没有 new
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
//const std::string name = "Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs";
const char *cname = "Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs"; // C-like的编码风格
#if STRING_view
std::string_view firstName(name, 3); //注意这里要去掉 .c_str()
std::string_view lastName(name + 4, 9);
#else
std::string firstName = name.substr(0, 3);
std::string lastName = name.substr(4, 9);
#endif
PrintName(name);
PrintName(firstName);
PrintName(lastName);
std::cout << s_AllocCount << " allocations" << std::endl;
return 0;
}
输出
1
2
3
4
5
//有#define STRING_view 1
Yan Chernosafhiahfiuauadvkjnkjasjfnanvanvanjasdfsgs
Yan
Chernosaf
0 allocations
C++ 小字符串优化
VS 开发工具在 release 模式下面 (debug 模式都会在堆上分配) ,使用size 小于 16的 string,不会分配内存,而大于等于 16 的 string,则会分配 32bytes 内存以及更多,所以 16 个字符是一个分界线 (注:不同编译器可能会有所不同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>
void* operator new(size_t size)
{
std::cout << "Allocated: " << size << " bytes\n";
return malloc(size);
}
int main()
{
// debug模式都会在堆上分配
std::string longName = "cap cap cap cap "; // 刚好16个字符,会在堆上分配32个bytes内存
std::string testName = "cap cap cap cap"; // 15个字符,栈上分配
std::string shortName = "cap";
std::cin.get();
}
//debug模式输出
Allocated: 16 bytes
Allocated: 32 bytes
Allocated: 16 bytes
Allocated: 16 bytes
//release模式输出:
Allocated: 32 bytes
对于 std::string
对象,分配行为可能会取决于 STL 实现及其使用的小字符串优化(SSO)策略。在 C++ 的许多实现中,短字符串被优化以避免动态内存分配,字符串内容直接存储在 std::string
对象的内存块中。这个阈值具体取决于 STL 库的实现和编译模式(例如 Debug 或 Release)。
对于 Visual Studio 环境下的 Release 模式:
- 如果
std::string
使用小字符串优化(SSO
),那么短于或等于某个特定长度的字符串(通常是 15 或 22 个字符,取决于库实现和体系结构)将在std::string
自身的空间中直接存储,而不会进行动态分配。