sting模拟实现
一、string的定义
在 C++ 中,sting是标准库提供的一个字符串类,用于表示和操作字符序列。它属于 std 命名空间,定义在 <string>
头文件中,本质上是模板类。
string文档链接:string - C++ Reference (cplusplus.com)
二、常见接口说明及实现
上面截取了string部分接口,由于历史设计原因string的设计显得并不简洁,对于其中部分接口我们查看文档心里大概知道有这么个接口就行,实在需要使用的时候再来查文档。
但对于其中使用频率较高的,我们就得记下。
sting类的成员变量的设计像C语言顺序表一样,由地址、有效元素个数、容器空间组成。
另外,为了防止我们自己实现的string跟库里的发生冲突,我们需要在自己的命名空间里实现string类。
namespace A
{class string{private:char* _str;size_t _size;size_t _capacity;};
}
接下来实现string常见的接口:
1、reserve
文档说明:
如上所述,reserve接口能将string中的capacity更改至参数n大小。
如果n大于capacity,进行扩容;反之C++并没有进行严格要求,也就是说存在capacity比n大的情况。
大多数编译器当n小于capacity时都不会选择缩容,一是缩容代价很大(性能损耗,主要原因)、二是C++标准灵活、三是存在功能替代接口shrink_to_fit()。
下面是代码实现。
void string::reserve(size_t n) {if (_capacity < n){_capacity = n;char* new_str = new char [n + 1];memcpy(new_str, _str,_size+1);delete[]_str;_str = new_str;}}
需要注意的是,string类型后面都会有终止符'\0',拷贝和开辟空间的时候不要忘记。
2、push_back
文档说明如下:
在数组后面尾插一个字符,当空间不够时注意扩容。
扩容操作可以复用上面实现过的reserve接口。
void string::push_back(const char c){if (_size >= _capacity){int new_capcity = _capacity == 0 ? 4 : 2 * _capacity;reserve(new_capcity);}_str[_size++] = c;_str[_size] = '\0';}
3、insert
insert接口看起来可多了,我们目前实现第一种在指定位置插入,之后的元素依次往后挪。
void string::insert(size_t n, const char s){assert(n <= _size);if (_capacity <= _size){int new_capacity = _capacity == 0 ? 4 : _capacity * 2;//注意数据迁移reserve(new_capacity);}//注意类型提升!!如果不强转的话int会类型提升为unsigned!!//当然这里使用for循环,这里没有这个问题for (int i = _size; i > n; i--){_str[i] = _str[i - 1];}_str[n] = s;_size++;_str[_size] = '\0';//稍微注意一下}
需要注意的是,由于_size为size_t类型,int类型参与其运算时可能会产生整形提升变成unsigned类型,造成下面这样的错误
int i=_size;
while(i<n)
{//…………n--;
}
当n=0实现头插时,因为i整型提升为无符号类型从而出现死循环。
4、size
直接返回_size.
5、pop_back
尾删直接将最后一个元素置为终止符,以及更新_size.
void string::pop_back(){assert(_size > 0);_size --;_str[_size] = '\0';//还是要注意下标从0开始}
6、find
find可分为两种,一种查找字符第一次出现的位置,第二种则是查找字符串第一次出现的位置。
pos参数是开始查找的位置,默认缺省值为0(这里声明定义分离看不到缺省值)。
第一种需要从头开始遍历字符串:
size_t string::find(const char c, size_t pos){assert(pos <= _size);for (int i = pos; i < _size; i++){if (_str[i] == c) return i;}return npos;}
第二种可以直接借助库里的查找子串函数strstr:
size_t string::find(const char* c, size_t pos){assert(pos <= _size);size_t len = strlen(c);const char* p = strstr(_str + pos, c);//调用库里面的查找字串函数if (p == nullptr) return npos;else return p - _str;}
7、erase
我们只看第一个版本,也就是删除当前位置及其之后len长度的数据
void string::erase(size_t pos, size_t len){if (len == npos || _size - pos <= len){_size = pos;_str[pos] = '\0';}else{size_t i = pos + len;memmove(_str + pos, _str + i, _size - i+1);//将'\0'一起移过去_size -= len;//更新_size}}
这里我们使用了memmove而非是memcpy,因为else情况存在内存重叠问题!如果使用memcpy很可能造成复制内容不准确。
memmve与memcpy的区别:
memcpy实现更简单直接,按顺序从源地址复制到目标地址,效率略高。
memmove会先判断内存是否重叠,若重叠则采用从后向前的复制方式,避免数据覆盖问题,因此实现稍复杂,效率略低。
8、getline
getline是将字符一个一个的读入str中,直到遇到字符delim为止。
getline中关于流方面的知识我们目前先不用管,看成固定模板先。
我们在使用sting进行常规的写入时存在空格无法读入的情况,为此库中给出getline这一函数。
getline实现的常规思路就是将读到的字符一个一个+=在string对象中,但这样会频繁调用+=运算符重载(后面讲)导致效率不高。
此时我们仍旧可以使用空间换时间的思路,将读入的字符暂时存放在一个固定大小的数组里,一旦当数组空间满了或者遇到指定字符delime就进行+=操作。
std::istream& getline(std::istream& is, string& s, char ch)
{s.clear();//使用前清理下s,防止被污染char arr[120];//数组大小可以直接随便搞,开这么多也够用int i = 0;char a = is.get(); while (a != ch){arr[i++] = a;if (i == 119){arr[i] = '\0';s += arr;i = 0;}a = is.get();}if (i > 0)//判断是否还剩未插入内容{arr[i] = '\0';s += arr;}return is;
}
另外,getline并不属于string,但它包含在我们创建的名字空间中。当我们在头文件声明该函数时需要注意一下不要写在string类中。
9、clear
清除空间只需要将终止符'\0'放到数组头部即可,不用进行额外的删除。
别忘了将_size置为0。
void string::clear(){_str[0] = '\0';_size = 0;}
10、append
append我们只看它第一个模板,也就是直接在后面加入一串字符。实现与push_back大体上差不多,注意扩容即可。
void string::append(const char* ch){size_t len = strlen(ch);if(_size+len>_capacity){int new_capacity = _capacity * 2 > _size + len ? 2 * _capacity : _size + len;reserve(new_capacity);}memcpy(_str + _size, ch,len+1);_size += len;}
11、substr
substr返回一个新构造的string对象,并且新对象以旧对象从下标pos位置开始的len个元素初始化。
string string::substr(size_t pos, size_t len){if (len == npos || len + pos >= _size){len = _size - pos;}string str;for (size_t i = 0; i < len; i++){str += _str[i + pos];}return str;}
但需要注意的是,我们返回的是由str的拷贝构造产生的临时对象,如果我们不写将会调用系统生成的浅拷贝构造。
这一浅拷贝就出问题了,由于函数结束后其内部的str会销毁,导致其返回对象实际上已经销毁!
为了解决浅拷贝造成的问题,我们需要手动创建合适的拷贝构造而不是使用系统给的默认拷贝构造。
三、string类的默认成员函数
1、拷贝构造
在上面我们已经直到string类不能进行浅拷贝构造,需要我们自己实现一个深拷贝构造。
string::string(const string& str){//旧方法//_str = new char[str._capacity+1];//memcpy(_str, str._str, str._size + 1);//_size = str._size;//_capacity = str._capacity;//现代方法,更简洁效率不提升string tmp(str._str);swap(tmp);}void string::swap(string& s){std::swap(_size, s._size);std::swap(_capacity, s._capacity);std::swap(_str, s._str);}
实现方法有新旧两种方法:旧方法直接老老实实的开辟新空间并将数据一个个拷贝过去,现代方法则是利用要拷贝的内容重新构造一个tmp对象,完成之后再将tmp对象与要拷贝得出的对象进行自己创建的swap交换就可以完成。
2、构造与析构
这个简单,直接上代码。
string::string(const char* str):_size(strlen(str)){_capacity = _size;_str = new char[_size + 1];memcpy(_str, str,_size+1);}string::~string(){_size = 0;_capacity = 0;delete[]_str;}
3、拷贝赋值重载
赋值重载也需要进行深拷贝,实现方法与上面的拷贝构造一样。
string& string::operator=(const string& str){if (*this != str)//判断是否是自己赋值给自己{//现代方法string tmp(str._str);swap(tmp);}return *this;}
三、运算符重载
1、+=运算
+=运算就是在原来的对象末尾插入其它其它字符或字符串。
对于插入字符我们可以复用push_back实现,对于插入字符串则复用append实现。
string& string::operator+=(char str){push_back(str);return *this;}string& string::operator+=(const char* str){append(str);return *this;}
2、比较运算符
大于、大于等于、小于……这一系列的比较运算符其实很好重载。
不过在真正实现的时候,我们仅需要实现小于(或大于)和判断是两个即可,通过复用这两个重载比较运算可以实现其它的比较运算。
//以字典序为例bool string::operator<(const string& str)const{size_t i1 = 0, i2 = 0;while (i1 < _size && i2 < str._size){if (_str[i1] < str[i2]){return true;}else if (_str[i1] > str[i2]){return false;}else if (_str[i1] == str[i2]){i1++; i2++;}}return i2<str._size;}bool string::operator==(const string& str)const{size_t i1 = 0, i2 = 0;while (i1 < _size && i2 < str._size){if (_str[i1] != str[i2]){return false;}else{i1++; i2++;}}return i1 == _size && i2 == str._size;}
bool string::operator<=(const string& str)const
{return (*this) < str || (*this) == str;
}bool string::operator>(const string& str)const
{return !((*this) <= str);
}bool string::operator>=(const string& str)const
{return !((*this)<str);
}bool string::operator!=(const string& str)const
{return !((*this) == str);
}
3、流操作运算符
流这一概念目前我们尚不了解,但实现过程也并不复杂,可暂时当个模板记下。
std::ostream& operator<<(std::ostream& out, const string& str){for (size_t i = 0; i < str.size(); i++){out << str[i];}return out;}
std::istream& operator>>(std::istream& in, string& str){//清理str中的缓存str.clear();char arr[120];int i = 0;char s=in.get();//in >> s; //在istream中流提取会将空格和换行视为分隔符且进行忽略,//故>>读不到看空格和换行!下面就死循环//所以需要使用get获取字符while (s != ' ' && s != '\n'){arr[i++] = s;if (i == 119){arr[i] = '\0';//在 C++ 中,只有字符串字面值初始化的字符数组会自动在末尾添加 \0//其他类型的数组(包括普通字符数组的手动初始化)不会自动添加 \0。//故需要手动添加str += arr;i = 0;}//str += s;//in >> s;s = in.get();}if (i > 0){arr[i] = '\0';str += arr;}return in;}