string的自主实现
string的自主实现
- 需要注意的地方
- 1.string 的构造
- 2.string的拷贝和赋值重载
- 2.1拷贝
- 2.2赋值重载
- 3. push_back
- 4.迭代器的访问,和for范围
- 5.末尾插入字符串append
- 6. 中间插入字符和字符串
- 7.earse删除数据
- 8.substr裁剪字符串
- 9. find,查找字符串
- 10.流插入<<和流提取>>
- 11.运算符重载==,>,>=
- 12.swap
需要注意的地方
1.string 的构造
对于无参的构造,初始化_str,我们并不能初始化为空,因为输出,末尾没有\0,会输出错误,cout会解引用,翻译成字符,但遇到的是nullptr,会出现崩溃。
string::string():_str(new char[1]{ '\0' })//因为c_str是返回\0结尾的字符串//_str(nullptr)如果这样写,cout会崩溃一直没有\0, _size(0), _capacity(0){}string::string(const char* str) {//常量字符串遇到第一个\0就中止,所以可以用strcpy_size = strlen(str);//不能用sizeof因为那个计算的是指针的大小_str = new char[_size + 1];//要多分配一个空间,因为strlen没有算\0,如果不加1,后续可能出现问题_capacity = _size;strcpy(_str, str);}
这就是构造的方式
2.string的拷贝和赋值重载
2.1拷贝
对于string s1(s2),我们不能单纯的使用编译器生成的拷贝函数,因为这样只是简单的浅拷贝,两个指向同一块空间,因为结束时,会自动调用析构,对同一个空间进行两次析构,会出现错误,所以要进行深拷贝
string::string(const string& s) {if (_str!=s._str) {_str = new char[s._capacity + 1];//开同样大的空间再加1,因为capacity不含\0memcpy(_str, s._str, s._size + 1);_capacity = s._capacity;_size = s._size;}}
2.2赋值重载
跟拷贝差不多
string& string:: operator=(const string& s) {char* temp = new char[s._capacity+1];delete[]_str;memcpy(temp, s._str, s._size + 1);this->_str = temp;_size = s._size;_capacity = s._capacity;return *this;}
*** 注意我们要用memcpy,因为strcpy能拷贝\0,遇到\0就停止拷贝了,如果中间有\0,不能拷贝完***
3. push_back
这个我们只需要注意扩容,注意reserve就可以实现扩容,我需要多大的空间,我们先利用reserve阔好,插入后末尾记得加上\0,不然字符串没有结束的标识,末尾会出现汉字
void string::push_back(char c){if (_size == _capacity) {reserve(_capacity == 0 ? 4 : 2 * _capacity);}_str[_size++] = c;//size要更新_str[_size] = '\0';//插入以后要加上\0,不然会崩溃}
void string::reserve(size_t n) {if (n > _capacity) {char* temp = new char[n + 1];memcpy(temp, _str,_size+1);delete[]_str;_str = temp;_capacity = n;//这个不能忘记,不然会出现错误}}
4.迭代器的访问,和for范围
因为迭代器的返回是iterator,我们typedef char* iterator,为什么说迭代器的返回类似于指针,对于有些常量只能调用const函数,所以迭代器还有const函数
iterator begin() {return _str;}iterator end() {return _str + _size;}
const_iterator begin()const {return _str;}const_iterator end()const {return _str + _size;}
范围for的实现
因为范围for的底层就是迭代器,我们实现了迭代器,就可以直接使用范围for。
//迭代器的循环tx::string s1("hello world");//不是const的变量iterator begin = s1.begin();while (begin != s1.end()) {std::cout << *begin;begin++;}std::cout << std::endl;//范围for的循环for (auto e : s1) {std::cout << e;}std::cout << std::endl;//const变量const tx::string s2("wbb is nb");//const成员变量必须调用const成员函数,还有const要写在最前面for (auto c : s2) {std::cout << c;}std::cout << std::endl;}
5.末尾插入字符串append
这里值得注意的是如果我们每次只插入一个字符,那么每次都需要扩容,效率太慢,我们如果插入字符串长度较小,多次插入,我们直接先阔一个大空间,如果插入字符串很长,二倍空间不够,可以直接扩容字符串长度大小
size要及时更新
string& string::append(const char* s) {if (_size+strlen(s) > _capacity) {reserve(std::max(strlen(s)+_size, 2 * _capacity));//max前面要写标准库,即使写了文件名}strcpy(_str + _size, s);//const常量字符串不用可以用strcpy_size += strlen(s);//要加上这个,不然后续_size没有更新,会一直在原地方加字符串return *this;}
6. 中间插入字符和字符串
因为pos是size_t,len也是int,–end,达到-1时会转换成最大的数,导致循环一直继续,
- 第一种方法是强制转换,吧pos转换成int,从_size+1,移到end=_size
- 第二中就是把end=_size移动end=_size+1
插入字符
string& string::insert(size_t pos,size_t n,char c) {assert(pos <= _size);if (_size == _capacity) {reserve(_capacity==0?4:2*_capacity);//strlen只能计算数组指针的长度,不能计算当个字符。}//size_t end = this->size();size_t end = _size+1;while (end > pos) {_str[end] = _str[end-1];//this指针指向的是对象end--;}//size_t end = _size;这种不行无符号,如果为负数也是最大的数//int end = _size;//二目运算符在运算时,不相同的类型,要转成相同的类型进行运算//while (end >= (int)pos) {// _str[end+1] = _str[end];// end--;//}_str[pos] = c;_size += 1;return *this;}
插入字符串时,要注意循环条件-1,跟插入单个字符很像
string& string::insert(size_t pos, const char* s) {assert(pos <= _size);size_t len = strlen(s);if (_size + len > _capacity) {reserve(std::max(len+_size,2 * _capacity));//max里面形参要是相同类型}size_t end = _size + len;while (end > pos + len-1) {//这里不能写成pos+len这样的话end等于pos+len就停止了,后面插入就会多一个字符格,填入中文_str[end] = _str[end-len];end--;}strncpy(_str + pos, s,len);_size += len;return *this;}
7.earse删除数据
这个简单,如果从pos 位置删完只需要在pos位置加个\0,如果删除len小于后面的长度只需要拷贝到pos 位置
string& string::erase(size_t pos, int len) {//分离,参数不能同时存在assert(pos < _size);if (len == npos || len > _size - pos) {_str[pos] = '\0';_size = pos;}//开始我这里没有加else,以为执行完if直接跳过,必须要加,不然len太大,又会执行,导致数组越界else {memcpy(_str + pos, _str + pos + len,_size-pos-len+1);_size = _size - len;}return *this;}
8.substr裁剪字符串
只需要开一块空间,将字符串memcpy过来
string string::substr(size_t pos, size_t len) {assert(pos <= _size);char* temp = new char[len + 1];memcpy(temp, _str + pos, len);temp[len] = '\0';//len就相当于temp的_size。return temp;}
9. find,查找字符串
我们可以利用c的函数strstr,来查找字符串,但它返回的是char*,我们只需要减去_str,就能知道在哪个位置了,如果字符串中间有\0,strstr就不行了,要有其他算法,strstr对于\0就停止
size_t string::find(const char* s, size_t pos) {assert(pos + strlen(s) < _size);char* ptr = strstr(_str, s);if (ptr) {return ptr - _str;}else {return npos;}}
10.流插入<<和流提取>>
我们开始利用c_str得到字符串的输出,但这样有弊端,如果字符串中间含有\0,c_str遇到\0就停止,所以我们自己实现cout,按照范围for,每个字符都输出直到size个字符都输出完了
std::ostream& operator<<(std::ostream& out, const string& s) {//out是流输入对象的引用。for (auto e : s) {out << e;}return out;}
对于流提取我们不能简单的in>>e,这样字符一个一个的进入,自动跳过空格和换行,导致不能停止,一直输入,因为一直跳过空格和换行,导致我们的循环条件被破坏所以我们可以用get
std::istream& operator>>(std::istream& in, string& s) {//这里s不能用const,因为s要改变,不能是常量s.clear();//s是目标字符串假如s有初值,再输入就会出现错误char ch;char buff[256];int i = 0;//in >> ch;//会自动跳过空格和换行,导致不能停止,一直输出in.get(ch);//可以读取空格和换行,一个字符一个字符的读。while (ch != ' ' && ch != '\n') {buff[i++] = ch;if (i == 255) {buff[i] = '\0';s += buff;i = 0;}//in >> ch;in.get(ch);}if (i > 0) {buff[i] = '\0';s += buff;}return in;}
大家注意我这里加了一个数组,因为我为了减少扩容,如果不加数组,每次+=可能会扩容
上面提到了>>,但是这个不能输入空格,为了方便getline可以输入空格,它跟>>差不多,只是它的结束条件是换行。
std::istream& getline(std::istream& in, string& s, char delim) {s.clear();//s是目标字符串假如s有初值,再输入就会出现错误char ch;char buff[256];int i = 0;//in >> ch;//会自动跳过空格和换行,导致不能停止,一直输出in.get(ch);//可以读取空格和换行,一个字符一个字符的读。while (ch != '\n') {buff[i++] = ch;if (i == 255) {buff[i] = '\0';s += buff;i = 0;}//in >> ch;in.get(ch);}if (i > 0) {buff[i] = '\0';s += buff;}return in;
11.运算符重载==,>,>=
我们写这个只是了解底层,肯定达不到库里面的那么完美,所以这个运算符重载中间有\0的没有仔细考虑,我们可以c 的strcmp来比较
bool string:: operator==(const string& s) const {//运算符重载是对类类型才有作用。return strcmp(_str, s._str) == 0;//形参是char*}bool string:: operator>(const string& s)const {return strcmp(_str, s._str) > 0;}bool string:: operator>=(const string& s)const {if (*this > s || *this == s) {return true;}return false;}
运算符重载,我们需要类类型成员,而strcmp需要指针,所以我们需要转换一下
12.swap
我们通过官网可以看见三个swap,有string的,有全局的,有库的标准函数的,哪我们应该怎么选择。
string的成员函数
库里面的
我们先分析一下差异,库里面,是通过三者来实现交换,如果string的成员通过库里面的就要开空间,进行深拷贝,要进行三次深拷贝,代价太大。而string里面的只是交换数组指针,size,capacity,没有深拷贝,所以string更好。
void string::swap(string& s) {//如果直接算法里面重载的swap,会进行多次深拷贝,浪费效率。std::swap(_str, s._str);//因为如果我们不指定空间域,std,这个swap会调我们实现,但是只有一个参数,会导致错误,要指定命名空间域std::swap(_size, s._size);std::swap(_capacity, s._capacity);}
因为如果我们不指定空间域,std,这个swap会调我们实现,但是只有一个参数,会导致错误,要指定命名空间域.
为了破局,实现全局函数的swap,如果有人调用库里面的,编译器会首先调用全局函数swap,然后调用string的成员函数swap;