目录
前言
本篇文章将要来讲述string的模拟实现,对于STL的容器,我们不仅要会用,还要了解他的底层
所以我们有必要去模拟实现一下,那我们接下来就开始模拟实现啦
一、string的结构
模拟实现的时候为了跟库里的区分开,所以要加上命名空间,这样还有一点好处就是随时可以做到跟库里去对比,验证我们实现的对不对
string底层是一个char类型的顺序表,所以结构也和顺序表类似
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
_str指向存储数据的空间
_size表示有效数据的元素个数
_capacity表示当前最大的存储容量
二、默认成员函数
构造函数
构造函数呢用一个字符串来初始化,同时还有一个默认构造,也就是空串,所以我们可以用缺省参数来给值
string(const char* str = "")
: _str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(strlen(str))
{
memcpy(_str, str, _size + 1);
}
这里开空间的时候需要多开一个空间给\0,同理memcpy拷贝的时候也需要多带一个\0
但是这里的效率是并不好的,strlen是一个O(N)的接口,在这里算了三次,我们算一个_size即可,剩下两个直接来使用。
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_size + 1])
{
memcpy(_str, str, _size + 1);
}
注意:这样写还会出现一个问题,就是成员变量声明的顺序就是他在初始化列表中初始化的顺序,会先初始化_str,这时候_size还是一个随机值,开空间就开出问题了,那应该调换一下成员变量声明的顺序,最终就是如下的方式:
class string
{
public:
string(const char* str = "")
: _size(strlen(str))
, _capacity(_size)
, _str(new char[_size + 1])
{
memcpy(_str, str, _size + 1);
}
private:
size_t _size;
size_t _capacity;
char* _str;
};
析构函数
析构函数负责来释放空间,new[]申请的,需要delete[]来释放,然后把指针置空,同时把
_size和_capacity变成0
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
拷贝构造
拷贝构造是需要我们自己实现的,如果浅拷贝会出现一块空间析构两次的情况
string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
所以我们要自己去手动开一块空间,然后把数据带过来,再把_size和_capacity进行赋值
赋值重载
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
赋值也是同样的道理,需要自己手动开空间,并把原空间释放,然后再重新改变_str的指向,注意赋值要判断自己给自己赋值的情况以及带上返回值。
我们说剩下的两个默认成员函数取地址运算符重载我们不需要管,用编译器默认生成的即可
三、下标访问
operator[]
顺序表因为是连续的物理空间,所以可以通过下标来访问,库中提供了两个版本,一个是普通版本,一个是const版本,普通对象就调用普通版本,也可以调用const版本,是权限缩小,const对象就只能调用const版本,普通版本可读可写,const版本只读,返回字符串的引用即可
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
我们在这里需要断言一下,要小于_size才可以访问,所以这也就是[]和at的区别,[]是断言报错,at是抛异常。
四、迭代器访问
首先我们来看一下迭代器是如何使用的,是怎么访问数据的
std::string s1("hello world");
std::string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it;
++it;
}
cout << endl;
迭代器是嵌在容器里面的,迭代器就像指针一样,模拟的是指针的行为,容器的底层结构各不相同,而成员变量又是私有,类外访问不到,迭代器则提供了统一的方式访问容器,不需要暴露底层实现,而对于string来说,迭代器直接就模拟的是原生指针,所以可以直接把char*的指针typedef
typedef char* iterator;
typedef const char* const_iterator;
begin/end
然后再提供一个begin和end的接口,begin返回的是首字符的地址,end返回的是最后一个有效字符下一个位置的地址,因为迭代器都是左闭右开,遍历到最后一个位置才能去判断结束了,所以要加上_size,begin/end和[]是一样的,也有两个版本,普通对象调用普通版本,也可以调用const版本,是权限缩小,const对象只能调用const版本,普通版本可读可写,const版本只读,而支持了迭代器就支持范围for,范围for就是单纯的傻瓜式的替换
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
这里我们先不对反向迭代器做介绍,只需要会使用即可,后面会专门写一遍文章来讲反向迭代器,反向迭代器都是一样的,会了一个就会其他的了。
五、容量相关
size/capacity
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
提供相对应的size和capacity接口用来返回元素个数,在函数内部也不改变成员,建议加上const
empty
bool empty() const
{
return _size == 0;
}
判空只需要判断_size是否为零就可以
clear
<