我们这篇文章进行string的模拟实现。
为了防止标准库和我们自己写的string类发生命名冲突,我们将我们自己写的string类放在我们自己的命名空间中:
我们先来搭一个class string的框架:
namespace CYF{
public:
//各种成员函数
private:
char _str;//存储字符串数组的指针
size_t _size;//记录当前字符串的有效长度
size_t _capacity;//记录当前字符串的容量
static const size_t npos;//静态成员变量,很多地方的缺省值
}
默认成员函数
构造函数
string(const char* str="")
{
_size = strlen(str);//初始字符串有效长度
_capacity = _size;//初始字符串容量设为字符串有效长度
_str = new char[_capacity + 1];//为存储字符串开辟空间,+1是为了保存'\0'
strcpy(_str, str);//将str字符串拷贝到开好的空间
}
拷贝构造函数
关于拷贝构造函数,我们首先需要了解一个知识点:深浅拷贝
浅拷贝就是拷贝出来的对象和原先的对象指向的是同一块空间,这样的话,其中一个对象对这块空间做了改变,也会影响另外一个对象。
深拷贝就是拷贝出来的对象跟原来的对象,指着的是两块不同的空间,两者相同指的是不同空间中的内容是相同的。
下图是深浅拷贝区别的形象化表现:
而在这里,显然我们并不想两者之间相互影响,所以我们要用到的是深拷贝。
所以我们要先开辟块容纳原有对象字符串的空间,然后将字符串拷贝过去,再将其他成员变量赋值过去即可,这是传统写法:
string(const string& str)//拷贝构造函数的传统写法
:_size(0)
,_capacity(0)
,_str(new char[_capacity + 1])
{
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
我们还有一种现代写法:
我们先根据原有字符串通过构造函数构造出一个tmp对象,然后再将tmp对象和拷贝对象的数据交换即可,这样的话,通过构造函数构造出来的tmp对象指向的空间和原对象的空间不同,并且交换之后,tmp是一个局部变量,出了作用域就会自动调用析构函数销毁,也就将tmp此时自身里拷贝对象原有的无用的数据全部删除了,一举两得:
string(const string& str)//拷贝构造函数的现代写法
:_str(new char[str._capacity+1])
,_size(0)
,_capacity(0)
{
string tmp(str._str);//调用构造函数,构造出一个C字符串为str._str的对象
swap(tmp);//交换这两个对象,我们在后面会介绍
}
关于拷贝构造函数我们还需要注意一点就是:传参的时候一定要传引用,如果传值的话,会再次调用拷贝构造函数,进而导致无限循环的调用拷贝构造函数。
赋值运算符重载
与拷贝构造函数一样,赋值运算符重载也涉及深浅拷贝问题,我们同样需要深拷贝,下面还是介绍传统和现代两种写法:
传统写法
我们首先要防止自己给自己赋值,然后释放原空间,开辟新空间,而后操作跟拷贝构造函数一样,最后返回值返回左值*this,以保证连续赋值。
string& operator=(const string& str)//赋值运算符重载的传统写法
{
if (this != &str)
{
delete[] _str;//释放原来的空间
_str = new char[str._capacity + 1];//开辟新的空间
strcpy(_str, str._str);//拷贝赋值
_size = str._size;
_capacity = str._capacity;
}
return *this;//返回左值(支持连续赋值)
}
现代写法
也和拷贝构造函数的现代写法类似,只不过我们这里可以直接采取传值的方式传参,在传参的过程中拷贝构造出tmp对象,因为拷贝构造函数要防止无限调用拷贝构造函数的错误,所以必须采用引用传参,而这里我们只需要用传值传参即可:
string& operator=(string str)//赋值运算符重载的现代写法1
{
swap(str);//交换两个对象
return *this;//返回左值(支持连续赋值)
}
但是这样做的弊端就是无法防止为自己赋值,当我们使用上面的operator+给自己赋值的时候,虽然操作后,对象的_str指向的字符串的内容不变,但是字符串的地址发生了改变,我们想改变的话就用下面的写法:
string& operator=(const string& str)//赋值运算符重载的现代写法2
{
if (this != &str)//防止给自己赋值
{
string tmp(str);//用str拷贝构造出对象tmp
swap(tmp);//交换两个对象
}
return *this;//返回左值(支持连续赋值)
}
析构函数
由于string内的成员对象_str指向一块从堆区开辟的空间,当对象销毁时,堆区对应的空间并不会自动销毁,所以为了避免内存泄漏,我们需要手动delete释放:
~string()
{
delete[]_str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
迭代器有关函数
string类的迭代器实际上就是字符指针,只是将char* typedef成iterator而已:
typedef char* iterator;
typedef const char* const_iterator;
begin && end
string中的begin和end函数实现的很简单:
string::iterator string::begin()
{
return _str;//返回字符串第一个字符的地址
}
string::const_iterator string::begin()const
{
return _str;//返回字符串第一个字符的const对象的地址
}
string::iterator string::end()
{
return _str + _size;//返回'\0'的地址
}
string::const_iterator string::end()const
{
return _str + _size;//返回'\0'的const对象的地址
}
所以我们在这就明白了,用迭代器遍历string对象的时候,实际上就是在用指针遍历字符数组而已:
string::iterator it = s.begin();
while (it != s.end())
{
cout <<*it;
it++;
}
cout << endl;
而且,实际上,范围for本质上也是通过迭代器来工作的,在代码编译的时候,编译器会自动将范围for替换成迭代器的形式,所以说要有迭代器的容器才会支持范围for,我们此时已经实现了我们自己的string的迭代器,所以我们可以实现范围for的使用:
for (auto& e : s)
{
cout << e;
}
cout << endl;
与容量大小有关的函数
size && capacity
size()返回的是当前字符串的有效长度,capacity()返回的是字符串的容量:
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
直接将_size和_capacity返回即可。
reserve && resize
我们首先要对这两个函数做一下区分
我们先看reserve函数:
- 当n大于对象当前capacity时,将capacity扩大到n或者大于n
- 当n小于对象当前capacity时,什么操作都不做
void reserve(size_t n)//若n>容量,才会扩容:则什么都不做
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//+1是为了放'\0'
strncpy(tmp, _str, _size + 1);//为了防止对象中有有效的字符'\0',strcpy无法拷贝
delete[]_str;
_str = tmp;
_capacity = n;
}
}
resize函数:
- 当n小于当前_size时,将_size缩小到n
- 当n大于当前_size时,将_size扩大到n,后面补的字符为c,c的缺省值为'\0'
void resize(size_t n, char c = '\0')
{
if (n <= _size)//n小于_size
{
_size = n;//_size调整为n
_str[_size] = '\0';//在第_size个字符后加\0
}
else
{
if (n > _capacity)//先看看是否用扩容
{
reserve(n);
}
for (size_t i = _size; i < n; i++)//将原先有效字符后直到第n个字符全都赋值成c
{
_str[i] = c;
}
_size = n;
_str[_size] = '\0';//字符串后面放上\0
}
}
empty
判断string对象是否为空,我们比较两个字符串的时候使用strcmp来实现,使用strcmp函数时若两个字符串大小相等返回0,两个字符串比较的时候不能使用==。
bool empty()//判断是否为空
{
return strcmp(_str, "") == 0;//两个字符串比较要用strcmp,不能直接用==
}
修改字符串相关函数
push_back
push_back的作用就是尾插一个字母,我们需要先判断是否需要增容,然后再进行尾插,而且我们需要在该字符的后面设置'\0',否则打印字符串的时候就很可能会非法越界,因为尾插的字符后面不一定就是'\0'。
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = c;
_str[_size + 1] = '\0';
_size++;
}
append
append的作用就是尾插一个字符串,我们依旧是需要先判断是否需要扩容,而后尾插。这里我们不需要在最后设置'\0',因为尾插的字符串最后自带'\0'。
void append(const char* str)
{
if (_capacity < _size + strlen(str))//若容量不够,则扩容
{
reserve(_size + strlen(str));
}
strcpy(_str + _size, str);
_size += strlen(str);
}
operator+=
operator+=的重载实现了字符串后面尾插字符和字符串的作用,我们可以直接调用上面实现的push_back和append函数:
string& operator+=(const string& str)//传string对象
{
append(str._str);
return *this;
}
string& operator+=(const char* str)//传C类型字符串
{
append(str);
return *this;
}
string& operator+=(char c)//传一个字符
{
push_back(c);
return *this;
}
insert
insert函数的目的是在任意位置插入字符或字符串,我们首先要判断pos的合法性,而后判断capacity是否能容纳插入字符或字符串后的内容,若不能则调用reserve函数进行扩容,而后进行插入:
//插入字符
string& insert(size_t pos, char c)
{
assert(pos <= _size);//检测pos是否合法
if (_size == _capacity)//判断是否需要增容
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t i = _size;
while (i >= pos)
{
_str[i + 1] = _str[i];
i--;
}
_str[pos] = c;
_size++;
return *this;
}
//插入字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//检测pos是否合法
if (_size + strlen(str) > _capacity)//判断是否需要增容
{
reserve(_size + strlen(str));
}
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + strlen(str)) = *end;
end--;
}
strncpy(_str + pos, str, strlen(str));
_size += strlen(str);
return *this;
}
我们要注意插入字符串的时候,要用strncpy,不能用strcpy,否则会将'\0'也拷贝进去。
erase
我们依然首先要判断pos是否合法,而后分两种情况进行操作。
我们这里只模拟实现下面这一种形式的erase函数:
string& erase (size_t pos = 0, size_t len = npos);
1.当pos位置及后面的有效字符都需要被删除时:
我们在pos位置上放置'\0'即可。
2.当pos位置及后面的有效字符只需要被删除一部分时:
我们将后面需要保留的有效字符覆盖前面需要删除的字符即可,此时也不用在字符串后面加'\0',因为字符串末尾有'\0'。
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);//判断pos是否合法
size_t n = _size - pos;
if (len >= n)//说明pos后面的字符全部删除
{
_size = pos;
_str[_size] = '\0';//字符串后面放上'\0'
}
else//说明pos后面还有一部分字符保留着
{
strcpy(_str + pos, _str + pos + len);//用需要保留的字符覆盖掉需要删除的字符
_size -= len;
}
return *this;
}
clear
clear函数用于将字符串清空
void clear()//将对象中存储的字符串置空
{
_size = 0;
_str[_size] = '\0';
}
swap
这里的swap函数是我们用于交换两个对象的数据,我们直接调用库里的swap模板函数将对象的各个成员变量进行交换即可,但是这样的话我们就需要在swap函数前加上std::,告诉编译器这是在std中的swap函数,否则根据就近原则,编译器会以为是我们正在实现的swap函数。
void swap(string& str)//交换两个string对象
{
std::swap(_size, str._size);//使用库函数
std::swap(_capacity, str._capacity);
std::swap(_str, str._str);
}
c_str
用于获取string对象中的C类型字符串
const char* c_str()
{
return _str;
}
用于访问字符串的函数
operator[ ]
operator[ ]是为了让string对象能够通过下标的方式进行随机访问。
1.我们通过operator[ ]的方式可能会需要进行读取和修改操作
char& operator[](size_t i)//可读可写
{
assert(i < _size);//检测下标的合法性
return _str[i];
}
2.某些场景下,我们只需要通过operator[ ]的方式读取字符而不冷修改。例如我们对一个const的string类对象进行[ ]+下标操作时就只能读,不能写。
const char& operator[](size_t i)const//只读
{
assert(i < _size);//检查下标的合法性
return _str[i];
}
find
find函数用于正向查找一个字符或者字符串,返回找到的字符或者字符串下标
1、查找第一个字符
首先要判断pos的合法性,然后遍历的从前往后找目标字符,若找到返回下标,没找到,返回npos。
size_t find(char c, size_t pos = 0)//正向寻找
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)//从pos位置开始向后找目标字符
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
2.查找第一个字符串
首先还是判断pos的合法性,然后我们通过strstr函数进行查找。strstr函数若找到了会返回目标字符串的起始地点,否则返回一个空指针。
size_t find(const char* str, size_t pos = 0)//正向寻找
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);//用strstr进行查找
if (ret)//若找到子字符串,则返回子字符串的起始位置
{
return ret - _str;//返会字符串第一个字符的下标
}
else//找不到就返回nullptr
{
return npos;//返回npos
}
}
关系运算符重载>,<,<=,>=,==,!=
>,<,<=,>=,==,!=这六个关系运算符很好模拟,我们只写几个,剩下的复用其他的即可。
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->operator>(s) || this->operator==(s));
}
bool operator!=(const string& s)const
{
return !(*this == s);
}
<<,>>运算符重载及getline函数
>>运算符重载
>>运算符重载是为了让我们能够使用>>直接进行输入。输入前我们需要先将对象中的C字符串置空,然后从标准输入流中读取字符,直到读到' '或'\n'停止。
std::istream& operator>>(std::istream& in, CYF::string& str)
{
str.clear();//先清空字符串
char ch = in.get();//读取一个字符
while (ch != ' ' && ch != '\n')//若读取的字符不是空格或\n的话,尾插到str后面后继续读
{
str.push_back(ch);
ch = in.get();
}
return in;//支持连续赋值
}
<<运算符重载
这是为了我们能直接用<<运算符进行输出,我们直接进行遍历即可。
std::ostream& operator<<(std::ostream& out, CYF::string& str)
{
for (size_t i = 0; i < str.size(); i++)
{
out << str[i];
}
return out;
}
getline
getline函数用于读取一行含有空格的字符串。直到读到'\n'的时候停下来,其余跟operator>>一样。
//getline跟>>基本相同,只不过是读取含有空格的字符串,知道读到\n的时候才停
std::istream& getline(std::istream& in, CYF::string& str)
{
str.clear();
char ch = in.get();
while (ch != '\n')
{
str.push_back(ch);
ch = in.get();
}
return in;
}
下面贴上完整代码:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <string>
#include <cassert>
#include <iostream>
namespace CYF
{
class string
{
public:
//string类的迭代器实际上就是字符指针
typedef char* iterator;
typedef const char* const_iterator;
string(const char* str="")
{
_size = strlen(str);//初始字符串有效长度
_capacity = _size;//初始字符串容量设为字符串有效长度
_str = new char[_capacity + 1];//为存储字符串开辟空间,+1是为了保存'\0'
strcpy(_str, str);//将str字符串拷贝到开好的空间
}
string(const string& str)//拷贝构造函数的传统写法
:_size(0)
,_capacity(0)
,_str(new char[_capacity + 1])
{
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
//string(const string& str)//拷贝构造函数的现代写法
// :_str(new char[str._capacity+1])
// ,_size(0)
// ,_capacity(0)
//{
// string tmp(str._str);//调用构造函数,构造出一个C字符串为str._str的对象
// swap(tmp);//交换这两个对象
//}
~string()
{
delete[]_str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
string& operator=(const string& str)//赋值运算符重载的传统写法
{
if (this != &str)
{
delete[] _str;//释放原来的空间
_str = new char[str._capacity + 1];//开辟新的空间
strcpy(_str, str._str);//拷贝赋值
_size = str._size;
_capacity = str._capacity;
}
return *this;//返回左值(支持连续赋值)
}
//string& operator=(string str)//赋值运算符重载的现代写法1
//{
// swap(str);//交换两个对象
// return *this;//返回左值(支持连续赋值)
//}
//string& operator=(const string& str)//赋值运算符重载的现代写法2
//{
// if (this != &str)//防止给自己赋值
// {
// string tmp(str);//用str拷贝构造出对象tmp
// swap(tmp);//交换两个对象
// }
// return *this;//返回左值(支持连续赋值)
//}
void swap(string& str)//交换两个string对象
{
std::swap(_size, str._size);//使用库函数
std::swap(_capacity, str._capacity);
std::swap(_str, str._str);
}
iterator begin();
const_iterator begin()const;
iterator end();
const_iterator end()const;
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
void reserve(size_t n)//若n>容量,才会扩容:则什么都不做
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//+1是为了放'\0'
strncpy(tmp, _str, _size + 1);//为了防止对象中有有效的字符'\0',strcpy无法拷贝
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char c = '\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] = c;
}
_size = n;
_str[_size] = '\0';
}
}
bool empty()//判断是否为空
{
return strcmp(_str, "") == 0;//两个字符串比较要用strcmp,不能直接用==
}
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = c;
_str[_size + 1] = '\0';
_size++;
}
void append(const char* str)
{
if (_capacity < _size + strlen(str))//若容量不够,则扩容
{
reserve(_size + strlen(str));
}
strcpy(_str + _size, str);
_size += strlen(str);
}
string& operator+=(const string& str)
{
append(str._str);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& insert(size_t pos, char c)
{
assert(pos <= _size);//检测pos是否合法
if (_size == _capacity)//判断是否需要增容
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t i = _size;
while (i >= pos)
{
_str[i + 1] = _str[i];
i--;
}
_str[pos] = c;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);//检测pos是否合法
if (_size + strlen(str) > _capacity)//判断是否需要增容
{
reserve(_size + strlen(str));
}
char* end = _str + _size;
while (end >= _str + pos)
{
*(end + strlen(str)) = *end;
end--;
}
strncpy(_str + pos, str, strlen(str));
_size += strlen(str);
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);//判断pos是否合法
size_t n = _size - pos;
if (len >= n)//说明pos后面的字符全部删除
{
_size = pos;
_str[_size] = '\0';//字符串后面放上'\0'
}
else//说明pos后面还有一部分字符保留着
{
strcpy(_str + pos, _str + pos + len);//用需要保留的字符覆盖掉需要删除的字符
_size -= len;
}
return *this;
}
void clear()//将对象中存储的字符串置空
{
_size = 0;
_str[_size] = '\0';
}
const char* c_str()
{
return _str;
}
char& operator[](size_t i)//可读可写
{
assert(i < _size);//检测下标的合法性
return _str[i];
}
const char& operator[](size_t i)const//只读
{
assert(i < _size);//检查下标的合法性
return _str[i];
}
size_t find(char c, size_t pos = 0)//正向寻找
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)//从pos位置开始向后找目标字符
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)//正向寻找
{
assert(pos < _size);
const char* ret = strstr(_str + pos, str);//用strstr进行查找
if (ret)//若找到子字符串,则返回子字符串的起始位置
{
return ret - _str;//返会字符串第一个字符的下标
}
else//找不到就返回nullptr
{
return npos;//返回npos
}
}
void reverse(iterator left, iterator right)
{
right = right - 1;
while (left < right)
{
char c = '\0';
c = *left;
*left = *right;
*right = c;
left++;
right--;
}
}
//size_t rfind(char c, size_t pos = npos)
//{
// string tmp(*this);
// reverse(tmp.begin(), tmp.end());
// if (pos > _size)
// {
// pos = _size - 1;//若pos大于等于字符串有效长度时,看作pos为字符串最后一个字符的下标
// }
// pos = _size - 1 - pos;//将pos改为镜像对称后的位置
// size_t ret = tmp.find(c, pos);
// if (ret != npos)
// return _size - 1 - ret;//若找到了,返回ret镜像对称后的位置
// else
// return npos;//若没找到,返回npos
//}
//size_t rfind(const char* str, size_t pos= npos)
//{
// string tmp(*this);//拷贝构造对象tmp
// reverse(tmp.begin(), tmp.end());//逆置tmp的C字符串
// size_t len = strlen(str);//待查找的字符串长度
// char* arr = new char[len + 1];//开辟空间,用于拷贝待查找的字符串
// strcpy(arr, str);
// std::cout << arr << std::endl;
// //逆置待查找的字符串
// size_t left = 0;
// size_t right = len - 1;
// while (left < right)
// {
// std::swap(arr[left], arr[right]);
// left++;
// right--;
// }
// if (pos >= _size)//pos大于字符串有效长度,pos设为字符串最后一个字符的下标
// {
// pos = _size - 1;
// }
// pos = _size - 1 - pos;//将pos改为镜像对称后的位置
// size_t ret = tmp.find(arr, pos);//复用find函数正向查找
// delete[]arr;
// if (ret != npos)
// {
// return _size - ret - len;//找到了,返回ret再镜像逆置回去的位置
// }
// else
// {
// return npos;//没找到,返回npos
// }
//}
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->operator>(s) || this->operator==(s));
}
bool operator!=(const string& s)const
{
return !(*this == s);
}
private:
char* _str;//存储字符串
size_t _size;//字符串当前有效长度
size_t _capacity;//当前字符串最大容量
static const size_t npos;//整形最大值(很多地方的默认值)
};
const size_t string::npos = (size_t) - 1;
string::iterator string::begin()
{
return _str;//返回字符串第一个字符的地址
}
string::const_iterator string::begin()const
{
return _str;//返回字符串第一个字符的const地址
}
string::iterator string::end()
{
return _str + _size;//返回'\0'的地址
}
string::const_iterator string::end()const
{
return _str + _size;//返回'\0'的const地址
}
std::istream& operator>>(std::istream& in, CYF::string& str)
{
str.clear();//先清空字符串
char ch = in.get();//读取一个字符
while (ch != ' ' && ch != '\n')//若读取的字符不是空格或\n的话,尾插到str后面后继续读
{
str.push_back(ch);
ch = in.get();
}
return in;//支持连续赋值
}
std::ostream& operator<<(std::ostream& out, CYF::string& str)
{
for (size_t i = 0; i < str.size(); i++)
{
out << str[i];
}
return out;
}
//getline跟>>基本相同,只不过是读取含有空格的字符串,知道读到\n的时候才停
std::istream& getline(std::istream& in, CYF::string& str)
{
str.clear();
char ch = in.get();
while (ch != '\n')
{
str.push_back(ch);
ch = in.get();
}
return in;
}
}
大家可能会发现,我的代码中还实现了一个rfind,但是rfind函数我一直没找到错在了哪里,因为他会在析构函数处报内存泄漏的错误,如果大家发现哪里出错了,欢迎大家在评论区留言或私信我!!谢谢大家!