C++初阶——string的模拟实现(上)
一、基本框架
在前两期的内容中,我们已经详细介绍了string
类的使用方法。其实,对于string
的使用,了解string
每个成员函数需要传递的参数,以及它对应的功能即可,这个在相应的网站都有对应的使用说明:link。今天的内容我们要深入探究一下string
类的底层,当然探究的方法有很多种,比如说,我们可以直接查看源代码,但是源代码比较复杂,而且代码风格和我们平时也不大相同,因此本期内容将要带大家进行string的模拟实现,自己写一个string类,并且将它放到命名空间My_String
中。
1.命名空间——My_String
由于C++标准库里面携带了string类,我们需要建立一个命名空间,将我们自己实现的与之分开,就像建立了一堵墙,然后通过展开命名空间或者使用域作用限定符来实现指定访问
2.string类——class string
string类
,是我们的重中之重,我们要先确定有哪些成员变量。我们还是以之前的顺序表为例,在这里处理字符串也一样——将每一个字符依次存入字符数组中,通过对字符数组的增删查改来完成string类对字符串的操作。
(1)成员变量
char* str
:指向字符数组的第一个位置(私有)_size
:类型size_t(无符号整型)表示目前有效字符串长度(私有)_capacity
:size_t(无符号整型)表示容量(私有)npos
:最大值:const static size_t
(静态无符号整型常量)表示最大值(公有,static成员在类外定义)
(2)迭代器
(3)成员函数
- 构造函数
- 析构函数
- 拷贝构造函数
- 运算符重载函数(
<<
、>>
、==
、+=
、=
、<
、>
、
[ ]
……) - 插入删除函数(
push_back
、insert
、append
、assign
、erase
) - 容量处理函数(
reserve
,resize
、clear
)
框架如图所示:
二、string的模拟实现
1、构造函数、拷贝构造函数
如图所示:
(1)构造函数
C++标准库里的string
的构造函数有很多种,这里我们模拟实现其中最为常用的一种。在这里,我们接收的参数是一个字符串,并且提供了缺省值。_size
就是使用strlen
计算一下字符串长度,作为有效值,而第一次初始化,容量capacity
就按照给出的字符串的大小初始化,但是,这并不是真正开辟的空间大小,开辟的空间应该是capacity+1
,为什么呢?由于为了兼容C语言,string类作为处理字符串的类,要和C语言中的字符串相呼应,因此就应该以’\0’作为遍历时找尾的依据,因此需要多开辟一个字符的空间。最后,将传递的字符串str通过memcpy拷贝给_str,当然需要注意拷贝个数是_size+1
,不能忘记'\0'
。
(2)拷贝构造函数
拷贝构造函数传递的是类对象的引用,这里的拷贝涉及到了动态内存开辟,属于深拷贝。为什么不能进行浅拷贝呢?浅拷贝就是简单的值拷贝,对于这里来说,拷贝一下_size和_capacity无伤大雅,因为他们是内置类型,值拷贝就足够了,但是对于char* _str,值拷贝就意味着新初始化的类对象的字符串起始位置和之前的一模一样,在析构的时候,拷贝构造出来的对象和原来的对象都要对同一块空间进行多次析构,程序崩溃,因此,我们必须进行深拷贝,也就是另外开辟一份空间,将字符串拷贝过来。
2.迭代器
代码如下:
在前两期的内容中,我们已经介绍了string
中的迭代器就相当于指针,用于字符串的遍历。对于遍历,分为可修改数据的遍历和只读遍历,就需要我们对char*
和const char*
类型重命名为iterator
和const_iterator
。和迭代器相关的是begin,end函数,返回字符串头一个位置的指针或者最后一个位置的指针即可,有一个需要注意的是,const迭代器版的beginend函数要在形参右侧加上一个const,表示this指针不可修改。
3.一些简单的成员函数
代码如下:
这里介绍了几个简单的函数,交换函数,顾名思义就是要将每个成员变量的值都进行交换,这里处于方便,调用了C++库里的交换函数来实现成员变量的交换;c_str
和size
就不必多言了,分别返回的是字符串首地址和字符串长度。当然这些函数虽然简单,但是通过可以覆用,进一步简化接下来的代码。
4.容量操作函数(reserve、resize、clear)
(1)reserve
代码如图所示:
reserve
分为两种情况,第一种传递的参数n小于目前string类的容量capacity
,此时不需要做任何处理,保持原样即可;如果n大于目前的容量,就需要进扩容。这里采用的依然是异地扩容,然后拷贝字符串,清理原来的空间,和顺序表的扩容方式一样。同样需要注意的是,这里扩容是n+1,要留一个空间给’\0’,它占用的空间不计算在容量内。这里目前还没有重载流插入运算符,先使用库里的代替。
(2)resize
代码如图所示:
resize在n小于目前容量时会做出处理,把超出n的字符去掉,这里的去掉不是清除空间,而是更改’\0’的位置,使得遍历提前结束,并且_size也改为n;当n大于capacity时,会进行扩容,多出来的空间会使用’\0’进行补充。
(3)clear
代码如下:
这里的clear也很简单,就是把代表结束的’\0’提到第一个位置来,_size也改为0即可,并没有涉及到capacity的变化,容量仍然保持不变。
总之,在这三个容量处理的函数中,并没有出现delete进行实质性的缩容,也就是说capacity一般是不会减小的。
5.插入删除函数(push_back、append、insert、erase)
(1)push_back
代码如下:
尾插函数主要涉及到的就是一个扩容问题,这里使用了一个三目运算符,意思是:当容量为0的时候,说明是刚开始插入数据,先开4个字符的空间;如果已经有数据了,容量就扩大到现有容量的两倍。需要注意的是,push_back每次实现的是插入一个字符,size++即可;同样,表示结尾的’\0’也要向后移动一位。
(2)append
代码如下:
先根据传入字符串的长度调整容量,然后在当前字符串的后面将想要追加的字符串拷贝上去即可,注意在最后调整一下_size的大小。
(3)insert
代码如下:
insert也是比较简单的,和顺序表的插入一模一样,先挪动尾部的数据,每个都向后挪一位,最后空出来的指定位置,也就是pos,插入对应的字符即可。
(4)erase
代码如下:
erase是从目标位置的下一个字符开始向前挪一位
6.析构函数
代码如下:
析构就不得不涉及到delete,用来清理资源了;指针也归为空指针,_size和_capacity都赋值为0。
三、本期代码资源
#pragma once
#include<assert.h>
namespace My_String
{
class string
{
public:
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str, _size + 1);
}
string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
typedef char* iterator;
typedef const 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;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
const char* c_str() const
{
return _str;
}
size_t size() const
{
return _size;
}
void reserve(size_t n)
{
if (n > _capacity)
{
std::cout << "reserve()->" << n << std::endl;
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
size_t i = 0;
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
reserve(n);
for (i = 0; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
void push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
memcpy(_str + _size, str, len + 1);
_size = _size + len;
}
void insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n > _capacity)
{
reserve(_size + n);
}
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
size_t i = 0;
for (i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
void 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;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
size_t i = 0;
for (i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
void erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
if (len == npos || pos + len >= _size)
{
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
private:
size_t _size;
size_t _capacity;
char* _str;
public:
const static size_t npos;
};
const size_t string::npos = -1;
}
本期总结+下期预告
本期为大家带来了string的模拟实现,下期内容继续!
感谢大家的关注,我们下期再见!