文章目录
- 👉string类👈
- 📕 概念
- 📕 成员变量
- 📕 构造函数、析构函数
- 📕 size() 、getstr() 函数
- 📕 拷贝构造
- 📕 赋值重载
- 📕 迭代器
- 📕 运算符重载
- 📕 尾插
- 📕 insert() 、erase() 函数
- 📕 流插入、流提取
- 👉源代码👈
👉string类👈
📕 概念
string是C++标准库的一个重要的部分,主要用于字符串处理。该类提供了上百个成员函数,本文并不逐一实现,只是实现部分 function 的功能。
📕 成员变量
如下,_str 标识字符串的指针,_size标识的是字符串长度,_capacity表示当前对象的最大存储容量,npos是在erase函数(删除单个字符或者字符串)里使用的,和 string 类的标准用法一样。
private:
char* _str;
int _size; // 目前的长度,不包括 \0
int _capacity; // 最大容量,不包括最后一个 \0
static size_t npos;
📕 构造函数、析构函数
如下,要使用初始化列表。同时,在这里需要注意几个点。
- 传参,使用缺省参数,如果无参数则为 “”
- 初始化列表只初始化 _size ,如果同时初始化 _capacity,根据初始化列表的规则,是按照成员变量声明的顺序来初始化的,所以如果成员变量的声明顺序改变,会导致一些错误(某个变量为随机值等等),所以在函数体内部对其他的变量进行赋值。
- _str 要开辟 _capacity+1 的空间,因为要拿多出的一个空间来存储 ‘\0’ 。
同时,_str 开辟 _capacity+1 长度的空间还有一个原因,就是为了方便析构,因为析构是使用delete[ ] 的,如果 _str = new char; 会导致 new 和 delete 不匹配。
string(const char* s = "")
:_size(strlen(s))
{
_capacity = _size == 0 ? 3 : _size + 1;
_str = new char[_capacity+1];
strcpy(_str, s);
}
析构函数应该不需要过多解释,如下。
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
📕 size() 、getstr() 函数
这两个函数要使用 const 修饰成员函数,一方面,该函数不需要改变对象里的任何值;另一方面,如果静态成员函数内部 需要调用该函数,要保证可以被调用(静态成员函数内部 不可以 调用非静态的成员函数和成员变量)。
int size()const
{
return _size;
}
char* getstr()const
{
return _str;
}
📕 拷贝构造
如下,strcpy 会自动复制 \0 ,所以不需要担心最后的 \0 问题。
string(const string& s)
:_size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(s._str, _str);
}
📕 赋值重载
赋值重载的注意点和代码如下。
- 要先排除赋值重载某个对象本身的情况,用 if 判断即可。
- 不能一开始就直接删除原来的数据,因为如果new失败,会导致错误。
string operator=(const string& s)
{
if (_str != s._str)
{
//delete[] _str; // 这样写并不好,可能会new失败
//_str = new char[s._capacity];
//_size = s._size;
//_capacity = s._capacity + 1;
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
_str = tmp;
_size = s._size;
_capacity = s._capacity + 1;
}
return *this;
}
📕 迭代器
如下,迭代器的设计可以使用函数重载,分别是静态和非静态。反向迭代器设计较为复杂,这里先不做介绍。
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
与此同时,迭代器中的 begin 设计完成之后,就可以使用范围 for 。范围 for 这个语法糖,必须要在类里面实现 begin ,函数名不能有错,也不能出现大写。
for (auto ch : s2)
{
cout << ch << " ";
}
📕 运算符重载
如下,对于直接使用方括号取值,可以重载,因为涉及到能改变和不能改变所取的值。
同时,对于比较运算符的重载,可以直接复用。
但是也要用 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];
}
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s)const
{
return (*this > s) || (s == *this); // 如果不设为const,这样会报错,因为 s 调用 == ,静态成员调用非静态成员函数
}
bool operator<(const string& s)const
{
return !(*this >= s);
}
bool operator<=(const string& s)const
{
return !(*this > s);
}
📕 尾插
首先,reserve 函数是将对象进行扩容,使其可以存储更多的数据,但是并不会更改原有的数据,所以其函数内部不涉及 _size 的修改。
pushback,尾插一个字符。
append,尾插字符串,先将一部分字符串后移,再插入。
当然了,我们更喜欢使用的是 += ,可以直接调用 pushback 和 append。
void reserve(int n) // 扩容,不改变数据
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
void pushback(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity*2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str); // 如果使用strcat,这个函数需要自己去找 \0 ,比较浪费时间
_size += len;
}
string& operator+=(const char ch)
{
pushback(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
📕 insert() 、erase() 函数
insert 函数重载,在任意位置插入单个字符或者字符串。
需要注意的是,insert是通过下标来访问和插入的,同时,存储下标的数据类型是 size_t ,无符号整形,该类型数据不会有负数,当取到理论上的 -1 时,实际上时它所能存储的最大正整数。如下图。
所以在 insert() 函数的第一个 while 循环语句上面,size_t end = _size +1 ; 这样子的话,最后end 的位置就是在下标为 1 的地方,不会变成 ffff (十六进制)而导致死循环。
那么为什么不直接用 int 类型的数据呢?因为参数 pos 是 size_t (STL标准里面也是这样),end 和 pos 比较,会把 end 当成 size_t 的类型和 pos 比,这里旨在模拟 STL 实现 string,所以要尽可能地贴合标准库。
erase 函数是在任意的位置删除任意长度的字符。
string& insert(size_t pos, const char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
// 要删除的内容大于等于pos之后的长度
if (len == npos || pos + len >= _size) // 要判断 len == npos ,否则如果 pos+len 溢出就麻烦了
{
_str[pos] = '\0';
_size = pos;
}
else // 要删除的内容没有超过最后一个字符
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
📕 流插入、流提取
在流插入的时候,不可以直接 in>>ch; 要当成一个字符一个字符输入,因为 cin 会把空格和换行当作是字符串之间的分割符,不会将其当作一个完整的字符串,所以要使用 istream 类里面的 get 函数,该函数可以拿到空格和换行,不会将其当作间隔处理 。
此外,如果输入字符串过长,每一次都 += ,可能会需要频繁扩容,空间消耗比较大,所以可以设计一个缓冲区
,存储满之后将缓冲区里面的数据放到 s 里面。当然,while 循环结束后也要判断缓冲区是否为空,因为可能最后一次没有将缓冲区填满,而导致没有将最后一段字符串放到 s 里面。
istream& operator>>(istream& in, string& s)
{
s.clear(); // 要输入之前先清空
char ch = in.get();
char buff[128]; // 缓冲区
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127) // 满了,最后一个位置放 \0 ,然后将缓冲区的数据放到s里面
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i) // 缓冲区还有数据没有存到s里面的情况
{
buff[i] = '\0';
s += buff;
}
return in;
}
ostream& operator<<(ostream& out, const string& s)
{
// 自己写了迭代器,就可以使用范围 for 了,可以不用友元函数
for (auto ch : s)
{
out << ch;
}
return out;
}
👉源代码👈
如下,是 string 类的源代码,将它放到一个命名空间里面,为了防止和标准库里面的某些名字冲突。
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<iostream>
#include<algorithm>
#include<assert.h>
namespace simulate
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// string(const char* s = nullptr) 是不可以的,传参 '\0' 也不可以。因为cout对于char*类型的,是一直读到 \0 为止
string(const char* s = "")
:_size(strlen(s)) // 如果同时初始化_capacity,会导致按照申明的顺序初始化,顺序改动会造成随机值
{
_capacity = _size == 0 ? 3 : _size + 1;
_str = new char[_capacity+1];
strcpy(_str, s);
}
string(const string& s)
:_size(s._size)
, _capacity(s._capacity)
{
_str = new char[_capacity + 1];
strcpy(s._str, _str);
}
string operator=(const string& s)
{
if (_str != s._str)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
_str = tmp;
_size = s._size;
_capacity = s._capacity + 1;
}
return *this;
}
char* c_str()const
{
return _str;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
int size()const
{
return _size;
}
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s)const
{
return (*this > s) || (s == *this); // 如果不设为const,这样会报错,因为 s 调用 == ,静态成员调用非静态成员函数
}
bool operator<(const string& s)const
{
return !(*this >= s);
}
bool operator<=(const string& s)const
{
return !(*this > s);
}
void resize(size_t n,char ch='\0')
{
// 删除数据,保留前n个
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else if (n > _size) // 不用考虑 n = _size 的情况,因为这种情况不需要改变任何值
{
if (n > _capacity)
{
reserve(n);
}
size_t i = _size;
while (i < n)
{
_str[i] = ch;
i++;
}
_size = n;
_str[_size] = '\0';
}
}
void reserve(size_t n) // 扩容,不改变数据
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
void pushback(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity*2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str); // 如果使用strcat,这个函数需要自己去找 \0 ,比较浪费时间
_size += len;
}
string& operator+=(const char ch)
{
pushback(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& insert(size_t pos, const char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos <= _size);
// 要删除的内容大于等于pos之后的长度
if (len == npos || pos + len >= _size) // 要判断 len == npos ,否则如果 pos+len 溢出就麻烦了
{
_str[pos] = '\0';
_size = pos;
}
else // 要删除的内容没有超过最后一个字符
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
size_t find(const char ch,size_t pos=0)
{
assert(pos <= _size);
for (int i = pos; i < _size; i++)
{
if (_str[ch] = i) return i;
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
char* p = strstr(_str, str);
if (p == nullptr)
{
return npos;
}
return p - _str;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
private:
char* _str;
int _size; // 目前的长度,不包括 \0
int _capacity; // 最大容量,不包括最后一个 \0
static const size_t npos;
};
const size_t string::npos = -1;
istream& operator>>(istream& in, string& s)
{
s.clear(); // 要输入之前先清空
char ch = in.get();
char buff[128]; // 防止频繁扩容,缓冲区
int i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127) // 满了,最后一个位置放 \0 ,然后将缓冲区的数据放到s里面
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i) // 缓冲区还有数据没有存到s里面的情况
{
buff[i] = '\0';
s += buff;
}
return in;
}
ostream& operator<<(ostream& out, const string& s)
{
// 自己写了迭代器,就可以使用范围 for 了,可以不用友元函数
for (auto ch : s)
{
out << ch;
}
return out;
}
}