目录
前言
一、接口总览
二、默认成员函数
1.构造函数
2.拷贝构造
写法一:传统写法
写法二:现代写法(复用构造函数)
3.赋值构造
写法一:传统写法
写法二:现代写法(复用拷贝构造)
4.析构函数
三、迭代器
begin和end函数
四、遍历与访问
operator[]函数
五、容量和大小
size
capacity
empty
resever(容量)
resize(大小)
六、修改
insert
erase
push_back
append
operator+=
clear(清理字符串内容)
c_str
swap
七、查找
find
substr(左闭右开区间)
八、非成员函数重载
swap(全局,string)
关系运算符重载
"<<"重载
">>"重载
getline函数
前言
在前面我们已经了解了string中常见的接口,我们也说过,对于STL库的学习,不仅仅只是熟用,还要明理!但是要注意一个道理:模拟实现不是为了比库里面更好,而是去学习它的一些底层,能够让自己有更深的了解,就比如我们并不需要去造车,这不是我们干的,但我们需要了解它为什么能这样做,它的底层是什么在驱动,这样才能更好的去发挥车的性能!string类的底层就是个字符顺序表!
一、接口总览
#include<assert.h>
#include<iostream>namespace STR
{class string{public:typedef char* iterator;typedef const char* const_iterator;static const size_t npos;public:string(const char* str = "");//构造函数,提供全缺省可以是空参数调用,也可以有参调//s2(s1)string(const string& s);//拷贝构造,传统写法string(const string& s);//拷贝构造,现代写法//s1=s2string& operator=(const string& s);//赋值重载,传统写法string& operator=(string tmp);//赋值重载,现代写法!~string();//析构函数//迭代器iterator begin();iterator end();const iterator begin()const;const iterator end()const;// 遍历char& operator[](size_t index);const char& operator[](size_t index)const;// 容量size_t size()const;size_t capacity()const;bool empty()const;void reserve(size_t n);void resize(size_t n, char c = '\0');// 修改void push_back(char c);string& operator+=(char c);void append(const char* str);string& operator+=(const char* str);void clear();void swap(string& s);const char* c_str()const;// 返回c在string中第一次出现的位置size_t find(char c, size_t pos = 0) const;// 返回子串s在string中第一次出现的位置size_t find(const char* s, size_t pos = 0) const;// 在pos位置上插入字符c/字符串str,并返回该字符的位置string& insert(size_t pos, char c);string& insert(size_t pos, const char* str);// 删除pos位置开始向后len个元素,并返回该元素的下一个位置string& erase(size_t pos, size_t len=npos);string sub(size_t pos = 0, size_t len = npos); private:char* _str;size_t _capacity;size_t _size;};const size_t string::npos = -1;//relational operators//s1<s2bool operator==(const string& s1, const string& s2);bool operator<(const string&s1,const string& s2);bool operator<=(const string& s1, const string& s2);bool operator>(const string& s1, const string& s2);bool operator>=(const string& s1, const string& s2);bool operator!=(const string& s1, const string& s2);ostream &operator<<(ostream& _out, const string& s);istream &operator>>(istream& _cin, string& s);istream& getline(istream& in, string& s);}
注意:这里模拟实现,需要我们使用我们自己的命名空间,防止与库里面的发生冲突!
二、默认成员函数
1.构造函数
构造函数一般提供缺省参数,这样使得无参有参也可以一起调用!
string(const char* str = "")//构造函数,提供全缺省可以是空参数调用,也可以有参调用,但是单参数构造会有隐式类型转化:_size(strlen(str))//初始时,大小设置为字符串长度{_capacity = _size;//容量设置为字符串长度_str = new char[_capacity + 1];//多开一个是为存放'\0'strcpy(_str, str);//按字节拷贝}
2.拷贝构造
对于拷贝构造,前面我们也说过一定要注意深拷贝和浅拷贝的区别,以及影响,这里再回顾一下!
浅拷贝:不写编译器默认生成的函数,多个对象共同指向同一块资源,那么释放时会造成同一块空间多次释放的问题!
深拷贝:我们自己写的,给每个对象分配独立的资源,保证多个对象之间不会因为共享资源而造成多次释放问题!
写法一:传统写法
思路简单:先开一个与原对象大小相同的空间,这样两个对象指向的空间就不一样了,相互独立!然后把字符串按字节拷贝过去,再把相应的值传给拷贝对象!
string(const string& s)//拷贝构造 {_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
写法二:现代写法(复用构造函数)
思路与传统的不同:它需要借助一个tmp对象,先将原对象的字符串通过调用构造函数创建出tmp对象,最后在将tmp和拷贝对象交换,这样两个对象指向的空间也是不一样的!
string(const string& s):_str(nullptr)//拷贝对象,一定要先置空,_size(0),_capacity(0){string tmp(s._str);//调用构造函数,不是拷贝构造,因为参数不是对象,是个字符串swap(tmp);//交换拷贝对象和tmp//tmp出了作用域会自动销毁,此时它指向的空间是个空,析构时不会出问题,如果是随机值就会出现问题//这就为什么_str先前要置空的原因}
需要注意上面的细节问题!!!!!!!
两种写法效率上都是一样,因为都要开空间,只不过传统写法是要自己写,而现代写法直接复用构造函数,喜欢写哪个就写哪个,没啥区别!!!
3.赋值构造
这里也是有深浅拷贝的问题,但是我们时刻都不能让多个对象指向同一块空间,所以还是要自己写深拷贝!!!
写法一:传统写法
注意:我们不能直接将原对象拷贝过去,因为两者空间容量大小可能不一样,所以要新开一个空间,在让赋值对象指向这块空间即可!
string& operator=(const string& s)//赋值重载
{if (this != &s){char* tmp = new char[s._capacity + 1];//开个空间strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}}
写法二:现代写法(复用拷贝构造)
string& operator=(string tmp)//赋值重载
{swap(tmp);//交换两个对象return *this;//返回左值,支持连续赋值
}
可以看到,这里的参数我们利用值传递,接受右值的方法,编译器会自动调用拷贝构造函数去生成一个tmp对象出来,最后在与左值进行交换即可,这样两者指向的空间就是不一样的!那么,这里或许会有疑问,传值不会死循环吗?这里当然不会,因为赋值它只会调用一次拷贝构造!但是拷贝构造是一定不能传值的,因为传值会再一次调用拷贝构造,一次又一次,最终形成死循环!
4.析构函数
对于string类,我们需要自己手动写析构,因为每个对象中的_str指向的是一块堆空间,不写编译器自动生成的析构,他只是会对内容进行清理,而不会自己释放空间,所以为了避免内存泄漏,我们需要手动delete!
~string()//析构函数
{delete[] _str;_str = nullptr;_capacity = _size=0;
}
三、迭代器
string类的迭代器底层就是个字符指针,只不过是把它重命名罢了!
typedef char* iterator;typedef const char* const_iterator;
但是需要注意的是并不是所有的迭代器都是指针!!只能说像,不一定是!上面我说的就是简单版本的
begin和end函数
这两个函数简单得可怕,begin实际上是返回第一个字符的地址,end就是返回最后一个字符的下一个位置的地址!
//begin
iterator begin()
{return _str;
}iterator begin()const //让const对象也能用
{return _str;
}
//end
iterator end()
{return _str + _size;
}iterator end()const
{return _str + _size;
}
这里提一嘴,在上一篇文章string类中提到过范围for,它并神奇,它的底层就是迭代器!实际上这个东西在底层是写死的,虽然是迭代器,但是如果我们在模拟实现时,把begin函数改成Begin函数,那么范围for会立马失效!!!也就是说它只认识begin和end,而不认识Begin和End,死的!大家可以尝试尝试!
四、遍历与访问
operator[]函数
我们之前能够通过【】+下标进行访问字符串,其实底层就是这个函数!这个函数就是参数就是一个位置,直接返回对应字符的引用即可!
//这个是可读可写的
char& operator[](size_t index)
{assert(index < _size);//对下标是否越界的判断return _str[index];//返回对应位置即可
}//这个是只读的,访问const对象用的
char& operator[](size_t index)const
{assert(index < _size);return _str[index];
}
五、容量和大小
size
//直接返回对象的变量_size就好了
//对于一些只需要进行读操作的,我们一般习惯加上const修饰
size_t size()const
{return _size;
}
capacity
size_t capacity()const
{return _capacity;
}
empty
bool empty()const
{if (_size == 0){return true;}return false;
}
resever(容量)
规则:如果n大于当前的capacity,那么就扩容到n甚至更大;如果小于,那就什么都不做
//只是改变容量,大小不变
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//多开一个存放‘\0’strcpy(tmp, _str,_size+1);//连着‘\0’也一起拷贝delete[] _str;_str = tmp;_capacity = n;}
}
resize(大小)
规则:如果n小于当前字符串长度,那就缩短至n个字符的长度,其他部分全部删除;如果n大于当前长度,那就扩大到n个字符的长度,扩大的部分用特定的字符表示,若未给出,则默认给'\0'!所以要提供个缺省值!
void resize(size_t n, char c = '\0')//半缺省
{if (n <=_size)//小于就缩到n个字符的长度{_str[n] = '\0';//字符串结尾都是以'\0'结尾_size = n;}else{reserve(n);//扩容for (size_t i = _size; i < n; i++){_str[i] = c;//多出来的部分用特定字符补充}_str[n] = '\0';_size = n;}
}
六、修改
insert
我们熟知,这个函数接口,它的功能:在pos位置前插入一个字符或者字符串,并返回字符该字符位置!既然是插入操作,那必然会引起扩容问题,这时我们又可以复用reserve!
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c)
{assert(pos <= _size);//判断pos位置是否合法if (_size == _capacity) //容量问题{reserve(_capacity == 0 ? 4 : 2 * _capacity);}//这里有个细节,那就是隐式类型提升//如果end的类型是size_t,那么就会一直循环//如果是int的话,因为pos是size_t,end还是会隐式类型转化成size_t//所以对于这样的情况,只能是将pos强转成int,才能对pos=0这样的情况处理/* int end = _size;while (end >= (int)pos){_str[end + 1] = _str[end];--end;}*///写法二就不会出现上面的情况size_t end = _size + 1;while (end > pos){_str[end] = _str[end-1];//依次向后挪动数据--end;}_str[pos] = c;++_size;return *this;
}
除了插入单个字符以外,我们还可以插入字符串,思路也是大差不差,我们首先都是对pos位置的合法性进行判断!然后再在要插入的位置后面的字符串整体向后挪动len个长度(这里的len长度实际上就是你要插入的字符串的长度),然后插入即可
string& insert(size_t pos, const char* str)
{assert(pos <= _size);//位置合法性size_t len = strlen(str);//待插入的字符串长度//扩容问题if (_size + len > _capacity){reserve(_size+len);}size_t end = _size +len;while (end > pos+len-1){_str[end] = _str[end - len];--end;}strncpy(_str + pos, str,len);_size += len;return *this;
}
erase
规则:擦除字符串中从字符位置pos开始并跨越len字符的部分(如果内容太短或len为string::npos,则擦除直到字符串末尾)
从规则就可以看出它的实现首先就是需要判断位置是否合法,如果合法那么长度是否达到要求。所要删除的字符串过长或者过短索处理的情况都是一样的!!!!
// 删除pos位置开始向后len个元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len=npos)
{assert(pos < _size);//位置的合法性判断if (len == npos || len >= _size - pos)//长度的合法性判断{_str[pos] = '\0';//直接在pos位置加上‘\0’即可将后面的字符全部清除_size = pos;}//长度和位置都合法,直接用pos位置后面len个长度的字符覆盖前面即可else{strcpy(_str + pos, _str + pos + len);//_size -= len;}return *this;
}
值得注意的是:字符串是以"\0"结尾的,所以要清楚后面的字符,直接在特定的位置上加上"\0"即可!!
push_back
这个函数的作用就是在尾部插入一个字符,实现起来十分的容易,一个简单的尾插操作!!
void push_back(char c)
{//写法一:常规写法if (_size == _capacity)//扩容问题{reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size] = c;++_size;_str[_size] = '\0';//写法二:复用inser函数insert(_size, c);
}
append
函数作用:在尾部追加字符串 !!
void append(const char* str)
{//写法一:常规写法size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);//将字符串拷贝到_size所指向的位置_size += len;//写法二:复用insertinsert(_size, str);
}
operator+=
这个函数的功能也是尾部追加,不过它既可以追加字符,也可以追加字符串!!!实现也是十分的简单!
//追加字符,复用push_back
string& operator+=(char c)
{push_back(c);return *this;
}//追加字符串,复用append
string& operator+=(const char* str)
{append(str);return *this;
}
clear(清理字符串内容)
//不改变空间大小,也就是_capacity不变
void clear()
{_size = 0;_str[_size] = '\0';
}
c_str
这个函数实际上将string对象转化为C类型的字符串,底层十分简单,就是返回_str即可
const char* c_str()const
{return _str;
}
swap
这里的交换实际上是调用库里面的全局的swap进行交换,但是要注意加上作用域限定符哦!!
void swap(string& s)
{//调用的是库里的全局交换函数std::swap(_str, s._str);std::swap(_size,s._size);std::swap(_capacity, s._capacity);
}
七、查找
find
这个函数的功能:从字符串pos位置(不写默认是0)开始往后找字符c/或者字符串,返回的是第一个匹配的第一个字符的位置。如果没有找到匹配项,函数返回string::npos(-1)。
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const//仅仅只是查找
{assert(pos < _size);//位置的合法性判断for (size_t i = pos; i < _size; i++)//遍历查找{if (c == _str[i]){return i;}}return npos;
}
当然了,除了可以查找单个字符,也可以查找一个字符串,查找字符串底层我们使用的是strstr函数,这个函数如果找到匹配的字符串,会返回目标字符串起始的位置,找不到就会返回空!若找到,那就用当前位置减去起始位置(指针-指针)就可以得到起始字符的下标!
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{assert(pos < _size);const char* p=strstr(_str+pos, s);if (p)//不为空,找到{return p - _str;//指针-指针}else {return npos;}
}
substr(左闭右开区间)
功能:从pos位置开始,截取n个字符,返回的结果是一个新的字符串对象,这个新对象的内容就是使用截取到子字符串去初始化!
string sub(size_t pos = 0, size_t len = npos)
{string sub;//定义一个接受子串的对象//所截长度大于当前字符串的长度,就截取到末尾if (len >= _size - pos){for (size_t i = pos; i < _size; i++){sub += _str[i];//将字符尾插到新对象即可}}//长度小于,直接从pos位置开始截len个字符,在尾插入新对象else{for (size_t i = pos; i < pos+len; i++){sub += _str[i];}}return sub;//返回结果
}
八、非成员函数重载
swap(全局,string)
在介绍string类时,我们说过,这个函数在库里面有两个,一个是作为string类的成员函数(上面提到的),一个就是这个全局的swap,但是要注意的是,这个全局的swap不是在算法库里的!算法库里的是一个模板!
调用算法库里的模板:
string s1("skkald");
string s2("vckjn");swap(s1,s2);//调用算法库里的swap模板!
但是需要注意的是,算法库的swap,会拷贝构造一个临时对象C,而后又进行两次赋值构造,最后在析构临时对象C,代价有点大,而且在底层调试时,实际走的是深拷贝,也就是他会重新开出两块空间,在交换,一定程度上也有开销!这也是为啥要在全局专门搞了一个string的swap的原因!为的就是避免上述情况的发生!!!那就相当于有三个swap(成员函数,string全局非成员函数,算法库的)!
//string的非成员函数
void swap(string& x, string& y)
{x.swap(y);//这里调用成员函数swap
}string s1("skkald");
string s2("vckjn");swap(s1,s2);//调用全局的string中swap,也就是上面的代码
同时需要注意,这里全局的swap和算法库里的模板swap并不会冲突,因为模板中我们提到,如果有现成的,那么编译器会优先去调用现成函数,没有的情况下才去考虑使用算法库的模板!!!
关系运算符重载
这包含了==,<,<=,>,>=,!=,其实他们的底层实现需要用到就是C语言中的strcmp函数,然后在进行相互之间的复用!
bool operator==(const string& s1, const string& s2)
{int ret = strcmp(s1.c_str(), s2.c_str());return ret == 0;//两字符串相等就返回0
}bool operator<(const string&s1,const string& s2)
{int ret = strcmp(s1.c_str(), s2.c_str());return ret < 0;//第一个小于第二个就返回<
}bool operator<=(const string& s1, const string& s2)
{return s1< s2 || s1 == s2;//复用上面的两个
}bool operator>(const string& s1, const string& s2)
{return (!(s1 <= s2));
}
bool operator>=(const string& s1, const string& s2)
{return (s1 > s2) || (s1 == s2);
}
bool operator!=(const string& s1, const string& s2)
{return !(s1 == s2);
}
"<<"重载
这个重载就使得我们能使用cout像内置类型那样输出,底层实现十分的简单,遍历就完事!
ostream &operator<<(ostream& _out, const string& s)
{for (auto ch : s){_out << ch;}return _out;
}
">>"重载
重载后,便可以像内置类型那样去输入内容,但是要先将对象字符串内容清空,然后再依次读入,直到遇到空格或者"\n"为止!
写法一:
istream &operator>>(istream& _cin, string& s)
{s.clear();//防止里面有脏数据!char ch;ch = _cin.get();//C++中可以使用get获取一个字符while (ch != ' ' || ch != '\n'){s += ch;//尾插ch = _cin.get();}return _cin;//支持连续提取
}
其实上面的写法是有点缺陷的,因为它涉及到空间消耗问题,尾插必定会引起扩容,所以上面的写法会频繁的去扩容,但是如果先reserve,那么要开多大的空间又是个问题,太大,万一字符串长度过小,又浪费空间,太小又可能会频繁扩容!所以这时就诞生了第二种写法,第二种写法的思路就是开个128大小的字符数组,当字符个数到128时,数组满,直接尾插到字符串,不足128时,也不会消耗太大空间!
写法二:
istream &operator>>(istream& _cin, string& s)
{//消耗小的写法,需要多大就开多大,这就不会频繁的去扩容,空间也不会浪费s.clear();char buff[128];size_t i = 0;char ch;ch = _cin.get();while (ch != ' ' || ch != '\n'){buff[i++] = ch;if (i == 127)//满128{buff[i] = '\0';s += buff;i = 0;}ch = _cin.get();}//小于128if (i > 0){buff[i] = '\0';s += buff;}return _cin;
}
getline函数
这个函数的存在就是弥补cind的缺陷,cin遇到空格就会停下,无法读取完整的一行,而这个函数就是读完一行为止!遇到空格不会停止
写法一:
istream& getline(istream& in, string& s)
{s.clear();//防止里面有脏数据!char ch;ch = in.get();//C++中可以使用get获取一个字符while (ch != '\n'){s += ch;//尾插ch = in.get();}return in;//支持连续提取
}
写法二:
istream& getline(istream& in, string& s)
{//消耗小的写法,需要多大就开多大,这就不会频繁的去扩容,空间也不会浪费s.clear();char buff[128];size_t i = 0;char ch;ch = in.get();while (ch != '\n'){buff[i++] = ch;if (i == 127)//满128{buff[i] = '\0';s += buff;i = 0;}ch =in.get();}//小于128if (i > 0){buff[i] = '\0';s += buff;}return in;
}