目录
浅浅介绍一下什么是STL:
string类需要知道的小知识
auto和范围for:
string类的常用接口:
实现一个string类:
1. 成员变量和构造函数,拷贝构造,析构函数
2. string类对象的容量操作
<1>size,capacity,empty,clear(初始化)
<2>reserve(扩容),resize
3. string类对象的访问和遍历
4. string类对象的修改操作
<1>push_back(尾插), append, operator+=
<2>insert(插入),erase(删除),operator=(赋值)
<3>c_str(返回C格式字符串)和find(查找)
<4>substr(复制子字符串)
5. string类非成员函数
浅浅介绍一下什么是STL:
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。STL有六大组件:仿函数,算法,迭代器,空间适配器,容器,配接器
本篇讲的就是STL容器部分的string类
string类需要知道的小知识
auto和范围for:
(1)在本篇的学习中将使用到关键字auto,它的作用是自动识别类型
注意:
1.使用auto声明指针类型,auto和auto*没有任何区别,但如果修饰引用类型则必须加上&
2.auto在对同一行声明多个变量时,变量必须是相同的类型,否则会报错,因为编译器实际只能对第一个类型进行推导,然后用推导出来的类型定义其他变量
//会出现编译报错:在声明符列表中,auto必须始终推导为同一类型
auto a = 1, b = 2.1;//在这之前已经推导出了int
3.auto不能够作为函数的参数,但可以作为返回值,想设为返回值请谨慎使用
4.auto不能设为数组
(2)范围for
1.对于一个知道范围的集合而言,就可以使用范围for
2.范围for可以作用到数组和容器上进行遍历
3.范围for的底层实际上就是替换为迭代器
int main()
{int a[]={1,2,3,4,5};for(auto& a1:a){cout<<a1<<" ";}return 0;
}
string类的常用接口:
string是一个比较早的容器所以在部分设计上会出现冗余的情况,我只会讲一些比较常用的接口,其他的接口请自行查看文档
string类接口详细文档传送门:string - C++ 参考 (cplusplus.com)
1.string类对象的常见构造
string() //构造出一个空的string对象
string(const char* ch) //使用一个字符串进行构造对象
string (size_t n,char c) //string类对象中包含n个字符c,这个基本不实现
string (const string& st) //拷贝构造
2.string类对象的容量操作
size //返回字符串有效长度
capacity //返回空间总大小
empty //判断字符串是否为空
clear //清空字符串
reserve //空间扩容
resize //将字符串大小调整为n个字符的长度,多出的空间用字符c填充
3.string类对象的访问和遍历
operator[] //返回pos位置的字符引用
begin //获取字符串起始位置字符的迭代器
end //获取字符串最后一位的下一位置的迭代器
4.string类对象的修改操作
operator= //将一个对象赋值给另一个对象
push_back //尾插一个字符或者字符串
append //在字符串后追加一个字符串
operator+= //在字符串后面追加一个字符或者字符串
c_str //返回C格式字符串
find+pos //在起始位置加上pos后的下标开始往后查找字符或者字符串出现的起始位置
substr //从对象的字符串的pos位置开始向后截取n个字符,然后返回
insert //在pos位置上插入一个字符或者字符串
5.string类非成员函数
operator>> //输入重载
operator<< //输出重载
实现一个string类:
这里都是我自己实现各项功能,实现参数部分可能会和文档有差异,请见谅
1. 成员变量和构造函数,拷贝构造,析构函数
string类主要有三个成员变量,还会设置一个静态成员常量,分别是指向数组的指针char*,记录有效字符数的_size,和容量_capacity,静态成员常量npos
构造函数上我们不使用初始化列表,参数设成一个带有缺省值的char*指针,这样可以同时解决单字符初始化,字符串初始化,空字符串对象三种初始化
注意:
1.npos固定为-1,因为在参数里会使用它作为缺省值,无符号整形的符号位无效所以当缺省值有效时它的大小是:4294967295。它的作用就是给一个很大的值,在后面的函数实现中erase(删除)和substr(复制子字符串)会使用到
2.字符串的结尾都会跟上一个'\0'它也是占空间的,但记录容量的_capacity并不会记录这个空间,所以在new开空间的时候要额外增加一个空间给'\0',如果不开这个空间就有两个结果:
<1>不在字符串尾部增添'\0'导致循环越界访问直至遇到'\0';
<2>增添'\0'的位置已经被别的变量申请走了,越界增添'\0'导致破坏了其他资源的完整性
class string { public:string(const char*ch="") //注意这里给的缺省值是没有有效字符的哦{size_t size=strlen(ch);_str=new char[size+1]; //加1为'\0'开空间strcpy(_str,ch);_size=_capacity=size;} private:char* _str;size_t _size;size_t _capacity;static const int npos = -1; }
拷贝构造现在也不应该是浅拷贝,因为出现了空间申请,需要使用深拷贝,现在我们实现一个swap(交换函数)来辅助进行拷贝构造的实现。
注意:直接使用库里的swap对对象(参数是对象)也可以完成交换,但是和我自己实现交换过程有很大差别,库里的实现是对对象的拷贝再交换,这样会导致频繁的触发拷贝构造(深拷贝)导致降低效率,所以不直接交换对象,交换成员变量
出现了资源申请析构函数也不能使用自动生成的了,需要自己实现
void swap(string& ch) {std::swap(_str, ch._str); //交换指针std::swap(_size, ch._size); //交换大小std::swap(_capacity, ch._capacity); //交换容量 } string(const string& str) {//std::swap(*this,str); 频繁触发拷贝构造会出现效率降低的问题string temp(str._str); //这里是用str的字符串初始化一个string对象swap(temp); //现在交换temp和this指针指向的对象 } ~string() {if(!empty()){delete[] _str;_str = nullptr;_size = _capacity = 0;} }
2. string类对象的容量操作
<1>size,capacity,empty,clear(初始化)
这四个都是比较简单的成员函数,直接给大家看实现,一看很快就能理解了
size_t size() //返回有效字符个数 {return _size; } size_t capacity() //返回容量大小 {return _capacity; } bool empty() //判空 {return _size==0; } void clear() {_str[0]='\0'; //这里要重载[]返回当前位置的引用才能直接修改_size=0; }
<2>reserve(扩容),resize
剩下的这两个,reserve负责扩容比较耳熟能详;resize它能调整字符串的长度,如果给的n大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展,直到达到n的长度,如果给的n小于当前长度,那么缩短为当前n个字符,并且删除第n个字符(包括n)以外的字符
下面直接看实现来理解更方便:
void reserve(size_t n) {if(n>_capacity){char* newstr=new char[n+1]; //这里也不要忘了'\0'的位置strcpy(newstr,_str); //拷贝一下旧空间delete[] _str; //删除旧空间_str=newstr; //指向新空间_capacity=n; //更新容量 } } void resize(size_t n, char c='0') {if (n > _capacity) //如果容量不够就扩容{reserve(_capacity * 2);}if (n < size()) {_str[n] = '\0'; //小于的话直接在n位置加'\0'_size = n; //更新有效字符数}else{for (int i = size(); i <= n; i++) {_str[i] = c; }_size=n; //更新_size_str[_size]='\0'; //不要忘了'\0'} }
3. string类对象的访问和遍历
这里就会涉及到迭代器的使用了。迭代器的主要目的是为了屏蔽底层细节,让代码有更好封装性和模块化,能够让使用者不要直接面对复杂的底层内部实现,只需要通过简化后的接口进行操作,使其更容易使用和理解,它就是封装的一种体现方式
class string { public:typedef char* iterator; //自定义命名char*iterator begin() //返回字符串起始位置的迭代器{return _str; }iterator end() //返回字符串末尾下一位的迭代器{return _str+_size;}const iterator begin()const //const迭代器{return _str;}const iterator end()const{return _str + _size;} }
operator[]:重载方括号方便字符串的访问和修改
char& operator[](size_t pos) //运算符重载 {assert(pos < _size); //当前下标必须有效return _str[pos]; //返回当前下标字符的引用 }
4. string类对象的修改操作
<1>push_back(尾插), append, operator+=
这三个成员函数在实现上都有着十分相似的情况,甚至可以相互复用所以直接放到一起讲
void push_back(const char ch) {if (_size == _capacity) //判断容量是否足够{reserve(_capacity == 0 ? 4 : _capacity * 2);} _str[_size++] = ch; //尾插一个字符_str[_size] = '\0'; }string& operator+=(const char ch) //复用push_back完成尾插一个字符 {push_back(ch); return *this; }string& operator+=(const char* ch) {size_t len = strlen(ch); //计算字符串长度if (_size+len > _capacity) //判断容量是否足够{reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}memcpy(_str + _size, ch, sizeof(char) * len); //拷贝空间_size += len; //更新_size_str[_size] = '\0'; //尾部增添'\0' return *this; }void append(const char* str) //在字符串后面增添字符串 {*this+=str; //调用operator+= }void push_back(const char* ch) //这个和append就一样 {*this += ch; //调用operator+= }
在实现完后就可以发现这些函数的功能重叠了好多,append和push_back尾插字符串功能就重叠了,这就是因为比较早出现的原因出现的设计冗余
<2>insert(插入),erase(删除),operator=(赋值)
insert(插入)在代码上容量扩容方面和上面基本一致,但注意不要把insert和前面的功能搞混,insert是插入字符串中的有效字符,上面的三个都是尾插需要在末尾手动添加'\0',假设调用insert时pos位置是尾巴,在代码段末端增加'\0'的代码那就和上面的功能无异,那pos在中间时那不就暴毙了,搁中间加'\0'那就错了,这也是为什么尾插无法复用insert,所以单独拉出来讲
void insert(size_t pos, char ch) //在pos位置插入一个字符 {assert(pos <= _size); //pos位置必须有效if (_size == _capacity) //判断容量是否足够{reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = _size + 1; //找到'\0'位置的下一个位置while (end >= pos) //把pos位置后的字符(包括'\0')全部后移{_str[end] = _str[end - 1];end--;}_str[pos] = ch; //插入字符_size++; //_size加1 }void insert(size_t pos, const char* ch) {assert(pos <= _size); //pos位置必须有效size_t len = strlen(ch); //计算插入字符串长度if (_size + len > _capacity) //容量大小是否足够{reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}int end = _size + len; //'\0'位置加上字符串长度 while (end >= pos + len ) //将包括'\0'的字符全部后移{_str[end] = _str[end - len];end--;}memcpy(_str + pos, ch, sizeof(char) * len); //直接拷贝进去_size += len; //_size增加插入字符串长度 }
erase(删除)的主要作用:要求在pos位置开始向后删除len个字符,len有缺省值,当缺省值有效时删除pos位置包括pos位置往后的所有字符,因为len此时超级大
void erase(size_t pos, int len=npos) {assert(pos < _size); //pos位置必须有效if (len >= _size - pos) //如果删除字符数大于剩下字符数{_str[pos] = '\0'; //直接pos位置给'\0'_size = pos; //更新_size}else{for (size_t i = pos + len; i <= _size; i++) //将pos+len的数据前移{ _str[i - len] = _str[i];}_size -= len; //更新_size} }
operator=(赋值):这个就很简单了,使用赋值对象构造一个对象,然后交换被赋值对象和构造出来的对象即可
string& operator=(string temp) //这里不用引用直接传参触发拷贝构造 {swap(temp); //交换(形参不影响实参)return *this; }
<3>c_str(返回C格式字符串)和find(查找)
在没有重载输出(cout)之前 cout<<st1(string类对象) 肯定是不行的,因为它是一个对象并不是字符串,c_str就是专门解决这个问题,它会返回字符串的起始位
const char* c_str() {return _str; } //没重载cout之前 cout<<st1<<endl; 无法打印字符串 //cout<<st1.c_str()<<endl; 这样就可以了
find(查找):返回在pos位置往后查找字符或者字符串出现的第一位置的下标,
查找一个字符:直接for循环遍历查找,找到返回下标
查找字符串:调用strstr函数直接查找字符串出现第一个位置的指针,通过这个指针减去起始位就是下标
下面看实现:
size_t find(char ch, size_t pos) {assert(pos < _size); //pos位置必须有效for (size_t i = pos; i < _size; i++){if(_str[i]==ch){return i; //返回下标}} }size_t find(const char* ch, size_t pos) {assert(pos < _size);const char* fin = strstr(_str + pos, ch); //找到并返回出现的第一个位置的指针if (fin == nullptr) //空代表没找到{return npos; //返回npos}else{return fin - _str; //当前指针减去起始位置得出下标返回} }
<4>substr(复制子字符串)
substr的主要作用:要求指定位置开始往后截取n个字符并返回,如果n没有给指定的长度那么缺省值有效,默认从指定位置开始向后截取全部字符返回,下面看实现理解更方便
string substr(size_t pos, size_t len=npos) {assert(pos < _size); //依旧是pos位置必须有效//len大于剩余长度,更新lenif (len > _size-pos) {len = _size-pos;}string sub; //构造一个新的string类对象sub.reserve(len); //给len长度的空间for (size_t i = 0; i < len; i++) //从pos位置开始向后截取len次字符 {sub += _str[pos + i]; //尾插}return sub; //返回当前类对象 }
5. string类非成员函数
比较主要的就是重载输入和输出了,这个比较简单直接看实现来理解
ostream& operator<<(ostream& out, const string& s) {for(auto ch:s) //使用范围for自动识别类型,自动迭代,自动判断停止{cout<<ch;}out << endl;return out; } istream& operator>>(istream& in, string& s) {s.clear(); //输入自然要对当前对象进行初始化char ch;ch=in.get()while(ch != ' ' && ch != '\n') //这个是判断什么时候停下来,我假设{ //遇到空格和换行就停止s+=ch;ch=in.get();}return in; }
本篇文章到这里主要内容已经讲完了,希望能够对你产生帮助,感谢阅读
需要看全部实现可以往下翻下面有完整实现
我自己实现的一个string类 :
//头文件
#include <iostream>
#include <assert.h>
using namespace std;namespace cool
{class string{public://简易迭代器typedef char* iterator;iterator begin() //普通迭代器{return _str;}iterator end(){return _str+_size;}const iterator begin()const //const迭代器{return _str;}const iterator end()const {return _str + _size;}string(const char*str="") //构造{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);} ~string() //析构 {delete[] _str;_str = nullptr;_capacity = _size=0;}void swap(string& str) //交换函数{std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}string(const string& str) //拷贝构造{string temp(str._str);swap(temp);}const char* c_str()const //返回C格式字符串{return _str;}size_t size()const //返回有效个数{return _size;}bool empty() //判空{return _size==0;}void clear() //初始化{_str[0] = '\0';_size = 0;}void reserve(size_t n) //扩容{if (n > _capacity){char* temp = new char[n+1];strcpy(temp, _str);delete[]_str;_str = temp;_capacity = n;}}void push_back(char ch); //尾插void push_back(const char* ch); //尾插的函数重载string& operator+=(const char ch); //重载+=string& operator+=(const char* ch); //+=的函数重载string& operator=(string temp); //重载=char& operator[](size_t pos) //重载[]const char& operator[](size_t pos)const; //const类型的[]重载void append(const char* str); //字符串后增加一个字符串void insert(size_t pos, char ch); //插入void insert(size_t pos, const char *ch); //插入的函数重载 void erase(size_t pos,int len=npos); //删除 size_t find(char ch, size_t pos = 0); //查找 size_t find(const char* ch, size_t pos = 0); //查找的函数重载string substr(size_t pos = 0, size_t len=npos); //复制一个子字符串 private:char* _str=nullptr;size_t _size=0;size_t _capacity=0;static const size_t npos=-1;};//重载输入输出ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);void test();void test1();void test2();void test3();void test4();
}
//实现文件
namespace cool
{void string::push_back(const char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';}void string::push_back(const char* ch){*this += ch;}string& string::operator+=(const char ch){push_back(ch);return *this;}string& string::operator+=(const char* ch){size_t len = strlen(ch);if (_size+len > _capacity){reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}memcpy(_str + _size, ch, sizeof(char) * len);_size += len;_str[_size] = '\0';return *this;}string& string::operator=(string temp){swap(temp);return *this;}char& string::operator[](size_t pos){assert(pos < _size);return _str[pos];}const char& string::operator[](size_t pos)const{assert(pos < _size);return _str[pos];}void string::append(const char* str){*this+=str; } void string::insert(size_t pos, char ch){assert(pos <= _size);if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}size_t end = size() + 1;while (end >= pos){_str[end] = _str[end - 1];end--;}_str[pos] = ch;_size++;}void string::insert(size_t pos, const char* ch){assert(pos <= _size);size_t len = strlen(ch);if (_size + len > _capacity){reserve(_size + len > _capacity * 2 ? _size + len : _capacity * 2);}size_t end = _size + len;while (pos + len <= end){_str[end] = _str[end - len];end--;}memcpy(_str + pos, ch, sizeof(char) * len);_size += len;}void string::erase(size_t pos, size_t len){assert(pos <= _size);if (len >= _size - pos){_str[pos] = '\0';_size = pos;}else{for (size_t i = pos + len; i <= _size; i++){_str[i - len] = _str[i];}_size -= len;}}size_t string::find(char ch, size_t pos){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch) {return i;}}return npos;}size_t string::find(const char* ch, size_t pos){assert(pos < _size);const char* ptr = strstr(_str + pos, ch);if (ptr == nullptr){return npos;}else{return ptr - _str;}}string string::substr(size_t pos, size_t len){assert(pos < _size);if (len >_size - pos){len = _size - pos;}string sub;sub.reserve(len);for (size_t i = 0; i <len; i++){sub += _str[pos + i];}return sub;}ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}istream& operator>>(istream& in, string& s){s.clear();const int N = 256;char buff[N];int i=0;char ch;ch = in.get();while (ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;i = 0;}return in;} //测试用例void test( ){string s1;string s2("h");string s3("hello");cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;}//尾插测试void test1(){string s1;s1.push_back('h');cout << s1 << endl;s1.push_back("ello");cout << s1 << endl;s1.append(" world");cout << s1 << endl;}//插入删除测试void test2(){string s1("hello world");s1.insert(5, 'x');cout << s1 << endl;s1.insert(0, "xxxx");cout << s1 << endl;s1.erase(0, 4);cout << s1 << endl;}//拷贝和赋值测试void test3(){string s1("hello world");string s2 = s1;string s3;s3 = s2;cout << s1 << endl;cout << s2 << endl;cout << s3 << endl;}//查找和返回字符串测试void test4(){string st("hello.cpp");size_t fin = st.find('.');string st1 = st.substr(fin);cout << st1 << endl;}
}