目录
在前几期我们学习了库中string类相关函数接口的使用,为了更深层次的理解string类,本期我们需要学习string类的模拟实现。
string(const char* str="")
:_str(new char[strlen(str) + 1])
,_size(strlen(str))
,_capacity(_size)
{
strcpy(_str, str);
}
构造函数,给缺省值的目的是为了让字符串有默认的"\0",使得编译器能够识别字符串的结束。
string(string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
string tmp(s._str);
std::swap(_str, tmp._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
创建了对象,交换了中间对象和新创建的对象的成员变量。?
string& operator =(string s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
return *this;
}
形参就是一个中间对象,通过实参对象的值传递进行了拷贝构造生成了中间对象。?
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
对象声明周期结束前先调用析构函数完成资源的清理。资源清理后,编译器再进行对象的销毁,与对象的创建和调用构造函数的顺序刚好相反。?
void reserve(size_t n)
{
if (n > _capacity)
{
//多申请一个空间是为了给'\0'
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
需要多申请一个空间,是为了留给'\0'。
void resize(size_t n,char ch= '\0')
{
if (n <= _size)
{
_str[n] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, ch, n - _size);
_size = n;
_str[_size] = '\0';
}
}
resize扩容时有三种情况,扩容的空间小于原来字符串有效字符的个数,扩容的空间大于原来字符的个数但是小于容量,扩容的空间大于原来字符的容量,后两种的实际操作可以分为同一种。
const_iterator begin()const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
迭代器本质上就是一个指针。 const成员函数const迭代器普通对象和const对象都可以进行访问。
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
普通迭代器只有普通对象可以访问。?
const char* c_str()const
{
return _str;
}
重载流提取之外的输出字符串的方法。?
size_t size()const
{
return _size;
}
什么时候设置为const成员函数什么时候设置成普通成员函数,这取决于我们对字符串的操作,求大小,打印,这些不需要改变结构的可以设置成const,方便const对象去进行访问。
char& operator[] (size_t pos)
{
return _str[pos];
}
size_t find(char ch)
{
for (size_t i = 0; i < _size; ++i)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
npos一个静态的无符号const成员变量,表示整型的最大值,再找不到元素时返回npos。
size_t find(const char* s, size_t pos = 0)
{
const char* ptr = strstr(_str + pos, s);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
因为返回的是下标,所以最终通过指针相减获得了对应字符串的首元素的下标,字符串首元素的下标就是整个字符串的下标,因为字符串是连续存储的。
void push_back(char ch)
{
if (_size = _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
与顺序表类似,因为string类本身就是一个数组,可以理解为它就是一个顺序表。因为刚开始会覆盖'\0',最终一定要在最后一个有效字符的下一位置添加上'\0' 。
void append(const char* s)
{
//先得求出要尾插的字符串的长度,因为此时空间不足的话不能以2倍扩容,因为一个字符串的长度可能远远大于容量
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
需要注意,扩容时不能像以往一样二倍扩容,因为字符串的长度是不能预期的。?
//重载+=,尾插字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//重载+=,尾插字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
本质上就是复用了push_back,append两个函数。?
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
插入字符时与顺序表类似,但是需要注意end的位置,因为pos时无符号整型,所以一般情况下要将end的前一个的位置移动到end位置,而不是把end位置移动到end+1位置,这点当无符号整型判断时要注意。
string& insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, s, len);
return *this;
}
我们要注意一定要使用strncpy函数控制字符的个数,如果使用strcpy会将'\0'拷贝过去,导致字符串异常结束。
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
要注意pos的范围,因为我们一般删除的都是有效字符,所以pos的范围就是有效字符下标的范围。缺省值是npos,所以如果不给定初始值,默认全部删除完。
namespace yjd
{
class string
{
public:
//迭代器
typedef char* iterator;
typedef const char* const_iterator;
//构造函数,给缺省值的目的是为了让字符串有默认的"\0",使得编译器能够识别字符串的结束
string(const char* str="")
:_str(new char[strlen(str) + 1])
,_size(0)
,_capacity(0)
{
strcpy(_str, str);
}
//拷贝构造函数的深拷贝进阶版本
string(string& s)
:_str(nullptr)
,_size(s._size)
,_capacity(s._capacity)
{
string tmp(s._str);
std::swap(_str, tmp._str);
}
//赋值运算符重载的深拷贝进阶版本2
string& operator =(string s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
return *this;
}
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//const迭代器,const对象和普通对象都可以调用调用
const_iterator begin()const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//普通迭代器,普通对象调用
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//以cout<<对象.c_string的方式打印字符串,什么时候设置成const类型的成员变量取决于我们的用法,如果只是打印,对象自然不会更改,所以我们就设置成const成员函数
const char* c_string()const
{
return _str;
}
//求字符串有效字符的个数
size_t size()const
{
return _size;
}
//重载[],从而以数组的形式访问字符串的每个元素
char& operator[] (size_t pos)
{
return _str[pos];
}
//进行空间的扩容,实现reserve的功能
void reserve(size_t n)
{
if (n > _capacity)
{
//多申请一个空间是为了给'\0'
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//进行空间的扩容,并且进行初始化,实现resize的功能
void resize(size_t n,char ch= '\0')
{
if (n <= _size)
{
_str[n] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, ch, n - _size);
_size = n;
_str[_size] = '\0';
}
}
//尾插一个字符,实现push_back的功能
void push_back(char ch)
{
if (_size = _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
//尾插一个字符,实现append的功能
void append(const char* s)
{
//先得求出要尾插的字符串的长度,因为此时空间不足的话不能以2倍扩容,因为一个字符串的长度可能远远大于容量
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
_size += len;
}
//重载+=,尾插字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//重载+=,尾插字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
//实现find,进行字符的查找
size_t find(char ch)
{
for (size_t i = 0; i < _size; ++i)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
//实现find,查找某个字符串
size_t find(const char* s, size_t pos = 0)
{
const char* ptr = strstr(_str + pos, s);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//插入元素,实现insert
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
//实现insert,插入字符串
string& insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + len;
while (end > pos)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, s, len);
return *this;
}
//进行字符串的删除,实现erase
string& erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
//定义静态常变量npos,找不到是就返回npos
static const size_t npos;
};
const size_t string::npos = -1;
以上便是string类常见接口的模拟实现,可以帮助大家对string类更深一步的了解,模拟实现不一定要全部掌握,但是库中的string类中的相关常见接口一定要掌握。
本期内容到此结束^_^