前言
在【C++】STL(一)string类的使用一文中已经对string类进行了简单的介绍,一般来说只要会正常使用即可,下面来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。
一、四个默认成员函数
1.构造函数
构造函数的实现思路就是给_str新开一片空间,并把参数中的字符串拷贝到新开的空间内。
代码如下:
class String
{
public:
String(const char* str = "")
:_str(new char[strlen(str) + 1])
{
strcpy(_str, str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
注意参数的默认值不能给nullptr,因为若不传参使用默认值时,strlen(str)会对空指针进行解引用操作从而报错,这里最好用"",也即一个仅有\0的空字符串。
另一种写法如下
代码如下:
class String
{
public:
String(const String& s)
:_str(nullptr)
{
String tmp(s._str);
swap(_str, tmp._str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
注意这是构造函数,所以新产生的对象在初始化前_str内存储着随机值,交换后tmp(临时对象,生命周期仅在这个函数体内)出作用域时调用析构函数会释放一段随机空间,这是非法访问,所以在初始化列表内给_str以nullptr,这样在析构函数调用时对nullptr进行delete[]没有问题。
2.析构函数
析构函数比较简单,只需释放空间并将指针置空即可。
代码如下:
class String
{
public:
~String()
{
delete[] _str;
_str = nullptr;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
3.拷贝构造
这里会设计到深浅拷贝的问题。
1.浅拷贝
先看一下若不写拷贝构造函数,使用编译器默认生成的是否能满足要求。
代码如下:
int main()
{
String s1("hello world");
String s2(s1);
return 0;
}
运行后程序会崩溃,那么原因是什么呢?
运行如下:
调试会发现,s1、s2中各自的_str指向同一片空间,也就是说在析构函数时会对同一块空间delete[]两次,这显然是不合法的,根本原因在于编译器给出的默认构造函数只进行值拷贝,即把s1中_str的值拷贝给s2中的_str,这样一来两个String对象中的_str指向同一块空间,不仅修改时会互相影响,在调用析构函数时程序更是会崩溃。
调试结果如下:
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一个对象的资源已经被释放,当继续对资源进项操作时,就会发生发生了访问违规。
2.深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般都是按照深拷贝方式提供。
代码如下:
class String
{
public:
String(const String& s)
:_str(new char[strlen(s._str) + 1])
{
strcpy(_str, s._str);
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
这样一来,两个String对象的_str指向的空间便成了独立的两块,互不影响。
调试结果如下:
另一种写法如下:
代码如下:
class String
{
public:
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
形参列表里的s是一个临时产生的String对象,在该函数结束后会调用析构函数,将s与*this的_str交换后,this内的_str便和希望得到的内容一样,而原来this的_str内的内容随着对象s析构函数的调用消失。
4.赋值运算符重载
若不写而使用编译器默认生成的赋值运算符重载,则同样会发生上面浅拷贝的问题,所以这里仍要考虑深拷贝。
代码如下:
class String
{
public:
String& operator=(const String& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
return *this;
}
private:
char* _str;
};
二、string类的其它函数
上面的四个默认成员函数主要是操作_str,基本不涉及_size和_capacity,下面的函数则需要用到,所以在private的成员变量中需要添加。
则上面的四个函数也需要稍微改动。
代码如下:
class String
{
public:
String(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
String(const String& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
String tmp(s._str);
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
String& operator=(String s)
{
swap(_str, s._str);
swap(_size, s._size);
swap(_capacity, s._capacity);
return *this;
}
~String()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
1.一些简单的函数
(1)size、capacity、c_str和clear
这几个函数非常简单,这里不作过多解释。
代码如下:
class String
{
public:
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
const char* c_str() const
{
return _str;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)比较函数
下面这些函数时比较两个string类的(实质是用strcmp比较两个对象的_str),代码较短且常用,所以可以设计为内联函数。
代码如下:
inline bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
inline bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
inline bool operator<=(const string& s1, const string& s2)
{
return (s1 < s2) || (s1 == s2);
}
inline bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
inline bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
inline bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
2.三种遍历方式
(1)下标+[]
要用到[]则需要进行重载,这样便可通过下标访问字符串。
代码如下:
class String
{
public:
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)迭代器
string类的迭代器就是一个char*指针(但其它容器不一定是),所以实现起来比较简单。
代码如下:
class String
{
public:
typedef char* Iterator;
Iterator begin()
{
return _str;
}
Iterator end()
{
return _str + _size;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(3)范围for
对于范围for的遍历方式,只要支持迭代器就可以使用,因为范围for的遍历方式最终会被编译器转换为迭代器。同时由于这个原因,模拟实现的迭代器命名必须与标准一致,begin、end名称不可修改,随意更换begin、end的大小写或单词拼写都将时范围for无法运行。
3.reserve和resize
(1)reserve
reserve会改变字符串能存储的最大有效字符个数。
注意拷贝字符串时要用strncpy把_size个字符全部拷贝,不能用strcpy,因为strcpy遇到’\0’拷贝即结束,但string类字符串的结束是_size限定的。
代码如下:
class String
{
public:
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strncpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)resize
resize会改变字符串存储的有效字符个数,同时可能会改变_capacity。若有多余的空间会将其初始化为指定的内容。
代码如下:
class String
{
public:
void resize(size_t n, char val = '\0')
{
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if (n > _capacity)
reserve(n);
for (size_t i = _size; i < n; i++)
_str[i] = val;
_size = n;
_str[_size] = '\0';
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
4.插入数据
(1)push_back插入字符
代码如下:
class String
{
public:
void push_back(const char ch)
{
if (_size == _capacity)
{
size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
reserve(newcapacity);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(2)append添加字符串
代码如下:
class String
{
public:
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len >= _capacity)
reserve(_size + len);
strcpy(_str + _size, str);
_size += len;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(3)operator+=
这里注意代码复用。
代码如下:
class String
{
public:
String& operator+=(const char ch)
{
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
append(str);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
(4)insert在任一位置插入字符或字符串
代码如下:
class String
{
public:
String& insert(size_t pos, const char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t newcapacity = (_capacity == 0) ? 4 : (2 * _capacity);
reserve(newcapacity);
}
size_t i = 0;
for (i = _size + 1; i > pos; i--)
_str[i] = _str[i - 1];
_str[pos] = ch;
_size++;
return *this;
}
String& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len + _size > _capacity)
reserve(len + _size);
size_t i = 0, j = 0;
for (i = len + _size; i >= pos + len; i--)
_str[i] = _str[i - len];
for (i = pos, j = 0; j < len; j++)
_str[i++] = str[j];
_size += len;
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
5.erase
代码如下:
class String
{
public:
String& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
size_t leftLen = _size - pos;
if (len >= leftLen)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos; i < pos + len; i++)
_str[i] = _str[i + len];
_size -= len;
}
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
const size_t String::npos = -1;
6.find
代码如下:
class String
{
public:
size_t find(const 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* str, size_t pos = 0)
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);
if (ret)
return ret - _str;
else
return npos;
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
7.输入输出重载
输入输出重载在之前【C++】类和对象3 中介绍运算符重载时详细介绍过,这里的实现方式与之前相近。
代码如下:
ostream& operator<<(ostream& out, const String& s)
{
for (auto e : s)
out << e;
return out;
}
istream& operator>>(istream& in, String& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
感谢阅读,如有错误请批评指正
|