class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
//可以初始化,只针对整型,double等类型不行
// static const size_t npos=-1;
};
const size_t string::npos=-1;
重命名模版类型T*指针为迭代器iterator和const类型的const_iterator迭代器
char str*为字符数组,string类中存储字符的指针就是它;
_size和_capacity分别是字符个数和空间大小,因为不可能是负数所以直接用size_t定义;
npos则是string类中默认赋值为-1的static const静态常量,无法被修改,且初始化需要在class类外部初始化。
Tip:整型的静态常量可以在私有区域初始化,但是只针对整型。
const char* c_str()
{
return _str;
}
size_t size() const //后面加const只有在类成员函数中修饰*this,无法改变this中的值
{
return _size;
}
c_str()是用C语言的方式返回字符串,即不带’\0’。
size()就是返回_size的大小,后面加上const修饰*this,无法改变this中的值。
void swap(string& s)
{
std::swap(_str,s._str);
std::swap(_size,s._size);
std::swap(_capacity,s._capacity);
}
直接掉用std库中的swap函数交换string类中的对应成员变量。
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;
}
断言越界,循环查找即可。
//char *strstr(const char *haystack, const char *needle)
//haystack -- 要被检索的 C 字符串。
//needle -- 在 haystack 字符串内要搜索的小字符串。
size_t find(const char* str,size_t pos=0)
{
assert(pos < _size);
char* p=strstr(_str+pos,str);//找到str的起始地址
if(p== nullptr)
{
return npos;
}
else
{
return p - _str;//str起始地址减去_str首地址就是第几个
}
}
使用strstr找到str的起始地址,找不到返回nullptr给指针p,返回npos值;
找到就返回str首地址p减去_str的首地址的值,即是str的位置。
string(const char* str="")//全缺省初始化 避免空str无法被输出和崩溃
:_size(strlen(str))
{
_capacity=_size==0 ? 3 : _size*2; //防止空间大小为0导致双倍扩容失败
_str=new char[_capacity+1];//为_str扩容初始化
strcpy(_str,str);
}
初始化采用全缺省初始化,如果用户不给值,则初始化为空字符串,这样做可以避免空str无法被输出和崩溃。
_size的大小取决于形参str的大小,放入初始化列表,值为str的长度。
_capacity的大小跟着_size走且应不为0,需要判断是否str为空,如果为空则另外扩容3个空间,防止空间大小为0导致双倍扩容失败。
_str扩容初始化后的大小应该为**_capacity+1**,因为字符串后还要有一个’\0’结束字符串读取,所以要多开一个空间,最后在用strcpy函数把形参str中的数据拷贝到类成员_str中。
至此一个string类型初始化才算完成。
string(const string& s)
:_size(s._size)
,_capacity(s._capacity)
{
_str=new char[s._capacity+1];
strcpy(_str,s._str);
}
如果没有拷贝构造,当用一个已存在的s1去初始化新的s2,只是把s1的地址拷贝给s2,这时s1和s2共用一个地址,当主函数结束,这个地址会被调用两次析构函数,被析构两次就会报错。
所以我们需要拷贝构造,初始化_size和_capacity让它们和s的空间大小一致,然后再重新开一片同样大小的空间给_str,再把s用strcpy复制过去即可,这样地址就不会冲突。
string& operator=(const string& s)
{
if(this != &s)
{
//如果开空间失败不会影响this._str
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);//再把拷贝值拷贝进tmp中
delete[] _str;//释放原来的值,如果不用tmp的话,无法释放之前的地址空间
_str = tmp; //再指向tmp的地址
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
重载操作符=,将const常量s拷贝给_str。
这里使用tmp做中间值,即使开空间失败也不会影响_str的空间,如果不用tmp,而直接用_str开空间,会导致无法释放_str开空间之前的地址空间。
然后再把s中的值用strcpy拷贝给tmp,再去释放原来的_str空间,此时_str是空指针。再将tmp地址赋值给_str,让_str指向拷贝后的数据空间,最后赋值字符串大小和空间大小即可。
~string()
{
delete[] _str;
_str= nullptr;
_size=_capacity=0;
}
析构函数没什么可说的,释放掉_str空间后再置成nullptr值即可,容量大小也置0。
char& operator[](size_t pos) //给普通用户调用
{
assert(pos<_size);
return _str[pos];
}
//函数重载
const char& operator[](size_t pos) const//给const成员调用
{
assert(pos<_size);
return _str[pos];
}
最简单的操作符重载
第一个重载用于普通数据,重载需要的参数为pos,即数组的下标位置,断言防止越界访问,返回的值是_str[pos],_str本身就是一个数组,可以用中括号[ ]访问数据。
第二个重载是专门给const类型使用,有时候数组中的数据如_str[0]需要传给一个函数,此时希望数组中的数据不被改变,需要加上const,这时const类型就能调用重载了。前面的const是用来限制返回值不被修改,后面的const是限制隐式参数this不被修改。
Tip:const成员调用非const函数是权限放大,无法调用,普通用户调用const是权限缩小
//数据不改变的重载最好加上const,让const成员也能用
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 || *this ==s;
}
bool operator!=(const string& s) const
{
return !(*this==s);
}
写出前面两个重载后,以防粗心写错,下面那些重载都可以复用前面两个。
加上const后const类型的成员也可以使用这些重载。
//迭代器底层实现
iterator begin()
{
return _str;//首地址
}
iterator end()
{
return _str+_size;//末地址
}
//const迭代器实现
//const迭代器只能读 但是const_iterator类型变量可以变 例如++it
const_iterator begin() const
{
return _str;//首地址
}
const_iterator end() const
{
return _str+_size;//末地址
}
分为两种迭代器,一种是可以改变指向数据值的迭代器;一种是const类型迭代器,无法改变值。
void reserve(size_t n)//扩容 容量小于现有大小,不改变容量
{
if(n>_capacity)//防止缩容
{
char* tmp=new char[n+1];
strcpy(tmp,_str);
delete[] _str;
_str=tmp;
_capacity=n;
}
}
reserve扩容需要传入一个扩容大小的值n,当n比原来的容量大时才进行扩容操作,且reserve不接受缩容操作。
扩容不是原地扩容,函数中扩容借助了tmp的帮助来拷贝_str中的数据,然后改变_str指向来完成深拷贝。
如果不用tmp,而直接用_str开空间,会导致无法释放_str开空间之前的地址空间。
void resize(size_t n,char ch='\0')
{
if(n<=_size)//resize的大小小于原有的长度,直接屏蔽后面
{
_size=n;
_str[_size]='\0';
}
else
{
if(n>_capacity)
{
reserve(n);
}
size_t i=_size;
while(i<n)
{
_str[i]=ch;
++i;
}
_size=n;
_str[_size]='\0';//末尾加上'\0'结束
}
}
resize扩容也需要传入一个扩容大小的值n,但是resize扩容可以自定义扩容后的空间中的值:ch,这个val值可以缺省,默认为’\0’。它的扩容和reserve相同,只不过支持空间初始化和删除多余n个的数据。
当需要扩容的容量n小于原本的大小,那么就删除原本数据,直到留下n个数据;扩容的容量大于原本空间时,则初始化后面所有的值为ch值,注意,这里用了一个变量i去取_size的值,不用_size是因为循环中不能改变_size的大小。
void push_back(char ch)//增加一个字符
{
if(_size+1>_capacity)
{
reserve(_capacity*2);//双倍扩容
}
_str[_size]=ch;//_size是最后'\0'位置
++_size;
_str[_size]='\0';
}
插入的时候一定要判断插入后的数据是否超出空间大小,是否需要扩容,然后在最后的位置插入值,_size往后移动并赋值’\0’,如果不加这个’\0’会越界访问未知的数据!
void append(const char* str)
{
size_t len=strlen(str);
if(_size+len>_capacity)
{
reserve(_size+len);
}
strcpy(_str+_size,str);
_size=_size+len;
}
和push_back大同小异,用strcpy函数,strcpy会在最后加上’\0’,这里就不需要手动加。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
+=重载虽然是复用push_back和append,但是实际用起来简单明了。
string& insert(size_t pos,char ch)
{
assert(pos<=_size);
if(_size+1>_capacity)
{
reserve(_capacity*2);
}
size_t end=_size+1;
while(end > pos)//无符号0-1时会变成最大值
{
_str[end]=_str[end-1];//往后挪
--end;
}
_str[pos]=ch;
++_size;
return *this;
}
string& insert(size_t pos,char* str)
{
assert(pos<=_size);
size_t len=strlen(str);
if(_size+len>_capacity)//扩容
{
reserve(_size+len);
}
size_t end=_size+len;
while(end>pos+(len-1))//len-1和下标位置对齐
{
_str[end]=_str[end-len];//挪动数据
--end;
}
strncpy(_str+pos,str,len);//在pos位置插入str中len个字符,不插入'\0'
_size+=len;
return *this;
}
首先需要断言pos大小,不能超出_size范围,利用函数重载,分为字符和字符串两种接口。
这里需要注意的是循环判断的问题,在end>pos处,end和_size比加上了1,当end=0时如果是_size那么就会变成-1,而_size是无符号整型,会直接变成最大值,永远大于pos,所以要借用end作为循环判断的条件之一,下面的end>pos+(len-1)亦是如此。
string& erase(size_t pos,size_t len=npos)
{
assert(pos<_size);
if(len == npos || pos+len >= _size)
{
_str[pos]='\0';
_size=pos;
}
else
{
strcpy(_str+pos,_str+pos+len);
_size-=len;
}
return *this;
}
首先断言pos大小,不能删除_size之后不存在的数据。
默认缺省参数len为npos=-1,即是全部删除。
如果len等于npos或者pos位置后的字符长度少于len,那么可以直接把pos位置的字符置‘\0’,屏蔽后面所有字符即可。
ostream& operator<<(ostream& out,const string& s)
{
for(auto ch:s)
{
out << ch;
}
return out;
}
流提取重载很简单,只需要用循环遍历插入到out容器中即可。
istream& operator>>(istream& in,string& s)
{
s.clear();//清空后_size=0,插入时插入的位置是_size位置
char ch=in.get();//get()获取字符到流插入中
char buff[128];//防止一直扩容,直接存入buff数组中,一次性插入
size_t i=0;
while( ch != ' ' && ch != '\n' )
{
buff[i++]=ch;
if(i==127)
{
buff[127]='\0';
s+=buff; //+=重载中,会在插入字符后在末尾插入一个'\0'
i=0;
}
ch=in.get();
}
if(i!=0)//不够127个字符时
{
buff[i]='\0';
s+=buff;
}
return in;
}
}
流插入有些麻烦,当进行到循环判断时,它会把\n和空格也一起当作字符插入,主要是因为这俩没有金缓冲区,被当作了分隔字符的判断,为了解决这个问题,使用get()函数获取字符到流插入in中,再赋值给ch字符变量。
为了防止频繁扩容,创建一个buff数组,把获取到值存入数组中,一次性插入,可以减少扩容次数。
当buff中的字符达到了127个,那么就开始插入到s中,记得把buff最后一个字符置’\0’否则以后编译器访问s._str会越界访问未知变量。
不够127个时,直接在最后一个字符后到位置插入’\0’即可。
到此string的基本功能都已经实现,附上完整的源代码:
#include
#include
namespace lty
{
class string
{
public:
//string(const char* str=nullptr)//不行,cout无法输出空指针的.c_str
//string(const char* str='\0')
//string(const char* str="\0")
string(const char* str="")//全缺省初始化 避免空str无法被输出和崩溃
:_size(strlen(str))
{
_capacity=_size==0 ? 3 : _size*2; //防止空间大小为0导致双倍 扩容失败
_str=new char[_capacity+1];//为_str扩容初始化
strcpy(_str,str);
}
//s2(s1)深拷贝
string(const string& s)
:_size(s._size)
,_capacity(s._capacity)
{
_str=new char[s._capacity+1];
strcpy(_str,s._str);
}
//s2=s1深拷贝重载
string& operator=(const string& s)
{
// if(this!=&s)
// {
// _size = s._size;
// _capacity = s._capacity;
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// }
// return *this;
if(this != &s)
{
//如果开空间失败不会影响this._str
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);//再把拷贝值拷贝进tmp中
delete[] _str;//释放原来的值,如果不用tmp的话,无法释放之前的地址空间
_str = tmp; //再指向tmp的地址
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
~string()
{
delete[] _str;
_str= nullptr;
_size=_capacity=0;
}
//在前后都加上const,返回值无法修改且this中的值也无法修改!
const char& operator[](size_t pos) const//给const成员调用
{
assert(pos<_size);
return _str[pos];
}
//函数重载
char& operator[](size_t pos) //给普通用户调用
{
assert(pos<_size);
return _str[pos];
}
const char* c_str()
{
return _str;
}
//const成员调用非const函数是权限放大,普通用户调用是const权限缩小
size_t size() const //后面加const只有在类成员函数中修饰*this,无法改变this中的值
{
return _size;
}
//迭代器底层实现
typedef char* iterator;
iterator begin()
{
return _str;//首地址
}
iterator end()
{
return _str+_size;//末地址
}
//const迭代器实现
//const迭代器只能读 但是const_iterator类型变量可以变 例如++it
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;//首地址
}
const_iterator end() const
{
return _str+_size;//末地址
}
//string比较大小重载
//数据不改变的重载最好加上const,让const成员也能用
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 || *this ==s;
}
bool operator!=(const string& s) const
{
return !(*this==s);
}
//插入字符
void reserve(size_t n)//扩容 容量小于现有大小,不改变容量
{
if(n>_capacity)//防止缩容
{
char* tmp=new char[n+1];
strcpy(tmp,_str);
delete[] _str;
_str=tmp;
_capacity=n;
}
}
void push_back(char ch)//增加一个字符
{
if(_size+1>_capacity)
{
reserve(_capacity*2);//双倍扩容
}
_str[_size]=ch;//_size是最后'\0'位置
++_size;
_str[_size]='\0';
}
void append(const char* str)
{
size_t len=strlen(str);
if(_size+len>_capacity)
{
reserve(_size+len);
}
strcpy(_str+_size,str);
_size=_size+len;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
void resize(size_t n,char ch='\0')
{
if(n<=_size)//resize的大小小于原有的长度,直接屏蔽后面
{
_size=n;
_str[_size]='\0';
}
else
{
if(n>_capacity)
{
reserve(n);
}
size_t i=_size;
while(i<n)
{
_str[i]=ch;
++i;
}
_size=n;
_str[_size]='\0';//末尾加上'\0'结束
}
}
string& insert(size_t pos,char ch)
{
assert(pos<=_size);
if(_size+1>_capacity)
{
reserve(_capacity*2);
}
size_t end=_size+1;
while(end > pos)//无符号0-1时会变成最大值,且又因为无符号和整形相比较会发生整形提升,所以用这种方式
{
_str[end]=_str[end-1];//往后挪
--end;
}
_str[pos]=ch;
++_size;
return *this;
}
string& insert(size_t pos,char* str)
{
assert(pos<=_size);
size_t len=strlen(str);
if(_size+len>_capacity)//扩容
{
reserve(_size+len);
}
size_t end=_size+len;
while(end>pos+(len-1))//len-1和下标位置对齐
{
_str[end]=_str[end-len];//挪动数据
--end;
}
strncpy(_str+pos,str,len);//在pos位置插入str中len个字符,不插入'\0'
_size+=len;
return *this;
}
//删除下标位置len个字符
string& erase(size_t pos,size_t len=npos)
{
assert(pos<_size);
if(len == npos || pos+len >= _size)
{
_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(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;
}
//char *strstr(const char *haystack, const char *needle)
//haystack -- 要被检索的 C 字符串。
//needle -- 在 haystack 字符串内要搜索的小字符串。
size_t find(const char* str,size_t pos=0)
{
assert(pos < _size);
char* p=strstr(_str+pos,str);//找到str的起始地址
if(p== nullptr)
{
return npos;
}
else
{
return p - _str;//str起始地址减去_str首地址就是第几个
}
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
//可以初始化,只针对整型,double等类型不行
// static const size_t npos=-1;
};
const 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();//清空后_size=0,插入时插入的位置是_size位置
char ch=in.get();//get()获取字符到流in中
char buff[128];//防止一直扩容,直接存入buff数组中,一次性插入
size_t i=0;
while( ch != ' ' && ch != '\n' )
{
buff[i++]=ch;
if(i==127)
{
buff[127]='\0';
s+=buff; //+=重载中,会在插入字符后在末尾插入一个'\0'
i=0;
}
ch=in.get();
}
if(i!=0)//不够127个字符时
{
buff[i]='\0';
s+=buff;
}
return in;
}
}