“STL(标准模版库)”是C++必不可少的一个数据结构和软件算法的库,今天我们来模式实现“string”类。
一、string类
1.定义
为了方便,string类可以定义在自己的命名空间里,它的本质相当于一个顺序表,所以有些操作可以用顺序表的经验:
namespace Mynamespace
{
class string
{
char* _str;
size_t _capacity;
size_t _size;
static const size_t npos = -1;
};
}
2.构造函数
string(const char* str = "")
{
_str = new char[strlen(str)+1];
char* der = _str;
const char* sour = str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = strlen(str);
_capacity = _size;
}
在构造函数中,我们使用了缺省参数,在开辟的空间上,默认size和capacity是不包括‘\0’的,但是要开辟给‘\0’的空间,所以需要多new一个字节的空间交给‘\0’,后续的操作就是将输入的字符串拷贝给new出的空间,为了防止打印的时候越界访问,所以需要手动加上‘\0’。为了提高运行效率,可以先开辟一部分空间,避免频繁扩容带来的效率低下的问题。
3.析构函数
~string()
{
delete[] _str;
_capacity = 0;
_size = 0;
}
析构函数的主要作用就是在对象的生命周期结束后释放资源,但是需要注意的是符号的对应(new 和 delete)。
4.拷贝构造
在模拟的string类中,因为涉及到对内存的管理,因此需要实现拷贝构造。
string(const string& s)
{
_str = new char[s._capacity];
char* der = _str;
const char* sour = s._str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = s._size;
_capacity = s._capacity;
}
拷贝构造是构造函数的一个重载,顾名思义,就是为了复制资源到要初始化的对象。
在这里。拷贝构造主要是深拷贝,避免两个对象中的str指针指向同一块内存空间导致析构的时候重复释放内存资源。因此需要重新new一片资源。
4.赋值运算符重载
string& operator=(const string& s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
char* der = _str;
const char* sour = s._str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = s._size;
_capacity = s._capacity;
return *this;
}
赋值运算符重载和拷贝构造相似,但是在用法上有差别。
在这里,我选择重新开辟一块空间用来保存右值的内容。为了避免内存泄漏口,一定要释放原来的空间。并更新size和capacity。
5.iterator迭代器(begin、end)
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
为了能够在范围for使用迭代器,这里将char*给typedef了,其中,“begin”和“end”函数返回的分别是指向首元素和“\0”的指针。
示例:
#include"string.h"
void test()
{
Mynamespace::string mystring1 = "123456789";
for (auto ch : mystring1)
{
cout << ch;
}
}
int main()
{
test();
return 0;
}
运行结果:
6.扩容
void Mynamespace::string::reserve(size_t n)
{
if (n > _capacity)
{
char* flag = new char[n + 1];
char* der = flag;
const char* sour = _str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
delete[] _str;
_str = flag;
_capacity = n;
}
}
由于C++中没有像C语言那样的“realloc”函数,所以需要手动扩容,另开空间,复制,清理原空间。值得注意的是,扩容和以前操作一样,要多开一个空间给‘\0’。不同编译器的操作不一样,这里我们只允许增加,不允许减少。
7.重构数据个数
void Mynamespace::string::resize(size_t n, char c)
{
if (n > _size)
{
reserve(n > _capacity ? n : _capacity);
for (size_t i = _size; i < n; i++)
{
_str[i] = c;
}
_str[n] = '\0';
_size = n;
}
else
{
_size -= n;
_str[_size] = '\0';
}
}
在这里,如果n小于当前数据长度,就把n之后的数据擦除,反之就用“c”补齐数据。
8.判空
bool Mynamespace::string::empty()const
{
return _size == 0;
}
9.返回容量(capacity)
size_t Mynamespace::string::capacity()const
{
return _capacity;
}
10.返回字符个数
size_t Mynamespace::string::size()const
{
return _size;
}
11.尾插一个字符
void Mynamespace::string::push_back(char c)
{
if (_capacity == _size)
{
size_t New = _capacity == 0 ? 4 : _capacity * 2;
reserve(New);
}
_str[_size++] = c;
_str[_size] = '\0';
}
尾插逻辑同顺序表。在增加数据之前一定要判读空间是否够用。
12.追加字符串
void Mynamespace::string::append(const char* str)
{
if (_size + strlen(str) >= _capacity)
{
reserve(_size + strlen(str));
}
char* der = _str + _size;
const char* sour = str;
while (*sour != '\0')
{
*(der++) = *(sour++);
}
*der = '\0';
_size = _size + strlen(str);
}
这个函数的目的是在原来的字符后追加字符串,都是一系列的拷贝过程。
13.清除所有数据
void Mynamespace::string::clear()
{
_size = 0;
_str[0] = '\0';
}
这里将size清零,为了防止字符的输出,这里将字符首元素修改为“\0”。
14.交换数据
void Mynamespace::string::swap(string& s)
{
Mynamespace::string flags = s;
s = *this;
*this = flags;
}
15.返回第一个字符指针
const char* Mynamespace::string::c_str()const
{
return _str;
}
这个操作可以变相的访问私有变量,在有些情况下可以代替友元。
16.返回字符在string中第一次出现的位置
size_t Mynamespace::string::find(char c, size_t pos) const
{
char* flag = _str;
for (size_t i = pos; i < _size; i++)
{
if (flag[i] == c)
{
return i;
}
}
return npos;
}
用一个循环来遍历整个字符串,找到相应字符就返回该字符下标,否则返回“-1”的无符号形式。
17.返回字符串第一次出现的位置
size_t Mynamespace::string::find(const char* s, size_t pos) const
{
const char* ch = s;
size_t beginnum = 0;
for (size_t i = pos; i <= _size - strlen(s); i++)
{
beginnum = i;
for (size_t j = 0; j < strlen(s); j++)
{
if (s[j] != _str[i + j])
{
break;
}
else
{
if (j == strlen(s) - 1)
{
return beginnum;
}
continue;
}
}
}
return npos;
}
这里用了简单粗暴的办法:为了便于理解直接从第一个位置一次对比,找到之后返回它的坐标。两个for循环导致时间复杂度较高。我们还可以复用第16点的函数,可以简化逻辑。
18.在指定位置插入字符
Mynamespace::string& Mynamespace::string::insert(size_t pos, char c)
{
if (_capacity <= _size + 1)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
char* der = &_str[_size];
const char* sour = &_str[_size - 1];
for (size_t i = 0; i < _size - pos; i++)
{
*(der--) = *(sour--);
}
*der = c;
_size++;
_str[_size] = '\0';
return *this;
}
插入单个字符的逻辑顺序同顺序表。写法多样,但要注意不要越界访问修改,越界访问可能会造成析构的时候报错。
19.在指定位置插入字符串
Mynamespace::string& Mynamespace::string::insert(size_t pos, const char* str)
{
for (size_t i = 0; i < strlen(str); i++)
{
insert(pos++, str[i]);
}
return *this;
}
这里复用了在指定位置插入字符的函数。逻辑上没有太大的难度,只需遍历插入的字符串即可。
20.删除指定位置的元素
Mynamespace::string& Mynamespace::string::erase(size_t pos, size_t len)
{
for (size_t i = pos; i < _size; i++)
{
_str[pos++] = _str[i + len];
}
_size -= len;
return *this;
}
逻辑同顺序表,只需将指定位置之后的数据往前覆盖即可,注意“\0”也需要前移。
21.+=运算符重载(字符)
+=一个字符本质上是在末尾加上一个字符,和尾插的逻辑一样,因此可以服用尾插函数
Mynamespace::string& Mynamespace::string::operator+=(char c)
{
push_back(c);
return *this;
}
22.+=运算符重载(字符串)
+=一个字符串本质上是在末尾加上一个字符串,这里可以复用在指定位置插入字符串(或者追加字符串)函数。size指向的位置就是末尾的位置。
Mynamespace::string& Mynamespace::string::operator+=(const char* str)
{
return insert(_size, str);
}
23.[ ]运算符重载
char& Mynamespace::string::operator[](size_t index)
{
return _str[index];
}
const char& Mynamespace::string::operator[](size_t index)const
{
return _str[index];
24.>,<,==,<=,>=,!= 运算符重载
bool operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool operator<=(const string& s)
{
return *this < s || *this == s;
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return !(*this < s);
}
bool operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool operator!=(const string& s)
{
return !(*this == s);
}
这里的函数主要是重载“<,==”的逻辑,使用“strcmp”函数来判断,其他运算符的重载可以根基相关符号的关系来判断。
25.<<运算符重载
ostream& Mynamespace::operator<<(ostream& _cout, const Mynamespace::string& s)
{
_cout << s._str;
return _cout;
}
直接在函数内部输出字符即可,为了实现连续的输出,所以要返回_cout,即左值:“cout”。
26.>>运算符重载
istream& Mynamespace::operator>>(istream& _cin, Mynamespace::string& s)
{
s.clear();
s.reserve(256);
char ch = _cin.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = _cin.get();
}
return _cin;
}
因为我在测试的时候,相同逻辑,使用“>>”取不到‘\0’,‘\n’,因此会死循环,而且“>>”会忽略空格,这导致如果想输入空格字符,用“>>”是输入不进去的,所以使用“cin”的成员函数“get”,它和C语言的getchar一样,可以一次性读取单个字符,其中包括“\0”和“\n”。
为了实现连续的输入,因此需要返回左值:“>>”。