为什么学习string类?
C语言中的字符串
C语言中,字符串是以'\0'结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列 的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问
标准库中的string类
string类(了解)
https://cplusplus.com/reference/string/string/?kw=string
在使用string类时,必须包含#include头文件以及using namespace std;
string类的常用接口说明
string类对象的常见构造
| 函数名称 | 功能说明 |
| string()(重要) | 构造空的string类对象,即空字符串 |
| string(const char* s)(重要) | 用c-string来构造string类对象 |
| string(size_t n, char c) | string类对象中包含n个字符c |
| string(const string&s)(重要) | 拷贝构造函数 |
范例:
void String() { string str; //构造空的对象 string str2("Hello World"); //用c格式的字符串构造string类对象 string str3(str2); //拷贝构造 string str4(str2, 1, 6); cout << str << endl; cout << str2 << endl; cout << str3 << endl; cout << str4 << endl; } int main() { String(); return 0; }string类对象的容量操作
| 函数名称 | 功能说明 |
| size(重要) | 返回字符串有效字符长度 |
| length | 返回字符串有效字符长度 |
| capacity | 返回空间总大小 |
| empty(重要) | 检测字符串释放为空串,是返回true,否则返回false |
| clear(重要) | 清空有效字符 |
| reverve(重要) | 为字符串预留空间** |
| resize(重要) | 将有效字符的个数该成n个,多出的空间用字符c填充 |
size()和length()的底层逻辑几乎一样,引入size()的原因是要和其他的接口保持一致,正常情况下都是用size()
reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于string的底层空间总大小时,resverse不会改变容量大小
string类对象的访问及遍历操作
| 函数名称 | 功能说明 |
| operator[](重要) | 返回pos位置的字符,const string类对象调用 |
| begin+end | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| rbegin+rend | begin获取一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
| 范围for | C++11支持更简洁的范围for的新遍历方式 |
补充:
iterator和const_iterator
这俩都是迭代器,一个可以修改指向内容的数据一个无法对指向的内容进行修改,通常搭配begin和end使用
代码示例:
void String() { string s1("123456"); const string s2("Hello World"); string::iterator str = s1.begin(); while (str != s1.end()) { (*str)--; ++str; } cout << s1 << endl; string::const_iterator str2 = s2.begin(); while (str2 != s2.end()) { //(*str2)--; //不能修改 ++str2; } cout << s2 << endl; }如果对const_iterator修改则会报错
运行结果:
除了上述两个外还有reverse_iterator和const_reverse_iterator,这俩和iterator和const_iterator相似,不过这俩是倒过来读取的
代码范例:
void String() { string s1("123456"); const string s2("Hello World"); string::reverse_iterator str3 = s1.rbegin(); while (str3 != s1.rend()) { cout << *str3 << " "; ++str3; } cout << endl; string::const_reverse_iterator str4 = s2.rbegin(); while (str4 != s2.rend()) { cout << *str4 << " "; ++str4; } cout << endl; }运行结果:
string类对象的修改操作
| 函数名称 | 功能说明 |
| push_back | 在字符串后尾插字符c |
| appned | 在字符串后追加一个字符串 |
| operator+=(重要) | 在字符串后追加字符串str |
| c_str(重要) | 返回C格式字符串 |
| find+npos | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
| rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
| substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
注意:
在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串
对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好
auto和范围for
auto关键字
在早期C/C++中auto的含义是:使用auto修饰的变量,是具有自动存储器的局部变量,后来这个不重要了。C++11中,标准委员会变废为宝赋予了auto全新的含义即:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得
在C++中可以使用auto来进行类型推导
void String2() { auto x = 20; auto z = 23.4; cout << x << endl; cout << z << endl; } int main() { String2(); return 0; }用auto声明指针类型时,用auto和auto*没有任何区别,但用auto声明引用类型时则必须加
当auto要声明引用类型时要加上引用操作符(“&”)
void String2() { int& y = x; auto& i = y; i++; }我们来进行调试:
此时x为20,我们接着往下走
y也变成了20,继续往下
i也变成了20,接着往下走
再往下走后就变成了21
那么如果auto后不加引用会发生什么事?
此时的i是y的拷贝,并非引用
当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际 只对第一个类型进行推导,然后用推导出来的类型定义其他变量
auto不能作为函数的参数,可以做返回值,但是建议谨慎使用
auto不能直接用来声明数组
范围for
对于一个有范围的集合而言,由程序员来说明循环的范围是多余的,有时候还会容易犯错误。因此 C++11中引入了基于范围的for循环。for循环后的括号由冒号“ :”分为两部分:第一部分是范围 内用于迭代的变量,第二部分则表示被迭代的范围,自动迭代,自动取数据,自动判断结束
for (auto ch : s1) { cout << ch << " "; } cout << endl;范围for可以作用到数组和容器对象上进行遍历
int array[] = { 1,2,3,4,5 }; for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++) { cout << array[i] << endl; } for (auto a : array) { cout << a << " "; } cout << endl;范围for的底层很简单,容器遍历实际就是替换为迭代器,这个从汇编层也可以看到
string模拟实现
初始化
这里我们写上const的和非const版本
namespace MyString { String::String() :_str(new char[1]{'\0'}) ,_size(0) ,_capacity(0) {} String::String(const char* str) :_str(new char[strlen(str)+1]) ,_size(strlen(str)) ,_capacity(strlen(str)) { memcpy(_str, str); } }取得字符串
namespace MyString { const char* String::c_str() const { return _str; } }我们来运行看看
运行结果:
取得字符串中的长度
namespace MyString { size_t String:: size() { return _size; } }这里我们要将字符串中的每个字符都往后+1
运行结果:
下标操作符重载
这里也写上const和非const的版本,这里需要使用迭代器
namespace MyString { char& String::operator[](size_t i) { assert(i < _size); return _str[i]; } const char& String::operator[](size_t i)const { assert(i < _size); return _str[i]; } }这里补充一些东西,这里遍历时无法使用迭代器,因为自定义类型并没有调用库中的东西
所以这我们begin和end要自己实现
这要先将char*进行重命名
typedef char* iterator;我们知道begin是要找到字符串中的第一个字符,end则是找到最后一个字符
namespace MyString { String::iterator String::begin() { return _str; } String::iterator String::end() { return _str + _size; } }运行结果:
尾插
在写尾插之前我们先模拟实现一个函数“reserve”,“resever”是用于分配内存空间的一个函数,它可以避免内存多次进行分配,有效提升元素插入时的运行效率
现在开始写代码:
void String::reserve(size_t n) { if (n > _capacity) { char* str = new char[n + 1]; memcpy(str, _str, _size + 1); delete[] _str; _str = str; _capacity = n; } }接下来继续写尾插代码
void String::push_back(char str) { if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } _str[_size] = str; ++str; _str[_size] = '\0'; }这里写的和之前的不同,这里多加了一个‘\0’
运行结果:
追加字符串
append是用于追加字符串的函数,可以追加任何形式的字符而不要进行重新分配,接下来进行模拟实现
void String::append(const char* str) { size_t len = strlen(str); if (_size + len > _capacity) { size_t newcapacity = 2 * _capacity > _size + len ? 2 * _capacity : _size + len; reserve(newcapacity); } //strcpy(_str + _size, str); memcpy(_str, str, len + 1); //这里可以使用strcpy,但是使用memcpy会更好 _size += len; }运行结果:
重载函数”+=“:
这里写const和非const版本
String& String::operator+=(char ch) { push_back(ch); return *this; } String& String::operator+=(const char* str) { append(str); return *this; }这里和尾插和追加字符串相似,当我们想要追加字符和字符串可以使用这个符号来代替
运行结果:
流输出符号重载
这个需要在类外面进行实现,需要使用友元才能使用私有成员,不过这并不需要访问类的私有成员
ostream& operator<<(ostream& out, const string& s) { //out << s.c_str(); //如果使用这个会有”坑“ for (size_t i = 0; i < s.size();i++) { out << s[i]; } return out; }我们来进行调试:
首先走到断点处
往下走,此时s1的值变为“hello world”
我们看到终端画面
这样就是输出成功,调试结束
指定位置插入字符
string& string::insert(size_t pos, char ch) { assert(pos <= _size); if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } /*int end = _size; while(end>= (int) pos) //size_t是无符号变量,end会从int变成unsigned int。这称为整型提升,因此这里要强转 { _str[end + 1] = _str[end]; --end; }*/ size_t end = _size + 1; while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = ch; ++_size; return *this; }指定位置插入字符串版本
string& string::insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size >= _capacity) { size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity; reserve(newcapacity); } /*int end = _size; while(end >= (int)pos) { _str[end + len] = _str[end]; --end; }*/ size_t end = _size + len; while (end > pos + len - 1) { _str[end] = _str[end - len]; --end; } for (size_t i = 0; i < len; i++) { _str[pos + i] = str[i]; } _size += len; return *this; }运行结果:
删除指定字符
在写这个代码前需要写npos的定义
const size_t string::npos = -1;//定义继续写erase的代码
string& string::erase(size_t pos, size_t len) { assert(pos < _size); //要删除的数据,要大于pos后面的字符个数 if (len == npos || len >= (_size - pos)) { _size = pos; _str[_size] = '\0'; } else { size_t i = pos + len; memmove(_str + pos, _str + i, _size + 1 - i); _size -= len; } return *this; }运行结果:
寻找指定字符
size_t string::find(char ch, size_t pos)const //寻找指定字符 { for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } size_t string::find(const char* str, size_t pos)const //寻找指定字符串和下一个字符的起始位置 { const char* p1 = strstr(_str + pos, str); if (p1 == nullptr) { return npos; } else { return p1 - _str; } }这里还需要写上substr
substr是用于提取字符串中提取子字符串的函数
代码:
string string::substr(size_t pos, size_t len)const { if (len == npos || len >= _size - pos) { len = _size - pos; } string ret; ret.reserve(len); for (size_t i = 0; i < len; i++) { ret += _str[pos + i]; } return ret; }这里find和substr要搭配使用
代码:
void spliturl(const string& url) { size_t i1 = url.find(':'); if (i1 != string::npos) { cout << url.substr(0, i1) << endl; } size_t i2 = i1 + 3; size_t i3 = url.find('/', i2); if (i3 != string::npos) { cout << url.substr(i2, i3 - i2) << endl; cout << url.substr(i3 + 1) << endl; } cout << endl; }运行结果:
大于、大于等于、小于、小于等于、等于和不等于符号重载
bool string::operator<(const string& s)const //大于 { size_t s1 = 0, s2 = 0; while (s1 < _size && s2 < s._size) { if (_str[s1] < s[s2]) { return true; } else if(_str[s1] > s[s2]) { return false; } else { s1++; s2++; } } return s2 < s._size; } bool string::operator<=(const string& s)const//大于等于 { return *this < s || *this == s; } bool string::operator>(const string& s)const//小于 { return !(*this <= s); } bool string::operator>=(const string& s)const//小于等于 { return !(*this < s); } bool string::operator==(const string& s)const//等于 { size_t s1 = 0, s2 = 0; while (s1 < _size && s2 < s._size) { if (_str[s1] != s[s2]) { return false; } else { s1++; s2++; } } return s1 == _size && s2 ==s._size; } bool string::operator!=(const string& s)const//不等于 { return !(*this == s); }这里我们以大于来做测试,这里如果是大于返回0否则返回1,最后的输出结果为:0、0、1
运行结果:
流输入符号重载
在写这个之前我们需要写一个clear的模拟实现,如果没写会出现bug
void string::clear() { _str[0] = '\0'; _size = 0; }接下来继续写写上流输入符号重载的代码
istream& operator>>(istream& in, string& s) { s.clear(); char buff[128]; int i = 0; char ch = in.get(); while (ch != ' ' && ch != '\n') { buff[i++] = ch; if (i == 127) { buff[i] = '\0'; s += i; i = 0; } s += ch; ch = in.get(); } if (i < 0) { buff[i] = '\0'; s += buff; } return in; }我们来进行调试:
先走到断点处
我们往下走并输入“Hello”和“world”
之后看到监视窗口,此时s1和s2分别为“hello”和“world”
调试结束
从流中提取字符串
istream& getline(istream& is, string& str, char delim) { str.clear(); char ch = is.get(); while (ch != delim) { str += ch; ch = is.get(); } return is; }运行结果:
拷贝构造函数
拷贝构造函数分为传统写法和现代写法,这里先写上传统写法
传统写法:
string::string(const string& s) { _str = new char[s._capacity + 1]; memcpy(_str,s._str, s._size + 1); _size = s._size; _capacity = s._capacity; }现代写法:
在写现代写法前要先写swap代码
void string::swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }接下来写上现代写法的拷贝构造函数
string::string(const string& s) { string tmp(s._str); swap(tmp); }赋值符号重载
赋值符号重载也分为两种写法,一个是传统写法一个是现代写法,先来写传统写法
传统写法:
string& string::operator=(const string& s)//赋值操作符重载(传统) { if (this != &s) { char* tmp = new char[s._capacity + 1]; memcpy(tmp, s._str, _size + 1); delete[] _str; _str = tmp; _size = s._size; _capacity = s._capacity; } return *this; }现代写法:
string& string::operator=(const string& s)//赋值操作符重载(现代) { if (this != &s) { string tmp(s); swap(tmp); } return *this; }现代写法还有更简洁的写法
string& string::operator=(string& tmp) //赋值操作符重载(现代) { swap(tmp); return *this; }