目录
string类构造函数析构函数
构造函数
官方的string类的构造有很多种,但是最常用的就无参构造和直接用字符串去构造而已,string类内部实际可以看做是一个存字符的顺序表。既然是顺序表,所以必然_size(实际有效字符),_capacity(空间的实际容量)。同时用char *_str来存具体字符串内容。同时为了和库里面的string类区别开来,所有的操作都放到自定义的命名空间里进行处理。npos
是在C++标准库中常见的一个静态成员变量,通常用于表示无效或结束位置的特殊值。在std::string
类中,npos
通常表示字符串的末尾位置或者无效位置,类似于-1
。因为是静态成员变量,所以放到类外来定义,const size_t string::npos = -1;
namespace ppt
{
class string
{
public:
private:
char* _str;//指向存放字符串的空间
size_t _size;//实际有效字符结尾
size_t _capacity;//实际空间容量
static const size_t npos;
};
}
字符串构造
string::string(const char* str)
:_size(strlen(str))
{
_str = new char[_size + 1];
strcpy(_str, str);
_capacity = _size;
}
_str是一个指向存放字符串空间的指针,new出来的空间是用来存传过来字符串的,new的空间+1是为了存\0,库里面capacity()和size()是不计算\0大小的,虽然它不计算,但是还是要开空间存它的。 strcpy(_str, str);是将传过来的字符串放到开辟好的空间里。strcpy复制前者是目的地,指要复制到的地方,后置指具体复制的内容
其实也可以全放到初始化列表里去处理,只是每次都要先计算出来strlen(str)才能初始化,总共要初始化三次。但是这种写法不能说错,只是效率没上面的高而已
string::string(const char* str)
:_size(strlen(str))
, _str(new char[strlen(str) + 1])
, _capacity(strlen(str))
{
strcpy(_str, str);
}
无参构造构造
无参构造不是说不开空间存字符串,虽然它确实什么也没传过来,但是对于c++来说,空字符串也依旧包含后面的\0的,所以依旧要开1个空间存\0;
string::string()
{
_str = new char[1] {'\0'};//开一个空间,并且直接把这一个空间初始化为\0
_capacity = 0;
_size = 0;
}
拷贝构造
为了避免浅拷贝,所以要手动开辟一块空间,然后将s1的字符串内容复制到新开辟的空间里。
string::string(const string& s1)
{
_str = new char[s1._size + 1];
strcpy(_str, s1._str);
_size = s1._size;
_capacity = s1._capacity;
}
析构函数
析构函数就很简单了,就是把开辟的空间直接delete掉,然后size和capacity直接置为0
如果不写析构函数,直接调默认的析构函数,会造成内存泄漏,_str开辟的空间没有释放掉,依旧在那里。默认的析构函数不会知道_str
是通过new
动态分配的,因此它不会释放这块内存
string::~string()
{
delete[]_str;//释放new出来存字符串的空间
_str = nullptr;
_size = _capacity = 0;
}
C_str()
c_str是将string类转换为字符串,所以直接返回_str就可以了
char* string::c_str()
{
return _str;
}
暂时还没写string类的cout输出,但是可以借助c_str()来进行输出
size()和capacity()
size()和capacity()就是直接返回成员_size和_capacity的值就可以了,因为他们不需要修改,所以我用const修饰,这样不管是const修饰的对象还是非const修饰的对象都可以调这个函数。权限可以缩小但是不能放大
size_t string::size() const
{
return _size;
}
size_t string::capacity() const
{
return _capacity;
}
迭代器
迭代器的基本功能和指针类似,所以模拟的话就直接用指针代替了。为了标准统一所以就直接typedef为iterator
typedef char* iterator;
typedef const char* const_iterator;//不能修改的迭代器
typedef char* reverse_iterator;//反向迭代器
起始迭代器begin和末尾迭代器end就直接用函数来表示,起始迭代器指向字符串的第一个字符位置,末尾迭代器指向最后一个字符的后一个位置(也可以看做指向\0),_size构造函数构造的时候是没有把\0给算进去的,实际上是有效字符个数,但是最后一个有效字符数组里面下标其实是_size-1。所以实际上_size作为数组下标其实就是\0的下标
begin()
begin迭代器指向第一个字符的,_str字符串名字或者数组名字其实就是首元素地址,所以直接返回字符串名就可以了
string::iterator string::begin()
{
return _str;//字符串和数组名是首元素地址
}
end()
end()迭代器指向最后一个有效字符的下一个,_size是有效字符个数,把_size当做数组下标来看到其实就是指向\0,也就是数组的有效字符的下一个。用首元素的地址加上_size大小偏移量也就得到了最后一个有效字符下一个地址。
string::iterator string::end()
{
return _str+_size;//_str是首元素地址,_size是末尾元素的下一个位置的下标
}
cbegin()和cend()
cbegin和cend与begin之类的类似,唯一的区别在于const系列的迭代器限制不能修改指向的空间的内容。所以这个成员函数末尾要加上cosnt。不能写成iterator string::const cbegin(),const在类型之后指的是指针本身不能改变,也就是不能加减偏移量之类的,但是指向的内容还是可以改变的。
string::const_iterator string::cbegin() const
{
return _str;
}
string::const_iterator string::cend() const
{
return _str + _size;
}
rbegin()和rend()
rbegin()和rend()与begin他们的区别在于。rbegin指向的是最后一个有效字符,而rend指向的是第一个有效字符的前一个
string::reverse_iterator string::rbegin()
{
return _str + _size-1;//_str是首元素地址,_size-1是最后一个有效字符
}
string::reverse_iterator string::rend()
{
return _str - 1;
}
通过迭代器来进行遍历输出
通过迭代器修改单个字符操作
除了const_iterator被const控制不能修改,其余的都修改成功
operator[]运算符重载
[ ]运算符重载可以本质是就是通过[ ]和数组下标挨个访问string类里成员_str的字符而已。所以可以有两种方法来完成这个运算符重载
第一种方法是利用c语言自带的字符串[ ]访问,因为还要通过[ ]进行修改字符,所以返回用引用。如果是传值传参,会首先产生一个临时变量,临时变量具有常性是不能修改的
char& string::operator[](size_t n)
{
assert(n < _size);//限制访问下标合法化
return _str[n];//成员函数可以直接访问成员
}
通过[ ]运算符访问输出并进行修改
reserve预留空间
reserve是将实际的capacity空间大小扩大为指定的数值,vs编译器不涉及到缩容,因为现在用的vs编译器所以就不实现缩容了。具体操作就是根据传过来的空间大小,重新开一片空间,把旧的字符串存到新空间里,然后把旧空间释放
void string::reserve(size_t n)
{
if (_capacity < n)
{
char* tamp = new char[n + 1];
strcpy(tamp, _str);
delete[]_str;//旧的空间删除
_str = tamp;//_str指向新空间
_capacity = n;
}
}
插入和删除
push_back插入
插入要考虑空间不过扩容问题,如果实际空间大小_capacity==已用的实际有效字符大小_size,那么就需要扩容。gcc是2倍扩容,而vs编译器大概是1.5倍括一次容。我采用了2倍扩容,用三目表达式去判断新空间大小。 size_t newsize = _capacity == 0 ? 4 : 2 * _capacity;如果是0,那么就直接给4个对象空间大小。重新开辟更大空间时因为已经写好了reserve扩容成员函数,所以直接利用reserve开空间就可以了。
void string::reserve(size_t n)
{
if (_capacity < n)
{
char* tamp = new char[n + 1];
strcpy(tamp, _str);
delete[]_str;//旧的空间删除
_str = tamp;//_str指向新空间
_capacity = n;
}
}
void string::push_back(char ch)
{
if (_capacity <= _size)
{
size_t newsize = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newsize);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
pop_back尾删
尾删只需要_size减1,然后把新的末尾位置加上‘\0’就可以了。--size前置--,先减然后再用,所以_[--_size]用的是减完了之后的size,也是末尾要删除那个字符的位置
void string::pop_back()
{
_str[--_size] = '\0';
}
insert插入
这里只实现常用的在指定位置单个字符插入和字符串插入
insert单个字符插入
在指定位置插入单个字符,首先要扩容足够的空间,所以依旧是利用reserve来实现。然后是把指定位置pos之后包括pos的字符往后挪到,把要pos上的位置空出来,插入新字符
void string::insert(size_t pos,char ch)
{
assert(pos <= _size);
if (_capacity ==_size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_size++;
_str[_size] = '\0';
}
也可以不在最后设置\0,反正字符串末尾本来就有\0,所以直接以它为基准往后挪到就可以了
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity == _size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
错误案例
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity ==_size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while (end >= pos)
{
_str[end+1] = _str[end];
end--;
}
_str[pos] = ch;
_size++;
}
这个案例永远不会end停止,因为end是有符号整数,运算符两边一个是有符号整数,一个是无符号整数。有符号整数会隐式转换为无符号整数,所以end会自动转size_t类型。而此时判断结束条件是end>=pos,如果pos是0的话,end必须到-1才能停止,但是它已经转为了无符号整数了,也就是永远不会停止。
解决办法有两个,第一种是把pos这个无符号整数强转为有符号数,这样两边都是有符号数就不会发生强转了
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity==_size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int end = _size;
while (end >= (int)pos)
{
_str[end+1] = _str[end];
end--;
}
_str[pos] = ch;
_size++;
}
第二种就是把end直接等于_size+1,这时候判断条件是end>pos,end直接等于pos的时候就会停下来,pos可取不到负数
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_capacity ==_size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
int end = _size+1;
while (end >pos)
{
_str[end] = _str[end-1];
end--;
}
_str[pos] = ch;
_size++;
}
insert字符串插入
插入单个字符是往后挪一个位置出来,而整个字符串插入是往后挪这个字符串长度的大小位置出来,以确保完全放得下。所以size_t end = _size+1;这个end要设置在_size+strlen(str)处。
那么为什么是end > pos+ret - 1做判断条件呢,因为要ret个位置放置str字符串,是从pos开始往后数ret个位置包括ret,所以pos到pos+ret(左闭右开不包括pos+ret)这个位置都是存字符串的,因此end最多只能取到pos+ret。当等于pos+ret-1的时候还进循环就会放不下了
挪完了之后怎么把它放回去呢,可以利用循环和数组下标挨个放进空挡里。也可以利用memcpy来进行复制放置,memcpy是从一个存储区复制到另一个存储区的函数,memcpy两个存储区参数都是用地址(指针)来表示。所以我要把str放置到pos位置上,只要知道pos的位置地址就可以了。而所有字符串名字都是首元素地址,首元素地址+pos偏移量就找到了pos的地址了。memcpy第三个参数是要复制的字节大小,也就是要复制多少,所以直接用str长度就可以了
void string::insert(size_t pos,const char* str)
{
assert(pos<=_str)
if (_capacity == _size)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
size_t ret= strlen(str);
size_t end = _size + ret;
while (end > pos+ret - 1)
{
_str[end] = _str[end-ret];
end--;
}
memcpy(_str+pos,str,ret);
_size += ret;
}
insert迭代器区间插入
迭代器区间插入基本和字符串插入差不多,只不过pos插入位置是通过规定的迭代器位置构造出来的,长度也是通过两个迭代器之间的差额偏移量构造出来,其他的都差不多
void string::insert(iterator p, iterator first, iterator last)
{
assert(p <= _str + _size);
size_t pos= p - _str;
size_t len = last - first;
size_t end = _size + len;
if (_capacity <= _size + len)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
memcpy(_str+pos, first, len);
_size += len;
}
memcpy(_str+pos, first, len); 为什么这里用_str+pos而不是p,这两个好像差不多。这是因为string类经历过扩容,原来的空间已经删除了,但是迭代器p依旧指向原来的空间位置,这样就造成了迭代器失效,所以不能通过原来的迭代器位置。
erase删除
删除一般是按指定位置pos开始往后删len个字符(包括pos上的字符),如果不给要删多少个字符len,只给了从哪开始删的pos位置,或者这个len远远pos往后剩余的有效字符(_size-pos)那么就是一直从pos开始删到最后。
第一种做法是将大于等于有效字符的个数控制尾删的循环,挨个删除尾部的数据。好处就是后面没有脏数据,坏处就是时间复杂度增加了
如果是正常的将从指定位置开始删len个字符,那么也就是把_str + pos + len往后的所有数据覆盖到_str+pos上,这样就相当于只删除了len个字符(从pos位置开始len个数据被覆盖了)。其实也可以循环来做,只是相对来说代码有点长
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >=_size-pos)
{
size_t tamp = _size - pos;//获取从pos开始到最后一共有多少个元素
while (tamp--)//挨个循环pop_back
{
pop_back();
}
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;//减去已经删除数据的长度
}
}
另一种做法是直接在pos位置上改成\0,因为字符串遇到\0就说明到字符串末尾了,所以就相当于删除了后面的数据。同时把_size的尺寸改成更改后的正确尺寸,也就是_size-=_size-pos。因为实际就是有多少字符就删多少,所以size是把pos后面最后有效字符个数减去,也就是_size-=_size-pos;
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size-pos)
{
_str[pos] = '\0';
_size -= _size - pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
那么是 怎么控制不给len长度依旧会把后面所有有效字符全删了,解决办法是在声明的时候加上缺省值npos,npos这个成员变量已经在一开始就设定了等于-1。在size_t无符号数中-1是最大的数,所以你不给值,缺省值限定了就是最大的值,也会一直删到最后
声明里给了缺省值,函数定义里就不能给缺省值了,会报重复
void erase(size_t pos = 0, size_t len = npos);
按迭代器区间删除
迭代器功能类似指针,所以可以通过strcpy把后面的指针的内容直接覆盖到前指针的位置就做到l把两个迭代器之间的内容删除的功能
void string::erase(iterator first, iterator last)
{
strcpy(first, last);
_size -= last - first;
}
append追加
追加s是指在字符串末尾加上另一个字符串,因为已经实现了insert插入字符串,所以直接调用就可以了。str是字符串首字符地址,通过首字符地址可以找到整个字符串,因为字符串是顺序存储
void string::append(const char* str)
{
insert(_size, str);//inser插入字符串str
}
追加n个指定的单字符
直接调n次push_back就可以了
void string::append(size_t n, char c)
{
while (n--)
{
push_back(c);
}
}
swap交换
string库里的交换是各成员变量相互交换,比如字符串,_capacity,_size之类的。直到注意的是std标准库里把swap写成了模版函数,所以把参数直接传给std库的swap就可以交换了。不想直接用库里的话,也可以自己写临时变量过渡手动交换
void string::swap(string& s1)
{
std::swap(_str, s1._str);
std::swap(_capacity, s1._capacity);
std::swap(_size, s1._size);
}
operator=赋值远算符重载
第一种写法
为了避免浅赋值,导致析构两次崩溃,所以定义一个临时变量开空间当做过渡,把字符串复制过去
为了避免野指针和_str以及tamp指向同一块空间的风险,所以tamp赋值完就直接置空
string& string::operator=(const string& s1)
{
if(this != &s1)
{
char* tamp = new char[s1._size + 1];
strcpy(tamp, s1._str);
delete[] _str;
_str = tamp;
tamp = nullptr;
_size = s1._size;
_capacity = s1._capacity;
}
return *this;
}
第二种方法,直接用s1拷贝构造一个临时变量,然后将临时变量和*this直接调swap函数交换就可以了
s2的空间因为是定义在函数栈里面的,所以在函数结束后会直接调析构函数销毁
string& string::operator=(const string& s1)
{
string s2(s1);
swap(s2);
return *this;
}
operator+=远算符重载
+=字符串的实现方式与append追加基本一模一样,所以直接调用append就可以了。因为+=是对本身的一个修改,所以是引用返回
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
+=单个字符直接调push_back就可以,反正都是在原有的基础上往后加
string& string::operator+=(char s1)
{
push_back(s1);
return *this;
}
operator+远算符重载
operator+与operator+=最大的区别在于,+=要改变自身,而+不改变自身,但是要得到加完了之后的结果。所以拷贝构造一个临时变量,返回临时变量加完后的结果
string string::operator+(const char* s1)
{
string s2(*this);
s2 +=s1;
return s2;
}
+单个字符代码原理都和+字符串基本一模一样,只是参数类型不一样
string string::operator+(char s1)
{
string s2(*this);
s2 += s1;
return s2;
}
比较运算符重载
c语言库函数strcmp是专门比较字符串的函数,所以string对象的比较,用这个为基础来实现
strcmp比较完返回三种情况,大于0,小于0,等于0,分别对应大于,小于,等于
bool string::operator<(const string& s) const
{
int tamp=strcmp(_str, s._str);
return tamp < 0;
}
bool string::operator>(const string& s) const
{
int tamp = strcmp(_str, s._str);
return tamp > 0;
}
bool string::operator==(const string& s) const
{
int tamp = strcmp(_str, s._str);
return tamp == 0;
}
对于大于等于来说,其实就是小于的取反,因为总共就只有大于等于小于三种情况,既然要同时满足大于等于,那么就一定是小于取反。同理小于等于就是大于取反,而不等于就是直接等于取反
bool string::operator<=(const string& s) const
{
return !((*this) > s);
}
bool string::operator>=(const string& s) const
{
return !((*this) < s);
}
bool string::operator!=(const string& s) const
{
return !((*this) == s);
}
find查找
单个字符的查找是通过循环变量字符串里的所有字符来进行查找,如果相同就返回i值,find返回的是物理下标,从0开始的,所以直接返回i。没找到按照库里面string规定一律返回npos,也就是-1
size_t string::find(char ch, size_t pos)
{
for (size_t i = pos; i < size(); i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
查找字符串借助了c语言库函数strstr子串匹配,strstr是在一个字符串中查找另一个字符串是否存在,返回的是被查找的字符串的第一个字符的指针,如果不存在返回的是空。匹配到的地址指针减去起始_str地址就得到了被查找字符串的下标
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
char *tamp=strstr(_str+pos, str);//从pos开始查找的,所以是从_str+pos开始往后匹配str
return tamp - _str;
}
substr取子串
取子串是先建一个空string,然后挨个把要取的字符加到它后面。
string string::substr(size_t pos, size_t len)
{
string tamp;
if (len >= _size-pos)//如果pos之后的字符小于len,那么就直接全取
{
for (size_t i = pos;i < size(); i++)
{
tamp += _str[i];
}
}
else
{
size_t ret = pos + len;
for (size_t i = pos; i < pos + len; i++)
{
tamp += _str[i];
}
}
return tamp;
}
也可以在len大于后面剩余字符时,把剩下的字符直接拷贝构造
string string::substr(size_t pos, size_t len)
{
// len大于后面剩余字符,有多少取多少
if (len > _size - pos)
{
string sub(_str + pos);
return sub;
}
else
{
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
}
clear清空
清空直接把字符串第一个字符置为‘\0’就可以了,代表这里面一个有效字符都没有。同时把_size置为0。_capacity是实际空间大小,清空是不用改的
void string::clear()
{
_str[0] = '\0';
_size = 0;
}
cout与cin输入输出string类
cout输出string
ostream& operator<<(ostream& co, string& S)
{
for (string::iterator it = S.begin(); it != S.end(); it++)//通过迭代器遍历
{
co << *it;//挨个把字符串的字符插入到屏幕上
}
return co;
}
ostream类是流插入,意思是把数据插入到屏幕上,也就是输出了。值得注意的是流插入和流提取都必须返回引用。如果是传值返回的话是拷贝构建一个临时对象来存ostream的值,可是ostream是不能被拷贝的。如果是传值返回的话会报隐式删除表明尝试以某种方式复制或拷贝了一个 std::ostream
对象,但这是不允许的。
为什么不把输出写成类的成员函数呢,而是写成全局函数。这是因为成员函数默认成员是左操作数,那么输出的时候就变成了s1<<cout,这样就很奇怪,所以把他置放为全局函数,就可以把左操作数改为cout。
cin输入
istream是流提取,是把输入的值提取出来存到指定的类型变量中
while循环逐个字符地从输入流中读取数据,直到遇到空格或换行符为止,将所有读取到的字符存到string 对象s当中
get它从输入流中读取一个字符并将其作为无符号字符返回。get函数不会自动过滤或跳过空格字符,它会将任何字符(包括空格)都当作输入读取到并返回
istream& operator>>(istream& is, string& s)
{
s.clear();//清空,免得重复输入时出现脏数据
char ch = is.get();
while (ch != ' ' && ch != '\n')//因为实际上cin遇到空格和回车就直接打断,所以只能这么写
{
s += ch;
ch = is.get();//持续输入
}
return is;
}