string的模拟实现
本文中会来进行对STL库中的SGI版本string的模拟实现,并解释其中遇到的一些问题。
为了避免与标准库中的string类重复,因此使用自己的命名空间。
成员变量
private:
//const char* _str;
char* _str;
size_t _size;
size_t _capacity;
构造函数
先简单的写一个构造函数:
string() :_str(nullptr) , _size(0) , _capacity(0) {} string (const char* str) :_str(str) // err ,_size(strlen(str)) , _capacity(strlen(str)) {}
在成员变量中给的是char*,但是拷贝构造中给予的是const char*,这里有权限放大的问题不能初始化。当我们给成员变量中添加const后可以解决报错的问题,但是又会产生其他的问题,当打印之中的字符串时:
const char* c_str() // 打印C风格的字符串(将string中的数据打印至"\0") { return _str; } void test_string1() { string s1; string s2("hello world"); cout << s1.c_str() << endl; // 会崩溃的问题是,cout自动识别类型const char* 会对字符串进行解引用就会露出null报错 cout << s2.c_str() << endl; } char& operator[](size_t pos) // 使用了const之后也无法进行类似于下面这样的修改操作 { assert(pos < _size); return _str[pos]; }
因此,在这里不能把常量字符串直接赋给string类,解决的方法就是new出新的空间。在上述的初始化列表中,不建议多次使用strlen,strlrn是一个O(N)的接口 ,同时不建议使用成员变量来初始化成员变量。做出如下的修改:
string() :_str(new char[1]) // 不能不添加[],在析构时可以统一管理 , _size(0) , _capacity(0) { _str[0] = '\0'; } string(const char* s) //:_str(str) :_size(strlen(s)) //, _capacity(strlen(str)) { _capacity = _size == 0 ? 3 : _size; _str = new char[_capacity + 1]; // 需要给string末尾添加"\0"留下位置 strcpy(_str, s); } // 最后综合成一个带有缺省参数的构造函数 // 缺省参数: // string(const char* str = nullptr) // 1、nullptr 崩溃 会解引用 // string(const char* str = '\0') // 2、类型不匹配,char被转换成为int,str也被当做空指针,崩溃 // string(const char* str = "\0") // 3、可以当时没有必要 string(const char* s = "") // 常量字符串默认以"\0"结束 //:_str(str) // 权限的放大不能将 char* 的变量赋给const char* :_size(strlen(s)) //, _capacity(strlen(str)) // strlen是一个O(N)的接口不宜连续调用 { _capacity = _size == 0 ? 3 : _size; _str = new char[_capacity + 1]; strcpy(_str, s); }
拷贝构造
在拷贝构造中需要注意的就是深浅拷贝的问题, 如果使用的是浅拷贝,首先修改拷贝的string类会对原string类造成影响,其次就是在析构是同一块空间会析构两次就会报错。
string(const string& s)
:_size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
赋值
赋值与拷贝构造一样都需要进行深拷贝,但是还有不同的地方:需要注意进行赋值前左操作数的空间大小的问题,这里可以先统一的进行临时的拷贝,对原string对象进行析构,再将临时拷贝的重新赋值给原来的string对象。
string& operator=(const string& s)
{
if (this != &s) // 处理自己给自己赋值
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
遍历string
遍历string的方式有很多,首先可以使用前文中所写的[ ],来进行遍历这与之前学习的数组非常的相似。在我们学习完&后一些函数的传参会写成如下的形式:
void Print(const string& s) // const对象 { for (size_t i = 0; i < s.size(); ++i) { cout << s[i] << " "; } cout << endl; } const char& operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
之前只编写了一个非const函数的[ ],然而const对象无法调用非const对象的成员函数,这里会有权限的放大,因此需要对[ ]进行重载。
迭代器
迭代器也可以进行遍历的操作,在这里我们编写的string迭代器使用的是指针:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const // 迭代器也是有const的问题,因此也需要重载
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
string::iterator it = s1.begin();
while (it != s1.end())
{
(*it)--;
++it;
}
cout << endl;
// 范围for的底层使用的就是迭代器,使用我们自己的迭代器也可以运行范围for
// 加入将迭代器中的begin改为Begin后,迭代器就会报错
for (auto ch : s)// 范围for的基底为迭代器, 不支持范围for因为是const对象,不能调用非const的函数
{
cout << ch << " ";
}
cout << endl;
reserve、resize
void reserve(size_t n)
{
if (n > _capacity) // 只有当修改的空间大于原先的空间时才进行操作
{
char* tmp = new char[n + 1]; // 需要多开辟一个空间用来存放"\0"
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0')
{
if (n > _size) // 同样需要考虑n和size的不同的情况
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; ++i) // 通过遍历的方式进行初始化
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
// 删除数据保留前N个
_str[n] = '\0';
_size = n;
}
}
尾插
void push_back(char ch)
{
if (_size + 1 >= _capacity)
{
reserve(_capacity * 2); // 空间越界的问题
}
_str[_size] = ch;
++_size;
_str[_size + 1] = '\0'; // 在末尾添加"\0",打印时可以终止
}
void append(const char* s)
{
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, s);
//strcat(_str, s); // 不好需要遍历字符串寻找"\0"
_size += len;
}
插入删除
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size + 1;
while (end > pos) // 当end = -1时由于是无符号整形数据因此会变为无符号整形的最大值,这里也不可以使用int类型,会有整形提升的问题
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
}
void 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 + len - 1) // 当len = 1 时就会与插入字符一致
{
_str[end] = _str[end - len];
--end;
}
// 拷贝插入
strncpy(_str + pos, s, len);
_size += len;
}
void earse(size_t pos, size_t len = npos)
{
assert(pos < _size);
if ((len == npos) || (pos + len >= _size)) // 分两种情况,当len==npos、pos+len>_size或
{
_str[pos] = '\0';
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
find
size_t find(char ch, size_t 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* s, size_t pos = 0)
{
assert(pos < _size);
char* ret = strstr(_str, s);
if (ret == nullptr)
{
return npos;
}
else
{
return ret - _str;
}
}
静态成员变量npos
static const size_t npos;
// 可以
//static const size_t npos = -1; // 添加了const之后就可以在类内初始化,只针对整形
/* static const size_t N = 10;
int _a[N];*/
// 不可以
//static const double dpos = 1.1;
正常写法:
类内声明:static const size_t npos;
类外定义:size_t string::npos = -1;
流插入
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch; // 在打印的时候,'\0'可能不会打出来
}
return out;
}
流提取
istream& operator>>(istream& in, string& s)
{
//s.clear();
//char ch = in.get(); // 获取每一个字符
in >> ch;
//while (ch != ' ' && ch != '\n') // 空格和换行没有进入缓冲区,空格和换行被视为多个值之间的间
//{
// s += ch; // 当输入的字符串比较长时,需要频繁的扩容
// //in >> ch;
// ch = in.get();
//}
// 预先开辟空间防止频繁的扩容
s.clear();
char ch = in.get();
char buff[128];
size_t i = 0;
//in >> ch;
while (ch != ' ' && ch != '\n') // 空格和换行没有进入缓冲区,空格和换行被视为多个值之间的间隔
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}