文章

04.C++字符串

04.C++字符串

04.C++ 字符串

字符串/字符数组的工作原理

  1. 字符串实际上是字符数组
    1. C++ 中有一种数据类型叫做 char,是 Character 的缩写,占用一个字节的内存。它很有用因为它能把指针转换为 char 型指针,所以你可以用字节来做指针运算。它对于分配内存缓冲区也很有用,比如分配 1024 个 char 即 1KB 空间。它对字符串和文本也很有用,因为 C++ 对待字符的默认方式是通过 ascii 字符进行文本编码。我们在 C++ 中处理字符是一个字符一个字节。A ascii 可以扩展比如 UTF-8、UTF-16、UTF-32,我们有 wide string(宽字符串)等。我们有两个字节的字符、三个字节、四个字节的等等。
  2. 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_stringstd::string 本质上就是这个 basic_stringchar 作为模板参数的模板类实例。叫模板特化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 个字符为字符串 str
  • string& 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 找到字符串后返回查找的第一个字符位置,找不到返回 -1
  • replace 在替换时,要指定从哪个位置起,多少个字符,替换成什么样的字符串

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 个字符 c
  • string& 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::stringconst char*

std::stringconst char* 都可以用来表示字符串

std::string
  1. 类型安全: std::string 是 C++ 标准库中的一个类,提供了一系列用于字符串处理的成员函数和操作符。
  2. 动态内存管理: std::string 会自动管理内存,动态调整大小来适应字符串内容的变化,无需用户手动分配和释放内存。
  3. 方法和操作: 它提供了大量用于字符串处理的方法,如 appendinsertfindsubstr 等,还重载了运算符如 + 和 == 以便字符串连接和比较。
  4. 异常安全: std::string 遵循 C++ 的异常处理模型,如果在内存分配等操作中发生错误,它会抛出异常。
  5. 鲁棒性: 出于安全和便利的考虑,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::stringchar*
  • char * 是一个指针
  • std::string 是一个类,内部封装了 char *,是一个 char * 型的容器
std::stringconst char*
  1. C 风格字符串: const char* 是指向字符数组的指针,这个字符数组以 null 字符(\0)结尾。
  2. 不进行内存管理: 使用 const char* 时,必须手动处理内存管理,这包括为字符串分配内存、释放内存以及处理字符串的生命周期。
  3. 标准库函数: C 提供了一套标准库函数(如 strcpy, strcat, strlen 等),用于 const char* 类型的字符串操作。
  4. 性能: const char* 可以在不涉及动态内存分配的情况下使用,这可能在某些性能敏感的应用中有优势。
  5. 兼容性: 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::stringconst 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::stringconst 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 自身的空间中直接存储,而不会进行动态分配。
本文由作者按照 CC BY 4.0 进行授权