namespace wyt
{
//模拟实现string类
class string
{
public:
typedef char* iterator;//迭代器
typedef const char* const_iterator;//const迭代器
//默认成员函数
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;
//容量和大小相关函数
size_t size();
size_t capacity();
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
bool empty()const;
//修改字符串相关函数
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len);
void clear();
void swap(string& s);
const char* c_str()const;
//访问字符串相关函数
char& operator[](size_t i);//可读可写
const char& operator[](size_t i)const;//只读
size_t find(char ch, size_t pos = 0)const;
size_t find(const char* str, size_t pos = 0)const;
size_t rfind(char ch, size_t pos = npos)const;
size_t rfind(const char* str, size_t pos = 0)const;
//关系运算符重载函数
bool operator>(const string& s)const;
bool operator>=(const string& s)const;
bool operator<(const string& s)const;
bool operator<=(const string& s)const;
bool operator==(const string& s)const;
bool operator!=(const string& s)const;
private:
char* _str; //存储字符串
size_t _size; //记录字符串当前的有效长度
size_t _capacity; //记录字符串当前的容量
const static size_t npos = -1; //静态成员变量(整型最大值)
//特例--c++允许const static整型在类里可以直接声明缺省值
};
//<<和>>运算符重载函数
istream& operator>>(istream& in, string& s);
ostream& operator<<(ostream& out, const string& s);
istream& getline(istream& in, string& s);
}
注: 为了防止与标准库当中的string类产生命名冲突,模拟实现时需放在自己的命名空间当中
构造函数设置为缺省参数,若不传入参数,则默认构造为空字符串。字符串的初始大小和容量均设置为传入C字符串的长度(不包括’\0’)
string(const char* str = "")
{
_size = strlen(str);//初始时,字符串大小设置为字符串长度
_capacity = _size;//初始时,字符串容量设置为字符串长度
_str = new char[_capacity + 1];//为存储字符串开辟空间(多开一个用于存放'\0')
strcpy(_str, str);//将C字符串拷贝到已开好的空间
}
在模拟实现拷贝构造函数前,我们应该首先了解深浅拷贝:
浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法:
写法一:传统写法
//拷贝构造
string(const string& str)
{
_str = new char[str._capacity + 1];
_capacity = str._capacity;
_size = str._size;
strcpy(_str, str._str);
}
传统写法的思想简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的。
写法二:现代写法
通过构造一个临时对象,将这个临时对象的私有变量全部和*this的私有变量交换
注意拷贝构造需要先将_str初始化为nullptr,防止后续tmp拿到随机地址。(tmp销毁将调用析构函数,对一块随机地址的空间进行析构程序将会崩溃)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
:_str(nullptr)//防止交换后tmp._str为随机值,析构出错
{
string tmp(s._str());//构造 ->等价于string tmp("hello world")
swap(tmp);//this->swap(tmp);
}
与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。下面也提供深拷贝的两种写法:
写法一:传统写法
赋值运算符重载函数的传统写法与拷贝构造函数的传统写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。
//赋值运算符重载
string& operator=(const string& str)
{
if (this != &str)//防止自己给自己赋值
{
char* tmp = new char[str._capacity + 1];//重新申请一块刚好可以容纳str._str的空间
strcpy(tmp, str._str);//拷贝
delete[] _str;//将原来_str指向的空间释放
_str = tmp;//指向新空间
_size = str._size;
_capacity = str._capacity;
}
return *this;
}
写法二:现代写法
赋值运算符重载函数的现代写法与拷贝构造函数的现代写法也是非常类似,但拷贝构造函数的现代写法是通过代码语句调用构造函数构造出一个对象,然后将该对象与拷贝对象交换;而赋值运算符重载函数的现代写法是通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代写法1
string& operator=(string s) //编译器接收右值的时候自动调用拷贝构造函数
{
swap(s); //交换这两个对象
return *this; //返回左值(支持连续赋值)
}
但这种写法无法避免自己给自己赋值,就算是自己给自己赋值这些操作也会进行,虽然操作之后对象中_str指向的字符串的内容不变,但是字符串存储的地址发生了改变,为了避免这种操作我们可以采用下面这种写法:
// s1 = s3;
string& operator=(const string& s)
{
if (this != &s)//防止自己给自己赋值
{
//string tmp(s._str);//构造
string tmp(s);//用s拷贝构造出对象tmp
//this->swap(tmp);
swap(tmp);//交换这两个对象
}
return *this;//返回左值(支持连续赋值)
}
但实际中很少出现自己给自己赋值的情况,所以采用“现代写法1”就行了。
string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间,而delete在内存管理文章中讲到,会自动调用析构函数。
//析构函数
~string()
{
delete[] _str; //释放_str指向的空间
_str = nullptr;//及时置空,防止非法访问
_capacity = _size = 0;
}
string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。
typedef char* iterator;
typedef const char* const_iterator;
注: 不是所有的迭代器都是指针。
string类中的begin和end函数的实现很简单,begin函数的作用就是返回字符串中第一个字符的地址:
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
const_iterator begin()const
{
return _str;
}
end函数的作用就是返回字符串中最后一个有效字符的后一个字符的地址(即’\0’的地址):
iterator end()
{
return _str + _size;
}
const_iterator end()const
{
return _str + _size;
}
在明白了string类中迭代器的底层实现,再来看看我们用迭代器遍历string的代码,其实就是用指针在遍历字符串而已。
void test_string()
{
string s1("hello world");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << (*it) << " ";
it++;
}
cout << endl;
}
在string的接口使用介绍中我们还说到,可以用范围for来遍历string,可能很多初学者都会觉得范围for是个很神奇的东西,只需要一点点代码就能实现string的遍历。
实际上范围for并不神奇,因为在代码编译的时候,编译器会自动将范围for替换为迭代器的形式,也就是说范围for是由迭代器支持的,现在我们已经实现了string类的迭代器,自然也能用范围for对string进
行遍历:
void test_string()
{
string s1("hello world");
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;
}
因为string类的成员变量是私有的,我们并不能直接对其进行访问,所以string类设置了size和capacity这两个成员函数,用于获取string对象的大小和容量。
size函数用于获取字符串当前的有效长度(不包括’\0’)。
size_t size()const
{
return _size;
}
capacity函数用于获取字符串当前的容量。
size_t capacity() const
{
return _capacity;
}
resize和reserve这两个函数的执行规则一定要区分清楚。
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
void reserve(size_t n)
{
if(n > _capacity)//当n大于对象当前容量时才需执行操作
{
char* tmp = new char[n + 1];//多开一个空间用于存放'\0'
strcpy(tmp, _str);
delete[] _ str;
_str = tmp;
_capacity = n;
}
resize规则:
void resize(size_t n, char ch = '\0')
{
if (n > _size)//如果大于_size,就需要判断是否需要扩容
{
reserve(n);//如果n大于_capacity扩容,不大于就不会扩容,在reserve中判断
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;//会将_size的大小改为n
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
empty是string的判空函数, 我们可以通过_size来判断,如果_size等于0,则表示字符串为空。
bool empty()const
{
return _size == 0 ? true : false;
}
clear函数用于将对象中存储的字符串置空,实现时直接将对象的_size置空,然后在字符串后面放上’\0’即可。
void clear()
{
_size = 0;
_str[0] = '\0';
}
push_back函数的作用就是在当前字符串的后面尾插上一个字符,尾插之前首先需要判断是否需要增容,若需要,则调用reserve函数进行增容,然后再尾插字符,注意尾插完字符后需要在该字符的后方设置上’\0’,否则打印字符串的时候会出现非法访问,因为尾插的字符后方不一定就是’\0’。
void push_back(char ch)
{
if (_size == _capacity)//判断是否需要增容
{
size_t newcapacity = _capacity == 0 ? 4 : (_capacity * 2);//将容量扩大为原来的两倍
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
注:增容时以二倍的形式进行增容,避免多次调用push_back函数时每次都需要调用reserve函数。
实现push_back还可以直接复用下面即将实现的insert函数。
//尾插字符
void push_back(char ch)
{
insert(_size, ch); //在字符串末尾插入字符ch
}
append函数的作用是在当前字符串的后面尾插一个字符串,尾插前需要判断当前字符串的空间能否容纳下尾插后的字符串,若不能,则需要先进行增容,然后再将待尾插的字符串尾插到对象的后方,因为待尾插的字符串后方自身带有’\0’,所以我们无需再在后方设置’\0’。
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)//判断是否需要增容
{
reserve(len + _size);
}
strcpy(_str + _size, str);//将str尾插到字符串后面
_size += len;//字符串大小改变
}
实现append函数也可以直接复用下面即将实现的insert函数。
//尾插字符串
void append(const char* str)
{
insert(_size, str); //在字符串末尾插入字符串str
}
+=运算符的重载是为了实现字符串与字符、字符串与字符串之间能够直接使用+=运算符进行尾插。
+=运算符实现字符串与字符之间的尾插直接调用push_back函数即可。
//+=运算符重载,+=一个字符
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
+=运算符实现字符串与字符串之间的尾插直接调用append函数即可。
string& operator+=(const char* str)
{
append(str);
return *this;
}
insert函数的作用是在字符串的任意位置插入字符或是字符串。
insert函数用于插入字符时,首先需要判断pos的合法性,若不合法则无法进行操作,紧接着还需判断当前对象能否容纳插入字符后的字符串,若不能则还需调用reserve函数进行扩容。插入字符的过程也是比较简单的,先将pos位置及其后面的字符统一向后挪动一位,给待插入的字符留出位置,然后将字符插入字符串即可。
注意: 在进行头插时需要注意循环变量的类型,否则容易陷入死循环。
//在pos位置插入一个字符
string& insert(size_t pos, char ch)
{
assert(pos <= _size); //检测下标的合法性
if (_size == _capacity) //判断是否需要增容
{
size_t newcapacity = _capacity == 0 ? 4 : (_capacity * 2);
reserve(newcapacity);
}
//挪动数据
/*size_t end = _size;
while (end >= pos)//会陷入死循环,因为当进行头插时,end由于是无符号整形,当end再次--,就会变成一个超大的数字,从而陷入死循环
{
_str[end + 1] = _str[end];
--end;
}*/
//方法一:
/*int end = _size;
while (end >= (int)pos)//pos强转后end就不会发生整形提升,否则就会发生整形提升。
{
_str[end + 1] = _str[end];
--end;
}*/
//方法二:
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
insert函数用于插入字符串时,首先也是判断pos的合法性,若不合法则无法进行操作,再判断当前对象能否容纳插入该字符串后的字符串,若不能则还需调用reserve函数进行扩容。插入字符串时,先将pos位置及其后面的字符统一向后挪动len位(len为待插入字符串的长度),给待插入的字符串留出位置,然后将其插入字符串即可。
//在pos位置插入一个字符串
string& insert(size_t pos, const char* str)
{
assert(pos <= _size); //检测下标的合法性
size_t len = strlen(str); //计算需要插入的字符串的长度(不含'\0')
if (_size + len > _capacity)//判断是否需要增容
{
reserve(len + _size);
}
//挪动数据
//方法一:
/*int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}*/
//方法二:
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
//插入数据
strncpy(_str + pos, str, len);//pos位置开始放上指定字符串
_size += len;//size更新
return *this;
}
注意: 插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的’\0’也插入到字符串中。
erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:
1、若没有传删除字符的个数n,则pos位置及其之后的有效字符都需要被删除。
这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。
2、pos位置及其之后的有效字符只需删除一部分。
这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);//检测下标的合法性
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';//字符串后面放上'\0'
_size = pos;//size更新
}
else
{
strcpy(_str + pos, _str + pos + len);//用需要保留的有效字符覆盖需要删除的有效字符
_size -= len;
}
return *this;
}
swap函数用于交换两个对象的数据,直接调用库里的swap模板函数将对象的各个成员变量进行交换即可。但我们若是想在这里调用库里的swap模板函数,需要在swap函数之前加上“::”(作用域限定符),告诉编译器优先在全局范围寻找swap函数,否则编译器编译时会认为你调用的是正在实现的swap函数(就近原则)。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
c_str函数用于获取对象C类型的字符串,实现时直接返回对象的成员变量_str即可。
//返回C类型的字符串
const char* c_str() const
{
return _str;
}
[ ]运算符的重载是为了让string对象能像C字符串一样,通过[ ] +下标的方式获取字符串对应位置的字符。
在C字符串中我们通过[ ] +下标的方式可以获取字符串对应位置的字符,并可以对其进行修改,实现[ ] 运算符的重载时只需返回对象C字符串对应位置字符的引用即可,这样便能实现对该位置的字符进行读取和修改操作了,但需要注意在此之前检测所给下标的合法性。
// 普通对象:可读可写
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
在某些场景下,我们可能只能用[ ] +下标的方式读取字符而不能对其进行修改。例如,对一个const的string类对象进行[ ] +下标的操作,我们只能读取所得到的字符,而不能对其进行修改。所以我们需要再重载一个[ ] 运算符,用于只读操作。
// const对象:只读
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
find函数和rfind函数都是用于在字符串中查找一个字符或是字符串,find函数和rfind函数分别用于正向查找和反向查找,即从字符串开头开始向后查找和从字符串末尾开始向前查找。
find函数:
1、正向查找第一个匹配的字符。
首先判断所给pos的合法性,然后通过遍历的方式从pos位置开始向后寻找目标字符,若找到,则返回其下标;若没有找到,则返回npos。(npos是string类的一个静态成员变量,其值为整型最大值)
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
2、正向查找第一个匹配的字符串。
首先也是先判断所给pos的合法性,然后我们可以通过调用strstr函数进行查找。strstr函数若是找到了目标字符串会返回字符串的起始位置,若是没有找到会返回一个空指针。若是找到了目标字符串,我们可以通过计算目标字符串的起始位置和对象C字符串的起始位置的差值,进而得到目标字符串起始位置的下标。
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str); //调用strstr进行查找
if (ptr == nullptr)//ptr为空指针,说明没有找到
{
return npos;
}
else//ptr不为空指针,说明找到了
{
return ptr - _str;
}
}
rfind函数:
实现rfind函数时,我们可以考虑复用已经写好了的两个find函数,但rfind函数是从后先前找,所以我们需要将对象的C字符串逆置一下,若是查找字符串,还需将待查找的字符串逆置一下,然后调用find函数进行查找,但注意传入find函数的pos以及从find函数接收到的pos都需要镜像对称一下。
1、反向查找第一个匹配的字符。
首先我们需要用对象拷贝构造一个临时对象tmp,因为我们并不希望调用rfind函数后对象的C字符串就被逆置了。我们将tmp对象的C字符串逆置,然后将所给pos镜像对称一下再调用find函数,再将从find函数接收到的返回值镜像对称一下作为rfind函数的返回值返回即可。
//反向查找第一个匹配的字符
size_t rfind(char ch, size_t pos = npos)
{
string tmp(*this); //拷贝构造对象tmp
reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
if (pos >= _size) //所给pos大于字符串有效长度
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为镜像对称后的位置
size_t ret = tmp.find(ch, pos); //复用find函数
if (ret != npos)
return _size - 1 - ret; //找到了,返回ret镜像对称后的位置
else
return npos; //没找到,返回npos
}
注: rfind函数规定,当所给的pos大于等于字符串的有效长度时,看作所给pos为字符串最后一个字符的下标。
2、反向查找第一个匹配的字符串。
首先我们还是需要用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,同时我们还需要拷贝一份待查找的字符串,也将其逆置。然后将所给pos镜像对称一下再调用find函数。注意:此时我们将从find函数接收到的值镜面对称后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置,而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置,所以还需做进一步调整后才能作为rfind函数的返回值返回。
//反向查找第一个匹配的字符串
size_t rfind(const char* str, size_t pos = npos)
{
string tmp(*this); //拷贝构造对象tmp
reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的C字符串
size_t len = strlen(str); //待查找的字符串的长度
char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串)
strcpy(arr, str); //拷贝str给arr
size_t left = 0, right = len - 1; //设置左右指针
//逆置字符串arr
while (left < right)
{
::swap(arr[left], arr[right]);
left++;
right--;
}
if (pos >= _size) //所给pos大于字符串有效长度
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为镜像对称后的位置
size_t ret = tmp.find(arr, pos); //复用find函数
delete[] arr; //销毁arr指向的空间,避免内存泄漏
if (ret != npos)
return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
else
return npos; //没找到,返回npos
}
关系运算符有 >、>=、<、<=、==、!= 这六个,但是对于C++中任意一个类的关系运算符重载,我们均只需重载其中的两个,剩下的四个关系运算符可以通过复用已经重载好了的两个关系运算符来实现。
例如,对于string类,我们可以选择只重载 > 和 == 这两个关系运算符。
//>运算符重载
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) || (*this == s);
}
//<运算符重载
bool operator<(const string& s)const
{
return !(*this >= s);
}
//<=运算符重载
bool operator<=(const string& s)const
{
return !(*this > s);
}
//!=运算符重载
bool operator!=(const string& s)const
{
return !(*this == s);
}
重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印。实现时我们可以直接使用for循环对对象进行遍历即可。
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;//支持连续输出
}
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。
istream& operator>>(istream& in, string& s)
{
s.clear();//清空字符串
//方法一
//char ch = in.get();//读取一个字符
//while (ch != ' ' && ch != '\n')//当读取到的字符不是空格或'\n'的时候继续读取
//{
// s += ch;//将读取到的字符尾插到字符串后面
// ch = in.get();//继续读取字符
//}
//优化 -- 防止多次扩容
char buff[128] = { '\0' };
size_t i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
// 满了
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;//支持连续输入
}
getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。
istream& getline(istream& in, string& s)
{
s.clear();
//char ch = in.get();
//while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
//{
// s += ch;
// //in >> ch;
// ch = in.get();
//}
//优化 -- 防止多次扩容
char buff[128] = { '\0' };
size_t i = 0;
char ch = in.get();
while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取
{
if (i == 127)
{
// 满了
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}