目录
string的模拟实现下
析构函数:
完善+=函数
空对象的构造函数:
头插函数的一些修正:
构造函数的完善:
实现append
插入函数:
插入函数(字符串)
erase删除函数:
实现find函数:
resize函数:
容量函数:
完善[]函数:
<<流插入:
>>流提取:
clear函数:
设计深拷贝
赋值重载:
string的模拟实现下
析构函数:
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
首先,我们先释放_str指向的内存空间,为了防止对已经释放过的内存进行访问,我们把_str置为空指针,既然已经对类对象完成了析构,那我们就把对象的容量和有效元素置为0.
完善+=函数
我们并没有实现对于空对象的构造函数,我们现在进行实现:
空对象的构造函数:
string()
{
_str = new char[1];
_str[0] = '\0';
_size = _capacity = 0;
}
我们首先申请一个字节的空间给_str,然后把_str的首位置置为\0,这个\0是字符串结束的标志,并不计入有效元素和容量的个数。
头插函数的一些修正:
void test_string1()
{
string s1;
s1 += 'x';
}
当我们使用空对象s1+=一个字符时,我们先查看具体的函数调用:
我们首先调用+=函数,函数内部调用尾插函数
尾插函数需要扩容,当有效元素和容量相等时,我们把容量扩容到原来的二倍即可。
但是因为我们的对象s1的容量本身就为0,无法实现扩容:因为0的2倍也为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';
}
当容量和有效元素相等时,我们进行判断,假如对象的容量为0时,我们把4赋给新的容量,假如对象的容量不为0时,我们把原容量扩容到原来的二倍赋给新容量,调用reserve函数进行扩容。
构造函数的完善:
我们需要考虑无参和有参的构造函数:
我们可以使用缺省值的方法:
一些同学会这样写构造函数,这样写对吗?
不行,因为strlen是查找到str指向字符串的\0为止,而对于空指针str,无法使用strlen函数。
一些同学又会这样写:
不行:因为char*表示字符类型的指针,所以不能用来接收'\0',但是我们可以取'\0'的ascll码,'\0'的ascll码是0,所以这里相当于str是空指针。对于空指针,我们是无法调用strlen函数。
我们可以这样写:
string(const char*str="")
{
_size = strlen(str);
_capacity = _size + 1;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
""就表示一个空字符串。
实现append
void append(const char*str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
append表示追加一个字符串,我们对代码进行分析:
首先我们调用strlen函数求出追加字符串的字符个数,然后进行判断,如果原来的元素个数加上新的字符个数大于我们的容量,我们就调用reserve函数修改容量,然后调用strcpy函数,把要追加的字符串str拷贝到原字符串的末尾。
插入函数:
我们可以先实现在某一个为止插入一个字符。
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)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
我们对代码进行分析:
因为库里面的insert函数接口是这些:
所以我们按照库里面走,返回值是string对象的引用。
这串代码的意思是我们从pos为止插入一个字符ch,我们首先需要断言,假如我们的pos大于string对象的元素个数时,会造成越界访问。
因为我们插入字符之后,string的元素个数增加,我们需要进行判断是否需要扩容。
假如我们的_size==_capacity表示我们的string对象的元素已满,我们要插入的话需要扩容,扩容分为两种情况,当string对象不为空时,我们可以直接扩容二倍,当string对象为空时,我们可以先给string对象四个字节的容量。
接下来,我们画图进行解释:
假如我们想要在如图所示的pos位置插入一个元素z,我们需要做的就是把pos位置之后的全部元素都后置一位,然后把z插入到pos位置处,然后增加元素的有效元素的个数,我们可以设置一个循环,我们可以设置字符串的最后一个元素的下一个位置为end
当pos小于end时,我们把end位置的元素挪到end+1位置,当循环终止时,我们在pos位置插入元素z即可。
还要注意一个问题:当我们进行头插时,我们的pos为0,又因为我们的end是无符号数,所以end始终满足>=0,所以会死循环,我们该如何解决呢?
有些同学会想:我们直接把end的类型换成有符号数就行了,这样就不会死循环了。
如图:
这样做是不行的,因为无符号数的范围比有符号数的范围大,当有符号数end和无符号数pos进行比较时,我们会把有符号数end提升为无符号数,所以又会导致死循环。
我们的解决方法如下:
我们可以在比较时,把end强制类型转换为有符号数,那么end就会小于0了,就不会导致死循环了。
插入函数(字符串)
string&insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return*this;
}
我们对代码进行分析:既然插入字符串,我们首先要判断是否需要扩容,我们先求出插入的字符串str的元素个数,我们进行判断,假如string对象的元素和str的元素大于string的容量,我们调用reserve函数进行扩容,接下来我们画图进行分析:
我们要把字符串str插入到pos位置处,还是之前的思路,我们设置最后一个元素的下一个元素为end
我们需要求出字符串str的元素个数len,然后进行判断,当pos<=end时,我们把end位置的元素挪到end+len位置,在循环中让end--,最后达到的结果是这样:
然后我们使用strncpy,不使用strcpy的原因是strcpy会把字符串末尾的\0也拷贝过去,会导致字符串提前读取结束,使用strncpy,我们把str的len个字节拷贝到_str+pos位置处,然后让_size+len,更新元素个数,返回*this。
erase删除函数:
我们要实现erase函数就需要创建一个npos参数,这个npos类型是无符号整型的最大值,也就是-1
静态成员变量在类里面定义,只能在类外面初始化,因为静态成员变量是属于整个类的,我们这样写对吗?
正确,这里存在特例:const修饰的静态成员变量可以在类里面完成初始化(只针对整型)
我们来完成erase函数:
string&erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || _size - pos <= len)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
我们首先进行断言:我们要保证删除的位置在string对象内,接下来,我们分情况进行讨论:
当要删除的元素大于等于剩余的元素,我们直接在pos位置处放\0,修改元素个数即可。
当要删除的元素小于剩余的元素时,我们直接调用strcpy,把后面的元素拷贝到前面即可,然后修改元素个数,我们画图进行演示:
1:假如我们要删除npos(缺省值)个元素时,我们直接把pos位置元素置空,然后修改元素个数。
2:
假如我们要删除的元素个数大于等于_size-pos时,我们把pos位置置空,修改元素个数。
3:假如我们要删除的元素个数小于等于_size-pos时,我们调用strcpy函数,我们把pos+len位置后的元素拷贝到pos位置处即可,然后修改元素个数。
实现find函数:
我们可以实现两个函数,一个函数是查找字符,一个函数是查找字符串
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
while (pos < _size)
{
if (_str[pos] == ch)
{
return pos;
}
++pos;
}
return npos;
}
find函数:在pos(缺省值为0)位置处开始找ch,找到了返回对应的下标,找不到返回npos
我们首先进行断言防止产生越界问题。
我们从pos位置到_size位置进行逐个遍历,找到与ch相等的元素,返回对应元素的下标,否则的话返回npos
strstr表示从字符串str1找str2,如果找到了返回指针指向str1的位置,如果没找到返回空指针,但是我们的find函数要的是下标,我们可以把指针转换为下标的形式:
size_t find(const string& str, size_t pos = 0)
{
assert(pos < _size);
const char*ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
我们先断言,防止越界,我们调用strstr函数,从_str+pos位置的字符串开始查找str字符串,我们进行判断,如果是空指针,我们返回npos,如果不是空指针,我们返回ptr-_str就是对应的下标。
resize函数:
resize是对函数的元素数量进行修改
这个函数有三种情况:
1:当n小于_size时,我们会进行尾删,删到只剩n个元素,并且把元素个数进行修改
2:当n大于_size且小于_capacity时,我们补全元素到n位,多余的这些元素用c来替代,然后修改_size。
3:当n大于_capacity时,我们先进行扩容,再把元素补全到n位,然后修改_size。
我们可以先把reserve函数进行修改,以配合我们的resize函数使用:
void reserve(size_t n)
{
if (n > _capacity)
{
char*tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
我们的修改是当n比容量大时,我们再进行扩容,我们不再需要缩容的操作,因为缩容没有意义。
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n);
for (size_t i = _size; i < n; ++i)
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
else
{
_str[n] = '\0';
_size = n;
}
}
首先进行判断,n是否大于我们的元素数量,如果大于的话,就调用reserve函数进行扩容,reserve函数内部也有条件判定,所以我们不需要考虑n大于容量的情况。
扩容完毕之后,我们对元素进行填充,使用for循环把多出来的元素全部赋值为ch,然后对元素的_size进行修改,在_size位置处放置\0,进行终止。
如果n不大于我们的元素数量,我们不需要进行缩容操作,把_size位置处的元素置为’\0',然后修改_size即可。
容量函数:
size_t capacity()
{
return _capacity;
}
完善[]函数:
char&operator[](size_t pos)
{
assert(pos <_size);
return _str[pos];
}
const char&operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
我们也写一个[],第一个针对的是可读可写的string对象,第二个针对的是只读的string对象。
<<流插入:
ostream& operator<<(ostream& out, const string&s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
注意:这里的流插入函数只能写在类的外面或者使用友元函数写在类的里面,写在类外面的原因是写在类里面this指针会默认抢占第一个参数的位置,而第一个参数是out。
我们可以这样理解:首先out是ostream类型的对象,我们把每一个元素都流插入到out,然后返回out,为了防止浅拷贝,需要用ostream的引用。
ostream& operator<<(ostream&out, const string&s)
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
>>流提取:
istream& operator>>(istream&in, string&s)
{
char ch;
cin >> ch;
while (ch != ' '&&ch != '\n')
{
s += ch;
cin>>ch;
}
return in;
}
这里参数不用const是因为我们是要把元素写入到string对象中,所以s一定是可读的。
我们创建字符ch,流提取字符,写入字符到ch,当ch不为空格或者ch不为回车时,我们让s+=ch即可,然后返回in。
这样写可以吗?
答:不行原因是:我们的cin默认就是以空格或者换行结束的,所以ch不可能等于空格或者换行
所以我们无法接收到空格或者换行,我们可以使用get函数。
istream&operator>>(istream&in, string&s)
{
char ch = in.get();
while (ch != ' '&&ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
get函数同样是得到一个字符,但是get函数不受空格和换行的限制。
但是当我们输入很多个字符时,我们又需要大量的扩容。
istream&operator>>(istream&in, string&s)
{
char buff[128] = { '\0' };
size_t i = 0;
char ch = in.get();
while (ch != ' '&&ch != ' ')
{
if (i == 127)
{
s += buff;
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
if (i >= 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
我们可以设置一个数组buff,我们可以把get得到的元素放到数组buff中,当buff数组满时,我们让s+=该数组,然后把i置为0,当buff未满,流提取的元素结束时,我们把buff数组的第i个元素置为/0,因为后面还有一些元素,我们能提前结束字符串,然后让s+=字符串即可。
clear函数:
void clear()
{
_str[0] = '\0';
_size = 0;
}
有了clear函数,我们可以完善流提取:对于已经有元素的string对象,不能直接流提取,我们可以先调用一个clear函数再进行流提取。
istream&operator>>(istream&in, string&s)
{
s.clear();
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;
char ch = in.get();
}
if (i >= 0)
{
buff[i] = 0;
s += buff;
}
return in;
}
设计深拷贝
string(const string&s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
浅拷贝的本质是同一块地址的相同数据,深拷贝是不同的地址的相同数据,调用函数完毕后,函数内部栈帧销毁,同一块地址的两个数据全部销毁,深拷贝不受影响。
完成深拷贝,首先要开辟一块空间,这里的+1是为\0预留的空间,然后修改容量,修改元素个数,最后进行内存拷贝即可。
赋值重载:
string&operator=(const string&s)
{
if (this != &s)
{
char*tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
this指针指向的是调用赋值重载函数的对象,我们先进行判断,参数与源对象是否相同,不同的话才做处理,我们的思路是这样的:我们首先创建一个临时变量tmp,为临时变量开辟空间,我们把参数对应的对象的内存拷贝到临时变量位置,然后删除掉源对象空间上的对象,把tmp上的内容拷贝给_str,修改成员变量即可。