1. string简介
C语言中,可以用字符数组来存储字符串,如:
char ch[] = "hello world";
C++中,可以使用string类对象来存储字符串,使用起来比C语言中的字符数组要方便得多,而且不用考虑容量的问题。
本篇博客,我会以实用为主,介绍string类常用的接口,包括常见的构造函数、容量操作、访问及遍历操作、修改操作、非成员函数等等。接着通过这些知识做几道练习题,通过题目,了解这些接口实际的用法。最后,我会讲解string的底层实现原理,并模拟实现。
由于是实用为主,某些不常用的接口我不做介绍。
2. string类对象的构造
2.1 string();
无参的构造函数,可以构造出空的string类对象,即空字符串。
string s; // 等价于string s("");
2.2 string(const char* s);
用C-string来构造string类对象。
string s("hello world");
cout << s << endl;
输出:
hello world
2.3 string(size_t n, char c);
string类对象中包含n个字符c。
string s(5, 'a');
cout << s << endl;
输出:
aaaaa
2.4 string(const string& s);
拷贝构造函数,使用一个string类对象构造另一个string类对象。
string s1("hello world");
string s2(s1);
cout << s1 << endl;
cout << s2 << endl;
输出:
hello world
hello world
2.5 使用迭代器构造
template<class InputIterator>
string(InputIterator first, InputIterator last);
利用任意类型的迭代器进行构造。
string s1("hello world");
string s2(++s1.begin(), --s1.end());
cout << s2 << endl;
输出:
ello worl
3. string类对象的容量操作
3.1 size
size_t size() const;
返回字符串有效字符长度,同length。引入size是为了与其他容器接口保持一致,我习惯使用size而不是length。
string s("hello world");
cout << s.size() << endl;
输出:
11
3.2 length
size_t length() const;
同size,返回字符串有效字符长度。
string s("hello world");
cout << s.length() << endl;
输出:
11
3.3 capacity
size_t capacity() const;
返回空间总大小,至少是size。
string s("hello world");
cout << s.capacity() << endl;
visual studio 2022环境下输出:
15
3.4 empty
bool empty() const;
检测字符串是否为空串,是返回true,否则返回false。
string s1;
cout << s1.empty() << endl;
输出:
1
3.5 clear
void clear();
清空有效字符,一般不改变底层空间大小。
string s("hello world");
s.clear();
cout << s.empty() << endl;
输出:
1
3.6 reserve
void reserve(size_t n = 0);
为字符串预留空间,不改变有效元素个数。当n比底层空间大时,会扩容,扩容后空间至少为n;当n比底层空间小时,一般不改变底层空间。
string s;
s.reserve(100);
cout << s.capacity() << endl;
visual studio 2022环境下输出:
111
3.7 resize
void resize(size_t n, char c = '\0');
将有效字符的个数改成n个。当n比字符串长度小时,只保留前n个字符;当n比字符串长度大时,会在后面填充字符c,直到字符串长度为n;当n比字符串底层空间大时,会扩容,扩容后空间至少为n,相当于执行了reserve(n)。
string s;
s.resize(5, 'x');
cout << s << endl;
s.resize(7, 'y');
cout << s << endl;
s.resize(3);
cout << s << endl;
输出:
xxxxx
xxxxxyy
xxx
4. string类对象的访问及遍历操作
4.1 operator[]
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
返回pos位置的字符。非const对象调用后返回的字符可读可写,const对象调用后返回的字符只读不写。pos必须在有效的下标范围内。
string s1("hello world");
// 可读可写
s1[0] = 'H'; // "Hello world"
cout << s1[0] << endl; // 'H'
const string s2("hello world");
// 只读不写
// s2[0] = 'H'; // 错误用法
cout << s2[0] << endl; // 'h'
输出:
H
h
4.2 begin + end
iterator begin();
const_iterator begin() const;
iterator end();
const_iterator end() const;
begin获取指向第一个字符的迭代器,end获取指向最后一个字符的下一个位置的迭代器。
非const对象调用后返回的iterator指向的字符可读可写,const对象调用后返回的const_iterator指向的字符只读不写。
string s1("hello world");
string::iterator it = s1.begin();
// 可读可写
while (it != s1.end())
{
(*it)++;
cout << *it << " ";
++it;
}
cout << endl;
const string s2("hello world");
// 只读不写
string::const_iterator cit = s2.begin();
while (cit != s2.end())
{
// (*cit)++; // 错误用法
cout << *cit << " ";
++cit;
}
cout << endl;
输出:
i f m m p ! x p s m e
h e l l o w o r l d
4.3 rbegin + rend
reverse_iterator rbegin();
const_reverse_iterator rbegin() const;
reverse_iterator rend();
const_reverse_iterator rend() const;
同begin和end,只不过是反向的。非const对象调用后返回的reverse_iterator指向的字符可读可写,const对象调用后返回的const_reverse_iterator指向的字符只读不写。
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
// 可读可写
while (rit != s1.rend())
{
(*rit)++;
cout << *rit << " ";
++rit;
}
cout << endl;
const string s2("hello world");
// 只读不写
string::const_reverse_iterator crit = s2.rbegin();
while (crit != s2.rend())
{
// (*crit)++; // 错误用法
cout << *crit << " ";
++crit;
}
cout << endl;
输出:
e m s p x ! p m m f i
d l r o w o l l e h
4.4 范围for
遍历string类对象更简洁的形式,底层会转换为迭代器。
string s("hello world");
for (auto ch : s)
{
cout << ch << " ";
}
cout << endl;
输出:
h e l l o w o r l d
5. string类对象的修改操作
5.1 push_back
void push_back(char c);
在字符串后尾插字符c。
string s("hello world");
s.push_back('!');
cout << s << endl;
输出:
hello world!
5.2 append
string& append(const string& str);
string& append(const char* s);
string& append(size_t n, char c);
在字符串后追加一个字符串。
string s1("abc");
string s2("defg");
s1.append(s2);
cout << s1 << endl;
s1.append("hijkl");
cout << s1 << endl;
s1.append(2, 'm');
cout << s1 << endl;
输出:
abcdefg
abcdefghijkl
abcdefghijklmm
5.3 operator+=
string& operator+=(const string& str);
string& operator+=(const char* s);
string& operator+=(char c);
在字符串后追加一个字符串或者字符。
string s1("abc");
string s2("defg");
s1 += s2;
cout << s1 << endl;
s1 += "hijkl";
cout << s1 << endl;
s1 += 'm';
cout << s1 << endl;
输出:
abcdefg
abcdefghijkl
abcdefghijklm
5.4 c_str
const char* c_str() const;
返回一个C格式字符串,以字符'\0'结尾。
string s("abcde");
cout << s.c_str() << endl;
输出:
abcde
5.5 find
size_t find(const string& str, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
size_t find(char c, size_t pos = 0) const;
static const size_t npos = -1;
从pos位置开始,往后找字符串或字符,返回该字符串或字符第一次出现的位置。若找不到,返回npos。
string s1("hello world");
size_t ret = s1.find('w');
if (ret != string::npos)
cout << s1[ret] << endl;
ret = s1.find("wor");
if (ret != string::npos)
cout << s1[ret] << endl;
string s2("or");
ret = s1.find(s2);
if (ret != string::npos)
cout << s1[ret] << endl;
输出:
w
w
o
5.6 rfind
size_t rfind(const string& str, size_t pos = npos) const;
size_t rfind(const char* s, size_t pos = npos) const;
size_t rfind(char c, size_t pos = npos) const;
static const size_t npos = -1;
类似find,从pos位置开始,往前找字符串或字符。相当于返回该字符串或字符最后一次出现的位置。若找不到,返回npos。
5.7 substr
string substr(size_t pos = 0, size_t len = npos) const;
从pos位置开始,向后截取len个字符并返回。若len太大,截取时超出字符串范围,则截取到字符串的尾部。
string s("hello world");
string ret = s.substr(6, 3);
cout << ret << endl;
输出:
wor
6. string类非成员函数
6.1 operator+
string operator+(const string& str1, const string& str2);
string operator+(const char* s, const string& str);
string operator+(const string& str, const char* s);
string operator+(char& c, const string& str);
string operator+(const string& str, char& c);
拼接字符串和字符串或者字符。尽可能少用,因为传值返回导致深拷贝,效率低。
string s1("hello ");
string s2("world");
cout << s1 + s2 << endl;
cout << s1 + "world" << endl;
cout << "hello " + s2 << endl;
cout << s1 + 'a' << endl;
cout << 'b' + s2 << endl;
输出:
hello world
hello world
hello world
hello a
bworld
6.2 operator>>
istream& operator>>(istream& in, string& str);
流插入运算符重载,以空格、换行作为分隔符。
string s("hello world");
cin >> s;
cout << s << endl;
输出示例(第一行为输入):
abc def
abc
6.3 operator<<
ostream& operator<<(ostream& out, const string& str);
流提取运算符重载。
string s("hello world");
cout << s << endl;
输出:
hello world
6.4 getline
istream& getline(istream& in, string& str, char delim = '\n');
获取字符并存储到字符串中,直到遇到字符delim,返回该字符串。该字符串不包括delim。一般用来获取一行字符串。
string s;
getline(cin, s);
cout << s << endl;
输出示例(第一行为输入):
abc def
abc def
6.5 关系运算符
bool operator>(const string& str1, const string& str2);
bool operator>(const string& str, const char* s);
bool operator>(const char* s, const string& str);
bool operator<(const string& str1, const string& str2);
bool operator<(const string& str, const char* s);
bool operator<(const char* s, const string& str);
bool operator>=(const string& str1, const string& str2);
bool operator>=(const string& str, const char* s);
bool operator>=(const char* s, const string& str);
bool operator<=(const string& str1, const string& str2);
bool operator<=(const string& str, const char* s);
bool operator<=(const char* s, const string& str);
bool operator==(const string& str1, const string& str2);
bool operator==(const string& str, const char* s);
bool operator==(const char* s, const string& str);
bool operator!=(const string& str1, const string& str2);
bool operator!=(const string& str, const char* s);
bool operator!=(const char* s, const string& str);
比较字符串的大小,比较的是字符串对应位置的字符的ASCII码。
7. 练习
7.1 仅仅反转字母
仅仅反转字母https://leetcode.cn/problems/reverse-only-letters/description/
定义左指针left和右指针right,两个指针从两边向中间移动,直到相遇为止。若遇到字母,就交换左右指针指向的字符。
class Solution {
public:
string reverseOnlyLetters(string s) {
int left = 0, right = s.size() - 1;
while (left < right)
{
// 左边找字母
while (left < right && !isalpha(s[left]))
++left;
// 右边找字母
while (left < right && !isalpha(s[right]))
--right;
swap(s[left++], s[right--]);
}
return s;
}
};
7.2 找字符串中第一个只出现一次的字符
找字符串中第一个只出现一次的字符原题链接https://leetcode.cn/problems/first-unique-character-in-a-string/description/
字符串中只会出现小写字母,所以可以用计数排序的思路,定义数组countA记录每个字符出现的次数,把每个字符ch映射到下标为ch-'a'的位置。第一次遍历字符串,统计出每个字符出现的次数。第二次遍历字符串,查找只出现一次的字符。
class Solution {
public:
int firstUniqChar(string s) {
int countA[26] = {0};
for (auto ch : s)
countA[ch - 'a']++;
for (int i = 0; i < s.size(); ++i)
if (countA[s[i] - 'a'] == 1)
return i;
return -1;
}
};
7.3 字符串里面最后一个单词的长度
字符串里面最后一个单词的长度原题链接https://www.nowcoder.com/practice/8c949ea5f36f422594b306a2300315da?tpId=37&&tqId=21224&rp=5&ru=/activity/oj&qru=/ta/huawei/question-ranking
由于输入包含空格,所以要使用getline获取字符串。接着使用rfind查找最后一个空格,从而确定最后一个单词的长度。
#include <iostream>
using namespace std;
int main() {
string s;
getline(cin, s);
// 查找最后一个空格
size_t ret = s.rfind(' ');
if (ret != string::npos)
cout << s.size() - ret - 1 << endl;
else // 字符串没有空格
cout << s.size() << endl;
return 0;
}
7.4 验证一个字符串是否是回文
验证一个字符串是否是回文原题链接https://leetcode.cn/problems/valid-palindrome/description/
定义左指针left和右指针right,两个指针从两边向中间移动,直到相遇为止。若遇到数字或字母,判断它们指向的字符转小写字母后是否相同,从而判断是否是回文串。
class Solution {
public:
bool isPalindrome(string s) {
int left = 0, right = s.size() - 1;
while (left < right)
{
// 左边找数字字母
while (left < right && !isalnum(s[left]))
++left;
// 右边找数字字母
while (left < right && !isalnum(s[right]))
--right;
if (tolower(s[left++]) != tolower(s[right--]))
return false;
}
return true;
}
};
7.5 字符串相加
字符串相加原题链接https://leetcode.cn/problems/add-strings/description/
从最低位开始,每次取对应位的数字相加,并计算进位。考虑把相加的结果插入到字符串头部,但是反复的头插效率太低,所以应把相加的结果尾插到字符串中,再整体反转。注意到插入的过程中可能会多次扩容,所以先用reserve保留足够的空间,避免扩容的消耗。
class Solution {
public:
string addStrings(string num1, string num2) {
string ret;
ret.reserve(max(num1.size(), num2.size()) + 1);
int carry = 0; // 进位
int end1 = num1.size() - 1, end2 = num2.size() - 1;
while (end1 >= 0 || end2 >= 0)
{
// 取出对应位置的数字
int n1 = end1 >= 0 ? num1[end1] - '0' : 0;
int n2 = end2 >= 0 ? num2[end2] - '0' : 0;
int sum = n1 + n2 + carry;
carry = sum / 10;
ret += ((sum % 10) + '0');
--end1;
--end2;
}
if (carry == 1)
ret += '1';
reverse(ret.begin(), ret.end());
return ret;
}
};
8. 模拟实现
8.1 最简易实现思路
如果不考虑容量问题,理论上只需要一个char*类型的指针就能管理字符串了。要实现一个最简易的string,只需要实现利用C-string的构造函数、拷贝构造、赋值运算符重载以及析构函数。除此之外,可以顺手实现C++11中的移动构造和移动赋值,以及size、c_str、关系运算符、operator[]等接口。
8.1.1 C-string构造
利用C-string构造,只需要new出足够大的空间,多开一个空间存储'\0'。
string(const char* s = "")
:_str(new char[strlen(s)+1])
{
strcpy(_str, s);
}
8.1.2 拷贝构造
拷贝构造,可以利用C++标准规定的现代写法,转为利用C-string构造。
string(const string& str)
:string(str._str)
{}
8.1.3 赋值运算符重载
赋值运算符重载,仍然使用现代写法,采取传值传参。
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
其中swap交换对应的_str指针即可。
void swap(string& str)
{
std::swap(str._str, _str);
}
8.1.4 析构函数
析构函数直接delete即可。
~string()
{
delete[] _str;
_str = nullptr;
}
8.1.5 移动构造和移动赋值
C++11中的移动构造和移动赋值,只需要交换_str指针。
string(string&& str) noexcept
:_str(str._str)
{
str._str = nullptr;
}
string& operator=(string&& tmp) noexcept
{
swap(tmp);
return *this;
}
8.1.6 其它接口
接下来c_str、size、operator[]都可以顺手实现。
size_t size() const
{
return strlen(_str);
}
const char* c_str() const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos <= size());
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos <= size());
return _str[pos];
}
关系运算符重载作为非成员函数,只需实现>和==,其余复用即可。
bool operator>(const string& str1, const string& str2)
{
return strcmp(str1.c_str(), str2.c_str()) > 0;
}
bool operator==(const string& str1, const string& str2)
{
return strcmp(str1.c_str(), str2.c_str()) == 0;
}
bool operator>=(const string& str1, const string& str2)
{
return str1 > str2 || str1 == str2;
}
bool operator<=(const string& str1, const string& str2)
{
return !(str1 > str2);
}
bool operator!=(const string& str1, const string& str2)
{
return !(str1 == str2);
}
bool operator<(const string& str1, const string& str2)
{
return !(str1 >= str2);
}
8.1.7 完整实现
完整的实现及测试代码如下:
#include<iostream>
#include<string.h>
#include<assert.h>
#include<utility>
using namespace std;
namespace xbl
{
class string
{
public:
string(const char* s = "")
:_str(new char[strlen(s)+1])
{
strcpy(_str, s);
}
string(const string& str)
:string(str._str)
{}
string(string&& str) noexcept
:_str(str._str)
{
str._str = nullptr;
}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
string& operator=(string&& tmp) noexcept
{
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
size_t size() const
{
return strlen(_str);
}
const char* c_str() const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos <= size());
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos <= size());
return _str[pos];
}
void swap(string& str)
{
std::swap(str._str, _str);
}
private:
char* _str;
};
bool operator>(const string& str1, const string& str2)
{
return strcmp(str1.c_str(), str2.c_str()) > 0;
}
bool operator==(const string& str1, const string& str2)
{
return strcmp(str1.c_str(), str2.c_str()) == 0;
}
bool operator>=(const string& str1, const string& str2)
{
return str1 > str2 || str1 == str2;
}
bool operator<=(const string& str1, const string& str2)
{
return !(str1 > str2);
}
bool operator!=(const string& str1, const string& str2)
{
return !(str1 == str2);
}
bool operator<(const string& str1, const string& str2)
{
return !(str1 >= str2);
}
void test_string()
{
string s1;
cout << s1.c_str() << endl;
string s2("hello world");
cout << s2.size() << endl;
cout << s2[1] << endl;
string s3(s2);
string s4 = s3;
s1 = s2;
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
cout << s3.c_str() << endl;
cout << s4.c_str() << endl;
cout << (s1 == s2) << endl;
}
}
输出:
11
e
hello world
hello world
hello world
hello world
1
8.2 带size和capacity的版本
8.1的实现思路适用于面试时,被要求模拟实现string。特点是实现简单,但效率不高,因为strlen遍历字符串的时间复杂度是O(N)。一个功能完善的string可以考虑带上size_t类型的_size和_capacity成员变量,分别用来记录有效元素的个数和容量。
8.2.1 C-string构造
这样C-string的构造函数就可以这样写。
string(const char* s = "")
:_size(strlen(s))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, s);
}
观察监视窗口:
8.2.2 拷贝构造
拷贝构造不能简单地利用C-string构造实现了,这是因为下面的情况中,C-string以'\0'结尾,那么'\0'后面的字符就不会被识别为有效字符。
所以,正确的做法是,使用memcpy,把空间上的有效数据拷贝过去。
string(const string& str)
{
char* tmp = new char[str._size + 1];
memcpy(tmp, str._str, (str._size + 1) * sizeof(char));
_str = tmp;
_size = _capacity = str._size;
}
观察监视窗口:
8.2.3 赋值运算符重载
采用现代写法,利用传值传参的深拷贝。
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
void swap(string& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
8.2.4 析构函数
释放_str指针指向的空间。
~string()
{
delete[] _str;
_size = _capacity = 0;
}
8.2.5 移动构造和移动赋值
交换对应的数据即可。
string(string&& str) noexcept
:_str(nullptr)
{
swap(str);
}
string& operator=(string&& str) noexcept
{
swap(str);
return *this;
}
8.2.6 c_str + size + capacity
返回对应的成员变量。
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
8.2.7 operator[]
实现const和非const两个版本。
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
8.2.8 关系运算符
不能直接使用strcmp,因为下面的情况中,如果使用C-string比较,s1和s2应该相等,但事实上,s1<s2,原因是s2更长。
std::string s1("hello world");
std::string s2("hello world");
s2 += '\0';
cout << (s1 == s2) << endl;
cout << (s1 < s2) << endl;
输出:
0
1
所以,要手动比较。先考虑operator<,同时遍历两个字符串,若遇到不相等的字符,则比较结束,结果是“小于”或者“大于”,判断是否是“小于”即可。若其中一个字符串遍历完了,那么长的字符串更大。再考虑operator==,当两个字符串的size相同,且所有字符均相同,则字符串相等。其余的关系运算符复用operator<和operator==即可。
bool operator<(const string& str1, const string& str2)
{
size_t i = 0;
while (i < str1.size() && i < str2.size())
{
if (str1[i] != str2[i])
{
return str1[i] < str2[i];
}
++i;
}
// 若有效字符均相等,则长的字符串更大
return str1.size() < str2.size();
}
bool operator==(const string& str1, const string& str2)
{
if (str1.size() != str2.size())
{
return false;
}
// size相同
for (size_t i = 0; i < str1.size(); ++i)
{
if (str1[i] != str2[i])
{
return false;
}
}
return true;
}
bool operator<=(const string& str1, const string& str2)
{
return str1 < str2 || str1 == str2;
}
bool operator>(const string& str1, const string& str2)
{
return !(str1 <= str2);
}
bool operator>=(const string& str1, const string& str2)
{
return !(str1 < str2);
}
bool operator!=(const string& str1, const string& str2)
{
return !(str1 == str2);
}