目录
前言(string简介,及深度理解重要性)
一、string的实例化构造
1.利用string类接口的构造
2.string构造的模拟实现
1、构造函数
2、拷贝构造
二、string的静态变量
1、npos介绍及原理
?编辑
2、npos模拟
三、string的遍历方法
1、下标遍历
下标方括号模拟
2、迭代器遍历
迭代器及相关函数模拟
3、范围for遍历
模拟使用范围for的注意事项
四、string的容量操作
1、max_size():
2、size()与capacity()
模拟及其原理
3、reserve()
reserve()原理及其模拟
4、resize()
五、string的增删查改
1、push_back()
模拟实现
2、append()
模拟实现
3、insert(),erase()
模拟实现
4、find()
模拟实现
5、c_str()
模拟实现
6、rfind(),substr()
模拟实现
7、find_first_of/find_first_not_of
六、string的非成员函数
swap()
输入输出流模拟
getline()
总结
(附)模拟实现的完整string类代码
测试代码
前言(string简介,及深度理解重要性)
1. string是表示字符串的字符串类
2.接口与常规容器的接口基本相同,添加了一些专门用来操作string的常规操作。
3. string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string
4.使用时,要加入头文件#include<string>,别忘了一定要展开作用域 using namespace std
#include<iostream> #include<string> using namespace std;
简而言之,string是一个可以存放字符串的容器,并且可以管理存放进去的字符串,应用string,通过string提供的管理方法来管理这些字符串,从而达到应用者的相关需求。要想得心应手的控制字符串达到一些需求(如统计,提取等等),我们就要熟练能控制管理它的string,在深刻理解string操作原理的同时才会更加熟悉string的操作,这也是本文所针对性阐述的缘由,本文每个部分也会分为应用与模拟细节与注意事项进行讲解。
一、string的实例化构造
1.利用string类接口的构造
通过上图string构造接口,我们可以看到大量且繁杂的构造方法,存在一些赘余设计,但有些比较使用,具体展示在下图
s(1,2)字符串构造,s2中理论上存在隐式转换(无参构造+拷贝构造)但vs中优化为直接构造(隐式转换的概念可以通过其他文章进行了解,简单来说就是所给类型自动转换为所需类型)
s(3,4)拷贝构造
s(5,6,7)截取部分的拷贝构造,参数表(截取的string,起始位置,截取个数(默认取完))
s(8)截取部分字符串的构造,参数表(截取的字符串,从起始位置开始的截取个数)
s(9)利用多个重复字符的构造,参数表(字符重复个数,字符)
2.string构造的模拟实现
1、构造函数
string(const char* str = "") { _size = (strlen(str)); _capacity = _size; _str = new char[_size + 1]; strcpy(_str, str); }
Tips:
(1)、保证可以进行无参构造,给予缺省值,这个缺省值不能是NULL,否则调用时会出现空指针解引用等问题,导致程序崩溃(如未添加元素时进行流插入操作),所以给空字符串自带‘ ’
(2)、不可将指针参数直接赋值给指针成员,而需要使用new开辟新的指向空间,在进行拷贝,否则会出现类型不匹配等问题(类型若强制匹配设为const,则成员无法改变,与预期不符),在这里我们也不适用参数表进行初始化,因为可能存在顺序改变后代码崩溃的情况
(3),开辟空间时要多开一个空间存放 ‘ ’
2、拷贝构造
有两种拷贝构造方法如下图所示
//传统写法 string (const string& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } //现代写法 string(const string& s) { string temp(s._str); swap(temp); }
现代写法中,创建临时的string对象,利用所给的对象的成员来构造这个临时对象,再将临时对象与*this所代表的主对象交换,当我们退出该函数时,临时变量会自动调用析构函数,释放成员指针所指向的空间。交换后。临时对象的成员指针指向的正是主对象成员指针交换之前指向的空间,这样就成功拷贝了新的string,并且原来的空间自然而然的就被清理掉了
我们借助图像来理解
这里补充一下交换函数
void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }
二、string的静态变量
1、npos介绍及原理
string的一个重要静态变量就是npos
使用时,我们需要以这样的形式调用 :string::npos
通俗来讲什么是npos呢,实际并不复杂,就是一个无符号整数,是有符号的-1转变为无符号size_t类型,在通俗一点,就是每一位取一所代表的数字,代表可能存放有效元素的最大值,但实际上由于各种空间消耗,是无法存放这么多的有效字符的;
2、npos模拟
可以直接调用string 中的npos,使用方法如右边: std::string::npos
三、string的遍历方法
1、下标遍历
只适用于部分容器,需要底层有连的链式结构,树形,哈希结构不可使用对应类型调用重载的最匹配函数
下标方括号模拟
//下标方括号访问 const char& operator[](size_t pos)const { assert(pos <= _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; }
实现重载,使得const参数和非const参数都可以进行访问操作,进而达到遍历操作
2、迭代器遍历
迭代器容器访问主流形态
使用迭代器进行遍历如下图
优点
(1)迭代器是泛型编程,可实例化为各种容器的迭代器,函数模板针各个迭代器实现
(2)可以配合算法使用
补充知识:
(1)const 类型,迭代器类型要使用对应const_iterator(),使得迭代器所指向的数据*it1不可被修改
(2)迭代器相关函数
begin(),end()为正向迭代器
rbegin(),rend()为反向迭代器
前缀有c的函数为针对const类型参数的相关操作,其实标准库中对以上四个函数中实现了非const类型,和const类型的重载,已经足够使用,后四个函数就显得有一些赘余
下面是一些使用细节以及注意事项:
1)end()返回位置为 位置,不算有效字符,直接打印会造成越界警告,所以end()-1
2)倒叙迭代器向前移动也是+操作,值得注意的是rend()返回的是首元素的前一个位置
3)可以使用auto简化代码
4)const 对象自动调用参数为const类型的重载函数,综上方法各有两个重载,对应八种情况
不使用重载函数,也可以使用其他函数替换(前缀为c的迭代器函数)
5)同样可以使用auto替代,但代码而可读性降低,
如const类型不能修改,带无法通定义看出来是const类型,所以必须熟悉相关函数返回值
迭代器及相关函数模拟
//迭代器使用函数 typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } typedef const char* const_iterator; const_iterator begin()const { return _str; } const_iterator end()const { return _str + _size; }
我们可以清楚地看到迭代器地本质实际上就是指针,利用typedef给对应地指针类型换名字
非const对象使用 iterator,const对象使用const_iterator
begin()与end()的返回值很简单,就是返回指针变量,即下标地址
3、范围for遍历
自动识别类型,直接遍历string;
模拟使用范围for的注意事项
规范书写迭代器才可以使用范围for,范围for是傻瓜式的迭代器替换,说的更加直观就是范围for会自动使用规范的迭代器函数,如果你的命名不规范,他就无法找到对应的函数来完成一系列的操作
四、string的容量操作
1、max_size():
返回理论上可以开的最大空间(实际上无法开到这么大的空间)2的(电脑位数)次方
2、size()与capacity()
(1),返回有效字符串有效字符串长度 返回空间总大小
(2),capacity总会略大于size
(3),capacity的自动扩容机制在不同编译器下各不相同,测试如下图所示
模拟及其原理
这两个函数的模拟非常简单,就是返回其成员_size,_capacity
这样做的原因是因为成员属性为私有,我们在类外面不能直接访问,而有些时候我们又需要去知道他的具体数值,所以写方法来访问这些成员内容,他是只读的是不能修改的,传入的为const指针,非const对象自动将其权限缩小后正常使用,保证了其安全性。
3、reserve()
为string预留空间,不会改变有效元素个数(不影响数据),这样我们可以避免其自动开辟空间,大大地提升了效率
(1)所传参数 > capacity
在vs环境下,用resever开空间,空间略大于所给参数,
在g++环境下,所开空间大小为所传参数
(2)size < 所传参数 < capacity
在vs环境下,不会缩小容量
在g++环境下,容量缩小为所给参数
(3)所传参数 < size
在vs环境下,不会缩小容量
在g++环境下,容量缩小为原来有效字符串长度大小(size)
reserve()原理及其模拟
//reserve 提前开空间 void reserve(size_t n) { if (n > _capacity) { char* temp = new char[n + 1]; strcpy(temp, _str); delete[] _str; _str = temp; _capacity = n; } }
tips:
(1)、只有当所给定空间大于_capacity时,才会进行预留空间,故只有增大容量操作
(2)、开辟新的空间,释放旧的空间,并非在原来空间基础上扩容,而是替换,注意开辟新空间时,要多开一个用于存放‘ ’
4、resize()
参数表(int n,char x)
改变元素个数,既影响数据,也影响容量
插入数据(尾插,空间不够会扩容)
删除数据(尾删,空间大小不会改变)
(1)、n>capacity时,size=n,capacity略大于size,用x填充有效字符
(2)、size<n<capacity时,size=n, capacity保持不变,默认插入
(3)、n<size时,size=n,capacity保持不变,有效数据减少,剩下前size个
应用实例如下图:
五、string的增删查改
1、push_back()
尾插字符,参数表(char ch)应用实例如下图:
模拟实现
void pushback(char ch) { if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } _str[_size] = ch; ++_size; _str[_size] = ' '; }
tips:
(1)、当存放字符数满了之后进行扩容,初始的情况下给定容量为4
(2)、下标位置是从0开始计数,原来尾部下标是_size-1,下标为_size存放的是’ ‘,故添加在下标为_size处,不要忘了在最后要添加’ ‘,即下标为新的_size 的位置
2、append()
追加字符串,功能与 += 字符串相同,用法为append(字符串)
(1)、尾插字符串
(2)、利用迭代器,给定字符串范围进行添加(范围是左闭右开的)
应用实例如下图:
模拟实现
void append(const char* str) { size_t len = strlen(str); if (len + _size > _capacity) { reserve(len + _size); } strcpy(_str + _size, str); _size += len; }
tips:
(1)、string的有效元素个数加字符串长度大于容量时,说明合并后放不下,故利用reverse进行更大的空间预留
(2)、合并可以利用strcpy函数将字符串复制到string的末尾,而有效元素个数直接加上所拼接的字符串长度
3、insert(),erase()
在指定为位置插入元素,insert参数为(插入的下标位置,插入内容)
删除元素,erase()
(1)双参数,第一个参数为开始删除的元素下标,第二个参数为删除元素个数
(2)单参数,删除到只剩下前n个元素
应用实例如下图:
模拟实现
//指定位置插入 void insert(size_t pos, char ch) { assert(pos < _size); if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } int end = _size; //使用有符号整型解决,防止头插的时候end永远大于pos=0 //所导致的无限循环 //并且防止类型提升 while ((int)pos <= end) { _str[end + 1] = _str[end]; --end; } _str[pos] = ch; _size++; } void insert(size_t pos, const char* str) { size_t len = strlen(str); assert(pos < _size); if (_size + len > _capacity) { reserve(_size + len); } int end = _size; while ((int)pos <= end) { _str[end + len] = _str[end]; --end; } strncpy(_str + pos, str, len); _size += len; }
tips:
(1)、当数据插入在某个位置时,从这个位置开始的数据都要向后移动,使用循环不断减小end进行数据向后移动,结束条件是end<pos,但是当pos=0是,也就是头插,如果你使用的end是size_t类型,所以它是不可能<0,则变成一个无限循环,故要切记使用有符号类型,如int。还要注意写表达式,要所有参与的数据强制转换成你需要的类型,否则存在整型提升,就还是size_t的运算
(2)、插入字符串时,直接利用strncpy(),将目标字符串插入对应位置
//删除元素 void erase(size_t pos, size_t len = std::string::npos) { assert(pos < _size); if (len == std::string::npos || pos + len >= _size) { _str[pos] = ' '; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } }
(1)、当所起始位置下标加上删除数量到达size时,说明从下标为pos开始,后面全部删除
4、find()
参数为(元素,起始位置的下标默为0)
找到对应元素的下标,没有返回npos(有效元素最大值)
应用实例如下图:
模拟实现
//查找子字符串或字符位置 size_t find(char ch, size_t pos = 0) { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return std::string::npos; } size_t find(const char* str, size_t pos = 0) { assert(pos < _size); const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) return std::string::npos; else return ptr - _str; }
tips:
(1)、利用strstr返回子字符串的地址,找不到返回空指针
(2)、利用找到的地址减去成员变量字符串的首元素地址所得到的数字,就是该字符串的首元素在成员变量字符串中对应位置下标。
5、c_str()
返回c语言格式的字符串
模拟实现
char* c_str() const { return _str; }
6、rfind(),substr()
rfind()参数与find相似,倒着寻找,找不到返回npos
参数为(元素,起始位置默认为最末尾字符)
substr()在str中从pos位置开始,截取n个字符,然后将其返回
参数为(第一个参数为起始位置,截取个数),默认截取完
substr与find或者rfind的具体应用:分割提取字符串
模拟实现
string substr(size_t pos = 0, size_t len = std::string::npos) { assert(pos < _size); size_t end = pos + len; if (len == std::string::npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); for (size_t i = pos; i < end; i++) { str += _str[i]; } return str; }
tips:
(1)、缺省值给npos,默认分割子字符串从起始位置取到结尾,或者起始位置下标加上长度超过有效字符串,则都代表从起始位置取到结尾,将结尾位置end给定为_size;
(2)、提前开辟空间end-pos个空间,采取赋值操作符逐个加入到新的字符串中
7、find_first_of/find_first_not_of
参数为(给定字符串,起始位置)
找所给字符串里的任意一个/找不是所给字符串里的任意一个,返回下标
具体例子如下图所示
六、string的非成员函数
swap()
在标准库和string类中都有swap(),标准库中实现的swap如图
存在临时变量的拷贝构造,还有赋值,开辟空间在拷贝在释放空间,这样会大大的降低效率,而在string里实现的swap则是,字符串指针交换指向,不需要开辟新的空间,其他内置类型成员变量直接交换数值,大大提升效率。若实现了有关特定string参数的swap重载(交换指针指向),那么调用库中swap,则直接使用swap(string),也是直接交换指针,效率也很高,swap模拟如下
void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }
输入输出流模拟
ostream& operator<<(ostream& out, const string& s) { for (auto ch : s) { out << ch; } return out; }
istream& operator>>(istream& in, string& s) { s.clear(); char ch = in.get(); char buff[128]; int i = 0; while (ch != ' ' && ch != ' ') { buff[i++] = ch; ch = in.get(); if (i == 127) { buff[i] = ' '; s += buff; i = 0; } } if (i > 0) { buff[i] = ' '; s += buff; } return in; }
开辟一个临时的空间用于贮存输入的字符,用于一次性添加多个字符,这也可以避免一个一个地添加数据导致string不断地自动扩容,降低效率,利用临时空间贮存再添加可以将string的空间一次性开辟到位,大大提高输入字符串的效率。具体的贮存空间存放数据具体实现细节见上图。
getline()
使用cin>>str要小心,比如你输入hello world时,str中只会录入hello,因为cin和scanf等接口都支持连续输入,而连续输入时是以换行符或者空格来进行间隔的,cin发现hello后带空格,则认为只需将hello给str即可,此时world会存储在用户层缓冲区里,后序可以通过cin将world赋给别人。如果想把完整的一句话,就比如hello world全部输入进str,则需要使用getline(),或者in.get();
其他
上图是vs编译器对string优化后的实际成员变量,VS认为,如果大量地创建string对象,但每个对象管理的字符串数据非常小,相当于在堆上开辟了很多个小块内存,这相当于有很多内存碎片,会影响动态开辟内存的性能,所以VS在string类中额外添加了一个char数组,这样当string对象管理的字符串数据很小的话就无需去堆上开辟内存,直接存进数组里即可,如果数组不够存,再去堆上开辟内存。避免很少数据却费劲开空间的情况,这样可以大大的提高短小string使用的效率。但VS的这个优化虽然提升了性能,也解决了堆上的内存碎片问题,但也有一个缺点,就是string对象变大了。故而在计算string大小的时候是28,而不是12.在编写的时候不需要考虑此优化。
(附)模拟实现的完整string类代码
#pragma once #include<string.h> #include<iostream> #include<assert.h> using namespace std; #pragma warning(disable : 4996) namespace zwb { class string { public: //打印输出是cout传指针,会被认为是打印字符串,自动对指针解引用, //所以不能初始化给空指针,会解引用错误 string(const char* str = "") { _size = (strlen(str)); _capacity = _size; _str = new char[_size + 1]; strcpy(_str, str); } // 拷贝函数,两种 //传统写法 /*string (const string& s) { _str = new char[s._capacity + 1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; }*/ //现代写法 string(const string& s) { string temp(s._str); swap(temp); //交换后,临时指针指向原来空间,退出函数时,自动被清理 } // 赋值运算符重载 //原始写法 //string& operator=(const string&s){ // if (this != &s) { // _size = s._size; // _capacity = s._capacity; // delete[] _str; // _str = new char[_capacity + 1]; // strcpy(_str, s._str); // // } // return *this; //} //现代方法 string& operator=(string s) { //不使用引用传参,使用拷贝传参,交换后自动清理原来空间 //不需要构建临时变量 swap(s); return *this; } string& operator+=(char ch) { pushback(ch); return *this; } string& operator+=(const char* str) { append(str); return *this; } char* c_str() const { return _str; } size_t size() const { return _size; } //迭代器使用函数 typedef char* iterator; iterator begin() { return _str; } iterator end() { return _str + _size; } typedef const char* const_iterator; const_iterator begin()const { return _str; } const_iterator end()const { return _str + _size; } //规范书写迭代器才可以使用范围for //范围for是傻瓜式的迭代器替换 //交换 void swap(string& s) { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); } //下标方括号访问 const char& operator[](size_t pos)const { assert(pos <= _size); return _str[pos]; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } //reserve 提前开空间 void reserve(size_t n) { if (n > _capacity) { char* temp = new char[n + 1]; strcpy(temp, _str); delete[] _str; _str = temp; _capacity = n; } } //尾插,字符串拼接 void pushback(char ch) { if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } _str[_size] = ch; ++_size; _str[_size] = ' '; } void append(const char* str) { size_t len = strlen(str); if (len + _size > _capacity) { reserve(len + _size); } strcpy(_str + _size, str); _size += len; } //指定位置插入 void insert(size_t pos, char ch) { assert(pos < _size); if (_size == _capacity) { size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2; reserve(newCapacity); } int end = _size; //使用有符号整型解决,防止头插的时候end永远大于pos=0 //所导致的无限循环 //并且防止类型提升 while ((int)pos <= end) { _str[end + 1] = _str[end]; --end; } _str[pos] = ch; _size++; } void insert(size_t pos, const char* str) { size_t len = strlen(str); assert(pos < _size); if (_size + len > _capacity) { reserve(_size + len); } int end = _size; while ((int)pos <= end) { _str[end + len] = _str[end]; --end; } strncpy(_str + pos, str, len); _size += len; } //删除元素 void erase(size_t pos, size_t len = std::string::npos) { assert(pos < _size); if (len == std::string::npos || pos + len >= _size) { _str[pos] = ' '; _size = pos; } else { strcpy(_str + pos, _str + pos + len); _size -= len; } } //查找子字符串或字符位置 size_t find(char ch, size_t pos = 0) { assert(pos < _size); for (size_t i = pos; i < _size; i++) { if (_str[i] == ch) { return i; } } return std::string::npos; } size_t find(const char* str, size_t pos = 0) { assert(pos < _size); const char* ptr = strstr(_str + pos, str); if (ptr == nullptr) return std::string::npos; else return ptr - _str; } string substr(size_t pos = 0, size_t len = std::string::npos) { assert(pos < _size); size_t end = pos + len; if (len == std::string::npos || pos + len >= _size) { end = _size; } string str; str.reserve(end - pos); for (size_t i = pos; i < end; i++) { str += _str[i]; } return str; } void clear() { _size = 0; _str[0] = ' '; } void print_str(const string& s) { for (size_t i = 0; i < s.size(); i++) { //s[i]++; cout << s[i] << " "; } cout << endl; string::const_iterator it = s.begin(); while (it != s.end()) { // *it = 'x'; cout << *it << " "; ++it; } cout << endl; } ~string() { delete[] _str; _size = 0; _capacity = 0; } private: char* _str; size_t _size; size_t _capacity; }; ostream& operator<<(ostream& out, const string& s) { for (auto ch : s) { out << ch; } return out; } istream& operator>>(istream& in, string& s) { s.clear(); char ch = in.get(); char buff[128]; int i = 0; while (ch != ' ' && ch != ' ') { buff[i++] = ch; ch = in.get(); if (i == 127) { buff[i] = ' '; s += buff; i = 0; } } if (i > 0) { buff[i] = ' '; s += buff; } return in; } }
测试代码
#include"string.h" namespace zwb { void test1() { string a1("hello world"); cout << a1.c_str() << endl << endl; string a2; cout << a2.c_str() << endl; for (size_t i = 0; i < a1.size(); i++) { a1[i]++; } cout << a1.c_str() << endl; string::iterator it = a1.begin(); while (it != a1.end()) { cout << *it << " "; ++it; } cout << endl; const zwb::string b("hello world"); cout << b.c_str() << endl; cout << b[0] << endl; string::const_iterator it2 = b.begin(); cout << *it2 << endl; } void test2() { string a1=("hello world"); a1.reserve(100); string a2; cout << a2.size() << endl; a2.pushback('x'); cout << a2.c_str() << endl; a2.append("hello"); cout << a2.c_str() << endl; a1 = a2; cout << a1.c_str() << endl; string a3(a2); cout << a3.c_str() << endl; a1 += 'x'; a1 += "cc"; cout << a1.c_str() << endl; } void test3() { string a1 = ("hello world"); a1.insert(2, '9'); cout << a1.c_str() << endl; a1.insert(0, '9'); cout << a1.c_str() << endl; a1.insert(0, "2004"); cout << a1.c_str() << endl; a1.erase(0, 4); cout << a1.c_str() << endl; cout << a1.find('h') << endl; cout << a1.find("he") << endl; string c = a1.substr(3, 3); cout << c.c_str() << endl; cin >> a1; cout << a1 << endl; string web("https://legacy.cplusplus.com/" "reference/string/string/substr/"); c = web.substr(0, 5); cout << c << endl; } void test4() { string web("https://legacy.cplusplus.com/" "reference/string/string/substr/"); cout << web << endl; size_t pos1 = web.find(':', 0); size_t pos2 = web.find('/', pos1 + 3); size_t pos3 = web.find('/', pos2 + 1); string sub1= web.substr(0, pos1); string sub2 = web.substr(pos1 + 3, pos2 - (pos1 + 3)); string sub3 = web.substr(pos2 + 1); cout << sub1 << endl; cout << sub2 << endl; cout << sub3 << endl << endl; } } int main() { using namespace zwb; test1(); test2(); test3(); test4(); }