
| 内容 | 网站 |
|---|---|
| 个人主页 | 艾iYYY |
| 文章代码仓库 | string模拟实现 |
前言
我们前面了解了, string的一些基础用法, 现在我们来看看string类的底层到底是什么 .
下面我会带着大家模拟实现一下,主要是实现string类的构造, 拷贝构造, 赋值运算符重载, 以及析构函数.大家可以参考一下.
我们首先可以将代码放到不同文件管理

定义这三个文件, 头文件包含所需头文件和函数,类声明
.cpp文件写对应所需的函数, test.cpp测试写的函数.
模拟实现构造函数析构函数
我们写的函数接口尽量和库里函数接口保持一致, 这样方便我们进行检查
首先我们可以定义一个命名空间在命名空间写我们的类.
cpp
namespace zy
{
class string
{}
}
由于这些构造函数都是短小且频繁调用我们可以, 将直接写在头文件中, 当inline函数使用可以提高效率.
默认构造和字符串构造
cpp
//string s
string()
:_str(new char[1])
,_size(0)
,_capacity(0)
{
_str = '\0';
}
cpp
//string = "hello world"
string(const char* str)
{
size_t size = strlen(str);
_size = size;
_capacity = size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
第一个是默认构造, 第二个是带参构造, 不过这两个也可以写成一个.
cpp
string(const char* str="")
{
size_t size = strlen(str);
_size = size;
_capacity = size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里的""就是'\0'的意思, 需要注意的是, 我们开空间时要给'\0'开一个空间的位置, 但是string规定的,size和capacity的大小都是不包含'\0'的,所以在new空间的时候要new char[_capacity+1].
拷贝构造
cpp
string(const string& s)
{
_capacity = s._capacity;
_size = s._size;
_str = new char[_capacity + 1];
strcpy(_str,s._str);
}
在拷贝构造中, 拷贝对象时, 一定是深拷贝, 而不是浅拷贝.
浅拷贝:_ str=s._ str;
这样写的话, 如果是s1=s2,就是s1和s2共同指向同一块空间

就像上图这样的场景, 当出了s1和s2所在的作用域时, 会调用析构函数, 那么这块空间就会析构两次, 就会出现程序报错.
而且当你改变其中一个对象的内容时, 另一个对象也会接着发生改变.
正确的写法就是, 新开辟一块空间, 然后再将s2的内容拷贝到新开辟的空间中.

这样析构的时候就不会受到影响了.
赋值重载
cpp
string& operator=(const string& s)
{
if (*this != s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
同理赋值重载时同样要进行深拷贝.
析构函数
cpp
~string()
{
delete[]_str;
_size = _capacity = 0;
}
这里的delete\[\] 别忘写\[\] 与前面对应.
string类的遍历的模拟实现
首先是operator\[\] 重载
cpp
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
这样我们就可以通过 \[\] 遍历数组了
迭代器的实现
这里我们以指针的形式实现一下简单的迭代器.
typedef char* iterator;我们在类中typedef一下, 这时iterator就是指针的形式了
这样一来begin和end的迭代器就简单实现了
cpp
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
iterator begin() const
{
return _str;
}
iterator end() const
{
return _str + _size;
}
有了begin和end我们就可以使用范围for来遍历数组了

不过需要注意的是这里的begin和end需要一个字母都不差的写好, 如果Begin这样写就会发生报错. 这侧面也说明了范围for就是傻的进行替换.

string空间和内容的操作
cpp
size_t size()const
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
bool empty()const
{
return _size == 0;
}
这些短小函数也可以直接放在头文件中.
扩容函数
cpp
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1];
strcpy(temp, _str);
delete[]_str;
_str = temp;
_capacity = n;
}
}
扩容时先申请一块空间, 然后再将原字符串的内容拷贝过来, 拷贝完后即可将,原空间释放销毁, 最将新开空间赋值给_str 并跟新一下空间大小.
这里一定注意给'\0' 额外多开一个空间
而且一定是先复制拷贝再销毁原空间
尾插和+=
cpp
void string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
尾插的时候要注意添加末尾的'\0'
cpp
void string::append(const char* str)
{
int len = strlen(str);
if (len > _capacity - _size)
{
reserve(len+_size > 2 * _capacity ? len + _size : _capacity * 2);
}
strcat(_str, str);
_size += len;
}
这里扩容的时候如果插入的字符串长度很大,就不要2倍扩容了, 直接给len+_ size的空间
cpp
string& string::operator+=(char c)
{
this->push_back(c);
return *this;
}
string& string::operator+=(const char* str)
{
this->append(str);
return *this;
}
+=就是对上面函数的复用.
在任意位置插入
cpp
string& string::insert(size_t pos, char c)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (int i = _size+1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = c;
_size++;
return *this;
}
在任意位置后插入需要挪动数据, 挪动时需要注意的时要从后往前的挪动, 不能从前往后会发生数据覆盖.
根据插入一个数据就可以写出插入字符串的函数
cpp
string& string::insert(size_t pos, const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
for (int i = _size + len; i > pos; i--)
{
_str[i] = _str[i-len];
}
for (int i = pos ; i < pos + len; i++)
{
_str[i] = str[i - pos];
}
_size += len;
return *this;
}
整体思路: 计算出需要插入字符串长度, 观察是否需要扩容, 先将pos后的数据往后挪动len个位置, 接着将字符串插入pos位置后, 最后调整_size的大小.
清除数据
cpp
string& string::erase(size_t pos , size_t len )
{
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (int i = pos; i < _size - len; i++)
{
_str[i] = _str[i + len];
}
_size -= len;
_str[_size] = '\0';
}
return *this;
}
整体思路: 先判断给定的len是否大于pos后面的数据个数, 如果大于等于后面就全部删除了, 直接在pos位置加'\0'就行.
如果小于, 需要挪动数据, 要把pos+len后面的数据挪动到, pos位置. 这时在循环中就需要找到两个位置, 一个是pos,一个是pos+len , 很明显pos+len大, 那么循环条件就是pos+len<_size .
现在第一个位置就是i=pos , 第二个就是i+len循环结束条件就是i<_size-len.
清空字符串
cpp
void clear()
{
_str[0]='\0';
_size = 0;
}
查找数据
cpp
size_t string::find(char c, size_t pos) const
{
assert(pos < _size);
for(int i=pos;i<_size;i++)
{
if (_str[i] == c)
{
return i;
}
}
return -1;
}
size_t string::find(const char* s, size_t pos) const
{
char* p = strstr(_str + pos, s);
if (p != nullptr)
{
return _str - p;
}
else
{
return -1;
}
遍历查找
cpp
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 (int i = pos; i < _size; i++)
{
sub += _str[i];
}
return sub;
}
substr可以配合find一起使用,
整体模拟思路: 判断len是否大于_size-pos的大小, 如果大于调整len的大小
定义一个新类, 将pos后的数据插入.
比较大小
cpp
bool operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool operator<=(const string& s)
{
return !operator>(s);
}
bool operator>(const string& s)
{
return strcmp(_str, s._str) > 0;
}
bool operator>=(const string& s)
{
return !operator<(s);
}
bool operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s)
{
return !operator==(s);
}
可以借助strcmp函数实现, 写两个之后就可以复用了.
流插入流提取
cpp
ostream& operator<<(ostream& _cout, const string& s)
{
for (auto c : s)
{
_cout << c;
}
_cout << endl;
return _cout;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
这里的流输入要写成in.get()
-
in.get()读取下一个字符,无论它是不是空白 (空格、换行、制表符等)。所以当遇到空格或换行时,
ch会被赋值为' '或'\n',循环条件立即失败,正常退出。 -
in >> ch则是一个格式化输入 ,它会自动跳过所有前导空白 ,然后才读取一个非空白字符。也就是说,
in >> ch永远不可能把空格或换行读到ch里 。因此
ch永远不等于' '且不等于'\n',循环条件永远为真 → 死循环。
结语
以上就是string基础的模拟实现, 感觉大家观看, 欢迎大家多多讨论,谢谢大家!!!