目录
前言
构造函数
析构函数
迭代器
拷贝构造和赋值
深浅拷贝问题
传统写法
现代写法
插入函数
reserve()
push_back()
append()
+=操作
insert()
erase()
流插入和流提取
流插入
流提取
运算符重载
其它函数实现
[]重载
resize()
find()
substr()
完整代码展示
前言
本文通过对STL中的string进行模拟实现来了解string的底层实现,我们模拟实现的目的并不是为了写出一个更好的string,而是为了学习这底层的基本实现,实际上库里面实现的细节比这复杂,还得是大佬。
关于string的基本用法可以参考这个网站cplusplus.com/reference/string/string/
构造函数
在实现构造函数时,我们可以实现一个无参的构造和一个有参的构造,但是对于无参的构造我们可以省略,而直接在有参中使用缺省值代替无参的情况。由于在对无参的对象进行初始化时,还要考虑加上'\0',所以我们的缺省值用一个空串代替即可。在使用初始化列表时,考虑到初始化列表的初始化顺序需要和变量声明的顺序一致,在我们后序想要加一些变量时不太方便,所以这里就没有使用初始化列表。
//构造函数
MyString(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);
}
当使用strlen函数给成员变量初始化时,我们只需要给一个成员变量使用strlen,其余的使用其赋值即可,因为每次使用strlen也是O(N)的消耗的。在我们初始化new一块空间时必需还考虑到末尾的'\0',也就是上述代码中_capacity + 1的原因。还有别忘了初始化完后的最后一步进行拷贝。
析构函数
//析构函数
~MyString()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
对new出来的空间需要delete掉,并且置空,防止野指针。
迭代器
迭代器还是很好实现的,不要被吓到了,它其实是个纸老虎,本质是一个char*的指针
//迭代器
typedef char* iterator;
typedef char* const_iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}const_iterator begin()const
{return _str;
}const_iterator end()const
{return _str + _size;
}
拷贝构造和赋值
深浅拷贝问题
如果我们不写拷贝构造,编译器会默认帮我们生成一个,但是编译器帮我们生成的是一个浅拷贝,例如在下图中,当用s构造s1时,编译器会调用默认的拷贝构造,最终导致的问题是,s和s1共用一块内存空间,在调用析构函数时,同一块空间会被释放两次而引起程序崩溃。这种就是浅拷贝带来的问题。
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
那么要解决这个问题就要用到深拷贝,也就是要开辟一块新空间,让s和s1都有属于自己的一块空间。
深拷贝:给每个对象独立分配资源,保证多个对象之间不会因为引共享资源而造成多次释放带来的程序崩溃问题。
传统写法
//拷贝构造——传统写法MyString(const MyString& s):_str(new char[s._capacity + 1]),_size(s._size),_capacity(s._capacity){strcpy(_str, s._str);}//赋值 s = s2——传统写法MyString& operator=(const MyString& 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;}return *this;}
在拷贝构造中我们需要开辟一块空间,之后调用strcpy将内容拷到新开的空间中去即可。在开辟空间时始终记得+1留给'\0'。
在传统写法中实现赋值时要先判断一下自己赋值给自己的情况(不然将空间delete掉之后就会使程序崩溃),并且对于原来的空间需要delete掉之后重新指向新开好的空间。
现代写法
void swap(MyString& tmp){::swap(_str, tmp._str);::swap(_size, tmp._size);::swap(_capacity, tmp._capacity);}//拷贝构造——现代写法MyString(const MyString& s):_str(nullptr), _size(0), _capacity(0){MyString tmp(s._str);swap(tmp);}//赋值 s = s2——现代写法MyString& operator=(const MyString& s){MyString tmp(s);swap(tmp);return *this;}
传统写法和现代写法效率上差不多,但是对比与传统写法,现代写法显得更简便,定义出来的tmp会调用构造函数来初始化,然后tmp就像打工人一样,将自己辛苦打拼的成果全交给了老板。其实上面的赋值还能在简化,把参数也考虑进去。
MyString& operator=(MyString s)
{swap(s);return *this;
}
插入函数
reserve()
在插入数据时要考虑到空间够不够的问题,所以先来实现一下reserve(),其实实现起来也很简单,当空间不够了就要重新开一块空间,开好后把原来的内容拷贝到新开好的空间中去,然后再释放掉原来的空间,最后再修改_capacity大小。
void reserve(size_t n)
{if (n > _capacity){char* tmp = new char[n + 1];//'\0'strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}
push_back()
void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';//一定要把'\0'加上}
push_back的实现和链表就很像了,插入时先判断一下空间够不够,不够就要扩容(要考虑一开始没有给初始值的情况),最后不要忘了加上'\0'。
append()
void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;}void append(const MyString& s){append(s._str);}void append(size_t n, char ch){reserve(_size + n);for (int i = 0; i < n; i++){push_back(ch);}}
append和push_back类似,但是要注意的时扩容时要把插入的字符串的长度考虑进去,然后在尾部插入字符串,最后再修改一下_size大小。
+=操作
MyString& operator+=(const char* str)
{append(str);return *this;
}
insert()
MyString& 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++;return *this;}MyString& 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){_str[end] = _str[end - len];end--;}strncpy(_str + pos, str, len);_size += len;return *this;}
这里的两个insert构成重载,先来讲解第一个在任意位置插入一个字符
比如下图中在 l 位置插入一个o,先要判断一下这块空间的大小支不支持我们插入,不支持的话就要扩容。显然下面的空间不支持我们插入,那么就需要我们扩容了。
扩完容后我们要挪动数据,将pos位置空出来,以便让我们插入,当我们挪动数据时要从后往前挪动,防止我们的数据被覆盖掉。如下所示
那么在任意位置插入字符串也是类似的,在插入是要先判断一下空间够不够,这里的判断机制和append一样,要把待插入字符串的长度考虑进去。之后挪动数据时也要将待插入字符串的长度考虑进去(end == _size + len这样才能空出len个位置出来),将位置空出来以便将待插入字符串插入进去。
在这里我为了方便就没把'\0'画出来了
在插入字符串时,我们可以用个strncpy就不用我们手动再去插入了。
最后不要忘了修改size的大小
insert实现完后,上面的push_back和append只要调用,就能实现相应的效果。
void push_back(char ch)
{insert(_size, ch);
}void append(const char* str)
{insert(_size, str);
}
erase()
在库里面的erase还有个npos,其实npos的本质就是个-1,我们在自己实现的string类中加上即可。注意静态变量加上了const之后就可以在类中初始化啦,这是C++给做的特殊处理。
void erase(size_t pos, size_t len = npos)
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}
}
在erase中我们要注意,如果len的大小超过了[pos,end]这个区间的大小或者pos==npos,我们直接在pos位置加上'\0'就行了,否则我们调用strcpy将数据覆盖掉就行了,不过要注意拷贝的范围问题。最后修改一下size就行了。
流插入和流提取
流插入
ostream& operator<<(ostream& out, const MyString& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
流插入中其实还是调用了库里面的内置类型,ostream里面就实现了,将字符一个一个插入进ostream,这个字符串有多长就会输出多长。
流提取
在流提取中,对于已经有数据的应该先进行一个清理的操作,那么很容易就会写出下面的代码
void clear(){_str[0] = '\0';_size = 0;}
istream& operator>>(istream & in, MyString & s){char ch;in >> ch;while (ch != ' ' && ch != '\n'){s += ch;in >> ch;}return in;}
这样写就会导致一个问题,这个循环不会停下来,因为它会把空格和换行当成是分隔符,这样就陷入了死循环
解决方法:用库里面的in.get()函数来对字符进行提取就能解决这个问题
效率问题:一个一个的字符插入,空间不够了就要扩容,这样就要扩容好多次,非常影响效率。
改进:我们可以用个类似于缓冲区的方式,先将提取到的字符弄进缓冲区,如果这个缓冲区满了,再一次性将缓冲区中的数据输入到instream中,这样就能有效解决了扩容带来的效率问题
istream& operator>>(istream& in, MyString& s)
{s.clear();char ch;ch = in.get();const size_t N = 64;char buff[N];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == N - 1){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}buff[i] = '\0';s += buff;return in;
}
运算符重载
这里只要实现了两个就行,让其它运算符复用即可。偷个懒,使用strcmp函数就能完成如下操作。
bool operator > (const mystring& s)const{return strcmp(_str, s._str) > 0;}bool operator == (const mystring& s)const{return strcmp(_str, s._str) == 0;}bool operator >= (const mystring& s)const{return *this > s || *this == s;}bool operator <= (const mystring& s)const{return !(*this > s);}bool operator < (const mystring& s)const{return !(*this >= s);}bool operator != (const mystring& s)const{return !(*this == s);}
其它函数实现
[]重载
重载了[]就可以支持下标访问了
const char& operator[](size_t pos)const{assert(pos < _size);return _str[pos];}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}
resize()
主要是实现对开辟的空间进行初始化,要注意的是如果需要开辟的容量小于原来的,那么就需要删除数据,删除数据的操作也很简单,直接在末尾加'\0' ,如果需要开辟的容量大于原来的,那么就需要插入数据给这块新空间中未被用到的做初始化
void resize(size_t n, char ch = '\0'){if (n > _size){//插入数据reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_str[n] = '\0';}else{//删除数据_str[n] = '\0';_size = n;}}
find()
size_t find(char ch, size_t pos = 0)const{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (ch == _str[i]){return i;}}return npos;}size_t find(const char* sub, size_t pos = 0)const{assert(sub);assert(pos < _size);const char* ptr = strstr(_str + pos, sub);if (ptr == nullptr){return npos;}else{return ptr - _str;//通过指针-指针的方式来获取下标}}
substr()
substr用来提取子串,在实现的过程中,需要对提取的真实长度进行判断,如果len太大直接将后面的所有字符串提取完。
MyString substr(size_t pos, size_t len = npos)const{assert(pos < _size);size_t realLen = len;if (len == npos || len + pos >= _size){realLen = _size - pos;}MyString str;for (size_t i = 0; i < realLen; i++){str += _str[i + pos];}return str;}
库里面还有很多函数如果有兴趣可以自己去动手实现一下哦
完整代码展示
string的模拟实现
今天的分享就到这里,如果有写的不好或者不对的地方,还望指出,谢谢!!!