string时STL中的模板库之一,类似于专门处理字符串的数据结构,在模拟实现并探讨其中构造的巧妙之处之前,我们短浅的认识一下STL是什么
目录
什么是STL
STL的诞生
关于string
string的模拟实现
构造函数和析构函数
实现简单的string打印
拷贝构造函数
摇人打工法实现拷贝构造
赋值操作符重载
resize和reserve
reserve
resize
push_back
append追加
+=操作符重载
insert
erase
find
<<和>>
<<
>>
string的迭代器
什么是STL
STL的全名:standard template libaray-标准模板库
STL的诞生
出自于惠普实验室的两位大牛,Alexander Stepanov,Meng Lee,奔着开源精神,两位大牛表示STL的源码可以随意传播修改乃至商业用途,但是要求必须和源码一样开源,这就是STL的始祖版本
STL的后序版本:P. J. 版本,RW版本,SGI版本,各类版本的区别以及优缺点就不做记述了,毕竟我们作为使用者不仅是学习使用,学习其范式编程的思维也是重点。
关于string
STL其实在开发过程中更像一个非常方便的工具包,相较于什么数据结构都需要自己实现的C语言来说,STL支持了不少的数据结构,只需要包含几个头文件,我们就能非常便利的使用,比方说我们接下来需要实现的string
就平常来说,我们在C语言阶段操作字符串是比较头疼的一个过程,而使用STL,则非常的方便。
比如,我希望简单的打印一个字符串,并且将其与另一个字符串链接在一块。
使用string,不仅不需要再写一个for循环来遍历字符串打印,甚至也能利用+=的操作实现字符串之间的相加!可以说是非常方便了,不过知其然不知其所以然就没啥意思了,毕竟单纯的使用这种事的门槛非常的低,我们还是需要了解其原理才能有所收获。
string的接口记述以及趣闻分享
string作为其中的一个模板库,由于创建的时间是有一阵子了,其中包含了许多冗余的接口,当然还有各式各样有用的接口,需要查阅的话可以跳转至这个网站:https://cplusplus.com/reference/
当然string也是有更多有趣的事情,陈皓前辈就讲了这个问题:STL 的string类怎么啦?_haoel的博客-CSDN博客
作为一个方便使用者开发的优秀代码集大成者,STL的重要性以及其他的特点我相信其他文章的作者一定写的比我更好,我也就不多说了。
string的模拟实现
基本的成员变量
class mystring
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
}
模拟实现string类就逃不开最基本的几个成员函数了,毕竟我们需要在堆上开辟空间,所以我们要自己实现构造函数
构造函数和析构函数
构造函数其实和顺序表差不多,但是有了new之后整个代码也变短了不少
//构造函数,字符串初始化版本,求长度,算容量,开空间
mystring( const char* str="")
{
_size = strlen(str);
_str = new char[_size + 1];//留一个给斜杠零
strcpy(_str, str);//拷贝数据到新空间
_capacity = _size;
}
析构函数,对应格式delete掉即可
//析构函数
~mystring()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
实现简单的string打印
我们在使用STL的sting的时候打印非常方便,string直接适配了cout,但是目前我们只需要简单的打印功能,重载流插入和流提取操作符的具体操作后面再一块说,我们先简单的实现一个。
首先,我们需要解引用整个字符串,然后打印出来就好了,由于string的本身是一个字符串数组,其本身就支持【】解引用操作
void Print()
{
for (int i = 0; i < _size; ++i)
{
cout << _str[i];
}
}
然后往里面塞一个字符串看看效果
当然我们还需要支持随机访问,也就是支持str的【】解引用功能,那么很简单,我们直接重载【】操作符
重载没什么难度,不过不要忘记多实现一个const版本。
char& operator [](size_t a)
{
assert(a < _size);
return _str[a];
}
const char& operator[](size_t index)const
{
assert(index < _size);
return _str[index];
}
关于const版本的成员函数,我打算在这里复盘一把,为什么要实现const版本的成员函数?
const修饰成员函数
const修饰的成员函数,其本质上修饰的并不是这个函数,而是当前这个类对象成员函数中的那个this指针
拿上文的const版本的成员函数举例,我们看到的是
const char& operator[](size_t index)const
而实际上,真实的样子是
const char& operator[](const mystring *const this ,size_t index)
const修饰了当前的This指针,this指针本身携带一个const,本身自带的这个const是用来保护this自己的,但是并不会保护this指针所指向的对象,而const成员函数则是保护所指向对象的。
实现const版本的成员函数,可以保证const类型的类对象能成功调用成员函数。
如果没有,const类对象就用不成了。
但是const版本的成员函数也不是都要实现,只有当满足以下条件时,实现才有必要。
1.任何不会修改成员的函数都应该声明为const类型。
2.const成员函数不可以修改对象的数据。
拷贝构造函数
拷贝构造函数需要传引用,为了得到原生的字符串头指针来使用strcpy,我们还需要额外实现一个c_str
mystring(const mystring& str )
{
_size = str._size;
_str = new char[str._capacity+1];
_capacity = str._capacity;
strcpy(_str, str.c_str());
}
char* c_str()
{
return _str;
}
char* c_str()const
{
return _str;
}
这种方法比较朴实无华,我们实现一个比较”现代“的写法,摇人打工
摇人打工法实现拷贝构造
//拷贝构造现代写法,摇人打工
mystring(mystring& str)
:_str(nullptr), _size(0), _capacity(0)
{
mystring tmp(str._str);
swap(str);
}
我们利用构造函数创建一个临时变量tmp,然后将tmp与当前的this交换,为了防止tmp析构的时候报错,我们给一个空指针到this,delete一个nullptr不会报错。
当然swap我们也要重写一个,算法库内部的swap效率较低。
题外话:T c(a); 这个看上去是个构造函数啊,是不是内置类型就不能用了?
C++在产生模板之后,为了能照顾到内置类型,为内置类型也提供了类似构造函数的初始化方法。
比如:
int i(10); int j = int();
所以这种调用构造函数的方法对内置类型也是生效的,不必担心
void swap(mystring& str)
{
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
赋值操作符重载
赋值,传入一个字符串,把当前的sting置换成传入的字符串
与构造函数类似,但是需要注意连续赋值的问题,str1 = str2 = str3
还有刁钻情况,自己给自己赋值。
mystring& operator=(const mystring& 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;
}
resize和reserve
resize和reserve这两个接口函数在控制sting的空间上还算是挺有用的,接下来就来实现一下
reserve
reserve的功能,我们查阅原码,简单来说实现的功能就是:重置容量,但是我们还是需要注意缩容的问题。也就是传入的参数大小小于当前的容量时,我们不能执行,不然就缩容了。
void reserve(size_t n = 0)
{
if (n > _capacity)
{
//新空间,之所以是n+1则是为了斜杠零而留出的空间
char* tmp = new char [n + 1];
//拷贝
strcpy(tmp, _str);
//更新容量
_capacity = n;
//销毁原空间
delete[]_str;
_str = tmp;
}
}
resize
阅读原码的解释,resize需要传递两个参数,一个是更新size的值,另一个则是当N大于当前size值时,我们可以选择是否往空出来的那一部分填充字符。那么在这里我们直接给个缺省值‘\0’
void resize(size_t n, char c = '\0')
{
if (n > _size)
{
reserve(n);
for (size_t i = _size; i < n; ++i)
{
_str[i] = c;
}
_size = n;
_str[_size] = '\0';
}
else
{
_size = n;
_str[_size] = '\0';
}
}
push_back
push_back 这个函数我们都非常熟悉了,不过这里面还是有一些细节需要处理
size的位置被strcpy直接覆盖掉了,我们不仅是扩容size自增,还需要在加上一个'\0'
void push_back(char c)
{
//尾插,如满,扩容
if (_size == _capacity)
{
int newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = c;
++_size;
//strcpy从\0处开始覆盖,需要在后面加上\0
_str[_size] = '\0';
}
append追加
这个函数用于在当前的string后面追加一个字符串,有多个重载·版本,在这里就实现最简单的,追加一个字符串
//字符串追加
void append(const char* str)
{
size_t size = strlen(str);
if (_size + size > _capacity)
{
reserve(_capacity + size);
}
strcpy(_str + _size, str);
//只是重置了容量大小,size还没有更新。
_size += size;
}
+=操作符重载
实现过了append,+=就非常简单了,直接复用即可
mystring& operator+= (const mystring& str)
{
append(str.c_str());
return *this;
}
insert
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len ;
while (end > pos + len-1)
{
_str[end] = _str[end -len];
--end;
}
//用strncpy是因为直接strcpy会把\0也拷过去,读的时候就直接断层了,用strncpy的话可以规避掉\0
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
erase
//删除
//给个npos的缺省值,当没有传递需要删除的个数的时候全部删除
//还需要额外处理,当len<size- pos的时候正常删除,如果后面还有剩余的字符串,直接挪动过去覆盖就好,没有的话就
//在POS位置放一个\0
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len < _size - pos)
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
else if (len == npos || len >=_size + pos)
{
_str[pos] = '\0';
_size = pos;
}
return *this;
}
find
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
char* ptr = strstr(_str+pos, str);
if (ptr != nullptr)
{
return ptr - _str;
}
else
{
return npos;
}
}
<<和>>
<<
对于流插入和流提取的问题,我们有一个前置条件,这两个重载不能称为成员函数,因为this指针会抢占第一个操作符的位置让我们使用起来非常难受。
那么我们将其写成全局函数。
这里有一个小问题,我们在类和对象的阶段所接触的<<由于访问限定符的限制原因,我们将其写成了友元成员函数,那么<<的重载函数一定要是友元么?
显然不是,不能直接访问你的成员变量,我可以写一个函数间接的去访问。
ostream& operator<<(ostream& out,const mystring& str)
{
for (size_t i = 0; i < str.size(); ++i)
{
out << str[i];
}
return out;
}
返回值使用ostream做返回值的对象,用来支持连续cout等操作
>>
提取的函数也不算困难,需要注意的是缓冲区的问题
我们先简单的实现一下,利用一下istream里面的get函数来获取当前缓冲区内部的字符,get的行为很像gets,不过我们还是在C和C++之间做出区别较好。
istream& operator>>(istream& in, mystring& str)
{
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
str += ch;
ch = in.get();
}
return in;
}
目前的版本仅能实现拿取空格和换行符之间的字符
当然要实现一次拿完也可以,修改条件即可
不过库里的实现就是这样的,我们跟着库来实现。不过这里有一些效率上的问题,应对短小的字符串+=的操作还算可行,可一旦字符串变长了,效率就下来了。
那我们可以分批次处理。
istream& operator>>(istream& in, mystring& str)
{
str.clear();
char ch = in.get();
char buff[128] = { '\0' };
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
//已满,+=数据
if (i == 127)
{
str += buff;
//i置零
i = 0;
}
buff[i++] = ch;
ch = in.get();
}
//已跳出循环,遇到'\n'或' '已取完当前缓冲区分割区域内的数据,
//由于上逻辑为满127才+=数据,那么一定会有剩余情况发生,在这里额外处理
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
string的迭代器
我们知道迭代器的使用方法类似指针,那么我们可以简单的实现一下。
//迭代器:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
到这,一个具有基本功能的stirng就模拟实现完成了
感谢阅读,希望对你有点帮助