本节笔记涉及的全部代码见以下链接,欢迎参考指正!
String · 王哲/practice - 码云 - 开源中国 (gitee.com)
C++中为什么设计了String类?
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想【面向对象编程(英文Object Oriented Programming)】,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
标准库中的string类:
简单了解string类:
1. string是表示字符串的字符串类
2. 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits,allocator> string;
4. 不能操作多字节或者变长字符的序列。
5.在使用string类时,必须包含#include头文件以及using namespace std;
想了解更多string类的相关信息,见以下链接:
https://cplusplus.com/reference/string/string/?kw=string
string类的常用接口说明(只总结最常用的接口):
string类对象常见构造:
(constructor)函数名称 | 功能说明 |
string()【重点】 | 构造空的string类对象,即空字符串 |
string(const string& str)【重点】 | 拷贝构造函数 |
string(const char* str)【重点】 | 用C-string来构造string类对象 |
string(size_t n,char ch)【常用】 | string类对象中包含n个字符c |
void Test1()//构造函数测试
{
string s1;//默认是构造一个空串
string s2("hello world!");
string s3(4, '#');
string s4(s3);
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
}

string类对象的容量操作
函数名称 | 功能说明 |
size_t size() const | 返回字符串有效字符长度 |
size_t length() const | 返回字符串有效字符长度 |
size_t capacity() const | 返回空间总大小 |
bool empty() const | 检测字符串释放为空串,是返回true,否则返回false |
void clear() | 清空有效字符 |
void reserve (size_t n = 0) | 为字符串预留空间 |
void resize (size_t n) void resize (size_t n, char c) | 将有效字符的个数该成n个,多出的空间用字符0/c填充 |
void Test2()//容量操作函数测试
{
//size(),length()函数功能相同,一般我们用size()
string s1;
string s2("hello world!");
cout << s1 <<" "<< s1.size()<<" " << s1.length() << " " << s1.capacity() << endl;
cout << s2 <<" "<< s2.size()<<" " << s2.length() << " " << s2.capacity() << endl;
cout << "-----------------------------------------------------------" << endl;
//输出0即false,表示字符串不为空,输出1即true,表示字符串为空
string s3("hello world!");
cout << s3.empty() << endl;
s3.clear();
cout << s3.empty() << endl;
cout << "-----------------------------------------------------------" << endl;
//reserve可以为字符串开想要的容量,参数表示想要存储有效数据的数量,且不会对size有影响
//实际容量比参数大1,因为字符串还要存储'\0'
string s4("hello world!");
s4.reserve(3);//若无参或传的参数小于实际容量,则不对容量进行处理,即一般情况下不会进行缩容操作
string s5("hello world!");
s5.reserve(30);
cout << s4 << " " << s4.size() << " " << s4.capacity() << endl;
cout << s5 << " " << s5.size() << " " << s5.capacity() << endl;
cout << "-----------------------------------------------------------" << endl;
//resize可以为字符串开想要的容量并初始化为想要的字符,会对size造成影响
string s6("hello world!");
s6.resize(30,'#');//对已经初始化的内存不做处理
string s7("hello world!");
s7.resize(3, '#');//传的参数小于实际容量,也不会对容量进行处理,但会改变size
cout << s6 << " " << s6.size() << " " << s6.capacity() << endl;
cout << s7 << " " << s7.size() << " " << s7.capacity() << endl;
}

总结:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字符个数增多时;resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,reserver不会改变容量大小。
string类对象的访问及遍历操作:
函数名称 | 功能说明 |
char& operator[] (size_t pos) const char& operator[] (size_t pos) const; | 返回pos位置的字符,const string类对象调用相应的静态成员函数 |
iterator begin() const_iterator begin() const iterator end() const_iterator end() const | 返回一个迭代器,begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器,const string类对象调用相应的静态成员函数 |
reverse_iterator rbegin() const_reverse_iterator rbegin() const reverse_iterator rend() const_reverse_iterator rend() const | 返回一个反向迭代器,rbegin获取第一个字符前一个位置的迭代器的迭代器 + rend获取最后一个字符的迭代器,const string类对象调用相应的静态成员函数 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
void Test3()//类对象访问及遍历测试
{
const string s2("hello world!");
cout << s2[4] << endl;//注意能访问的的范围是size的范围
cout << "-----------------------------------------------------------" << endl;
//访问非静态成员对象,调用普通成员函数
//既可以遍历又可以修改值
//可以先把迭代器理解成和指针一样的东西,方便理解【但迭代器不是指针!!!后续会补充】
string s3("hello world");
string::iterator it = s3.begin();//熟练后可以直接用auto自动识别类型,不用写出复杂的迭代器类型
while (it != s3.end())
{
cout << *it << " ";
++it;
}
cout << endl;
string s4("hello world");
string::reverse_iterator rit = s4.rbegin();
while (rit != s4.rend())
{
cout << ++(*rit) << " ";
++rit;
}
cout << endl;
cout << "-----------------------------------------------------------" << endl;
//访问静态成员对象,调用相应静态成员函数
//仅支持遍历,这里的const限制的是iit/riit指向的内容,即iit/riit本身可变,但它指向空间的内容不可变
const string s5("hello world");
string::const_iterator iit = s5.begin();
while (iit != s5.end())
{
cout << *iit << " ";
++iit;
}
cout << endl;
const string s6("hello world");
string::const_reverse_iterator riit = s6.rbegin();
while (riit != s6.rend())
{
cout << *riit << " ";
++riit;//
}
cout << endl;
cout << "-----------------------------------------------------------" << endl;
//利用范围for可以实现字符串的遍历及修改
for (auto ch : s5)
{
cout << ch << " ";
}
cout << endl;
for (auto ch : s6)
{
ch++;
cout << ch << " ";
}
}

总结:
1.清楚地认识到普通成员变量和const 修饰的成员变量在进行相关访问操作时都会优先调用参数最匹配的函数。
2.迭代器返回值类型用const_修饰表示并不是返回的迭代器不能修改,而是它指向空间的内容不能修改。
string类对象的修改操作:
函数名称 | 功能说明 |
void push_back (char c) | 在字符串后尾插字符c |
1、string& append (const string& str) 2、string& append (const string& str, size_t subpos, size_t sublen); 3、string& append (const char* s) 4、string& append (const char* s, size_t n) 5、string& append (size_t n, char c) |
|
|
|
const char* c_str() const | 返回C格式字符串 |
静态常量 size_t npos = -1; | npos是一个静态成员常量值,具有size_t类型元素的最大可能值,当在string的成员函数中用作len(或sublen)参数的值时,此值表示“直到字符串末尾”。 作为返回值,通常用于表示不匹配。此常量定义为-1 值,因为size_t是无符号整数类型,所以它是此类型的最大可能表示值 |
| 从字符串pos位置开始往后找字符c,返回该字符/字符串第一个字符在字符串中的位置,没找到返回npos
|
| 从字符串pos位置开始往前找字符c,返回该字符/字符串第一个字符在字符串中的位置,没找到返回npos
|
string substr (size_t pos = 0, size_t len = npos) const | 在str中从pos位置开始,截取n个字符,然后将其返回 |
void Test4()//尾插字符及追加字符串测试
{
string s1("hello world");
s1.push_back('!');
cout << s1 << endl;
string s2("I'm WZ");
s1.append(s2);
cout << s1 << endl;
s1.append("012345678",4,3);
cout << s1 << endl;
s1.append("I'm from",5);
cout << s1 << endl;
s1.append(4,'*');
cout << s1 << endl;
s1 += "666";
cout << s1 << endl;
s1 += 'y';
cout << s1 << endl;
}

void Test5()//查找字符串、返回字符串测试
{
string s1("hello world! hello world!");
cout << s1.c_str() << endl;
string s2("hello");
string s3("wz");
cout << s1.find(s2) << endl;
cout << s1.find(s2,4)<< endl;
cout << s1.find("hello", 4, 3) << endl;
cout << s1.find(s3) << endl;
cout << s1.find('w') << endl;
cout << s1.find('w',10) << endl;
cout << "---------------------------------------" << endl;
cout << s1.rfind(s2) << endl;
cout << s1.rfind(s2, 4) << endl;
cout << s1.rfind("hello", 4, 2) << endl;
cout << s1.rfind(s3) << endl;
cout << s1.rfind('w') << endl;
cout << s1.rfind('w', 10) << endl;
cout << "---------------------------------------" << endl;
cout << s1.substr(5, 15)<<endl;
cout << s1.substr(7, 4) << endl;
cout << s1.substr(5) << endl;
}

总结:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好,因为反复扩容会影响效率。
//insert()插入函数和erase()删除函数
//这两个函数能不用就不用,因为涉及到了数据的移动,会影响效率
void Test6()
{
string s1("hello world!");
cout << s1 << endl;
string s3("wangzheggg");
cout << "---------------------------------------" << endl;
s1.insert(4,s3,4,3);//参数分别表示,(原字符串插入位置,字符串对象,待插入字符串起始位置,待插入字符串元素个数)
cout << s1 << endl;//最后一个参数若大于剩余元素个数,则表示剩余元素全部插入
cout << "---------------------------------------" << endl;
s1.insert(8,s3,2);//最后待插入字符串元素个数在最新语法标准下支持缺省!!!注:C98/C11均不支持缺省
cout << s1 << endl;
cout << "---------------------------------------" << endl;
s1.insert(0,3,'4');//参数分别表示,(原字符串插入位置,插入字符个数,待插入字符)
cout << s1 << endl;//第二个参数若为1,则表示在pos位置插入一个字符,不唯为1,则表示从pos开始插入几个一样的字符
cout << "---------------------------------------" << endl;
s1.insert(2, "wz", 3);//参数分别表示,(原字符串插入位置,指向一个字符串的指针,待插入字符串元素个数)
cout << s1 << endl;//最后一个参数若大于字符串元素总个数,则所有元素全部插入
cout << "---------------------------------------" << endl;
s1.erase(2,10);//参数分别表示,(开始删除的位置,待删除的字符串元素个数)
cout << s1 << endl;
cout << "---------------------------------------" << endl;
s1.erase(4);//最后一个参数若大于字符串元素总个数或者缺省,则后面所有元素全部删除
cout << s1 << endl;
}

5. string类非成员函数(底层原理在模拟实现string类时会详细总结)
函数名称 | 功能说明 |
string operator+ (const string& lhs, const string& rhs) | 尽量少用,因为传值返回,导致深拷贝效率低 |
istream& operator>> (istream& is, string& str) | 输入运算符重载 |
ostream& operator<< (ostream& os, const string& str) | 输出运算符重载 |
istream& getline (istream& is, string& str, char delim) istream& getline (istream& is, string& str) | 获取一行字符串 |
relational operators | 大小比较 |
以上总结了string类的一些最常用到的操作,string类中还有一些其他的操作,这里不一一列举,大家在需要用到时不明白了查文档即可。
练习使用标准库中的string类:
!练习题单独总结到了以下链接,有兴趣的自行参考练习!
string类的模拟实现:
为了和库里的string做出区分,我们将自己实现的string在一个命名空间中定义
1.构造函数、拷贝构造
//构造函数
string(const char* str=" ")//注意:缺省值是一个空串,不是nullptr也不是'\0'
:_size(strlen(str))
{
_capacity = _size == 0 ? 5 : _size;
_str = new char[_capacity + 1];//实际容量要比可存字符数量多1,因为要考虑'\0'
strcpy(_str, str);//将str中存的字符串(包括'\0')复制到_str中完成_str的初始化
}
//拷贝构造
string(const string& str)
:_size(str._size)
, _capacity(str._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, str._str);
}
测试结果如下:

2.赋值重载
string& operator=(const string& str)
{
if (this != &str)//自己给自己赋值的情况下,直接返回*this即可
{
//由于this对象和str的容量大小都不确定,因此为了避免复杂的容量操
// 直接开str._capacity+1大小的空间
//把str._str中的字符串拷贝到新空间,再让_str指向它即可
char* tmp = new char[str._capacity + 1];
strcpy(tmp, str._str);
delete[] _str;//释放空间放在新空间开好之后,不至于在新空间没开好的情况下丢失原数据
_str = tmp;
_size = str._size;
_capacity = str._capacity;
}
return *this;
}
测试结果如下:

3.析构函数
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
测试结果如下:

4.访问及遍历
//返回C格式字符串
const char* c_str() const
{
return _str;
//[]重载
//满足普通对象的访问
char& operator[](size_t n)
{
assert(n >= 0 && n < _size);
//return *(_str + n);
//printf("common ");//仅用于测试是否调用,实现时不出现
return _str[n];
}
//满足const 对象的访问
const char& operator[](size_t n)const
{
assert(n >= 0 && n < _size);
//return *(_str + n);
//printf("const ");//仅用于测试是否调用,实现时不出现
return _str[n];
}
//返回大小
size_t size()
{
return _size;
}
//返回容量
size_t capacity()
{
return _capacity;
}
//迭代器
//现阶段我们可以把迭代器看成是和指针一样的东西
typedef char* iterator;//即iterator是字符指针类型重命名的类型
typedef const char* const_iterator;
//普通对象
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
//const修饰的对象
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
测试结果如下:

//从某位置开始寻找是否有匹配的字符串
size_t find(const string& str, size_t pos = 0) const
{
assert(pos < _size);
/*if (_size - pos < str.size())
{
return npos;
}
while (_str[pos] != '\0')
{
int i = 0;
int j = pos;
while (_str[j] == str[i])
{
i++;
j++;
if (str._str[i]== '\0')
return pos;
}
pos++;
}
return npos;*/
char* p = strstr(_str + pos, str._str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
//从某位置开始寻找是否有匹配的字符
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
while (_str[pos] != '\0')
{
if (_str[pos] == c)
return pos;
pos++;
}
return npos;
}

5.大小比较
//比较大小
bool operator==(const string& str)const
{
return strcmp(_str, str._str)==0;
}
bool operator>(const string& str)const
{
return strcmp(_str, str._str)>0;
}
//下面直接复用即可
bool operator>=(const string& str)const
{
return (*this==str)||(*this>str);
}
bool operator<(const string& str)const
{
return !(*this>=str);
}
bool operator<=(const string& str)const
{
return !(*this>str);
}
bool operator!=(const string& str)const
{
return !(*this==str);
}
测试结果如下:

6.容量操作
//reserve
void reserve(int n)
{
assert(n >= 0);
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//resize
void resize(int n, char ch)
{
assert(n >= 0);
if (n > _size)
{
if (n > _capacity)
{
/*char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;*/
reserve(n);
}
for (int i=_size;i<n;i++)
{
_str[i] = ch;
}
}
_size = n;
_str[n] = '\0';
}
测试结果如下:

//任意位置插入字符
string& install(size_t pos,char ch)
{
assert(pos < _size&&pos>=0);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
for (int i = _size+1; i > pos; i--)
{
_str[i] = _str[i-1];
}
_str[pos] = ch;
_size++;
return *this;
}
//任意位置插入字符串
string& install(size_t pos, const char* str)
{
assert(pos < _size&& pos >= 0);
if (_size + strlen(str) > _capacity)
{
reserve(_size+strlen(str));
}
for (int i = _size + strlen(str); i > pos + strlen(str)-1; i--)
{
_str[i] = _str[i - strlen(str)];
}
strncpy(_str + pos, str, strlen(str));
_size+=strlen(str);
return *this;
}
//删除任意位置之后指定长度的字符
string& erase(size_t pos, size_t len=npos)
{
assert(len >= 0 < _size-pos);
if (len==npos||pos+len>=_size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
/*for (int i = _size; i > pos + len-1; i--)
{
_str[i-len] = _str[i];
}
_size-=len;
*/
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
测试结果如下:

//尾插字符
void push_back(char ch)
{
//判断容量
if (_size == _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
//尾插字符串
void append(const char* str)
{
//判断容量
if (_size + strlen(str) > _capacity)
{
reserve(_size + strlen(str));
}
strcpy(_str + _size, str);
_size += strlen(str);
}
测试结果如下:

//+=追加字符,复用push_back
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//+=追加字符串,复用append
string& operator+=(const char* str)
{
append(str);
return *this;
}
测试结果如下:

//清除字符串
void clear()
{
_str[0] = '\0';
_size = 0;
}
7.<<和>>的重载
//<<
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
//>>,不能简单的像实现<<一样操作,具体代码如下:
std::istream& operator>>(std::istream& in, string& s)
{
s.clear();//写入之前要先将原字符串内容清除
char ch = in.get();
char buff[128];//预留一个空间,输入字符每满128原字符串就+=一次
size_t i = 0;//这样可以避免反复增容
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)//buff没满128则直接+=即可
{
buff[i] = '\0';
s += buff;
}
return in;
}
cin和scanf一样在循环输入时,都会把空格‘ ’和换行‘\n’把自动识别为输入多个对象之间的间隔,这样用cin就会导致我们在想结束输入的时候无法结束,因此我们用的是in的成员函数get(),它会想读取其它字符一样正常读取空格和换行,通过控制读取条件,就能达到遇到‘ ’或‘\n’就自动结束读取。
本节笔记涉及的全部代码见以下链接,欢迎参考指正!