目录
2.2. C++ 11 的 string::operator=
2.7. Non-member function overloads
1. string 是什么
- string是表示字符串的一个类;
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作;
- string在底层实际是: 通过 basic_string 这个类模板实例化的具体类,即basic_string<char> ;
- typedef basic_string<char> string;
- 不能操作多字节或者变长字符的序列;
- 在使用string类时,必须包含 #include<string>。
2. 标准库中的string
2.1. C++11 的 string::string
首先看看 string 的默认构造:
int main()
{
std::string s1;
return 0;
}
现象如下:
用一个字符串构造一个string:
int main()
{
std::string s1("hello string");
return 0;
}
现象如下:
通过上面的两个例子,我们发现,对于标准库中的 string 来说,不管 string 对象是空串还是非空串,都会有一个 '\0' 作为字符串的结尾,这是为了满足C风格字符串的约定和方便字符数组的处理,同时,我们发现,'\0' 这个字符并不计入 string 中的 size;
C++11 的 string 构造函数具体如下:
//default(1)
string();
//copy (2)
string (const string& str);
//substring (3)
//这里的npos 是一个 static const size_t npos = -1;
string (const string& str, size_t pos, size_t len = npos);
//from c-string (4)
string (const char* s);
//from buffer (5)
string (const char* s, size_t n);
//fill (6)
string (size_t n, char c);
//range (7)
template <class InputIterator>
string (InputIterator first, InputIterator last);
// initializer list (8)
string (initializer_list<char> il);
// move (9)
string (string&& str) noexcept;
下面是上述构造函数的使用示例:
void Test1()
{
// default
std::string s1;
std::cout << "s1: " << s1 << std::endl;
// from c-string
std::string s2("cowsay hello");
std::cout << "s2: " << s2 << std::endl; //cowsay hello
// copy
std::string s3(s2);
std::cout << "s3: " << s3 << std::endl; //cowsay hello
// substring
std::string s4(s2, 5, 3);
std::cout << "s4: " << s4 << std::endl; //y h
// 如果此时不显示给最后一个参数,将会使用缺省值npos
std::string s5(s2, 5); // size_t npos = -1;
std::cout << "s5: " << s5 << std::endl; //y hello
// 如果此时给最后一个参数,传递了一个很大的值(从pos位置超过了字符串的结尾),那么此时字符串有多长拷多长
std::string s6(s2, 5, 20);
std::cout << "s6: " << s6 << std::endl; //y hello
// from buffer
// 拷贝一个字符串前n个字符
std::string s7("cowsay hello", 5);
std::cout << "s7: " << s7 << std::endl; //cowsa
// fill
std::string s8(10, 'x'); // 实例化一个n个同字符组成的字符串
std::cout << "s8: " << s8 << std::endl; //xxxxxxxxxx
// range
// 利用一个对象的迭代器实例化一个对象
std::string s9(s2.begin(), s2.end());
std::cout << "s19: " << s9 << std::endl; //cowsay hello
// 最后再补充一点
std::string s10 = "cowsay world"; //在这里实际上是 构造 + 拷贝构造 然后 被编译器优化成了 直接构造
std::cout << "s10: " << s10 << std::endl; //cowsay world
// initializer list
std::string s11 = { 'a', 'b', 'c', 'd' };
std::cout << "s11: " << s11 << std::endl; // abcd
// move
std::string&& s12 = std::move(s11);
std::cout << "s12: " << s11 << std::endl; // abcd
}
现象如下:
2.2. C++ 11 的 string::operator=
C++11 的 string 的 operator= 具体如下:
// string (1)
string& operator= (const string& str);
// c-string (2)
string& operator= (const char* s);
// character (3)
string& operator= (char c);
// initializer list (4)
string& operator= (initializer_list<char> il);
// move (5)
string& operator= (string&& str) noexcept;
测试用例如下:
void Test2(void)
{
// std::string::operator=
// string(1)
std::string s1 = "cowsay hello";
std::string s2 = "cowsay world";
std::cout << "s1: " << s1 << std::endl;
std::cout << "s2: " << s2 << std::endl;
std::cout << "---------------------------------" << std::endl;
s1 = s2;
std::cout << "s1: " << s1 << std::endl;
std::cout << "s2: " << s2 << std::endl;
std::cout << "---------------------------------" << std::endl;
// c-string(2) 一个字符串赋值
s2 = "hello world";
s1 = s2;
std::cout << "s1: " << s1 << std::endl;
std::cout << "s2: " << s2 << std::endl;
std::cout << "---------------------------------" << std::endl;
// character (3) 一个字符赋值
s1 = 'a';
std::cout << "s1: " << s1 << std::endl;
std::cout << "---------------------------------" << std::endl;
// initializer list (4) 初始化列表
s1 = { 'a', 'b', 'c', 'd' };
std::cout << "s1: " << s1 << std::endl;
std::cout << "---------------------------------" << std::endl;
// move (5) 移动赋值
s1 = std::move(s2);
std::cout << "s1: " << s1 << std::endl;
std::cout << "---------------------------------" << std::endl;
}
现象如下:
2.3. element access
2.3.1. operator[]
如果要遍历一个 string 对象,该如何做呢?
通过 std::string::operator[]:
//std::string::operator[]
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
测试用例:
void Test3(void)
{
string s("cowsay hello");
//遍历
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i];
}
cout << endl;
}
如果我想改这个 string 对象中的元素,也可以这样做:
void Test3(void)
{
std::string s("cowsay hello");
//遍历
for (size_t i = 0; i < s.size(); ++i)
{
++s[i]; //修改
std::cout << s[i];
}
std::cout << std::endl;
}
之所以可以这样,是因为此时是引用返回,并且没有加const,可读可写;
如果是一个const string对象,理论上是不可以修改的:
const 对象只有读权限,没有写权限;
const string s("cowsay world");
for (size_t i = 0; i < s.size(); ++i)
{
cout << s[i]; //const对象只有读权限
}
operator[] 可以让 string 对象向数组一样操作,那么会不会存在越界访问的情况呢? 如下:
void Test4(void)
{
std::string s("haha");
std::cout << s[10] /* 越界访问 */ << std::endl;
}
现象如下:
可以清楚的看到, 在访问 string 对象时,如果存在越界访问,会导致进程挂掉 (assert)。
2.3.2. at
at 是 string 类的一个成员函数,而 opeartor[] 是一个运算符重载的成员函数,它们的作用和用法几乎一样,只不过operator[] 失败是断言检查 (assert),at 失败是抛异常;
//std::string::at
char& at(size_t pos);
const char& at(size_t pos) const;
证明:at 失败会抛异常,测试用例如下:
void Test4(void)
{
std::string s("haha");
s.at(10) = 'x'; // 越界访问
}
int main()
{
try
{
Test4();
}
catch (const std::exception& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
现象如下:
可以看到, std::string::at 失败会抛异常。
这里有一道 string 练习题 (仅仅反转字母): https://leetcode.cn/problems/reverse-only-letters/solution/
代码如下:
class Solution {
public:
bool is_letter(const char& c) const
{
if((c >= 'A' && c <= 'Z')
|| (c >= 'a' && c <= 'z'))
return true;
else
return false;
}
string reverseOnlyLetters(string s) {
size_t left = 0;
size_t right = s.size() - 1;
while(left < right)
{
while(left < right && !is_letter(s[left]))
++left;
while(left < right && !is_letter(s.at(right)))
--right;
swap(s[left++],s.at(right--));
}
return s;
}
};
2.4. iterator
2.4.1. begin() and end()
//begin是第一个有效元素的位置
iterator begin();
const_iterator begin() const;
//end是最后一个有效元素位置的下一个位置
iterator end();
const_iterator end() const;
我们知道, string 和 vector<T> 这种类所实例化的对象中的元素所在的地址本质是连续的,即是一段连续的空间,因此,这种类,一般情况下,迭代器就是原生指针。
用户可以通过迭代器访问 string 对象中的各个元素,示例如下:
void Test5()
{
std::string s = "cowsay hello";
//利用迭代器遍历string实例化的对象
std::string::iterator it = s.begin(); //begin 是第一个element的位置
while (it != s.end()) //end是最后一个有效element下一个的位置,在这里就是'\0'
{
std::cout << *it;
++it;
}
std::cout << std::endl;
}
现象如下:
如果是const string 对象,那么所使用的迭代器就是 const_iterator,此时只能读,不能写,示例如下:
void Test6(void)
{
const std::string s("haha");
std::string::const_iterator cit = s.begin();
while (cit != s.end())
{
std::cout << *cit;
++cit;
}
std::cout << std::endl;
}
现象如下:
迭代器是所有容器通用的访问方式,用法十分类似,用户需要重视,而在以前的学习中,我们知道C++有一个语法,即范围for,事实上,在底层实现,范围for就是通过迭代器实现的。
在这里回顾一下范围for的使用:
void Test7()
{
std::string s = "cowsay hello";
// 范围for: a.自动迭代 b.自动判断结束
// 如果想在范围for里面进行写操作,需要加上引用 for(auto& e : s)
for (auto e : s)
{
std::cout << e;
}
std::cout << std::endl;
}
现象如下:
我们也可以通过汇编验证范围for的底层是否是通过迭代器实现的呢? 验证如下:
可以清晰的看到,范围for底层的实现本质就是通过迭代器和相关操作 (operator++、operator!=) 来实现的。
2.4.2. rbegin() and rend()
// rbegin 是最后一个有效元素的位置
iterator rbegin();
const_reverse_iterator rbegin() const;
// rend 是第一个有效元素位置的前一个位置
iterator rend();
const_reverse_iterator rend() const;
测试用例如下:
void Test9()
{
std::string s("abcdef");
//std::string::reverse_iterator rit = s.rbegin(); //这样写太麻烦了,可以这样写
auto rit = s.rbegin();
while (rit != s.rend())
{
std::cout << *rit;
++rit;
}
std::cout << std::endl;
}
现象如下:
string 也会存在const 反向迭代器,使用如下:
void Test10()
{
const std::string s("cowsay world");
std::string::const_reverse_iterator crit = s.crbegin();
while (crit != s.crend())
{
std::cout << *crit;
++crit;
}
std::cout << std::endl;
}
现象如下:
总结:对于 std::string 来说,我们一般很少使用迭代器,因为 operator[] 更好用、更方便;但是迭代器 iterator 是所有容器的通用访问方式,用法类似,因此也很重要。
2.4. Modifiers
2.4.1. push_back
// std::string::push_back
void push_back (char c);
std::string::push_back 只支持一次插入一个字符,示例如下:
void Test11(void)
{
std::string s;
s.push_back('a');
s.push_back('b');
s.push_back('c');
s.push_back('d');
for (const auto& it : s)
{
std::cout << it;
}
std::cout << std::endl;
}
现象如下:
2.4.2. append
// string (1)
string& append(const string& str);
// substring (2)
string& append(const string& str, size_t subpos, size_t sublen);
// c-string (3)
string& append(const char* s);
// buffer (4)
string& append(const char* s, size_t n);
// fill (5)
string& append(size_t n, char c);
// range (6)
template <class InputIterator>
string& append(InputIterator first, InputIterator last);
void Test9(void)
{
std::string s1("cowsay");
std::string s2("hello");
std::cout << s1 << std::endl;
std::cout << s2 << std::endl;
//string(1):
//追加一个string实例化的对象
s1.append(s2);
std::cout << s1 << std::endl;
//substring(2):
//从s2这个对象的下标为2的位置上进行拷贝,因为第三个参数为-1,即拷贝到结尾
s1.append(s2, 2, -1);
std::cout << s1 << std::endl;
//如果第三个参数很大,那么就会一直拷贝到结尾
s1.append(s2, 2, 20);
std::cout << s1 << std::endl;
//如果第二个参数超过了 is copied 的对象结尾,那么就会 try catch ,
//在这里就会抛异常 /* invalid string position */
s1.append(s2, 6, 10);
std::cout << s1 << std::endl;
//c-string (3):
//追加一个字符串
s1.append("haha");
std::cout << s1 << std::endl;
//buffer (4):
//追加字符串的前n个字符
s1.append("hehe", 2);
std::cout << s1 << std::endl;
//fill (5):
//追加n个相同的字符
s1.append(10, 'A');
std::cout << s1 << std::endl;
//range (6):
//利用迭代器进行追加
s1.append(s2.begin(), s2.end());
std::cout << s1 << std::endl;
}
2.4.3. operator+=
// string (1)
string& operator+= (const string& str);
// c-string (2)
string& operator+= (const char* s);
// character (3)
string& operator+= (char c);
void Test10(void)
{
string s1("hello");
string s2("world");
//string (1):
// +=一个string实例化的对象
s1 += s2;
cout << s1 << endl;
//c-string (2):
// +=一个字符串
s1 += "haha";
cout << s1 << endl;
//character (3)
// +=一个字符
s1 += 'X';
cout << s1 << endl;
}
在底层层面,operator+= 底层就是调用的是 append() 和 push_back();
2.4.4. assign
Assigns a new value to the string, replacing it's current contents.
//string (1)
string& assign (const string& str);
//substring (2)
string& assign (const string& str, size_t subpos, size_t sublen);
//c-string (3)
string& assign (const char* s);
//buffer (4)
string& assign (const char* s, size_t n);
//fill (5)
string& assign (size_t n, char c);
//range (6)
template <class InputIterator>
string& assign (InputIterator first, InputIterator last);
void Test1(void)
{
//assign
string s1("cowsay hello");
string s2("cowsay world");
cout << "s1 = " << s1 << endl << "s2 = " << s2 << endl;
/*
* s1 = cowsay hello
* s2 = cowsay world
*/
s1.assign(s2);
cout << "s1 = " << s1 << endl << "s2 = " << s2 << endl;
/*
* s1 = cowsay world
* s2 = cowsay world
*/
//assign的作用相当于赋值
}
2.4.5. insert
在指定的pos(或者p)位置之前,插入字符串(或者字符)或者string对象
//string (1)
string& insert (size_t pos, const string& str);
//substring (2)
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
//c-string (3)
string& insert (size_t pos, const char* s);
//buffer (4)
string& insert (size_t pos, const char* s, size_t n);
//fill (5)
string& insert (size_t pos, size_t n, char c);
void insert (iterator p, size_t n, char c);
//single character (6)
iterator insert (iterator p, char c);
//range (7)
template <class InputIterator>
void insert (iterator p, InputIterator first, InputIterator last);
2.4.6. erase
删除字符串的一部分
//sequence (1)
string& erase (size_t pos = 0, size_t len = npos);
//character (2)
iterator erase (iterator p);
//range (3)
iterator erase (iterator first, iterator last);
2.5. string实例化对象的扩容机制
首先看一下,string 对象是如何扩容的:
void Test11(void)
{
std::string s;
size_t my_capacity = s.capacity();
for (size_t i = 0; i < 1000; ++i)
{
if (my_capacity != s.capacity())
{
cout << "old_capacity = " << my_capacity << "--->" << "new_capacity = " << s.capacity() << endl;
my_capacity = s.capacity();
}
s.push_back('x');
}
}
运行结果:
old_capacity = 15--->new_capacity = 31
old_capacity = 31--->new_capacity = 47
old_capacity = 47--->new_capacity = 70
old_capacity = 70--->new_capacity = 105
old_capacity = 105--->new_capacity = 157
old_capacity = 157--->new_capacity = 235
old_capacity = 235--->new_capacity = 352
old_capacity = 352--->new_capacity = 528
old_capacity = 528--->new_capacity = 792
old_capacity = 792--->new_capacity = 1188
在vs2013的环境下,string实例化的对象扩容机制:按照1.5倍的方式进行扩容;
相同的代码,在g++ 编译器下,string 实例化的对象扩容机制:按照2倍的方式进行扩容;
虽然二者扩容机制稍有差异,但是如果一个对象已经知道需要开多少空间了,我们可不可以提前把空间开好,避免了前期的频繁扩容呢? 当然,STL考虑到了这一点;
2.5.1. reserve
//std::string::reserve
void reserve (size_t n = 0);
预先开好n个空间,只有capacity会发生变化,其size是不发生变化的;
void Test11(void)
{
std::string s;
s.reserve(1000); //预先开好空间
size_t my_capacity = s.capacity();
for (size_t i = 0; i < 1000; ++i)
{
if (my_capacity != s.capacity())
{
cout << "old_capacity = " << my_capacity << "--->" << "new_capacity = " << s.capacity() << endl;
my_capacity = s.capacity();
}
s.push_back('x');
}
}
在vs2013的环境下,现象如下:
在g++的环境下,现象如下:
不论是vs2013还是Linux环境下,在用户已经知道需要多少空间时,reserve 都可以在一定程度上减少前期频繁扩容的问题;
2.5.2. resize
std::string::resize
void resize (size_t n);
void resize (size_t n, char c);
预先开好n个空间,并初始化,如果不显式初始化,那么resize默认的初始化值是0,并且此时capacity和size都会更新;
此时如果不显示初始化,默认值是0;
如果我们显示初始化了,那么值就是我们显式定义的;
2.6. string operations
2.6.1. c_str
用于返回一个以空字符结尾的 C 风格字符串(即以 '\0' 结尾的字符数组)
//std::string::c_str
const char* c_str() const;
通常用于将 std::string 对象转换为 C 风格字符串进行字符串处理或与 C 风格字符串相关的 API 进行交互。
void Test4(void)
{
std::string s1("cowsay hehe");
cout << s1 << endl;
cout << s1.c_str() << endl;
s1 += '\0';
s1 += "i am a superman";
cout << s1 << endl;
cout << s1.c_str() << endl;
}
运行结果:
cowsay hehe
cowsay hehe
cowsay hehe i am a superman
cowsay hehe
我们可以看到,二者差异的起因是因为中间插入了一个 '\0',c_str()是以 '\0' 作为结束标识符的,而string自己重载的operator<<是以_size作为结束的,它不受'\0'影响;
2.6.2. find and substr
// string (1)
size_t find (const string& str, size_t pos = 0) const;
// c-string (2)
size_t find (const char* s, size_t pos = 0) const;
// buffer (3)
size_t find (const char* s, size_t pos, size_t n) const;
// character (4)
size_t find (char c, size_t pos = 0) const;
return:如果找到了对应的string对象/字符串/字符,返回对应的位置;没找到返回string::npos;
string substr (size_t pos = 0, size_t len = npos) const;
substr从 pos 到 pos + len(默认到string对象的结尾)构造一个新的对象并返回;
构造对象的范围:[pos,pos + len)
如何得到一个文件后缀:
void Test5(void)
{
string s("Test.cpp");
//如何得到该文件的后缀?
size_t pos = s.find('c');
if(pos != string::npos)
{
string ret = s.substr(pos);
cout << ret << endl;
}
}
但有时候呢,有的文件后缀是这样的
如果我们继续用find这个成员函数,可能就不符合我们的需求了,因此std::string里面也提供了rfind;
string s("Test.cpp.tar.zip");
size_t pos = s.rfind('.');
if (pos != string::npos)
{
string ret = s.substr(pos);
cout << ret << endl;
}
rfind()和find的区别:前者是从后往前找,后者从前往后找;
如何解析一个URL (得到协议、域名、资源路径)
void Test5(void)
{
string s("https://legacy.cplusplus.com/reference/string/string/find/");
size_t pos1 = s.find("://");
if (pos1 == string::npos)
{
printf("Protocol error");
exit(1);
}
else
{
//协议
string protocol = s.substr(0,pos1+3);
cout << protocol << endl;
}
size_t pos2 = s.find('/', pos1 + 3);
if (pos2 == string::npos)
{
printf("Domain error");
exit(1);
}
else
{
//域名
string domain = s.substr(pos1 + 3, pos2 - pos1 - 3);
cout << domain << endl;
}
//路径
string path = s.substr(pos2);
cout << path << endl;
}
现象如下:
sdt::string::find_first_of
用于在字符串中查找第一个匹配指定字符集合中任意字符的位置。
//string (1)
size_t find_first_of (const string& str, size_t pos = 0) const;
//c-string (2)
size_t find_first_of (const char* s, size_t pos = 0) const;
//buffer (3)
size_t find_first_of (const char* s, size_t pos, size_t n) const;
//character (4)
size_t find_first_of (char c, size_t pos = 0) const;
理解:
该函数用于执行字符级的搜索和匹配操作。它会从指定位置开始逐个检查字符串中的字符,如果遇到指定字符集合中的任何字符,就立即返回该字符在字符串中的位置。如果没找到,搜索会一直进行,直到遍历完整个字符串或找到第一个匹配字符为止。
void Test6(void)
{
string s = "cowsay hehe";
size_t pos1 = s.find_first_of('w');
size_t pos2 = s.find_first_of("x");
cout << s[pos1] << endl; //正常打印
cout << s.at(pos2) << endl; //抛异常
}
运行结果:
w
invalid string position
find_first_of是从起始位置向结束位置遍历,find_last_of与之相反;
find_first_not_of 接口如下:
//string (1)
size_t find_first_not_of (const string& str, size_t pos = 0) const;
//c-string (2)
size_t find_first_not_of (const char* s, size_t pos = 0) const;
//buffer (3)
size_t find_first_not_of (const char* s, size_t pos, size_t n) const;
//character (4)
size_t find_first_not_of (char c, size_t pos = 0) const;
该函数的作用是,从起始向结束位置遍历,找第一个与之不匹配的string对象/字符串/字符,找到了就返回对应位置,没找到返回string::npos;
void Test7(void)
{
string s = "cowsay world";
size_t pos1 = s.find_first_not_of('w'); //找第一个不是这个string对象/字符串/字符的位置
size_t pos2 = s.find_first_not_of('c');
cout << s[pos1] << endl;
cout << s[pos2] << endl;
}
运行结果:
c
o
find_first_not_of是从起始位置向结束位置遍历,find_last_not_of与之相反;
2.7. Non-member function overloads
2.7.1. getline
指的是与 std::string 相关的非成员函数,在C++中也称为自由函数或全局函数
//std::getline (string)
istream& getline (istream& is, string& str, char delim);
istream& getline (istream& is, string& str);
参数:
- is:输入流,可以是 std::cin(标准输入)或其他输入流对象;
- str:存储读取文本的目标字符串;
- delim:可选参数,用于指定行分隔符,默认为换行符 '\n'。
返回值:
- 输入流 is 的引用;
功能:
- 该函数会从输入流中读取字符,直到遇到行分隔符(默认为换行符),或者到达流的末尾。读取的字符会保存到目标字符串 str 中,包括行分隔符(如果存在)。
测试实例:
void Test9()
{
string s;
std::cin >> s; //遇到空格/换行终止
cout << s << endl;
}
结果:
hehe haha
hehe
void Test9()
{
string s;
getline(std::cin, s); //遇到换行/到达流结尾终止
cout << s << endl;
}
结果:
hehe haha
hehe haha
2.7.2. to_string
std::to_string 是一个全局函数,用于将整数、浮点数等基本数据类型转换为对应的字符串表示形式。
下面是它的具体函数声明:
string to_string (int val);
string to_string (long val);
string to_string (long long val);
string to_string (unsigned val);
string to_string (unsigned long val);
string to_string (unsigned long long val);
string to_string (float val);
string to_string (double val);
string to_string (long double val);
参数:
- val:要转化为字符串的值;
返回值:
- 该函数返回转换后的字符串;
示例:
void Test10(void)
{
int i_val;
double d_val;
std::cin >> i_val >> d_val;
string i_str = std::to_string(i_val);
string d_str = std::to_string(d_val);
cout << i_str << endl << d_str << endl;
}
输入
10
11.1输出
10
11.100000
2.7.3. 字符串转化成对应的数字类型:
具体如下:
这里以 stoi 举例:
//std::stoi
int stoi (const string& str, size_t* idx = 0, int base = 10);
std::stoi 是 C++ 标准库中的一个函数,用于将字符串转换为对应的整数值。
参数介绍:
- str:要转换的字符串;
- pos:可选参数,指向一个对象的指针,用于存储转换结束后的下一个字符的位置。如果不提供该参数,或者该参数为 0,表示不需要存储位置信息;
- base:可选参数,指定字符串中的数字的基数,默认为 10。
示例:
void Test11()
{
string i_str;
std::cin >> i_str;
int i_val = std::stoi(i_str);
cout << i_val << endl;
}
输入
20
输出
20
有了上面的理解,我们需要模拟实现一下 string,详细请看,string 的模拟实现-CSDN博客