前言
为了加深对string类的理解,本节我们来学习一下string类的模拟实现,那么废话不多说,我们正式进入今天的学习。
(模拟实现的所有函数我都做了声明和定义分离,但其实string的构造等一些短小而频繁调用的函数其实可以不做声明和定义分离,可以直接在类中定义)
我们通过之前的学习知道,string的底层的核心是这三个私有成员变量(代码写在string.h中):
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
1.实现无参数的string功能
(该代码可不做声明和定义分离)
我们知道,当我们不给string中传入参数时,string就会构造一个空的字符串,所以我们可以很轻松的写出如下的代码:
string::string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{}
我们在这种情况下采用初始化列表让_size和_capacity变量为0,_str指向空指针
此时代码其实存在一点问题,假设我们在测试的时候想要打印字符串,因为没有重载流插入和流提取,所以我们先来模拟实现一下 c_str 用于打印字符串:
const char* string::c_str() const
{
return _str;
}
再来写一下测试代码:
void test_string1()
{
string s1;
string s2("hello world");
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
}
我们运行代码 的时候发现打印打不出来,程序崩溃了
这是为什么呢?这是因为s1是一个空的string对象,它是用空指针来初始化的。当打印s1的时候它会去解引用空指针,此时就导致了错误,所以这就说明我们写的不含参数的string的代码是错误的,我们应该要改正一下代码,不能直接给空指针,此时也应该开辟一个字节的空间,里面给 "\0"
(代码写在string.h中)
string();
(代码写在string.cpp中)
string::string() :_str(new char[1]{'\0'}) , _size(0) , _capacity(0) {}
此时再来运行一下测试代码:
可以发现,更改完代码以后就能成功打印了
2.实现参数仅为字符指针时的string功能
(该代码可不做声明和定义分离)
当给string中传入参数时,并且参数仅为指向一个字符串的字符指针时,string类就会拷贝字符指针指向的字符串来初始化。
这里我们用初始化列表来写就会有点不合理,理由如下:
当我们用初始化列表写的时候,我们要给_str来new一块新的空间用于存储拷贝的字符串:
:_str(new char[strlen(str)+1])
我们此时开辟的空间的大小应该是str指向的字符串的大小+1,+1是因为要给 " \0 " 留空间。在_capacity和_size的初始化中我们还需要使用strlen:
string::string(const char* str)
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(strlen(str))
{}
连续使用三次strlen会比较麻烦,我们可能会想到如下的处理方式:
string::string(const char* str)
: _size(strlen(str))
, _str(new char[_size + 1])
, _capacity(_size)
{}
我们会想到:先初始化_size,再用_size来初始化其他的成员变量。但这样做其实是不可行的,我们在学习初始化列表的时候提到了:初始化列表的顺序并不是变量出现的顺序,而是变量声明的顺序。所以此时先初始化的变量是_str,而此时的_size是一个随机值。
基于这些存在的隐患,我们在处理这一类代码的时候就最好不用初始化列表去初始化
(代码写在string.h中):
string(const char* str);
(代码写在string.cpp中):
string::string(const char* str) { _size = strlen(str); _capacity = _size; _str = new char[_size + 1]; strcpy(_str, str); }
需要注意的一点是:_capacity中不包含 " \0 " 所以不需要+1
**************************************************************************************************************
我们其实可以把1和2中的string用全缺省函数来合并
(代码写在string.h中):
string(const char* str = " ");
(代码写在string.cpp中):
string::string(const char* str = " ")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
注意:常量字符串后面默认有 " \0 " ,所以这里不要自己写 " \0 "
**************************************************************************************************************
3.实现string的析构
(该代码可不做声明和定义分离)
这个代码非常的基础,所以不做解释了
(代码写在string.h中):
~string();
(代码写在string.cpp中):
string::~string() { delete[] _str; _str = nullptr; _size = _capacity = 0; }
4.实现size、capacity函数
(该代码可不做声明和定义分离)
这个代码非常的基础,所以不做解释了
(代码写在string.h中):
size_t size() const; size_t capacity() const;
(代码写在string.cpp中):
size_t string::size() const { return _size; } size_t string::capacity() const { return _size; }
5.实现operator[]重载
(该代码可不做声明和定义分离)
我们首先应该要加上断言,必须保证pos小于_size,防止发生越界访问的问题:
assert(pos < _size);
它还要提供一个const的版本,给const对象使用。
根据这些,我们可以写出代码如下
(代码写在string.h中)
char& operator[](size_t pos); const char& operator[](size_t pos) const;
(代码写在string.cpp中):
char& string::operator[](size_t pos) { assert(pos < _size); return _str[pos]; } const char& string::operator[](size_t pos) const { assert(pos < _size); return _str[pos]; }
我们来测试一下(代码写在string.cpp中):
void test_string2()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
s1[i] += 1;
}
cout << s1.c_str() << endl;
}
6.实现迭代器、begin函数、end函数
那么同样的条件下,我们能不能使用范围for呢?
void test_string2()
{
string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
s1[i] += 1;
}
cout << s1.c_str() << endl;
for (auto ch : s2)
{
cout << ch << " ";
}
}
我们可以发现它报错了。因为范围for的底层就是迭代器,那么我们该怎样简单的实现迭代器呢?
其实很简单(代码写在string.h中):
typedef char* iterator;
因为迭代器模拟的是指针的行为,所以我们可以这样写代码。iterator在底层其实全部都是typedef过来的,帮助我们获取了一个统一的名字,但是其实这些类型不全是指针,还有可能是自定义类型。我们这里只是把迭代器代码简化了,真正的迭代器代码不是这样实现的。我们之所以能用原生的指针来实现string类中的迭代器,是因为string类的结构有优势,因为他是一个数组。本质是数组的都可以用原生指针来做迭代器(eg:链表不是一个数组,如果我们以原生指针作为迭代器,也就是节点作为迭代器,此时迭代器++无法走到下一个节点,这样就会出现问题)
迭代器的设计其实是一种封装的体现,因为迭代器的底层可能是数组、链表甚至是一串数。迭代器屏蔽了底层实现的细节,都提供了统一的方法去访问容器,不需要使用者关心底层结构和实现细节
接着再来实现一下begin和end函数:
iterator begin() const { return _str; } iterator end() const { return _str + _size; }
当加上迭代器、begin、end的代码,此时范围for的程序就可以正常运行了(范围for在替换的时候,函数名必须和库中的函数名一致,例如begin函数不能写成Begin)
(反向迭代器暂时不做讲解,需要用到适配器)
7.实现push_back、reserve、append函数、重载+=
要实现push_back,首先需要先扩容,我们采取扩2倍的形式扩容,所以此时应该先来模拟实现reserve函数
因为reserve函数是只扩容不缩容的,所以首先就应该判断n是否大于_capacity
还要注意我们要多开一个字节的空间留给 " \0 "
void string::reserve(size_t n) { if (n > _capacity) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } }
接下来来实现push_bacak函数:
首先需要进行判断,如果_size等于_capacity,此时就说明空间已经满了,需要进行扩容。我们调用reserve来完成扩容2倍。
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity * 2);
}
}
如果像上述代码那样写是错误的,因为_capacity有可能是0,所以要使用三目操作符判断一下_capacity并且修改:
扩完容了以后就很简单了:
void string::push_back(char ch)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
_str[_size] = ch;
++_size;
}
}
实现完push_back以后,重载运算符+=也很容易了,先来解决参数是字符的+=:
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
我们来测试一下+=的代码:
void test_string3() { string s("hello wor"); s += 'l'; s += 'd'; cout << s.c_str() << endl; }
可以看到后面出现了乱码,因为我们此时没有去处理"\0",我们现在需要修改一下push_back的代码,加入"\0":
void string::push_back(char ch) { if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } _str[_size] = ch; ++_size; _str[_size] = '\0'; }
此时代码就没有任何的错误了
接下来我们来实现一下append函数:
append函数此时肯定不能继续采用二倍扩容的形式,我们来举一个例子:假设原字符串是"hello world",我们需要插入一个很长很长的字符串,当插入的字符串长度加原字符串长度大于二倍扩容的空间时,就会出现问题。所以必须要根据需求扩容:
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
然后我们就可以使用strcpy拷贝字符串:
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
strcpy(_str + _size, str);
_size += len;
}
现在有了append函数,我们就可以完善+=的重载:
string& string::operator+=(char ch) { push_back(ch); return *this; } string& string::operator+=(const char* str) { append(str); return *this; }
8.模拟实现insert、erase函数、npos
我们先来实现insert插入一个字符:
假设想要在某个位置插入一个字符,首先就必须要保证pos要小于等于_size(等于的时候相当于尾插),然后要把插入位置后面的所有字符向后挪动一位,最后在实现插入。
要注意的一点是,当空间不够的时候,我们还需要实现扩容
根据上面几点,我们可以写出代码如下:
void string::insert(size_t pos, char ch)
{
assert(pos < _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//挪动数据
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
其实这样的写法存在一个bug,我们来测试一下:
void test_string4()
{
string s1("hello world");
s1.insert(5, '&');
cout << s1.c_str() << endl;
}
当我们在第五个数据的位置插入&时没有出现错误,此时我们再来看在边界位置处的情况:
void test_string4()
{
string s1("hello world");
s1.insert(5, '&');
cout << s1.c_str() << endl;
s1.insert(0, '&');
cout << s1.c_str() << endl;
}
此时程序崩溃了,我们来分析一下程序为什么会崩溃:
我们来看一下insert代码中的这一段:
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
因为我们给pos的值是0,所以循环结束的条件是end >= 0,而end的数据类型是size_t,是不可能会小于0的,此时它再执行--操作就会得到无符号整型的最大值。
这里我们可能会想到把end的类型改成int来解决问题,实际上这样也是不行的,这是C语言留下的一个问题,为了更直观的看到变化,我们来调试一下:
**************************************************************************************************************
这里来讲解一个调试的小技巧,当调试的数据很大的时候,我们一次一次的按f10或者f11就会很麻烦,而且容易按错,我们可以这样去做来观察最后一两次的情况:
while (end >= pos)
{
if (end == 0)
{
//..
}
_str[end + 1] = _str[end];
--end;
}
...里面的内容随便写什么都可以,目的是为了让它到条件的时候停下来
**************************************************************************************************************
可以看到,end都为-1了,而停下来的条件是end<0,但是此时程序并没有停止,这是为什么呢?
这就要涉及到我们之前在C语言阶段学习过的一个知识:当操作符两边的操作数类型不相同的时候,编译器会自动进行类型的转换,而对于这种大于和小于的比较时,通常会把范围小的变量向范围大的变量进行转换,所以这里end变量就会自动转换成pos变量的类型,而pos变量的类型是size_t,所以此时即使end小于0了,程序仍然没有停止。
要想解决这个问题,第一种办法就是把参数里面的pos类型也改成int,此时就不存在类型转换的问题了。
但是如果这样解决问题的话本身就不太合理。首先库中的pos变量的类型也是size_t,其次pos变量表示一个数据的位置,而位置不可能是负数,所以这种方法可以先排除。
第二种方法就是在while循环条件中把pos强制转换成int类型
while (end >= (int)pos)
此时重新运行代码,发现就可以正常运行了:
第三种方法:可以不用下标进行访问,用指针进行访问,因为指针不可能为空,这种方法不作详细的讲解了
第四种方法:我们还是让pos和end的数据类型都为size_t,我们把挪动数据的代码改成把end-1处的数据挪到end处
_str[end] = _str[end-1];
我们接下来实现在pos位置处插入字符串:
首先还是需要先判断pos位置有没有越界
assert(pos <= _size);
接着我们就需要判断是否需要扩容,这里因为字符串的大小是不确定的,不能直接进行简单的二倍扩容,具体要扩容多少要看添加的字符串的长度。大于2倍,需要多少空间就扩容到多少空间;小于2倍就扩容2倍。(这里的代码细节就不讲解了,和之前的append的扩容模式一模一样)
接下来的步骤就是挪动数据+插入数据
void string::insert(size_t pos, const char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if (_size + len > _capacity)
{
reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity);
}
size_t end = _size + len;
while ()
{
_str[end - len] = _str[end];
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
_size += len;
}
我们来想想while循环结束的条件是什么?
1. end > pos
先来看一下这个条件,当end小于或者等于pos的时候就不会继续循环了,我们假设原字符串是"hello world",假设这里的pos是5,len为3。故end的最后一个有效的位置就是在w位置,因为挪动的条件是_str[end - len] = _str[end],因为end-len = 5 - 3 = 2 ,此时w位置的前三个数据也会被挪动
2. end > pos + len
我们直接来测试一下:
void test_string4()
{
string s("hello world");
s.insert(5, "&&&");
cout << s.c_str() << endl;
}
通过运行结果,可以知道循环结束的条件是有问题的,我们来分析一下:
因为这里出现了乱码,所以我们可以猜测这个问题至少与'\0'有关,通过调试我们发现条件写反了,应该写成:
_str[end] = _str[end - len];
解决完这个问题我们再来运行一下程序:
可以发现插入&&&以后,原来的空格变成了r了,所以还是有问题,接着来分析一下:
就拿刚才的举的例子为例,因为要插入的字符串有三个字节,所以pos位置之后的数据需要往后挪动三个字节,所以end的结束位置在字符r处,也就是说r的位置就是pos+len,因为r也要挪动,而循环结束的条件是小于或者等于,所以当end = pos + len的时候也需要挪动,所以我们一个修改一下循环结束的条件:
while (end >= pos + len)
因为pos + len不会为0,所以这个条件不会存在错误,我们重新运行一下代码:
现在可以看到,代码运行成功了,所以循环条件是end >= pos + len
(或者写成end > pos + len - 1,但是这样写需要先判断右边的数加起来不能等于或者小于0,如果小于0就直接返回)
故insert的全部代码如下:
void string::insert(size_t pos, char ch) { assert(pos < _size); if (_size == _capacity) { reserve(_capacity == 0 ? 4 : _capacity * 2); } //挪动数据 int end = _size; while (end >= (int)pos) { if (end == 0) { int i = 0;//调试断点 } _str[end + 1] = _str[end]; --end; } _str[pos] = ch; ++_size; } void string::insert(size_t pos, const char* s) { assert(pos <= _size); size_t len = strlen(s); if (_size + len > _capacity) { reserve(_size + len > 2 * _capacity ? _size + len : 2 * _capacity); } size_t end = _size + len; while (end >= pos + len) { _str[end] = _str[end - len]; --end; } for (size_t i = 0; i < len; i++) { _str[pos + i] = s[i]; } _size += len; }
再来实现一下npos
要注意:npos不能写在头文件中,写在头文件中会报链接错误
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos;
};
const size_t string::npos = -1;
**************************************************************************************************************
这里在头文件中也可以直接这样写:
{
private:
char* _str;
size_t _size;
size_t _capacity;
static const size_t npos = -1;
};
但是不建议这么写,因为这个语法比较奇怪。之前我们学习过”静态的成员变量不能直接在类中给缺省值,因为静态的成员变量不走初始化列表,只走声明和定义分离“。
但是这里可以这么写,因为这样的形式就相当于是一个定义了,static const才能这么写
然而最奇怪的一点在于只有整型(int、size_t)可以这么写,double等类型不能这么写
这样设计是因为方便定义数组:
**************************************************************************************************************
最后来实现erase函数:
首先我们需要保证pos不能越界:
assert(pos < _size);
erase需要两个参数pos和len,作用是把从pos开始的len个字符删除掉。如果给的len的长度大于pos后面剩余数据的长度时就直接删除pos后面的所有数据,这里需要注意,如果是要把pos位置后面的数据全部删除的话,不需要把pos后的所有数据给抹除掉,但是要把pos处的后一个位置加上'\0',同时改一下_size的大小
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len >= _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
我们来测试一下代码:
void test_string5()
{
string s1("hello world");
s1.erase(6, 100000);
cout << s1.c_str() << endl;
string s2("hello world");
s1.erase(6);
cout << s2.c_str() << endl;
string s3("hello world");
s3.erase(6, 3);
cout << s3.c_str() << endl;
}
9.实现find、substr函数
find函数可以查找字符也可以查找字符串,我们先来从完成字符的查找:
find函数查找字符需要两个参数:ch和pos,作用是从pos位置开始寻找ch字符,如果找到了就返回ch字符的下标
find查找字符下标的代码非常的简单,就不做过多的讲解了:
size_t string::find(char ch, size_t pos = 0)
{
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
下面我们来讲解一下find查找字符串:
要想实现find查找字符串,我们首先需要了解一下strstr函数:
(符串匹配的BM算法来实现字符串的查找,有兴趣的可以自行了解)
strstr函数可以返回指向 str1 中第一次出现的 str2 的指针,如果 str2 不是 str1 的一部分,则返回 null 指针
我们首先还是需要判断pos位置是否越界
接着我们用strstr来查找有没有匹配的字符串,如果有就返回下标,没有就返回npos
size_t string::find(const char* str, size_t pos = 0)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (str == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
现在find函数的代码已经全部写完了,我们来将代码整合一下:
size_t string::find(char ch, size_t pos = 0) { for (size_t i = 0; i < _size; i++) { if (_str[i] == ch) { return i; } } return npos; } size_t string::find(const char* str, size_t pos = 0) { assert(pos < _size); const char* ptr = strstr(_str + pos, str); if (str == nullptr) { return npos; } else { return ptr - _str; } }
接下来我们来实现一下substr函数:
首先我们需要判断pos有没有越界:
assert(pos < _size);
实现sbustr的过程中会出现两种情况:
第一种情况是len的长度已经大于pos位置后的字符串的长度,此时有多少数据就拷贝多少数据
第二种情况是len的长度不大于pos位置后字符串的长度,此时就拷贝len个数据
根据这个条件我们就可以写出代码如下:
string string::substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i < len; i++)
{
sub += _str[pos + i];
}
return sub;
}
我们结合之前写的find来测试一下:
测试条件是寻找一个文件名的后缀,并返回
void test_string6()
{
string s("test.cpp.zip");
size_t pos = s.find('.');
string suffix = s.substr(pos);
cout << suffix.c_str() << endl;
}
虽然程序运行成功了,但是这里的代码本身是有问题的,因为VS2022编译器的优化,所以才没有出现问题,我们来分析一下问题吧:
假设目前的编译器环境不是VS2022,假设是VS2019。(VS2022的优化很激进,它会和三为一,把sub变量和临时变量都优化掉,用suffix去充当sub)因为这里的sub是一个传值返回,本来是会构造一个临时变量,用临时变量去拷贝suffix。但是经过编译器的优化,就会直接用sub去拷贝suffix,因为我们目前还没有写拷贝构造,所以这里的拷贝是一个浅拷贝。所以此时sub和sufiix的_str也相同,当sub销毁的时候suffix也被销毁了。所以此时suffix就会指向一个野指针,就会导致程序出现乱码的错误
为了解决这个可能会存在的隐患,我们就需要写一个拷贝构造函数:
string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
像这样写一个深拷贝就能避免程序出现意外
10.实现比较大小的符号重载
string类中的成员函数现在已经实现的差不多了,我们来实现string类中的大小比较符号的重载。比较大小符号的重载写成的是全局函数,因为它想要支持string类和string类比较、字符串和string类比较等。
我们还是使用复用的方法,这样我们就可以节省代码的空间
1.先来实现一下小于符号的重载
我首先需要知道的是:字符串之间的比较不是按照长度来比较的,而是按照ASCII码表来进行比较的,所以此时我们就可以使用到strcmp函数来比较大小
bool operator<(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) < 0; } bool operator<=(const string& s1, const string& s2) { return s1 < s2 || s1 == s2; } bool operator>(const string& s1, const string& s2) { return !(s1 <= s2); } bool operator>=(const string& s1, const string& s2) { return !(s1 < s2); } bool operator==(const string& s1, const string& s2) { return strcmp(s1.c_str(), s2.c_str()) == 0; } bool operator!=(const string& s1, const string& s2) { return !(s1 == s2); }
我们来测试一下代码:
void test_string7()
{
string s1("hello world");
string s2("hello world");
cout << (s1 < s2) << endl;
cout << (s1 == s2) << endl;
cout << ("hello world" < s2) << endl;
cout << (s1 == "hello world") << endl;
}
我们在这里需要注意,在进行大于小于比较的时候需要加上括号,因为 << 的优先级更高
我们这里没有重载string和字符串、字符串和string的比较,但是为什么还是可以成功比较呢?这是因为走了隐式类型的转换
但如果是下面的这种形式就不会走隐式类型转换:
cout << ("hello world" == "hello world") << endl;
因为运算符的重载必须要有一个类类型的参数,这里的代码是两个常量字符串的比较,编译器在识别的时候就会识别为一个指针,相当于是两个指针的比较了
11.实现流插入和流提取的重载
因为流插入是读取string的值,并且将它的值插入到一个ostream类型的对象中去,所以要用以下以下的形式处理代码的const:
ostream& operator<<(ostream& out, const string& s)
而流提取是要将istream类型的数据写入一个string类型的对象中去,所以此时情况就与流插入相反:
istream& operator>>(istream& in, string& s)
我们先来处理一下流插入代码的细节:
string对象是不支持直接插入的,但是流插入库里面已经实现好了,对于内置类型的成员是支持插入的,所以此时我们就需要把string转换成一个一个的字符再插入,这里我们就可以使用范围for来取出所有的字符:
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
我们来测试一下代码:
void test_string8()
{
string s1("hello world");
cout << s1 << endl;
}
接下来我们来实现流提取的代码:
我们首先需要一个字符一个字符的提取数据,遇到换行和空格就默认作为字符串的分割:
istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '/n')
{
s += ch;
in >> ch;
}
return in;
}
接着来测试一下:
void test_string8()
{
string s1;
cin >> s1;
cout << s1 << endl;
}
此时我们就会发现一个问题:无论是空格还是换行都不会停止输入,程序一直在循环的输入,不会跳出:
出现这个问题的原因是:cin和scanf输入和提取任何类型的值默认都是以空格和换行为分隔符,所以我们在输入一个一个的字符的时候,也会自动忽略掉换行和空格
所以我们就不能使用流提取来提取这里的char数据,此时我们就需要调用到istream里面的一个叫做get的函数:
这里的get不会管分隔符,也不涉及任何的类型,只会一个字符一个字符的读取,和C语言中的getc函数相似,所以我们使用get函数对代码进行修改:
istream& operator>>(istream& in, string& s)
{
char ch;
ch = in.get();
while (ch != ' ' && ch != '/n')
{
s += ch;
ch = in.get();
}
return in;
}
此时重新运行代码,就可以看到代码没有出现任何的错误
我们再看一下,假设s1中原先就已经存在字符串:
void test_string8()
{
string s1("hello world");
cin >> s1;
cout << s1 << endl;
}
我们可以发现:在输入数据以后,原来的数据依旧存在,这一点与库函数不同,库函数在输入以后不会保留原来的数据,我们需要保证模拟实现的函数功能要与库函数中的一致,所以我们此时对代码进行修改:
因为在输入之前我们先要清空原来的所有数据,所以我们此时还需要在string类中写一个clear函数:
clear函数的实现十分简单,我们给数组中的第一个数据赋予 \0 ,再把size改成0,capacity不修改就好了:
void clear()
{
_str[0] = '\0';
_size = 0;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
while (ch != ' ' && ch != '/n')
{
s += ch;
ch = in.get();
}
return in;
}
此时我们重新运行代码,就不会出现问题了:
其实流提取的代码还有优化的空间:
假设我们输入的是一个很长很长的字符串时,此时s就会不断的去 += ch,不断的+=就会不断的去扩容,此时就会对程序的效率产生一定的影响。要是我们可以提前开辟好空间,就会提高程序的效率。单此时我们就会遇到一个麻烦:我们不知道要开辟多大的空间才合理
此时有人就提供了这样的一种解决方案:
先创建一个buff数组,数组中的数据个数任定,每次提取的字符都先放到buff中去,假设输入数据的个数很大,buff满了,此时直接在s后面+=buff。一直这样循环,直到数据结束:
istream& operator>>(istream& in, string& s)
{
s.clear();
const int N = 256;
char buff[N];
int i = 0;
char ch;
ch = in.get();
while (ch != ' ' && ch != '/n')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
这样处理比起直接reserve的好处是可以减少空间浪费或者频繁的扩容
**************************************************************************************************************
这里来提出一个问题:我们之前实现日期类的时候将流插入和流提取定义为一个友元函数,那么流插入和流提取是不是必须写成友元函数呢?
答案是:否,假设我们不需要访问一个对象的私有成员就不需要写成友元函数,就像上面写的流插入的代码一样,可以直接使用迭代器或者下标+[ ]就能访问的就不用写成友元函数
**************************************************************************************************************
结尾
string类的主要功能到这里就实现的差不多了,自己去模拟实现string类不仅可以提升我们的代码水平,还可以加深我们对于库函数的理解。那么本节的的内容就到此结束了,希望可以给您带来帮助,谢谢您的浏览!!!