
为什么学习string类C语言中的字符串C语言中字符串是以\0结尾的一些字符的集合为了操作方便C标准库中提供了一些str系列 的库函数但是这些库函数与字符串是分离开的不太符合OOP的思想而且底层空间需要用户自己管理稍不留神可能还会越界访问标准库中的string类string类(了解)https://cplusplus.com/reference/string/string/?kwstring在使用string类时必须包含#include头文件以及using namespace std;string类的常用接口说明string类对象的常见构造函数名称功能说明string()重要构造空的string类对象即空字符串string(const char* s)重要用c-string来构造string类对象string(size_t n, char c)string类对象中包含n个字符cstring(const strings)重要拷贝构造函数范例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否则返回falseclear重要清空有效字符reverve重要为字符串预留空间**resize重要将有效字符的个数该成n个多出的空间用字符c填充size()和length()的底层逻辑几乎一样引入size()的原因是要和其他的接口保持一致正常情况下都是用size()reserve(size_t res_arg0)为string预留空间不改变有效元素个数当reserve的参数小于string的底层空间总大小时resverse不会改变容量大小string类对象的访问及遍历操作函数名称功能说明operator[]重要返回pos位置的字符const string类对象调用beginendbegin获取一个字符的迭代器 end获取最后一个字符下一个位置的迭代器rbeginrendbegin获取一个字符的迭代器 end获取最后一个字符下一个位置的迭代器范围forC11支持更简洁的范围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在字符串后尾插字符cappned在字符串后追加一个字符串operator(重要)在字符串后追加字符串strc_str(重要)返回C格式字符串findnpos从字符串pos位置开始往后找字符c返回该字符在字符串中的位置rfind从字符串pos位置开始往前找字符c返回该字符在字符串中的位置substr在str中从pos位置开始截取n个字符然后将其返回注意在string尾部追加字符时s.push_back(c) / s.append(1, c) / s c三种的实现方式差不多一般情况下string类的操作用的比较多操作不仅可以连接单个字符还可以连接字符串对string操作时如果能够大概预估到放多少字符可以先通过reserve把空间预留好auto和范围forauto关键字在早期C/C中auto的含义是使用auto修饰的变量是具有自动存储器的局部变量后来这个不重要了。C11中标准委员会变废为宝赋予了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对于一个有范围的集合而言由程序员来说明循环的范围是多余的有时候还会容易犯错误。因此 C11中引入了基于范围的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; } }这里还需要写上substrsubstr是用于提取字符串中提取子字符串的函数代码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的模拟实现如果没写会出现bugvoid 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; }