string
的模拟实现系列文章:
文章目录
- [7. 字符串操作](#7. 字符串操作)
-
- [7. 1 `c_str()`和`date()`](#7. 1
c_str()
和date()
) - [7. 2 `find()`](#7. 2
find()
)
- [7. 1 `c_str()`和`date()`](#7. 1
- [8. 迭代器相关](#8. 迭代器相关)
-
- [8. 1 迭代器实现](#8. 1 迭代器实现)
- [8. 2 `begin()`](#8. 2
begin()
) - [8. 3 `end()`](#8. 3
end()
)
- [9. 运算符重载](#9. 运算符重载)
-
- [9. 1 流插入和流提取](#9. 1 流插入和流提取)
- [9. 2 比较运算符](#9. 2 比较运算符)
- [10. `string`模拟实现的现代写法](#10.
string
模拟实现的现代写法) - [11. 补充阅读:写时拷贝](#11. 补充阅读:写时拷贝)
7. 字符串操作
7. 1 c_str()
和date()
cpp
const char* c_str() const;
返回string
对象底层的字符串的指针,且不可通过返回的指针修改字符串。
实现时直接返回即可。
cpp
const char* string::c_str()const
{
return _str;
}
但是要注意的是,如果在使用该函数并将其返回值存储起来后,如果后续对string
操作时导致string
的字符串的地址发生了改变,那么之前存储起来的指针就已经失效了,是个野指针。
这个函数用来兼容一些C语言的不兼容string
类型的接口。
date()
接口和c_str()
的效果是基本一样的。
7. 2 find()
首先是find()
,它有四种重载:
cpp
size_t find (const string& str, size_t pos = 0) const;
size_t find (const char* s, size_t pos = 0) const;
size_t find (const char* s, size_t pos, size_t n) const;
size_t find (char c, size_t pos = 0) const;
虽然有四种,但它们的目的是一样的:从string
对象的第pos
(缺省为0)个元素开始向后查找到数据尾部,查找与 str/s
前n
个字符完全相同的字符串,并返回第一个匹配项的第一个字符的位置,如果没有匹配项就返回npos
。
这里模拟实现一下第2个和第4个重载,其他重载也是同理。
cpp
size_t string::find(char c, size_t pos) const
{
// 首先要断言,不然会数组越界访问
assert(pos < _size);
// 从pos位置开始找与字符 c 相同的字符
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
return i;
}
// 没找到返回 npos
return npos;
}
size_t string::find(const char* s, size_t pos) const
{
assert(pos < _size);
// strstr是C语言中用于比较两个字符串是否相同的库函数,使用方法请看下文。
char* tmp = strstr(_str + pos, s);
if (tmp == nullptr)
return npos;
return tmp - _str;
}
补充:strstr的使用,在第10章。
如果是带有size_t n
形参的函数,可以先把str/s
的前n
个字符拷贝出来再进行相同的过程。
而与find()
功能相似的一系列函数:
cpp
// 与find()相同,只是从后往前找
rfind()
// 在字符串中搜索与其参数中指定的任何字符匹配的第一个字符。
find_first_of()
// 从后往前找
find_last_of()
// 找第一个不是的
find_first_of()
// 从后往前找第一个不是的
find_last_not_of()
可以自行尝试实现,与find()
其实都大同小异,这里就不再赘述了。
8. 迭代器相关
在上文第三章我们提到过,可以直接使用
cpp
typedef char* iterator;
来模拟实现迭代器,这里做进一步补充。
8. 1 迭代器实现
尽管我们已经通过这种方式得到了迭代器,但迭代器除了普通迭代器之外还有const
迭代器,对于一个被const
修饰的string
类型,如果尝试取出一个普通迭代器,会发生权限放大;以及在一些场景下,我们不希望迭代器有对对象中的数据进行修改的权限,就可以使用const
迭代器,它的声明如下:
cpp
typedef const char* const_iterator;
注意不要修改const_iterator
这个名称!
对于普通的迭代器,当需要需要修改数据时,直接进行解引用然后修改就行了,而const
迭代器虽然也能解引用,但是不能修改,只能查看数据。
8. 2 begin()
直接返回_str
的第一个元素位置。
注意要实现两个版本,一个是普通版本的,还有一个const
版本的。
cpp
string::iterator string::begin()
{
return _str;
}
string::const_iterator string::begin() const
{
return _str;
}
8. 3 end()
返回指向_str
的最后一个元素的下一个位置的指针。
也要提供两个版本。
cpp
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::end() const
{
return _str + _size;
}
注意迭代器区间是左开右闭的,所以end()
指向的是有效数据的下一位。
rbegin()
和rend
的实现相对复杂,这里先不讲。
9. 运算符重载
9. 1 流插入和流提取
在类和对象(中)5.1章第13点我们提到过,流插入和流提取必须重载为全局函数或者使用友元,这离我们依然将其重载为全局函数。
cpp
std::ostream& operator<<(std::ostream& out, const test::string& s);
std::istream& operator>>(std::istream& in, test::string& s);
- 流插入比较简单,只需要直接将
_str
插入到流中就可以了。
cpp
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
cout << s[i];
}
cout << endl;
return out;
}
- 但流提取比较复杂,因为我们不知道会插入多长的数据,所以不能直接包装
cin>>_str
,我们可以每次读取一个字符,然后判断和这个字符是不是' '
或者'\n'
,如果是就停止读取,不是就插入到字符串中。但是每次都进行插入可能会有性能问题,为了缓解这一问题,我们可以创建一个字符数组,每次读取到字符之后先放到这个字符数组中,如果这个字符数组满了或输入结束,就把它加到string
对象中,这样可以缓解性能问题。
补充:istream::get()
类似于getchar()
,可以从流中得到一个字符。
cpp
istream& operator>>(istream& in, string& s)
{
char ch = '\0';
// 流提取会把原来的数据全部删除
s.clear();
// 先提取一次,避免错误数据被插入
in.get(ch);
// 临时数组
char tmp[256];
int times = 0;
while (ch != '\n' && ch != ' ')
{
if (times == 255)
{
tmp[times] = '\0';
s += tmp;
times = 0;
}
tmp[times++] = ch;
in.get(ch);
}
// 如果是输入结束,也要把tmp再次追加到string对象后面
if (times != 0)
{
tmp[times] = '\0';
s += tmp;
}
return in;
}
9. 2 比较运算符
cpp
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
比较操作符虽然可以在类中重载,但也可以将其重载为全局函数。
虽然看着很多,但实际上我们只需要实现两个------==
和>
,其他的就可以全部进行复用这两个来快速解决这六个函数了。
另外在实现operator==
和operator>
时,也可以直接使用C语言的库函数strcmp
,使用方式可见:字符串函数第六章。
cpp
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 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 || s1 == s2;
}
bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
10. string
模拟实现的现代写法
这是两个复制重载,你觉得哪一个更好?
cpp
// 注意这个形参没有引用符号
String& operator=(String s)
{
swap(_str, s._str);
return *this;
}
string& string::operator=(const string& s)
{
string tmp(*this);
clear();
if (s._size > _capacity)
{
size_t newcapacity = 2 * _capacity > s._size ? 2 * _capacity : s._size;
reserve(newcapacity);
}
strcpy(_str, s._str);
return *this;
}
实际上从性能的角度分析,第一个写法是不如第二个写法的,但是如果面试中面试官想要你快速实现一个string
类的框架,那么第一个写法可以更加快速地实现出来,在这种场景是更优秀的。
现代写法体现的是复用这一思想,可以更快地帮助我们开发,也能相对减少Bug出现的可能,所以除了对性能要求特别高的情况下,都可以使用类似的思想来加快开发。
本文的许多地方已经使用了现代写法,比如赋值操作符重载和比较运算符重载。
11. 补充阅读:写时拷贝
在一些编译器如g++中,有时不会使用深拷贝,而是使用写时拷贝(也称写时才拷贝)。
其原理大致为:
- 对于每一个不相关的
string
对象,其指向的字符数组_str
都有一个对应的计数器time
。 - 当发生拷贝时,直接进行浅拷贝,并
time++
。 - 当有指向这块空间的
string
对象析构时,time--
,只有在time
为0时才会释放这块空间 - 如果在析构之前,有对象尝试对这块空间的数据进行修改(写入),就会进行深拷贝,让这个对象指向一块新的空间。
那么通过这个原理我们可以看出,如果是一些string
对象指向同一块空间并且都不进行修改的情况,写时拷贝可谓如鱼得水,效率相对我们之前使用的读时拷贝效率高了非常多。
但写时拷贝也有一定的缺陷:C++的std::string的"读时也拷贝"技术! | 酷 壳 - CoolShell。
谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章