【C++初阶】--- string类模拟实现
1.基础函数
1.1成员函数
成员函数主要是_str、_size、_capacity这三个。npos是size_t 的最大值,用于当作后续成员函数的参数的缺省值。
class string
{
private:
char* _str = nullptr;//指向字符串的指针
size_t _size = 0;//字符串长度
size_t _capacity = 0;//空间大小
static const size_t npos;
};
1.2默认构造函数
默认构造我们给str一个缺省值"",即没有传参的时候字符串为空,只有一个’\0’,因为strcpy会把’\0’拷贝过来
//默认构造
string(const char* str = "")//如果没传值的话会拷贝'\0''
{
_capacity = _size = strlen(str);
_str = new char[_capacity + 1];
strcpy(_str, str);
}
1.3拷贝构造
拷贝构造有两种写法,一种是普通写法,一种是现代写法
- 普通写法:
我们先根据要拷贝的对象确认空间大小,然后new一块空间,new出空间后利用strcpy将字符串中的内容拷贝过来
//拷贝构造
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
- 现代写法:
我们利用要拷贝的那个字符串构造一个string对象tmp,然后我们自己写一个swap函数用于交换两个string对象的内容,这样我们就可以将tmp中的内容交换到我们要拷贝构造的这个对象中。
需要注意的是注意相关成员变量需要给缺省值,否则交换后tmp的_str可能是野指针。因为tmp是个局部对象,出了拷贝构造就要调用析构函数。
//交换两个字符串的值
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代写法,拷贝构造
string(const string& s)
{
//利用s._str构造tmp,再使用tmp与this交换
//注意相关成员变量需要给缺省值,否则交换后tmp的_str可能是野指针
string tmp(s._str);
swap(tmp);
}
1.4赋值运算符重载
赋值运算符重载也分为普通写法和现代写法
- 普通写法:
在不是给自己赋值的情况下,我们先delete原对象字符串的的空间,然后new一个和赋值对象字符串一样大的空间,在利用strcpy拷贝内容,更新长度和空间大小。
//赋值运算符重载
string& operator=(const string& s)
{
//不能自己给自己赋值
if (this != &s)
{
delete[] _str;//先销毁原空间
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
- 现代写法
写法1我们可以利用对象的字符串来构造tmp,或者直接用对象拷贝构造tmp。构造出tmp后交换此对象与tmp对象,两者内容也就交换了。
写法2更为简介,首先我们参数不再是引用,而是传值,则tmp只是一份拷贝。传值传参,对形参的的改变不会影响实参,我们直接用传过来的tmp进行交换。
//现代写法,赋值运算符重载
//写法1
string& operator=(const string& s)//不用引用,则tmp只是一份拷贝
{
if (this != &s)
{
//string tmp(s._str);//用s._str构造tmp
string tmp(s);//用s拷贝构造tmp
swap(tmp);
}
return *this;
}
//写法2,优化
string& operator=(string tmp)//不用引用,则tmp只是一份拷贝
{
//交换tmp不会对tmp原本的值造成影响
swap(tmp);
return *this;
}
1.5析构函数
因为我们有资源的申请,需要显示写析构函数将new的空间delete
//析构
~string()
{
//不析构空指针
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
2.可以写在类里的成员函数
短小的函数和频繁调用的函数可以直接写在类中,在类中写的成员函数函数默认是inline函数(内联函数)
2.1size()和capacity()
size()返回的是字符串的长度,capacity返回的是空间的大小
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
2.2empty()、clear()、c_str()
- empty()用于判断字符串是否为空,空返回true,非空返回false
- clear()用于清除字符串中的内容,一般不会清除空间
- c_str()用于获取等效的字符串
//判空
bool empty() const
{
return _size == 0;
}
//清除字符串,不清除空间
void clear()
{
_str[0] = '\0';
_size = 0;
}
//获取等效的字符串
const char* c_str() const
{
return _str;
}
2.3operator[]
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
[]重载,可以获取字符串中对应下标的元素,前提确保不能越界,const重载返回的元素不能逆行修改。
char& operator[](size_t index)
{
//确保有效位置
assert(index < _size);
return _str[index];
}
const char& operator[](size_t index) const
{
assert(index < _size);
return _str[index];
}
3.迭代器iterator
迭代器的底层我们暂且可以认为是指针
typedef char* iterator;//正向迭代器
typedef const char* const_iterator;//正向只读迭代器
3.1begin()和end()
- begin()会返回一个指向字符串的第一个字符的迭代器。
- end()会返回一个指向字符串的最后一个字符的下一个位置的迭代器,即’\0’位置
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
4.修改空间大小的函数
4.1resver()
void reserve (size_t n = 0);
resver()用于请求更改容量
• 如果 n 大于当前字符串容量,则该函数会导致容器将其容量增加到 n 个字符(或更大)。
• 如果n <= 当前字符串容量,容量是否变化取决于编译器。
• 此函数对字符串长度没有影响,也无法更改其内容。
void string::resver(size_t n)
{
//n <= _capacity的话我们选择不改变容量
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
4.2resize()
resize()用于调整字符串大小
• 将字符串大小调整为 n 个字符的长度。
• 如果 n 小于当前字符串长度,则当前值将缩短为其前 n 个字符,并删除第 n个字符以外的字符。
• 如果 n 大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展当前内容,以达到 n 的大小。如果指定了 c,则新元素将初始化为 c 的副本,否则,它们是值初始化字符(空字符)。
memset将 ptr 指向的内存块的前num字节数设置为指定值value
void * memset ( void * ptr, int value, size_t num );
//调整字符串大小
void string::resize(size_t newSize, char c)
{
//n 大于当前字符串长度,则通过在末尾插入所需数量的字符来扩展当前内容
if (newSize > _size)
{
//newSize比空间大,需要进行扩容
if (newSize > _capacity)
{
resver(newSize);
memset(_str + _size, c, newSize - _size);
}
else
{
memset(_str + _size, c, _capacity - _size);
}
}
//n 小于当前字符串长度,则当前值将缩短为其前 n 个字符,并删除第 n个字符以外的字符
else
{
_str[newSize] = '\0';
_size = newSize;
}
_capacity = newSize;
_str[newSize] = '\0';
}
5.用于修改字符串内容的函数
5.1push_back()
push_back()用于尾插一个字符
void push_back (char c);
插入字符前我们先考虑容量是否足够,不够的话我们就2倍扩容,不要忘了结尾加上’\0’
//尾插字符
void string::push_back(char c)
{
//考虑扩容
if (_size == _capacity)
{
resver(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0';
}
5.2operator+=()
operator+=()用于尾插字符、字符串、string对象,我们模拟实现尾插字符和字符串。
string& operator+= (const string& str);
string& operator+= (const char* s);
string& operator+= (char c);
尾插字符我们可以直接复用push_back(),两者功能一样
//在末尾追加字符
string& string::operator+=(char c)
{
//直接复用
push_back(c);
return *this;
}
在末尾追加字符串我们可以直接复用append(),这个等下讲,append()用于尾插字符串。
//在末尾追加字符串
string& string::operator+=(const char* str)
{
//直接复用
append(str);
return *this;
}
5.3append()
依旧先考虑扩容问题,但因为我们是2倍扩容,如果_size + len比2倍还大,那空间依旧不够用,那就他要多少我们扩多少。扩容完我们strcpy将字符串中内容进行拷贝。
//在末尾追加字符串
void string::append(const char* str)
{
//考虑扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
//比2倍大就按需扩容,否则就直接2倍扩
resver(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
5.4insert()
insert()用于将其他字符插入字符串中 pos(或 p)指示的字符之前,并返回该字符的位置
string& insert (size_t pos, const char s);
string& insert (size_t pos, const char* s);
需要注意pos不能越界,然后考虑扩容,然后移动pos位置及之后的数据向后移动一位('\0’一起移动),移动完后在pos位置放入对应字符。
//在pos位置插入字符
string& string::insert(size_t pos, const char c)
{
//pos可以在尾部,相当于尾插
assert(pos <= _size);
//考虑扩容
if (_size == _capacity)
{
resver(_capacity == 0 ? 4 : 2 * _capacity);
}
//'\0'一起移
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
return *this;
}
在pos位置插入字符串和插入字符操作差不多,不同的是扩容的操作和向后移动的距离。
//在pos位置插入字符串
string& string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
resver(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
for (size_t i = 0; i < len; i++)
{
_str[i + pos] = str[i];
}
_size += len;
_str[_size] = '\0';
return *this;
}
5.5erase()
erase()用于擦除字符串的一部分,减少其长度
const size_t string::npos = -1;
string& erase (size_t pos = 0, size_t len = npos);
先判断pos是否越界,如果要删除的长度超过字符剩余长度,说明pos位置及之后的字符都需要删除,我们直接将pos位置的数据修改成’\0’,即字符串的结尾。
如果要删除的长度小于字符剩余长度,则说明说明后面还剩余有效字符,需要将这些字符向前移动len长度。
//删除pos位置开始的len个元素
string& string::erase(size_t pos, size_t len)
{
assert(pos < _size);
//len超过剩余长度,则说明后面全删了
if (len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
//后面还剩几个字符没删
else
{
for (size_t i = pos + len; i < _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
return *this;
}
6.查找字符串中的内容
6.1find()
find()用于在字符串中搜索和参数(字符或者字符串)匹配的第一个匹配项,并返回其在字符串中的下标。
size_t find (const char* s, size_t pos = 0) const;
size_t find (char c, size_t pos = 0) const;
判断pos是否越界,然后从pos位置开始向后找,找到就返回下标,没找到则返回npos。
//从pos位置开始往后找字符c,返回下标
size_t string::find(char c, size_t pos) const
{
//pos有效性
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
//没有找到
return npos;
}
strstr()函数可以返回指向 str1 中第一次出现的 str2 的指针,如果 str2 不是 str1 的一部分,则返回 null 指针。
char * strstr (char * str1, const char * str2 );
如果tmp不为nullptr,则说明子串存在,返回的则是str2第一次出现的指针,将tmp指针与字符串指针_str相减,两指针相减即地址相减,可以得出下标。
//返回字串在string中第一次出现的位置
size_t string::find(const char* str, size_t pos) const
{
assert(pos < _size);
const char* tmp = strstr(_str + pos, str);
if (tmp == nullptr)
return npos;
else
return tmp - _str;
}
6.2substr()
substr()用于生成子字符串,可以生成一个从pos位置开始,长度为len的字符串,如果剩余长度小于len,则拷贝至末尾停止。
如果没有给定len,则按照缺省值拷贝,npos是个无符号整型,-1转换成无符号整型是INT_MAX,即从pos位置拷贝至末尾
const size_t string::npos = -1;
string substr (size_t pos = 0, size_t len = npos) const;
确定拷贝长度,然后为子字符串开空间,并拷贝字符。
//生成子字符串
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
//len大于剩余长度,将len更新为剩余长度
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.resver(len);//开空间
for (size_t i = 0; i < len; i++)
{
sub += _str[i + pos];
}
return sub;
}
7.运算符重载
7.1比较运算符重载
我们将以下运算符进行重载,就可以比较对象字符串的内容大小
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1,const string& s2);
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
7.2流插入、流提取运算符重载
ostream& operator<< (ostream& os, const string& str);//流插入
istream& operator>> (istream& is, string& str);//流提取
流插入我们使用范围for将字符串一个一个插入即可
ostream& operator<<(ostream& out, const string& s)
{
//范围for
for (auto ch : s)
{
out << ch;
}
return out;
}
流提取我们则要先清除对象字符串中的字符,buff的作用是为了避免插入字符过程中的频繁扩容,使用用buff先存储输入的字符,满了之后我们将buff字符串尾插到对象s的字符串中。
cin遇到空格或者换行就不读取了,我们模拟实现也是遇到空格或者换行即停止。
我们读取字符的时候使用ch = in.get();而不是cin>>ch是因为cin默认空格和换行是间隔符,不会进行读取,也就是说我们永远读取不到空格和换行,程序将陷入死循环,而get()可以读到空格和换行。
注意循环结束后如果i>0说明buff中还有字符,我们需要将其尾插至对象s的字符串中。
istream& operator>>(istream& in, string& s)
{
//先清除s中的字符
s.clear();
const int N = 256;
char buff[N];
int i = 0;
//不能用in>>ch,因为读不到空格和换行,默认是间隔符
char ch = in.get();
while(ch != ' ' && ch != '\n')
{
buff[i++] = ch;
//满了,最后一个填'\0'
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
//继续读下一个字符
ch = in.get();
}
//不足256个不会尾插,将剩下的尾插进去
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}