STL String是最重要,最常用的STL之一,能够大大降低我们处理字符串的工作量。今天我们来自己写一个MyString,在编码中理解它的工作原理。
基本结构
从一个容器的角度讲,string的结构并不复杂,本质上还是一个顺序表。如图可以生动展示一个string所占用的空间。其中**_size表示当前已经使用的空间,_capacity表示最大容量。特别需要注意的是,string的字符串结尾会固定存放一个’/0**’来表示字符串结尾。在实践中我发现在不主动调用reserve()方法与resize()方法的情况下,string.capacity()的值往往与string().size()相同。
由此可以得到string的私有部分定义:
1 | private: |
构造方法
下面给出string的构造方法:
1 | public: |
上述代码基本没有新难点。
重载运算符
下面代码对一些运算符进行了重载
1 | MyString& operator=(MyString str){ //对=符号进行重载 |
string的方法为了能够满足地址,指针和string三种情况的调用,往往要多次重写方法。建议先实现一些常用的方法再来完成运算符重载,这样可以直接调用已经写好的方法,避免出错,简化代码。例如上述代码中的**push_back()**与append()方法可以提前写好调试完成。
assert()方法在<assert.h>头文件中定义,用来对内部条件进行判定。一旦内部表达式为false,就向stderr打印一条错误信息,并终止程序。
方法实现
下面代码实现了一些常用的方法。
1 | size_t strlen(const char *str){ //strlen方法从当前指针位置开始向后累加,计算到\0为止的字符串长度 |
这部分代码量最然比较大,但大部分内容还是常规的顺序表操作。
值得一提的是,其中reserve()方法用来修改的是_capacity的值,也就是最大容量。而resize()方法用来修改的是_size,也就是已占用的空间。这让我对_size和_capacity的意义出现了混淆。所以我使用原版的string进行了验证,令我意外的是原版的string中_size与_capacity似乎是始终相等的。于是我通过下面的代码进行测试:
1 | #include<iostream> |
得到了以下结果:
当我使用reserve()方法与resize()方法修改string的长度时,情况果然发生了改变。为了让结果更加明显,设置了500和600两个比较夸张的数字,得到的字符串后面出现了大量的空白。猜测是resize()导致的原字符串后面的填充,于是让程序输出后面随意一个空格的ASCII码,果然其值为0.也就是说,在string中,如果用户强行将其_size提高,那么新增的有效空间会用ASCII码为0的空字符填充。
由此可见,假如用户实际使用的空间<_size<_capacity,那么此时string中的存储情况是这样的:
另外,起初我使用的resize()的值大于reserve()的值,导致结果中size和capacity仍然是同样大的。这种现象是因为,当resize()的参数值大于reserve()时,会自动调用reserve()将string进行扩容,并扩容到resize()的大小。这一点在上面的resize()方法中已经体现。
迭代器
下面代码实现了string的迭代器。由于是顺序存储,所以迭代器并不复杂。
1 | typedef char* iterator; |
其它
为了让string能够符合用户习惯地输入和输出,需要对输入和输出进行一些处理。本质上其实属于’<<’和’>>’运算符的重载。
在MyString对象类中,加入以下代码:
1 | friend ostream& operator<<(ostream& out, const MyString& str); |
其中friend是声明友元函数。友元函数可以调用本类中的私有部分。
在MyString对象之外加入以下代码:
1 | ostream& operator<<(ostream& out, MyString& str){//重载输出运算符,注意添加头文件iostream |
在对‘<<’的重载中,主要实现的是用户可以通过cout<<StringName;的方式直接打印整个字符串。在对’>>’的重载中,主要实现的是以用户输入的回车键作为输入的结尾。当然,如果你喜欢也可以做一些个性化的输入输出设置。
整体代码
整体的代码如下:
1 | #ifndef MYSTRING_H |