string是我们在C++中常用得组件之一,这里我们简单的模拟实现一下string中的函数接口,便于对string 的使用和底层有更深入的理解。
构造函数和析构函数
class string
{
private:
size_t _size;
size_t _capacity;
char* _str;
public:
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
};
我们自制一个string至少包含长度,容量和字符串这几个成员,用来描述string,所以分别定义了_size,_capacity,_str来分别描述它们。
_size是string的长度,_capacity是string内部已经开辟空间的数目,_str是字符串。因为string与字符串很像,所以对string的操作底层就是对字符串数组的操作。
上面我们还写了对应的构造函数,来为成员变量赋值。
size(),capacity(),c_str()
获取长度,容量,和字串本身
长度就是字符串的长度,容量是内部开辟空间数量的多少,对于使用来讲不需要关心他具体值是多少,但是因为我们这里是要去简单实现一下string,涉及很多扩容的工作,所以需要使用到这个变量。
这三个函数是对外访问私有成员的接口,直接写个函数返回他们就可以了。
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
const char* c_str()
{
return _str;
}
operator[ ]
他的功能就是使string也像数组一样可以按下标来访问元素。
char& operator[](int pos)
{
assert(pos <= _size);
return _str[pos];
}
const char& operator[](int pos) const
{
assert(pos <= _size);
return _str[pos];
}
可以看到对于[ ] 的重载是很容易的,我们就是使用数组的下标访问。这里要注意的点是对于元素的访问,有时候是仅读的,不需要做修改,有时候不仅要读,而且还要修改,所以这里写了const和非const两种。
iterator迭代器
这里我们迭代器就实现两个功能,返回字串的开始位置和结束位置,直接返回_str的首地址和末尾位置即可。这里我们也是实现了const 和非const版本,const版本迭代器指向的具体内容可以被访问,但是不可被修改。
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;
}
不同容器的迭代器实现方式不同,因为string是顺序存储的,所以在string中,迭代器的底层实现就是指针,但是对于别的非线性存储的容器,迭代器就不是通过指针来实现的了。
不同的容器均具有迭代器,可以说体现了C++封装的思想,使用者不必关心不同迭代器的底层实现,但是他们都有类似的用法。
operator=
这个类似于赋值功能
string& operator=(const string& s)
{
return *this;
}
reserve()
将string扩容到指定大小(这里扩容指内部开辟空间)
void reserve(size_t len)
{
if (len > _capacity)
{
char* tmp = new char[len + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = len ;
}
return;
}
如果指定的值比当前容量小的话,我们不做操作,如果指定值比当前容量大我们才进行扩容工作。先新开辟一段扩容好的空间,然后将之前_str的内容拷贝到新空间中,将原_str的空间释放后,再让他指向新开辟好的空间,这样就完成了扩容,(类似于C语言中的realloc操作,只不过这里需要我们自己写)然后对应成员变量也做对应修改。
这里有一个小心思就是实际扩大后的容量比指定的多一些,也是为了更好的保证数据的安全。
push_back()
尾插一个字符的功能
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
先考虑当前容量是否足够,如果需要扩容就先扩容,然后再进行尾插,对应_size++,将最后一个字符赋值为‘\0’。
append()
也是尾插操作,只不过实现的是尾插一段字符串。
void append(char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size , s);
_size += len;
}
与上文类似,先判断是否需要扩容,然后再进行尾插操作,尾插也很好实现,直接将需要插入的字符串拷贝到原_str末尾即可,记得对_str进行修改。
operator+=
能够实现尾插的功能,因为上面分别实现了尾插一个字符和尾插一段字符串,所以这里就直接使用了。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
insert()
从pos 位置开始,插入字符或者字符串。
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
}
void insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len == _capacity)
{
reserve(_size + len);
}
int end = _size;
while (end >= int(pos))
{
_str[end + len ] = _str[end];
--end;
}
strncpy(_str + pos, str,len);
_size += len;
}
这里分别实现了对字符和字符串的插入操作,因为是插入,所以首先要考虑容量的问题 ,如若容量不足就扩容。主要操作就是从结尾 开始依次往后挪动,直到pos位置,然后将要添加的字符或字符串添加即可。
在代码中有一处强转操作,那是因为Int与size_t类型不匹配,防止出现死循环。int的-1对应着size_t的最大值。
erase()
从pos位置开始,删除len个字符。
npos是一个很大的整数,这里缺省的意思就是不传参默认最大值全删除。
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)//npos是整形最大值
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
如果len大于容量或者为npos时全部删除,否则就从pos位置开始删除。
这里的删除不是真正的删除,算是一种覆盖,可能有一部分数据还在开辟的空间内,但是并不承认他,当新的字符来的时候直接覆盖,就相当于是把他删除了。
swap()
交换两个对象的数据内容,这里我们利用std标准库中的交换来完成我们的交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
find()
从pos 位置开始查找字符ch,或者字符串str,返回的是字符的位置或者字符串的首地址。
size_t find(char ch,int pos = 0)
{
assert(pos <= _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t find(const char* str,int pos = 0)
{
const char* ptr = strstr(_str+pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
先判断输入的pos位置是否合理,npos代表无限大,意思就是找不到。
查找字符直接遍历一遍看第一次在哪出现即可,查找字符串的话我们利用strstr函数,先找到符合条件的字符串的首字符的位置,然后减去原字符串_str的首地址,差就是我们要找的位置。
substr()
返回从pos位置开始,长度为len的一个字符串
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t end = pos + len;
if (len == npos || pos + len >= _size)
{
end = _size;
}
string str;
str.reserve(end - pos);
for (int i = pos; i < end; i++)
{
str += _str[i];
}
return str;
}
判断pos位置是否合理,然后再判断pos之后是否有长度为len个字符,如果超出了已有的长度,则返回pos位置之后的全部字符,否则则返回len长度的字符串。
这里是先开辟一个空间为len的string对象,然后将对应的字符逐个赋值进去,然后返回新开辟的字符串即可。
clear()
对string对象进行清空操作,但不会释放空间
void clear()
{
_size = 0;
_str[0] = '\0';
}
给_str[0]赋值为‘\0’是为了保持一致,因为这里clear操作本质是通过修改当前_size值来完成覆盖,所以给一个结尾。
operator<<
完成对流提取的重载
std::ostream& operator<<(std::ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
注意一般重载为全局
operator>>
完成对流插入的重载
std::istream& operator>>(std::istream& in, string& s)
{
s.string::clear();
char ch=in.get();
while(ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
这里流插入以' ' 和' \n'即空字符和换行作为结尾,而in无法提取出来空字符和换行,所以这里用in.get()来获取字符,然后进入循环来判断。
这里只是对常用的接口进行一个简单的重写以实现与原接口相同或者相似的功能,此文非string底层实现源代码。通过对接口的模拟实现能够提升我们对于string接口的理解和使用,如果大家有更好的模拟实现方法,也希望能够与大家交流分享呀!
以上就是本文的全部内容了,欢迎大家在评论区交流讨论,批评指正。