前言
前文我们已经介绍了string类的使用,为了加深我们对于string类的理解,本章我们来模拟实现string类,本文的代码都放在了这里
准备工作:
首先我们为了和C++标准库里面的string
不冲突,我们将我们自己实现的string
定义在一个namespace
命名空间内,并将这个命名空间放进头文件MyString.h
内,我们将我们.c
文件包含这个头文件。
对于string
的成员变量,我们可以用顺序表来进行管理字符串,并配上顺序表容量capacity
与顺序表里面的元素个数size
(capacity与size都只统计有效字符,不包含’\0’)。
因此我们可以先写出下面的代码:
namespace mystring
{
class string
{
public:
string();
~string();
private:
char* _str;//指向存储的字符
int _size;
int _capacity;
static const size_t npos;//静态成员变量
};
const size_t string::npos = -1;
}
我们在用string
类定义对象时主要有两种情况:
①空串
②用一个字符串初始化
因此对于string
的构造函数我们可以使用缺省参数,如果给了字符串,我们就用给的字符串去初始化,如果没有给字符串我们就用空串去初始化。
class string
{
public:
string(const char* str = "");//缺省参数,如果没有给字符串我们就用空串去初始化。
~string();
private:
char* _str;
int _size;
int _capacity;
};
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;//第一次如果是空串的话,默认开3个char类型的空间
char* tmp = new char[_capacity + 1];//多申请一个用于存放'\0'
_str = tmp;
strcpy(_str, str);//strcpy拷贝时默认会将'\0'拷贝进目标位置中
}
注意:空串时不能给nullptr
代替""
,因为如果空串时给nullptr,那么在打印时会发生空指针解引用!同理也不能给'\0'
因为会发生隐式类型转化,转换成了nullptr
string类的析构函数很简单,我们只需要释放申请的空间,然后将_str
置空就行了。
string::~string()
{
delete[] _str;
_str = nullptr;
}
对于string的拷贝构造,我们实现起来并不难,我们只需要申请和传入对象一样大的空间,然后将字符串拷贝进新申请的空间,然后将capacity
size
依此赋值进行了。
代码如下:
string::string(const string& s)
{
char* tmp = new char[s._capacity + 1];//多申请一个用于存放'\0'
_str = tmp;
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
由于string对象涉及资源管理,如果我们不写赋值重载函数,编译器就会帮我们生成一个按字节拷贝的赋值重载函数,如果是按字节拷贝的话,那么在内存上我们两个对象的_str
指针会指向同一块空间,在我们程序运行结束时,就会导致同一个地址析构两次,从而造成程序奔溃!
那么我们应该怎么写赋值重载函数呢?
对于string对象的size
和capacity
我们可以直接采取直接赋值,对于_str
指针,我们在赋值时可能有以下几种情况:
如果我们按照上面的方法进行_str
的赋值的话,会导致情况很多,也很复杂,同时也可能会有空间的浪费,因此上面的实现方法并不是一个好的方法,我们可以换一种思路:
我们可以先直接申请一个和s2中_str
指向的字符串一样的大小空间,然后直接释放掉s1中的原有的旧空间,然后让s1中_str
指向新申请的空间,然后直接进行字符串拷贝就行了,这样我们就不用考虑空间不够的问题了,也不用担心空间浪费的问题了。
具体代码如下:
string& string::operator=(const string& s)
{
if (this != &s) // 防止自己给自己赋值
{
char* tmp = new char[s._capacity + 1];
delete[] _str;
_str = tmp;
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
c_str函数的实现并不困难,我们只需要返回string成员变量中的 char *_str
指针就行了。
const char* string::c_str()const //加入const为了让const对象与非const对象都能使用
{
return _str;
}
size与capacity函数就是返回当前对象的元素个数与空间容量。
size_t string::size()const
{
return _size;
}
size_t string::capacity() const
{
return _capacity;
}
对于string类的迭代器,我们可以用指针去模拟,用指针指向字符的地址,解引用拿到地址里面的数据。
typedef char* iterator; //用指针去模拟迭代器
//迭代器的首地址
string::iterator string::begin()
{
return _str;
}
//迭代器的尾地址
string::iterator string::end()
{
return (_str + _size);
}
对于const
类型的迭代器,我们可以采用同样的方法进行模拟,const迭代器只不过是指针指向的内容不能修改,但是指针可以改变指向。
typedef const char* const_iterator;
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return (_str + _size);
}
reserve
函数的主要功能就是帮助我们的string类申请空间,但是切记,reserve
函数不能进行缩容,因为缩容是有代价的,而且可能我们后续使用string时还可能因为空间不够需要扩容,因此我们一般不缩容。
我们的实现原理,也是先申请新空间,新空间申请出来后,将原有的数据拷贝进新空间,然后释放旧空间,让指针指向新空间。
实际代码:
void string::reserve(size_t capacity)
{
//防止出现缩容,如果新的空间大小小于原有的空间大小,则什么都不做
if (capacity > _capacity)
{
char* tmp = new char[capacity + 1];//多申请一个用于存放'\0'
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = capacity;
}
}
resize
函数也不会缩容,在新空间小于原有的有效字符串时,只会保留前面的的字符,但并不缩容,在新空间大于原有的有效字符个数时就会对后面的空间进行初始化。
void resize(size_t capacity, char ch = '\0');//半缺省
void string::resize(size_t capacity, char ch)
{
//新空间小于原有的有效字符串时
if (capacity <= _size)
{
_str[capacity] = '\0';
_size = capacity;
}
else
{
//新空间大于原有空间时要进行扩容
if (capacity > _capacity)
{
reserve(capacity);
}
//对有效字符串后面的空间进行初始化
size_t begin = _size;
while (begin < capacity)
{
_str[begin++] = ch;
}
//最后一个位置放上'\0',代表字符串结束
_str[capacity] = '\0';
_size = capacity;
}
}
push_back
函数主要用来尾插一个字符,实现这个函数我们只需要先判断空间是否足够,然后在_size
位置插入字符,最后再在后面补上\0
就行了。
void string::push_back(char ch)
{
//判断是否需要扩容
if (_size + 1 > _capacity)
{
string::reserve(_capacity * 2);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
append
函数主要是用来在尾部尾插一段字符串,对于它的实现与push_back
类似。
void string::append(const char* str)
{
size_t length = strlen(str);
//判断是否需要扩容
if (_size + length > _capacity)
{
string::reserve(_size + length);
}
strcpy(_str + _size, str);
_size += length ;
}
insert
函数有两个版本,一个是插入字符版本,一个是插入字符串版本,这两个版本构成函数重载!
我们先来看字符版本:我们可以先判断是否需要扩容,然后根据要插入的位置,将要插入位置后面的所有元素集体向后移动,等pos位置也挪动完了,我们就插入数据。
void string::insert(size_t pos, char ch)
{
assert(pos < _size);
//判断空间是否足够
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
//end表示要移动到的位置
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
}
明白了字符版本后我们再来看字符串版本:字符串版本与字符版本大体思路一致,只不过我们一次移动的长度更长了。
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t length = strlen(str);
//判断是否需要扩容
if (_size + length > _capacity)
{
reserve(_size + length);
}
//end表示要移动到的位置
size_t end = _size + length;
while (end > pos + length - 1)
{
_str[end] = _str[end - length];
--end;
}
strncpy(_str + pos, str, length);
//重新设置_size
_size += length;
}
erase
函数主要用来删除字符串,我们可以先判断我们要删除的起始pos位置后面有多少个字符,如果字符个数能够满足要删除的个数,那就用后面的元素覆盖前面要删除的元素就行了,如果字符个数不够,我们直接在pos位置放上\0
就行了。
void erase(size_t pos, size_t len = npos);
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
//pos后面的元素个数
int true_length = _size - pos;
if (len < true_length)
{
true_length = len;
}
//要放入的位置
int put_pos = pos;
//要移动的位置
int move_pos = put_pos + true_length;
//当move_pos == _size时,搬运的是 '\0'
while (move_pos <= _size)
{
_str[put_pos++] = _str[move_pos++];
}
//重新设置_size
_size -= true_length;
}
clear
函数主要用来清空字符串,实现起来也是非常简单。
void string::clear()
{
_str[0] = '\0';
//重新设置_size
_size = 0;
}
对于operator[]函数,我们可以直接返回_str[]位置的引用就行了。
//非const对象调用
char& string::operator[](size_t pos)
{
assert(pos >= 0 && pos <= _size);//防止发生数组越界
return _str[pos];
}
//const对象调用
char string::operator[](size_t pos) const
{
assert(pos >= 0 && pos <= _size);
return _str[pos];
}
对于比较类的运算符重载函数,我们可以实现>
==
或<
==
的运算符重载函数然后将剩下要实现的运算符重载函数复用我们上面实现的两个运算符重载函数,这样可以提高代码的可维护性。
//加入const修饰,让const对象与非const对象都能使用
// > 运算符
bool string::operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
// == 运算符
bool string::operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
// >= 运算符
bool string::operator>=(const string& s) const
{
return (*this > s || *this == s);
}
// < 运算符
bool string::operator<(const string& s) const
{
return !(*this >= s);
}
// <= 运算符
bool string::operator<=(const string& s) const
{
return !(*this > s);
}
// != 运算符
bool string::operator!=(const string& s) const
{
return !(*this == s);
}
利用上面实现的push_back
函数和append
函数我们能够实现operator+=
函数的重载。
字符版本
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
字符串版本
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
find
函数主要用来匹配字符或字符串,也是两个版本。
字符版本:我们可以直接采用遍历查看是否匹配
size_t string::find(char ch, size_t pos)
{
assert(pos <= _size);
for (size_t i = pos; i <= _size; ++i)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
字符串版本:我们可以直接采用库函数strstr
查看是否匹配
size_t string::find(const char* str, size_t pos)
{
char* pstr = strstr(_str, str);
if (pstr == nullptr)
{
return npos;
}
return pstr - _str;
}
流提取运算符对于string对象就是打印对象里面的内容,不同于c_str
函数的是,利用c_str
函数打印的字符串是遇到\0
终止,利用流提取运算符打印的字符串是根据_size
的个数来打印的,即使遇到\0
也不会停止。
//string的流提取
ostream& operator<<(ostream& out, const string& s)
{
for (auto& e : s)
{
out << e;
}
return out;
}
流提取运算符的实现,我们可以利用ostream
里面的get
函数从缓冲区中拿到每一个元素,然后赋值给我们想赋值的对象就行了。
//string的流插入
istream& operator>>(istream& in, string& s)
{
//先清空原有的字符串
s.clear();
char ch;
int i = 0;
//定义一个小的缓冲区,缓冲区满了再赋值给对象s,防止频繁扩容
char buffer[128];
ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
buffer[i++] = ch;
ch = in.get();
}
if (i != 0)
{
buffer[i] = '\0';
s += buffer;
}
return in;
}