目录
①String类的主体
②String类的具体实现
1.构造函数、拷贝构造函数、赋值运算符、析构函数
⑴构造函数
⑵拷贝构造函数
⑶赋值运算符
⑷析构函数
2.迭代器(范围for的实现原理)
3.修改:push_back, apppend, +=, clear, swap, c_str
⑴push_back
⑵apppend
⑶+=
⑷clear
⑸swap
⑹c_str
4.容量:size, capacity, empty, resize, reverse
⑴size
⑵capacity
⑶empty
⑷resize
⑸reverse
5.下标[]重载
6.逻辑运算符:<, <=, >, >=, ==, !=
7.查找:find
8.固定位置插入元素:insert
9.清除固定位置元素:erase
10.流插入、流提取运算符重载:<<, >>
⑴流插入<<
⑵流提取>>
①String类的主体
为了与库函数里面的string分开,我们将模拟实现的string类存放在自己定义的命名空间中,同时尽可能的将对应功能函数的名字与库函数一一对应,即
namespace my_string
{
class string
{
friend ostream& operator<<(ostream& _cout, const my_string::string& s);
friend istream& operator>>(istream& _cin, my_string::string& s);
public:
typedef char* iterator;
typedef const char* const_iterator;
public:
// 构造函数
string(const char* str = "");
// 拷贝构造函数
string(const string& s);
// 赋值运算符重载
string& operator=(const string& s);
// 析构函数
~string();
// 迭代器
iterator begin();
iterator end();
const_iterator begin() const;
const_iterator end() const;
// 修改
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
// 容量
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
// 下标
char& operator[](size_t index);
const char& operator[](size_t index)const;
// 逻辑运算符
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
string& insert(size_t pos, char c);
string& insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
string& erase(size_t pos, size_t len);
private:
char* _str;
size_t _capacity;
size_t _size;
};
};
②String类的具体实现
1.构造函数、拷贝构造函数、赋值运算符、析构函数
⑴构造函数
// 构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];// 字符串后有'\0'因此要+1
memcpy(_str, str, sizeof(char) * (_size + 1));// 与上述同理
}
⑵拷贝构造函数
// 拷贝构造函数
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
// 这里不能直接让_str=s._str(即浅拷贝),
// 不然会导致两个string最后析构时对它们指向的空间析构两次
// 此外,如果其中一个对象对这片空间的元素进行修改,会对另一个对象造成影响
// 因此,此处只能使用深拷贝
_str = new char[s._capacity + 1];
memcpy(_str, s._str, sizeof(char) * (s._size + 1));
}
⑶赋值运算符
在模拟实现赋值运算符时,有两种写法,一种是与拷贝构造逻辑类似的写法,即
// 赋值运算符重载
string& operator=(const string& s)
{
if (this != &s)// 与拷贝构造逻辑相同
{
_size = s._size;
_capacity = s._capacity;
_str = new char[s._capacity + 1];
memcpy(_str, s._str, sizeof(char) * (s._size + 1));
}
return *this;
}
另一种则是通过拷贝构造一个临时对象tmp,将this与tmp所指向的空间交换,在结束时tmp还能顺便释放自己所指向的空间,即
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 赋值运算符重载
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
图示如下
从图中我们可以明显看出,在此函数调用完后,tmp也会顺带将s1先前所指向的空间释放
⑷析构函数
// 析构函数
~string()
{
_size = _capacity = 0;
delete[] _str;
_str = nullptr;
}
2.迭代器(范围for的实现原理)
从string类的主体中我们不难看出,string类的迭代器其实就是指针,即
这是因为string类的对象本身处在一个连续的空间内,可以直接通过指针来操控,因此我们便可以得到
// 迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
在这里我们可以看看范围for是如何做到的,首先我们直接使用有
而当我们将任意一个迭代器(begin或end注释掉后可以发现)
因此我们可以发现,其实所谓的范围for不过就是将这段代码固定的替换成如下这段代码
my_string::string::iterator it = s.begin();
while (it != s.end())
{
cout << *it;
it++;
}
而且范围for只会固定的寻找end和begin名字的迭代器,略微修改名字也会报错
3.修改:push_back, apppend, +=, clear, swap, c_str
⑴push_back
void push_back(char c)
{
// 当size与capacity相等时需要扩容
if (_size == _capacity)
{
// 扩容逻辑与reserve类似
char* tmp = new char[_capacity * 2];
memcpy(tmp, _str, sizeof(char) * _size);
delete[] _str;
_str = tmp;
_capacity *= 2;
}
_str[_size++] = c;
}
⑵apppend
void append(const char* str)
{
// 与push_back的插入逻辑类似
// 但是扩容大小需要作出变化
size_t len = strlen(str);
if (len + _size >= _capacity)
{
reserve(2 * (len + _capacity));
}
for (size_t i = 0; i < len; i++)
{
push_back(str[i]);
}
}
⑶+=
// 直接复用即可
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
⑷clear
void clear()
{
// 容量一般不缩小,以防重复开辟空间降低效率
_str[0] = '\0';
_size = 0;
}
⑸swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
⑹c_str
const char* c_str()const
{
return _str;
}
4.容量:size, capacity, empty, resize, reverse
⑴size
size_t size()const
{
return _size;
}
⑵capacity
size_t capacity()const
{
return _capacity;
}
⑶empty
bool empty()const
{
return _size == 0;
}
⑷resize
在这里分为多种情况,如图
实现如下
void resize(size_t n, char c = '\0')
{
if (n < _size)
{
_str[_size] = '\0';// 直接将截止位置提前到size的位置即可
}
else
{
// 如果是情况三先进行扩容,然后统一插入数据
if (n > _capacity)
{
reserve(n);
}
for (int i = _size; i < n; i++)
{
_str[i] = c;
}
}
_size = n;
}
⑸reverse
在这里,我们一般不选择将capacity缩小,因为缩小也是需要代价的(可能会开辟一片新的空间),因此实现如下
void reserve(size_t n)
{
if (n > _capacity)
{
// 扩容可能会开辟新空间
char* tmp = new char[n + 1];
memcpy(tmp, _str, sizeof(char) * (_size + 1));
delete[] _str;
_str = tmp;
_capacity = n;
}
}
5.下标[]重载
// 下标
char& operator[](size_t index)
{
assert(index < _size);//此处因为传入参数为size_t,因此不需要判断index是否小于0
return *(_str + index);
}
const char& operator[](size_t index)const
{
assert(index < _size);
return *(_str + index);
}
6.逻辑运算符:<, <=, >, >=, ==, !=
字符串比较大小的规则是逐字符比较ASCII码值,相等的话就比较下一个,在这里两个字符串相比较也会有多种情况,如下图所示
我们可以实现任一比较大小和相等之后,不断复用来做到简化,即
// 逻辑运算符
bool operator<(const string& s)
{
int i = 0;
// 一直迭代到两字符不相等或下标i越界
while (i < _size && i < s.size() && s[i] == _str[i])
{
i++;
}
// 如果下标i超过了string1的size,需要进行特殊判断
if (i >= _size)
{
//如果两个字符串的大小相等说明此时它们均相同
if (_size == s.size())
{
return false;
}
else//反之表示string1截止在i这个下标处,string1<string2
{
return true;
}
}
// 如果下标i超过了string2的size,此时string1始终是>或=string2的
// 因此返回false
if (i >= s.size())
{
return false;
}
if (_str[i] < s[i])
{
return true;
}
else
{
return false;
}
}
bool operator<=(const string& s)
{
return (*this < s || *this == s);
}
bool operator>(const string& s)
{
return !(*this <= s);
}
bool operator>=(const string& s)
{
return (*this > s || *this == s);
}
bool operator==(const string& s)
{
int i = 0;
while (i < _size && i < s.size() && s[i] == _str[i])
{
i++;
}
// 此时,下标i要么越界,要么就在两个字符串中分别指向不同的字符
// 因此,只有当i越界,且两个字符串大小相等时,才能相等,其余情况都不行
if ((i >= _size || i >= s.size()) && _size == s.size())
{
return true;
}
else
{
return false;
}
}
bool operator!=(const string& s)
{
return !(*this == s);
}
7.查找:find
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
int i = 0;
// 寻找到与c相同的字符或者越界为止
while (i < _size && _str[i] != c)
{
i++;
}
if (_str[i] == c)
{
return i;
}
else
{
return -1;
}
}
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
assert(pos < _size);
const char* tmp = strstr(_str + pos, s);
if (tmp != nullptr)
{
return tmp - s;
}
else
{
return -1;
}
}
8.固定位置插入元素:insert
这里先实现插入单个字符
// 在pos位置上插入字符c,并返回该字符
string& insert(size_t pos, char c)
{
assert(pos <= _size);
// 检查是否需要扩容
if (_size == _capacity)
{
reserve(2 * _capacity);
}
// 从最后一个数据开始将每个数据向后挪动
int i = 0;
for (i = _size; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[i] = c;
_size++;
return *this;
}
然后是实现插入一串子串,举例如下
即
// 在pos位置上插入字符串str,并返回该字符的位置
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_size + len >= _capacity)
{
reserve(2 * (_size + len));
}
// 向后挪位置腾出空间
int i = 0;
// 在这里i与pos比较时会发生隐式类型转换,因此还需要添加一个条件
for (i = _size - 1; i != -1 && i >= pos; i--)
{
_str[i + len] = _str[i];
}
// 从pos位置开始将str的内容拷贝到string中
memcpy(_str + pos, str, sizeof(char) * len);
_size += len;
return *this;
}
9.清除固定位置元素:erase
// 删除pos位置上的元素,并返回字符
string& erase(size_t pos, size_t len)
{
assert(pos < _size);
// 如果pos+len超过了_size,则表示要将pos及其之后的字符全部删除
if (pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
int i = 0;
// 从第pos+len的位置开始将数据分别向前挪动覆盖
for (i = pos+len; i < _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
return *this;
}
10.流插入、流提取运算符重载:<<, >>
⑴流插入<<
friend ostream& operator<<(ostream& _cout, const my_string::string& s)
{
for (char c : s)
{
cout << c;
}
cout << endl;
return _cout;
}
⑵流提取>>
friend istream& operator>>(istream& _cin, my_string::string& s)
{
// 在每次输入前需要对缓冲区作出处理
s.clear();
// 删除存在于最前方的空格与换行符
char ch = _cin.get();
while (ch == ' ' || ch == '\n')
{
ch = _cin.get();
}
// 在这里,我们创建一个临时的缓冲器来存放s
// 这样的话每次插入字符数只有超过了128时,将其尾插到s中
// 之后归零i,从而避免扩容,以此提高插入效率
char buff[128] = { 0 };
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
// 当i到达127时将此位置置为'\0',然后将i重置为0
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = _cin.get();
}
if (i != 0)
{
buff[i] = '\0';
s += buff;
}
return _cin;
}