string类的模拟实现
完整代码:string模拟实现/string模拟实现/string.cpp · swi/c语言 - 码云 - 开源中国 (gitee.com)
class string
{
friend ostream& operator<<(ostream& _cout, const bit::string& s);
friend istream& operator>>(istream& _cin, bit::string& s);
public:
static const int npos;
typedef char* iterator;
typedef const char* const_iterator;
//成员函数
private:
//成员变量
char* _str = nullptr;
size_t _capacity = 0; //容量
size_t _size = 0; //大小,不含\0
};
const int string::npos = -1;
一、默认成员函数
构造函数
string(const char* str = "")
:_size(strlen(str)) //初始化列表先初始化了 _size
{
_capacity = _size; //初始化容量
_str = new char[_capacity + 1]; //开空间
strcpy(_str, str); //拷贝数据
}
注意:_str 多开了一个空间是用来存放 \0 的,缺省参数处的 "" 表示空串。
拷贝构造函数
(1)传统写法
传统写法就是老老实实自己开空间,拷贝数据。
string(const string& s)
{
//初始化容量和大小
_size = s._size;
_capacity = s._capacity;
//开空间,拷贝数据
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
(2)现代写法
用传入参数的 _str 作为参数运行构造函数,让 tmp 帮我们开空间、拷贝数据,然后再让tmp和this交换,如此便实现了this的构造,同时在退出拷贝构造时,tmp会自己调用析构函数。
string(const string& s)
{
//使用默认构造
string tmp(s._str);
//交换两者地址(this与tmp)
swap(tmp);
}
析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
赋值运算符重载
(1)传统写法
传统写法要注意判断 this != &s 否则 delete [ ] 后就无法进行 strcpy(_str,s._str)
string& operator=(const string& s)
{
if(this != &s)
{
_size = s._size;
_capacity = s._capacity;
//因为是赋值,所以原来的数据就不要了
delete[] _str;
_str = new char [_capacity + 1];
strcpy(_str,s._str);
}
return *this;
}
(2)现代写法
现代写法还是让 tmp 帮忙初始化,交换后还能顺便让 tmp 析构不要的数据,且不需要判断 this 等不等于 &s。
string& operator=(const string& s)
{
string tmp(s._str);
swap(tmp);
return *this;
}
二、增删查改
扩容函数
注意,只有 n 大于原来的容量时才扩容,并且扩容不影响大小 _size。
void reserve(size_t n)
{
if (n > _capacity)
{
//更新容量
_capacity = n;
//开空间,拷贝数据
char* tmp = new char[n + 1];
strcpy(tmp, _str);
//回收空间
//赋值,使_str指向tmp申请的空间
delete[] _str;
_str = tmp;
}
}
(1)增
指定位置插入一个字符
将数据往后挪动,腾出一个位置插入新字符即可。但写挪动数据时一定要注意细节,若pos = 0时,因为循环条件是(end > pos),此时如果出现end跑到-1的情况那么循环无法停止。因为 pos 和 end 的类型都是size_t(无符号),而 -1 用二进制表示是32位1,所以导致条件恒成立。
//指定位置插入一个字符
void insert(size_t pos, char ch)
{
assert(pos <= _size);
// 扩容2倍
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//挪动数据
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
//赋值
_str[pos] = ch;
++_size;
}
指定位置插入一串字符串
将数据往后挪动,腾出 len 个位置插入新字符串即可。(因为要腾出 len 个位置,所以挪动数据时要注意处理好映射关系)
//指定位置插入一串字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
//扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
int end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//拷贝数据
strncpy(_str + pos, str, len);
//更新长度
_size += len;
return *this;
}
尾插一个字符串
void append(const char* str)
{
//复用insert函数
insert(_size, str);
}
string& operator+=(const char* str)
{
//insert(_size, str);
append(str);
return *this;
}
尾插一个字符
//尾插
void push_back(char c)
{
//复用insert函数
insert(_size, c);
}
//重载+=
string& operator+=(char c)
{
push_back(c);
return *this;
}
(2)删
string& erase(size_t pos, size_t len = npos)
{
assert(_size > pos);
//len为npos 或者 从pos开始加len的长度
//大于_size就直接删除pos后面所有数据
if (len == npos || _size - pos <= len)
{
_str[pos] = '\0';
_size = pos;
}
else
{
//否则拷贝数据到pos的位置覆盖原数据并更新_size
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
(3)查
在string中查找一个字符(找到返回其下标,反之返回npos)
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (int i = 0; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
在string中查找一个字串 (找到返回其首元素下标,反之返回npos)
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
const char* p = strstr(_str + pos, s);
if (p)
{
return p - _str;
}
else
{
return npos;
}
}
(4)改
返回值的类型为char&,不仅可以减少一次拷贝,还能使其可读可写。
//返回值可读可写
char& operator[](size_t index)
{
assert(index < _size);
return _str[index];
}
//返回值仅可读
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
三、运算符重载
(1)大于、小于、等于......
注意事项:像这类的运算符一般会重载到类外,在类中实现有一个问题,必须以 string类 的对象作为第一个参数。若出现 "abc" == s 这种情况则无法使用。
//下面的运算符重载定义在类外无法直接访问类内的成员变量
//使用c_str()加以辅助(c_str定义在类内)
const char* c_str()const
{
return _str;
}
//因为其本质是字符串,所以使用strcmp函数进行判断
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator>(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
/*——————————————以上为需要现实的部分————————————————*/
/*————————————————————————————————————————————————*/
/*————————————————以下为上面的复用—————————————————*/
bool operator<=(const string& s1, const string& s2)
{
return s1 == s2 || s1 < s2;
}
bool operator>=(const string& s1, const string& s2)
{
return s1 == s2 || s1 > s2;
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
(2)输入流/输出流
依旧是重载在类外,还是跟上面一样的问题,在类内实现第一个参数必须是string类的对象,使用时要写成: s << cout (属于是倒反天罡)。
输入流中使用缓存数组可以避免频繁开辟和销毁空间,减少消耗。(提前开空间,开多了浪费,少了要频繁申请)
//输出流
ostream& operator<<(ostream& _cout, const bit::string& s)
{
for (auto ch : s)
{
cout << ch;
}
return _cout;
}
//输入流
istream& operator>>(istream& _cin, bit::string& s)
{
s.clear();
char ch = cin.get();
//buff 缓存数组,避免频繁扩容(毕竟扩容也是有消耗的)
char buff[128];
int i = 0;
//遇到空格或回车停止
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
ch = cin.get();
//数组满了就写入一次,并重置i
if (i == 127)
{
buff[127] = '\0';
i = 0;
s += buff;
}
}
//判断缓存数组中是否还有未写入的数据
//有则将buff[i]赋值成\0标记尾部,并向s写入数据
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return cin;
}
四、其他功能
(1)begin和end函数
返回起始地址和结束地址,以实现迭代器。
/*typedef char* iterator;
typedef const char* const_iterator;*/
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
(2)getline
获取带空格字符串。
写法与输入流基本无差异,循环的判断条件处去掉了不是空格的判断。
istream& getline(istream& in, string& s)
{
s.clear();
char ch = cin.get();
char buff[128];
int i = 0;
while (ch != '\n')
{
buff[i++] = ch;
ch = cin.get();
if (i == 127)
{
buff[127] = '\0';
i = 0;
s += buff;
}
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return cin;
}
(3)substr
在_str中获取从pos开始到pos+len的子串
string substr(size_t pos = 0, size_t len = npos)
{
string sub;
//pos+len可能会溢出,所以判断条件改为 len >= _size - pos
//满足条件则获取pos到结尾的字串
if (len >= _size - pos)
{
for (size_t i = pos; i < _size; i++)
{
sub += _str[i];
}
}
//否则获取从pos开始到pos+len的子串
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
(4)resize
改变_str的大小
void resize(size_t n, char c = '\0')
{
//n小于原_size,则删除至n,不缩容
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
//否则就扩容至n,并且扩容部分赋值成c
else
{
reserve(n);
for (int i = _size; i < n; i++)
{
_str[i] = c;
}
_size = n;
_str[_size] = '\0';
}
}
(5)部分小功能
//清空(不缩容)
void clear()
{
_size = 0;
_str[0] = '\0';
}
//返回大小
size_t size()const
{
return _size;
}
//返回容量
size_t capacity()const
{
return _capacity;
}
//判断是否为空
bool empty()const
{
return _size == 0;
}