本系列暴死,不会更新了。

本篇尚未完工

该系列为本人的学习笔记,主要由本人整理书写而成。部分内容来自教材、视频课程等,不能保证完全原创性。

萌新的学习笔记,写错了恳请斧正。

# C 语言里的字符串

在学 C 语言的时候,我们使用过字符串,当时字符串是以 '\0' 结尾的字符的集合。同时,C 语言标准库里还提供了一些 str 函数用于处理字符串。但是这种字符串的使用有很大的缺陷:

  1. 关于字符串处理的库函数和字符串本身是分离的,不符合面向对象的思想
  2. 内存管理非常复杂,依赖于程序员对每一个字符串进行分配和释放内存,容易出现内存泄露或者越界访问
  3. 很多 str 函数的操作并没有那么方便,想实现一些功能会有比较繁琐的操作,还要考虑内存大小的问题容易出错。
  4. C 语言的字符串并不方便动态扩展,需要手动重新分配内存。
  5. 虽然本质上 C 语言的字符串就是一个字符数组,但是这里字符串和字符数组表现出来的性质是有所不同的(详见 C 语言笔记 #20 的最后一个样例),这导致两者容易被搞混!

# C++ 中的 string 类

# string 类是什么

C++ 中的 std::string 类是标准库中用于处理字符串的类。它提供了一种高效、灵活且安全的方式来操作字符序列,替代了 C 语言中基于 char* 的字符串。 std::string 自动管理内存,支持动态扩展,避免了手动内存分配的复杂性。它提供了丰富的成员函数和操作符重载,如拼接、查找、截取、比较等操作,极大简化了字符串的处理。此外, std::string 还兼容 STL 的容器风格,支持迭代器和各种算法。

# string 类与 STL 的关系

首先,我们要知道 string 并不属于严格意义上的 STL。在早期的 C++ 标准化过程中,string 类是 C++ 标准库中一个独立设计的类,设计初衷是提供比 C 语言中的 char* 更强大、灵活的字符串操作能力。STL 和 string 的发展是同时的,因此 STL 中并没有单独做一个 string,但是 STL 比 string 更早被标准化并引入 C++ 标准。

# string 类其实也是一个类模板

C++ 中的 string 本质上是基于类模板的。虽然我们通常使用 string,但它实际上是 basic_string 类模板的一个特例化版本( std::basic_string<char> )。采用类模板主要是因为字符是有很多类型的,常见的是 ASCII 字符(char),但是还有宽字符(wchar_t)、Unicode 字符(char16_t、char32_t)等其他字符需要支持。

# string 类的迭代器 iterator

迭代器是 C++ 标准库中用于遍历容器(如数组、向量、链表、字符串等)元素的对象。能够访问容器中的元素,同时还提供了灵活的接口来进行容器的遍历和操作。

iterator 大概可以理解为一个作为容器(这里是 string)内部类的类似指针的东西。注意,不一定是指针,在不同编译器的实现下可能不同,而且部分容器无法用指针的方式作为迭代器。比方说,我使用 typeid 打印 string::iterator 的类型,得到的结果是:

class std::_String_iterator<class std::_String_val<struct std::_Simple_types<char> > >

这里 iterator 显然是一个类,而不是单纯的指针。

这里演示一下 string 的迭代器的简单使用,其中涉及的部分接口将在下文介绍。

#include <iostream>
#include <string>
using namespace std;
int main() 
{
    string str = "Hello";
    //iterator 正向遍历
    for (string::iterator it = str.begin(); it != str.end(); ++it) 
    {
        cout << *it << ' ';  // 输出: H e l l o
    }
    cout << endl;
    // 使用 const_iterator 只读遍历
    for (string::const_iterator cit = str.cbegin(); cit != str.cend(); ++cit) 
    {
        cout << *cit << ' ';  // 输出: H e l l o
    }
    cout << endl;
    // 使用 reverse_iterator 反向遍历
    for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit) 
    {
        cout << *rit << ' ';  // 输出: o l l e H
    }
    cout << endl;
    return 0;
}

# string 类常用接口(并不完全)

# string 类常见构造
构造解释
string();构造一个长度为 0 的空字符串。
string (const string& str);拷贝构造一个字符串。
string (const string& str, size_t pos, size_t len = npos);从一个已有字符串的 pos 位置开始,选取长度为 len 的子字符序列来构造一个字符串。(npos 默认为 - 1,在 size_t 中即为 4294967295,也就是说如果省略第三个参数就是往后有多少拷多少)
string (const char* s);C 语言字符串转为 string。
string (const char* s, size_t n);从字符数组 s 中选前 n 个字符构造字符串。
string (size_t n, char c);构造一个长度为 n,每一个字符都是 c 的字符串。
#include <string>
using namespace std;
int main()
{
	string s1 = "Hello world!";
	string s2(s1);
	string s3(s1, 5, 5);
	string s4("Hello world!");
	string s5("Hello world!", 5);
	string s6(10, 'x');
	return 0;
}
# string 类常用容器操作
# size 与 length

size_t size() const;

size_t length() const;

size 和 length 两个函数的作用和原理完全相同,size 用的更多。

size 函数是为了与 STL 标准容器的接口保持一致而产生的,也更符合容器的抽象概念。但是 length 这个更符合自然语言的接口也被保留下来了,为了兼容性。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("Hello, World!");
	cout << s.size() << endl;
	cout << s.length() << endl;
	return 0;
}
# capacity

size_t capacity() const;

capacity 函数返回字符串的容量。

注意:string 会默认为字符串预先分配一段最小的初始容量,这个容量可以根据编译器实现不同而有所不同。而当容量超出先前的容量时,就会以顺序表的方式进行容量扩展,而不是精准到每一个字节扩展。(可见数据结构笔记 #1)

比方说下面的程序在我的环境下运行输出为:15、15、15、31。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string str1;
	cout << str1.capacity() << endl;
	string str2 = "Hello, World!";
	cout << str2.capacity() << endl;
	string str3 = "123456789012345";
	cout << str3.capacity() << endl;
	string str4 = "1234567890123456";
	cout << str4.capacity() << endl;
	return 0;
}
# empty

bool enpty() const;

检测字符串是否为空串,如果是返回 true,如果否返回 false。

#include <iostream>
using namespace std;
int main()
{
	string s1 = "a";
	cout << s1.empty() << endl;	//0
	string s2 = "";
	cout << s2.empty() << endl;	//1
	string s3;
	cout << s3.empty() << endl;	//1
	return 0;
}
# clear

void clear();

clear 函数将 string 中的有效字符清空,但是不会改变底层空间的大小。

#include <iostream>
using namespace std;
int main()
{
	string s = "a";
	cout << s.size() << endl;	//1
	cout << s.capacity() << endl;	//15
	s.clear();
	cout << s.size() << endl;	//0
	cout << s.capacity() << endl;	//15
	cout << s << endl;
}
# reserve

void reserve(size_t n=0);

reserve 函数为字符串预留空间,但是不改变有效元素个数。也就是说,reserve 会改变 capacity,但是不会影响 size。另外,reserve 的参数小于 string 底层空间总大小时,不会进行改变。

注意,reserve 并不是将 capacity 改成其参数 n 的大小。而是继续以顺序表扩容的方式增大其容量以至于足以容纳这么多空间。比方说:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s("Hello world!");
	cout << s.capacity() << endl;
	s.reserve(5);
	cout << s.capacity() << endl;
	s.reserve(111);
	cout << s.capacity() << endl;
	s.reserve(112);
	cout << s.capacity() << endl;
	return 0;
}

该程序在我的环境下的输出为:

15
15
111
166
# resize
void resize (size_t n);
void resize (size_t n, char c);

resize 函数用于改变字符串的有效内容的大小。

  1. 当 n 比原先的 size 小:字符串将被截断到指定的大小。
  2. 当 n 比原先的 size 大:扩大字符串,用给定的字符 c 来填充扩展部分。如果没有给定 c,则使用 \0 来填充。
#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s = "Hello";
	cout << "s = " << s << endl;
	cout << "s.size() = " << s.size() << endl;
	s.resize(4);
	cout << "s = " << s << endl;
	cout << "s.size() = " << s.size() << endl;
	s.resize(6, '!');
	cout << "s = " << s << endl;
	cout << "s.size() = " << s.size() << endl;
	s.resize(10);
	cout << "s = " << s << endl;
	cout << "s.size() = " << s.size() << endl;
	s.resize(12, '?');
	cout << "s = " << s << endl;
	cout << "s.size() = " << s.size() << endl;
	return 0;
}

上述程序输出为:

s = Hello
s.size() = 5
s = Hell
s.size() = 4
s = Hell!!
s.size() = 6
s = Hell!!
s.size() = 10
s = Hell!!??
s.size() = 12

注意,这里 !!?? 之间是存在 4 个 \0 的,只是不会被打印出来。但实际上我们检视内存就能看到:

内存

# string 类对象的访问和遍历操作
# operator[]
# char& operator[] (size_t pos);
# const char& operator[] (size_t pos) const;

与数组的访问操作符类似,返回 pos 位置的字符。如果 pos 等于字符串的有效字符数,也能返回对一个 \0 的引用。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string str = "Hello";
	for (int i = 0; i < str.size(); i++)
	{
		cout << str[i] << endl;
	}
	str[0] = 'h';
	for (int i = 0; i < str.size(); i++)
	{
		cout << str[i] << endl;
	}
	return 0;
}
# begin 与 end (包括 r 系与 c 系)

iterator begin();

iterator end();

begin 和 end 函数返回指向字符串起始位置结束位置下一位置的迭代器。cbegin 和 cend 返回的则是常迭代器,rbegin 和 rend 返回反向迭代器,crbegin 和 crend 自然就是反向常迭代器。

#include <iostream>
#include <string>
using namespace std;
int main() 
{
    string str = "Hello";
    //iterator 正向遍历
    for (string::iterator it = str.begin(); it != str.end(); ++it) 
    {
        cout << *it << ' ';  // 输出: H e l l o
    }
    cout << endl;
    // 使用 const_iterator 只读遍历
    for (string::const_iterator cit = str.cbegin(); cit != str.cend(); ++cit) 
    {
        cout << *cit << ' ';  // 输出: H e l l o
    }
    cout << endl;
    // 使用 reverse_iterator 反向遍历
    for (string::reverse_iterator rit = str.rbegin(); rit != str.rend(); ++rit) 
    {
        cout << *rit << ' ';  // 输出: o l l e H
    }
    cout << endl;
    return 0;
}
# 范围 for (C++11)

C++11 支持的范围 for 也可以用来遍历 string 类对象。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string str = "Hello, World!";
	for (auto& c : str)
	{
		cout << c << endl;
	}
	return 0;
}
# string 类对象的修改操作
# push_back

void push_back (char c);

push_back 在字符串后尾插字符 c。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string str = "Hello";
	cout << "str: " << str << endl;
	str.push_back(' ');
	cout << "str: " << str << endl;
	str.push_back('W');
	cout << "str: " << str << endl;
	str.push_back('o');
	cout << "str: " << str << endl;
	str.push_back('r');
	cout << "str: " << str << endl;
	str.push_back('l');
	cout << "str: " << str << endl;
	str.push_back('d');
	cout << "str: " << str << endl;
	return 0;
}
# append
string& append (const string& str);
string& append (const string& str, size_t subpos, size_t sublen = npos);
string& append (const char* s);
string& append (const char* s, size_t n);
string& append (size_t n, char c);

append 用于在字符串后追加一个字符串,有很多重构形式。比方说上面列出的几个(还有更多)。

它们的用法看参数名就能理解,下面直接演示一下:

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s = "append:";
	string t = "uvwxyz";
	s.append(t);		// 将字符串 t 的内容追加到字符串 s 的末尾
	cout << s << endl;	// append:uvwxyz
	s.append(t, 2, 3);	// 将字符串 t 的子串(从 t [2] 开始数 3 位)追加到字符串 s 的末尾
	cout << s << endl;	// append:uvwxyzwxy
	s.append("123");	// 将字符串 "123" 追加到字符串 s 的末尾
	cout << s << endl;	// append:uvwxyzwxy123
	s.append("456", 2);	// 将字符串 "456" 的前 2 个字符追加到字符串 s 的末尾
	cout << s << endl;	// append:uvwxyzwxy12345
	s.append(3, 'x');	// 将字符 'x' 追加到字符串 s 的末尾 3 次
	cout << s << endl;	// append:uvwxyzwxy12345xxx
	return 0; 
}
# operator+=
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);

同样是在字符串后追加一个字符串 str。

#include <iostream>
#include <string>
using namespace std;
int main()
{
	string s = "append:";
	cout << (s += "abc") << endl;	// append:abc
	cout << (s += s) << endl;		// append:abcappend:abc
	cout << (s += '.') << endl;		// append:abcappend:abc.
	return 0;
}
# c_str

const char* c_str() const noexcept;

c_str 函数将 C++ 字符串转换为 C 字符串。使用简单无需示例。

# find
# rfind
# npos
# substr
# string 类非成员函数
# operator+
# operator>>
# operator<<
# getline
# relational operators(==、!=、<、<=、>、>=)