#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include
#include
namespace tao
{
class string
{
public:
typedef char* iterator;//普通迭代器
typedef const char* const_iterator; //const迭代器
//无参构造---不代表没有数据,是有一个\0的,但没有大小,因为\0不计入大小计算中
/* string()
{
_size = _capacity = 0;
_str = new char[1];
_str[0] = '\0';
}*/
string(const char* str="")//可以与无参构造一起复用。全缺省//常量字符串后面默认有\0
{
_size = strlen(str);
_capacity = _size;
//_str=str
//不能直接将str赋给_str,因为str是const修饰的,库里规定的,不可以被修改,而_str不是const类型的
//而需要开辟一块跟str一样大的空间,然后拷贝给_str
_str = new char[_capacity + 1];
memcpy(_str, str,_size+1);
}
string (const string& s)//深拷贝
{
_str = new char[s._capacity+1];
memcpy(_str, s._str,s.size()+1);
_size = s._size;
_capacity = s._capacity;
}
//s1=s3
string& operator=(const string& s)
{
if (*this != s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s.c_str(), s._size);
delete[] _str;
_str = tmp;
_size = s.size();
_capacity = s._capacity;
}
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
//要实现遍历,首先需向大小
size_t size() const //一般只读,不给修改
{
return _size;
}
char& operator[](int pos)//可以引用返回,因为出了函数值还在
{
assert(pos < _size);
return _str[pos];
}
//有两种重载类型,一种是上面的另一种是const修饰的对象,只读,不给修改的
const char& operator[](int pos) const
{
assert(pos < _size);
return _str[pos];
}
//通过迭代器进行遍历,迭代器是一种类型,是string类里的一种类型,可以是内部类,也可以是自定义的。
//我们在这里自定义一个iterator。
iterator begin()//begin返回的是指向开头位置的迭代器
{
return _str;
}
iterator end()//end返回的是指向最后一个字符的下一个位置
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1];
memcpy(temp, _str,_size+1);
delete[] _str;
_str = temp;
_capacity = n;
}
}
//增
void push_back(char ch)//尾插首秀按需要考虑是否需要扩容--->扩容最好用reserve来扩容
{
if (_size >= _capacity)
{
//可以直接扩容2倍,但要注意一种情况,当为空串时
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void append(const char * str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//这个不可以直接2倍扩容,因为可能2倍扩容后的容量还不够
//至少需要扩容到_size+len大小
reserve(_size + len);
memcpy(_str + _size, str,len+1);
_size += len;
}
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void insert(size_t pos, size_t n, char ch)
{
//第一步检查pos的合法性
assert(pos <= _size);
//检查是否需要扩容---》直接用reserve扩容
if (_size+n > _capacity)
{
reserve(_size + n);
}
//第三步挪动数据
size_t end = _size;
//这里有一个坑,当pos位置为0时,也就是头插时会出问题,因为while循环的调试是end>=pos
//也就是end需要小于0时才可以停下来,当end=0时,进入循环里,end--,后不会变成-1,因为end是size_t,会变成很大是数
//所以有问题,解决方法是再加上一个条件,那就是end>=pos&&end!=npos时当满足这两个条件时
while (end >= pos&&end!=npos)
{
_str[end + n] = _str[end];
end--;
}
for (int i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
void insert(size_t pos, const char* str)
{
//第一步检查pos的合法性
assert(pos <= _size);
//检查是否需要扩容---》直接用reserve扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
//删
void erase(size_t pos, size_t len=npos)
{
assert(pos <= _size);
if (len == npos || pos + len > _size)//删除完
{
_str[pos] = '\0';
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
//查/改
size_t find(char ch, size_t pos = 0)
{
assert(pos <= _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos <= _size);
const char* tmp = strstr(_str+pos, str);//返回的是指向str位置的指针
if (tmp == nullptr)
{
return npos;
}
return tmp - _str;
}
string substr(size_t pos,size_t len=npos)
{
assert(pos <= _size);
size_t n = len;
if (len == npos || pos + len > _size)
{
n = _size - pos;
}
string tmp;
for (size_t i = pos; i < n + pos; i++)
{
tmp += _str[i];
}
return tmp;
}
void resize(size_t n,char ch='\0')
{
if (n < _size)
_size = n;
else
{
reserve(n);//不管n是否大于capacity都给他扩容到n即可
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
//比较大小
bool operator<(const string& s)
{
int i1 = 0;
int i2 = 0;
while (i1 < _size && i2 < s.size())
{
if (_str[i1] < _str[i2])
{
return true;
}
else if (_str[i1] > _str[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
//"tao" "taox"
//taox tao
//tao tao
if (i1 == _size && i2 != s.size())
{
return true;
}
else
{
return false;
}
}
bool operator==(const string& s)
{
return _size == s.size() && memcmp(_str, s.c_str(), _size);
}
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);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
private:
char* _str;
size_t _size;
size_t _capacity;
public:
size_t static npos;
};
size_t string::npos = -1;
/* ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}*/
istream& operator>>(istream& in, string& s)
{
//每次进入流提取之前都要把之前的缓存清理掉。
s.clear();
char ch;
ch = in.get();//in流提取,会跳过空格和换行
//但是这样很麻烦,需要不断的扩容,从小扩到大。
//
//还要清理字符之前的空格和换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
//所有又采取一种方法,把ch提取的字符县附近一个数组里面
//这样扩容就不会频繁的扩容,一段一段的扩容,累加一定量再放进去
char buf[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buf[i++] = ch;
if (i == 127)
{
buf[i] = '\0';
s += buf;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buf[i] = '\0';
s += buf;
}
return in;
}
};
//c的字符数组以\0为终止长度
//string不看\0,以size为终止长度
//成员变量
private:
char* _str;
size_t _size;
size_t _capacity;
1.无参构造和带参构造可以一起复用。
2.构造初始化 不可以这样写_str=str。
不能直接将str赋给_str,因为str是const修饰的,库里规定的,不可以被修改,而_str不是const类型的,而需要开辟一块跟str一样大的空间,然后拷贝给_str。这才是正确的做法。
3.拷贝构造采取的是深度拷贝,一般分成三步。
①首先需要给_str开辟一块跟要拷贝对象一样大的空间。
②将要拷贝的对象的值拷贝给要创建的对象
③初始化对象的大小与容量。
//无参-----不代表没有数据,是有一个\0的,但没有大小,因为\0不计入大小计算中
/* string()
{
_size = _capacity = 0;
_str = new char[1];
_str[0] = '\0';
}*/
//带参构造----可以与无参构造一起复用。全缺省//常量字符串后面默认有\0
string(const char* str="")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
memcpy(_str, str,_size+1);
}
//拷贝构造
string (const string& s)//深度拷贝
{
_str = new char[s._capacity+1];
memcpy(_str, s._str,s.size()+1);
_size = s._size;
_capacity = s._capacity;
}
赋值运算符重载的做法跟拷贝类似,但有一步不同。那就是需要释放_str对象的空间,因为拷贝,原对象是没有空间的,所有不需要释放。
①首先开辟一块跟赋值对象一样大小的空间,由temp指向。
②然后将赋值对象的值拷贝到tmp里。
③将被赋值对_str象的空间释放。
④最后将tmp赋给_str.
⑤将_str对象的大小和容量都与s对象一致。
//s1=s3
string& operator=(const string& s)
{
if (*this != s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s.c_str(), s._size);
delete[] _str;
_str = tmp;
_size = s.size();
_capacity = s._capacity;
}
return *this;
}
析构,要与new[]对应用delete[]释放空间。
然后将指针置空,大小容量置0
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
1.遍历,首先需要知道对象的大小,这里直接用了,因为size的写法在下面,可以去下面看。
2.而遍历的方法有多种,其中有下标+[]遍历,利用迭代器遍历,或者范围for遍历。
3.下标+[pos]方法首先需要判断pos位置是否合法。
4.下面给了两个重载,一个是用于普通对象遍历,一个用于const修饰的对象遍历。也就是一个可读可写,另一个只读不能写。
//可以引用返回,因为出了函数值还在
char& operator[](int pos)
{
assert(pos < _size);
return _str[pos];
}
//有两种重载类型,一种是上面的另一种是const修饰的对象,只读,不给修改的
const char& operator[](int pos) const
{
assert(pos < _size);
return _str[pos];
}
1.iterator其实在string里来说本质上可以看成一个指针类型。
2.而想要定义一个新的类型,要么使用内部类,或者自己typedef定义一个,这里iterator是自己定义。
3.定义完iterator类型后,就可以写begin()和end()了。begin返回的是指向开头位置的迭代器,end返回的是指向最后一个字符的下一个位置。
//我们在这里自定义一个iterator。
typedef char* iterator;//普通迭代器
typedef const char* const_iterator; //const迭代器
//通过迭代器进行遍历,迭代器是一种类型,是string类里的一种类型,可以是内部类,也可以是自定义的。
iterator begin()//begin返回的是指向开头位置的迭代器
{
return _str;
}
iterator end()//end返回的是指向最后一个字符的下一个位置
{
return _str + _size;
}
// const修饰的对象进行遍历
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
范围for其实底层就是迭代器。
如果迭代器写正确了,那么范围for就可以用了。
这里可以演示一下范围for如何使用。
tao::string::iterator it = s1.begin();
while (cit != s1.end())
{
cout << (*cit);
cit++;
}
for (auto ch : s1)//范围for底层其实就是迭代器
{
cout << ch << endl;
}
1.在尾插之前我们需要干什么呢?首先需要考虑是否需要扩容。
2.怎么扩容呢?我们可以直接利用reserve来扩容,这里直接使用reserve()。
3.当实际数据的大小超过容量大小时,我们就需要进行扩容。扩多大呢?一般是扩2倍。
4.但要考虑一种情况那就是一开始容量为0,那扩容2倍后还是0,所以需要讨论一下。
5.扩完容,就可以将字符插入到尾部了,插完后,size需要++,要将最后一位放入’\0’。因为本来str最后一位就是’\0’,现在被覆盖了,就需要手动添加上了。
void push_back(char ch)//尾插首秀按需要考虑是否需要扩容--->扩容最好用reserve来扩容
{
if (_size >= _capacity)
{
//可以直接扩容2倍,但要注意一种情况,当为空串时,就不能用2倍乘了,直接赋给4即可。
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
1.跟尾插一个字符一样,现在要尾插一个字符串,第一步仍然需要考虑是否要扩容。
2.扩容还是用我们的reserve来扩容,那扩容多大呢?因为尾插的是一个字符串(字符串长度为len),如果要扩2倍的话也有可能扩容后还是不够,所以至少要扩容到size+len个长度。
3.扩容完,就可以将要尾插的字符串直接用memcpy拷贝过去,然后需要将大小控制一致。
void append(const char * str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
//这个不可以直接2倍扩容,因为可能2倍扩容后的容量还不够
//至少需要扩容到_size+len大小
reserve(_size + len);
}
memcpy(_str + _size, str,len+1);
_size += len;
}
1.其实尾插一个字符或者字符串最喜欢的不是push_back和append,最好用的是+=。
2.+=运算符重载其实就是直接复用这两个函数即可。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
1.insert成员函数,这里写了两个,一个是用来插入字符的,一个用来插入字符串。
2.功能:在某个位置pos插入n个字符/在某个位置插入一个字符串。
①首先第一步需要检查pos位置的合法性。
②第二步检查是否需要扩容,直接用reserve()扩容即可。
③第三步挪动数据,从后往前挪动。要插入n个字符,从后面开始的位置上每个字符就需要挪动n个位置,这样pos位置上才可以留出n个位置。
④将n个字符插入到留出的位置上去。大小需要控制一致。
3.注意当pos为0时的坑,需要增加额外条件来判断,end>=pos&&end!=npos.。npos是一个静态变量,需要在类里声明,类外定义喔。
void insert(size_t pos, size_t n, char ch)
{
//第一步检查pos的合法性
assert(pos <= _size);
//检查是否需要扩容---》直接用reserve扩容
if (_size+n > _capacity)
{
reserve(_size + n);
}
//第三步挪动数据
size_t end = _size;
//这里有一个坑,当pos位置为0时,也就是头插时会出问题,因为while循环的调试是end>=pos
//也就是end需要小于0时才可以停下来,当end=0时,进入循环里,end--,后不会变成-1,因为end是size_t,会变成很大是数
//所以有问题,解决方法是再加上一个条件,那就是end>=pos&&end!=npos时当满足这两个条件时
while (end >= pos&&end!=npos)
{
_str[end + n] = _str[end];
end--;
}
for (int i = 0; i < n; i++)
{
_str[pos + i] = ch;
}
_size += n;
}
void insert(size_t pos, const char* str)
{
//第一步检查pos的合法性
assert(pos <= _size);
//检查是否需要扩容---》直接用reserve扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + len] = _str[end];
end--;
}
for (int i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
1.erase()删除某个位置len个字符,len给了缺省值npos,也就是不写长度时,默认从pos位置一直删除到尾。
2.所以会出现两种情况:删除后面的全部字符/删除后面的部分字符。这取决于len是否给值和len是否大于size-pos
3.当没有给len值是使用缺省值,那将会删除完后面的,当len大于size-pos时,也会将后面的删除完。将后面删除完并不需要真的将后面的数据全部删除,只需要将pos位置改成’\0’即可,并将大小修改即可。
4.而pos位置后面不完全删除,就需要挪动数据覆盖了。从后往前覆盖。最后需要将大小减去len长度。
void erase(size_t pos, size_t len=npos)
{
assert(pos <= _size);
if (len == npos || pos + len > _size)//删除完
{
_str[pos] = '\0';
_size = pos;
_str[_size] = '\0';
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos++] = _str[end++];
}
_size -= len;
}
}
1.删除数据并不需要真的删除,只需要将第一个位置上修改成’\0’即可。
2.大小修改成0.
void clear()
{
_str[0] = '\0';
_size = 0;
}
//要实现遍历,首先需向大小
size_t size() const //一般只读,不给修改
{
return _size;
}
1.find()可以从某个位置开始查找某个字符,返回改字符的位置。或者查找某个字符串,返回该字符串的位置。
2.从某个位置开始查找字符:
①首先需要判断pos位置是否合法。
②直接利用遍历从pos位置开始查找该字符ch
③如果找到直接返回该下标,如果没有找到则返回npos。
3.从某个位置开始查找字符串。
①首先需要判断pos位置合法性
②可以直接利用string.h库函数strstr来查找字符串,找到智慧返回指向该字符起始位置的指针。
③指针-指针等于长度,所以tmp减去起始位置就是tmp的位置。
size_t find(char ch, size_t pos = 0)
{
assert(pos <= _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos <= _size);
const char* tmp = strstr(_str+pos, str);//返回的是指向str位置的指针
if (tmp == nullptr)
{
return npos;
}
return tmp - _str;
}
返回C格式的字符串
const char* c_str() const
{
return _str;
}
1.substr()截取部分字符串,从pos位置上截取长度为len的字符串。len给了缺省值npos,说明如果不给定长度,则默认从pos位置一直截取到尾。所以这里需要讨论一下,有两种情况:从pos位置往后全部截取/从pos位置往后部分截取。
2.当len不给定长度时,则全部截取,当len大于size-pos时,则全部截取。如果给定长度,并且长度小于size-pos时则部分截取。
3.定义一个新的string对象,将pos位置后面len长度的字符串尾插到对象上。
string substr(size_t pos,size_t len=npos)
{
//pos位置合法性
assert(pos <= _size);
size_t n = len;//需要讨论一下len的长度是否是缺省值或者大于size-pos
if (len == npos || pos + len > _size)
{
n = _size - pos;//以上两种情况都是从pos位置截取完,所以只要让n=size-pos就可以截取完。不然n的长度就是给定的len长度。
}
string tmp;
for (size_t i = pos; i < n + pos; i++)
{
tmp += _str[i];
}
return tmp;
}
1.reserve()预留空间。主要用来扩容。
2.当实际数据大小大于容量时就要进行扩容。
3.扩容逻辑也很简单:其实就是异地扩容,重新开出一块空间。
①:扩容n个大小,那就开辟n个大小的空间,不过这里需要n+1,这一个位置是留给’\0’的。
②开完空间后,就可以将原空间数据拷贝过来
③释放原空间。
④将开辟的空间再赋给_str.最后容量需要保持一致。
void reserve(size_t n)
{
if (n > _capacity)
{
char* temp = new char[n + 1];
memcpy(temp, _str,_size+1);
delete[] _str;
_str = temp;
_capacity = n;
}
}
1.resize(n,ch=‘\0’)修改数据的大小为n。多出的数据用ch来填充。
当ch不给定时,默认用0填充,当ch给定时用字符ch填充。
2.当n大于原来的size时,size要改变,而且capacity也要改变,也就是要扩容。当n小于size时,size需要改变,但capcaity不用改变。
3.扩容完将字符ch填充到原size位置后面。最后size大小需要保持一致,最后一位需要手动写上’\0’
void resize(size_t n,char ch='\0')
{
if (n < _size)
_size = n;
else
{
reserve(n);//不管n是否大于capacity都给他扩容到n即可
//因为当n小于capacity时reserve也不会改变capacity.
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
1.string对象比较跟字符串比较是一样的,这里最好不要直接用strcpy来比较,有很多坑,这里最好自己手动一个一个比较。每个位置进行一一比较。当相同时就一起再往后走,当小于时就返回。
2.因为两个字符串对象大小不一定相等,比较大小肯定是要按照长度小的来比,因为如果按照长度长的比较那么就越界了。
3.要考虑下三种情况 tao tao 或者 taoxx tao 或者 tao taoxx。前面都一样。后面只需要判断一下如果第二个字符串长的话那么一定第一个字符串小于第二个字符串。其他两种情况都是flase。
bool operator<(const string& s)
{
int i1 = 0;
int i2 = 0;
while (i1 < _size && i2 < s.size())
{
if (_str[i1] < _str[i2])
{
return true;
}
else if (_str[i1] > _str[i2])
{
return false;
}
else
{
++i1;
++i2;
}
}
//"tao" "taox"
//taox tao
//tao tao
if (i1 == _size && i2 != s.size())
{
return true;
}
else
{
return false;
}
}
1.两个相同的字符串肯定长度一样长,并且比较大小都一样。
bool operator==(const string& s)
{
return _size == s.size() && memcmp(_str, s.c_str(), _size);
}
1.写了前面两个,后面的比较都可以复用前面的两个。
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);
}
bool operator!=(const string& s)
{
return !(*this == s);
}
1.流插入cout,利用运算符<<重载,要注意这个函数不能写成成员函数,因为this指针会抢占左操作数,而左操作数应该是流插入cout。所以必须写在类外。不能写在类里。
2.而写在类外的话,想要访问类里的私有成员就得需要使用友元,而这里可以不需要使用友元就可以访问私有成员,那就是一个一个字符打印就可以访问了。
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
1.跟流插入操作符一样,流提取也不能写在类里,要写在类外。
2.流插入需要考虑很多方面:
①cin和scanf当遇到空格或换行都会停止读取。
②cin.get()函数可以读取不管是换行还是空格。我们这里使用get。
③读取字符之前,要清理字符之前的空格和换行,这样才可以读取到。
④每次读取之前都需要将缓冲区内容清空,不然下一次读取就会将上一次的内容也读取下来。
3.这里因为如果每次都读取一个字符会很麻烦,因为会不断的扩容,如果提取的字符很长,就会从小到大扩容。所以这里采取的是将提取的字符放入一个数组里,当提取部分或者全部提取之后再放进去。这样就可以减少扩容次数了。注意最后一位要放入’\0’.
istream& operator>>(istream& in, string& s)
{
//每次进入流提取之前都要把之前的缓存清理掉。
s.clear();
char ch;
ch = in.get();//in流提取,会跳过空格和换行
//但是这样很麻烦,需要不断的扩容,从小扩到大。
//
//还要清理字符之前的空格和换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
//所有又采取一种方法,把ch提取的字符县附近一个数组里面
//这样扩容就不会频繁的扩容,一段一段的扩容,累加一定量再放进去
char buf[128];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buf[i++] = ch;
if (i == 127)
{
buf[i] = '\0';
s += buf;
i = 0;
}
ch = in.get();
}
if (i != 0)
{
buf[i] = '\0';
s += buf;
}
return in;
}