目录
前言
什么是string? 为什么要学习使用string?string的优势?
因此,string类的成员变量也如图顺序表一样,如下图所示:
构造函数
拷贝构造
析构函数
size() 、capacity()
operator[]
迭代器 与 范围for
reserve
push_back
append
operator +=
insert
erase
resize
operator =
swap
find
substr
clear()
string的重载运算符
string类的 流插入 和 流提取
前提须知:
实现:
流插入
流提取
完整代码:
扩容机制解读:
getline()
前言
本篇博客是从底层角度来模拟实现STL的string,在C++的学习过程中string是一个非常重要的知识点,在之前的介绍讲解中,我们了解到string有多个接口,且每个接口的功能和接收参数都不一样,所以本篇将会重点介绍和讲解几个重要接口的常用方式。
前情提要:C++:string相关内容的简单介绍-CSDN博客
什么是string? 为什么要学习使用string?string的优势?
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。不是很方便。
C++中有string类来提供各种各样的函数接口,大大提高了办事效率。
学过数据结构的朋又应该能够理解,string的本质其实就相当于是存放数据类型为char的顺序表。
换句话说,string是表示字符串序列的类。
因此,string类的成员变量也如图顺序表一样,如下图所示:
private:
char* _str;//指针,表示string开辟空间的地址
size_t _size;//字符串长度
size_t _capacity;//string开辟的空间的空间大小
构造函数
在使用string时,我们通常会发现在讲一串字符使用string修饰时,我们无需亲自为字符开辟空间,这是因为在底层的逻辑中,字符串的空间早就被string类使用构造函数开辟了。
string (const char*str = "")
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str,str);
}
以上的代码设立了缺省值为空,表名了这是一个即是无参又是有参的一个构造函数。
设立缺省值const char*str = ""好处在:
当传递了一个无参的参数时,该构造函数会因为缺省值传输参数,但是参数是无效的,
所以再_size时就会得到长度为0,从而应发空间为0,
但是会因为 string s; 这种代码的存在,空间还是需要被开辟的,所以还是需要开辟空间。
所以 _capacity + 1表示无空间时进行开辟,表明存在,有空间时表示为delete的标识符,方便删除
拷贝构造
拷贝构造在之前的学习过程中,分为了深拷贝和浅拷贝。编译器自动生成的默认构造函数使用的是浅拷贝,浅拷贝的拷贝方式与取别名 & 的功能有些类似,单纯的拷贝,就连空间地址也是一模一样的完美复制,所以string类中的拷贝构造使用的是开创新空间进行存放的深拷贝:
使用了深拷贝方式进行拷贝
string(const string& s)
{
_str = new char[s._capacity +1];
strcpy(_str,s._str);
_size = s._size;
_capacity = s._capacity;
}
深浅拷贝相关介绍:C++ : 类的简单介绍(五)————— 拷贝构造函数 & 函数传参 & 运算符重载-CSDN博客
析构函数
~string()
{
delete[]_str; //空间的释放
_str = nullptr;//指针的释放
_size=_capacity =0;//内容的清空
}
size() 、capacity()
size_t size()const//const是为了方便const对象使用的
{
return _size;
}
size_t capacity()const
{
return _capacity;
}
operator[]
operator[]在C++语言中是一种特殊的遍历方式,使用的是重载运算符[]和C语言中的数组遍历,结合出的一种全新遍历方式,在string中可以使用operator[]对字符串中的字符进行遍历,甚至是改变其中的字符:
而根据operator[]在string内部的使用情况operator[]需要分为两种,一种是const使用的一种是非const使用的,而operator的内部情况无疑于指针的遍历。
char& operator[](size_t pos)
{
assert(pos<_size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos<_size);
return _str[pos];
}
迭代器 与 范围for
在某一些的编辑器中,迭代器的底层逻辑其实就是指针,两个分别指向字符串首字符和字符串末尾\0的指针 begin与end,所以在一些罗普大众的教材中,通常会直接把迭代器和指针挂钩,在它们看来,迭代器只不过是char换了个名字,然而功能还是和char类型指针一样的指针:
typedef char* iterator
iterator begin()
{
return _str;
}
iterator end()
{
return _str+size;
}
与operator[]相同的是,作为迭代器的iterator也有分为const使用和非const使用的类别:
typedef const char* const_iterator //const使用的
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str+size;
}
同时在迭代器是否是指针的问题上,我们需要认识范围for,范围for其实就是一种迭代器,在C++的底层,范围for使用的真是迭代器的代码进行运转:
for (auto ch :s3)
{
cout << ch << "";
}
cout << endl;
而且,如果我们手搓了迭代器,且名字只要是迭代器的名字 iterator、 begin、 end 、const_iterator 范围for就可以使用我们手搓的迭代器
但是这几者中间名字对不上那范围for就对不上,用不了,只是所以范围for就是一种傻傻的替换并不会灵活变通的迭代器。
reserve
作为string的内部扩容函数,在底层中常常被其他插入函数作为扩容机制调用,比如较常使用的插入函数:push_back、append等等。
void reserve(size_t n)
{
if(n>_capacity)//扩容机制
{
char*tmp = new char[n+1];
//当然因为delete的问题,开辟空间需要额外开辟一个标识符进行使用,所以需要n+1
//这个n+1并不是给\0使用的而是给为了让delete辨认而多开辟的标识符使用的
strcpy(tmp,_str);//把原来空间的内容放到新空间内部
//delete的删除机制,删除原来的空间
delete[]_str;
_str = tmp;//指针和地址的赋予
_capacity = n;//表名空间已经完成
}
}
push_back
push_back的主要作用是在字符串的尾部插入一个字符,增长字符串的长度,以及可能会扩大字符串所在空间的大小等等,同时需要注意:push_back的扩容机制是二倍扩容。
void push_back(char ch)
{
//进行二倍扩容
if(_size == _capacity)//如果长度和空间大小一样就需要扩容
{
reserve(_capiacty == 0? 4:2*_capacity);//同时需要考虑到长度和空间大小都是0
//但是需要插入字符,所以需要进行空间的扩容
}
_str[_size] = ch;//ch表示插入的字符,_size表示插入的长度也表示需要插入的位置
++_size;
_str[_size] = '\0';//在插入字符后面加上\0表示字符插入完成
}
append
append最常用的功能是在字符串尾部插入字符或者字符串,而且所以和push_back不同的是,它的扩容机制不能遵循二倍扩容,因为如果字符串的长度过长,使用二倍的空间可能不够用,所以append扩大的空间可能需要遵循字符串的长度来扩充 :
void append(const char* str)
{
//扩容
size_t len = strlen(str);
if(_size + len > _capacity)//使用插入的长度和字符串长度相加 和空间大小对比
{
reserve(_size + len);//进行空间的扩大
}
strcpy(_str + _size , str);//在扩大空间后的原字符串尾部插入新的字符串
_size + = len;//表名插入了字符串
}
operator +=
operator += 最常用的两个功能在底层的逻辑上就是调用了刚才写的push_back 和 append
insert
insert 在指定位置插入字符 或者字符串,而根据insert在string内部底层的方式和效率来看,insert的底层是需要进行数据的挪动,以此达到插入字符串的操作。
和string的其他插入函数一样,都具有扩容的功能的同时还有判断是否越界的功能
插入字符功能的insert:
void insert(size_t pos, char ch)
{
assert(pos <= _size);//进行越界机制的断言
if(_size == _capacity)//判断是否需要扩容操作
{
reserve(_capacity == 0? 4:2*_capacity);
}
size_t end = _size+1;//进行底层的数据移动操作
while(end>pos)
{
_str[end] = _str[end-1];
--end;
}
}
插入字符串功能的insert:
void insert(size_t pos, const char *str)
{
assert(pos <= _size);//进行越界机制的断言
szie_t len = sizeof(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(_str + pos,str,len);
_size + len;
}
strncpy 函数介绍:strncpy-CSDN博客
erase
erase的主要功能是在指定位置删除指定个数的字符。
erase分为两种删除,而erase的删除需要根据 npos进行判断,所以需要加入前置条件:npos ,在整个类的外面加上 const int string::npos = -1,与此同时在类的成员变量中加入public的变量 static const int pos;这样 npos才能够正常的使用
void erase(size_t pos,size_t len = npos)
{
assert(pos<_size);
if(len == npos || len >= _size-pos);
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos , _str+pos +len);
_size -= len;
}
}
earse的删除分为两种
1.全部删除:
对于全部删除满足的条件是指定删除的字符个数大于要剩下的长度或者是没有直接说明删除的字符个数,
这两种情况相当于在指定位置上的字符直接变成\0
代码:
if(len == npos || len >= _size-pos);
{
_str[pos] = '\0';
_size = pos;
}
2.区间删除:
区间删除,就是把这段区间后面的字符覆盖区间需要删除的空间位置,
可以使用strcpy进行拷贝,把需要删除的区间的首个字符地址上传,
然后把区间之后的字符串的首个字符上传
代码:
else
{
strcpy(_str + pos , _str+pos +len);
_size -= len;
}
关于npos :
- npos是C++标准库中的一个常量,它代表了一个无效的或者不存在的位置。具体来说,npos是一个static成员变量,它的值是一个特别大的无符号整数,通常是-1。在字符串和容器类中,npos常常用于表示查找失败或者无效位置的情况。
- 在字符串中,npos常常与find()函数一起使用。当find()函数无法找到指定的子串时,它会返回npos作为结果。例如,如果我们使用string类的find()函数查找一个不存在的子串,那么它会返回npos。
- 在容器类中,npos常常与find()函数、rfind()函数、find_first_of()函数、find_last_of()函数等一起使用。这些函数在查找元素或者子串时,如果找不到匹配项,就会返回npos。
- 总之,npos是一个表示无效位置的常量,在字符串和容器类中经常用于表示查找失败或者无效位置的情况。
resize
在之前的学习中我们知道resize接口是用于调整字符串长度的,且分为三种情况进行使用:
一:指定的长度比字符串长度小,删除多余的部分
二:指定的长度比字符串大,但是在空间大小范围内,使用\0进行填补到相对因的指定长度
三:指定的长度比字符串大,且比空间大小大,需要进行扩容,且其余的指定长度位置的范围内的空缺地方用\0填补
void resize(size_t n , char ch ='\0')
{
if( n <= _size)//情况一
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);//需要扩容进入情况三不需要进入情况二
for(size_t i = _size ; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
在进入情况二和情况三之前先使用reserve进行检查,
因为reserve有一个检查机制,可以检查我们指定的长度是否大于空间大小
如果比空间大则扩容进入情况三,如果比空间小则不变进入情况二,
检查之后就开始情况2和情况3都有的共同操作:填充\0
operator =
operator= 的赋值过程其实就是一个深拷贝的过程,需要开辟单独的空间讲数据进行复制转移
string & operatro=(const string& s)
{
char*tmp = new char[s.capacity];
strcpy(tmp , s._str);
delete[];
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
swap
默认的或者说直接用C++里的swap进行字符串和字符串之间的交换较为的复杂,因为需要调用深拷贝以及析构函数等等,且拷贝的过程中字符串的转移需要进行至少三次,也就是三次的拷贝,同时还需要利用析构函数解决中间用来存储字符串的变量,于是就有了string专用的swap:
void swap(string& s)
{
std::swap(_str , s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
直接利用C++的swap进行一个地址交换,但是交换的是成员变量和地址
find
find的主要功能是从指定的位置(默认是索引为0的位置)查找字符或者字符串,如果找到了返回字符的位置,找不到返回npos,默认是从0开始查找字符,,如果是查找字符串,则找到了返回字字符串首个字符的位置,找不到就返回npos
查找字符:
size_t find(char ch ,size_t pos = 0)
{
assert(pos < _size);//确认查找的位置是否存在
for(size_t i =pos; i < _size; i++)
{
if(_str[i] == ch)//进行对照查找
return i;
}
return npos;
}
查找字符串:
size_t find(const char* sub ,size_t pos = 0)const
{
assert(pos < _size);
const char* p =strstr(_str +pos ,sub);//借用了strstr的功能
if(p)
{
return p - pos;//使用地址-地址的方式得到索引下标位置
}
else
{
return npos;
}
}
strstr函数相关介绍:strstr函数-CSDN博客
substr
substr的功能是从指定位置获取字符,并可以指定获取字符的个数,如果没有指定获取字符的个数,那么将会从指定的位置获取字符,获取到原字符串的结尾。
string substr(size_t pos = 0;size_t len = pos)
{
string sub;
if(len == npos || len >= _size - pos)//获取指定的个数大于剩下长度时
{
for(size_t i =pos;i < _size;i++)
{
sub += _str[i];//sub从pos位置获取到字符串尾部
}
}
else
{
for(size_t i = pos;i < pos + len;i++)//获取指定个数小于剩下长度时
{
sub += _str[i];//sub只能获取指定长度的字符
}
}
return sub;
}
clear()
clear的主要功能就是清除数据,但仅仅只是数据的清除,空间并不一定清理掉,也就空间不会被释放或者空间不会被销毁!只是清除了空间内部的数据。
void clear()
{
_size = 0;
_str[_size] = '\0';
}
string的重载运算符
string类的 流插入 和 流提取
前提须知:
string的流插入和流提取是指将string对象与输入输出流进行交互的过程。通过流插入,可以将string对象的内容插入到输出流中;而通过流提取,可以将输入流中的内容提取到string对象中。
流插入操作使用输出流对象(如cout)和插入运算符(<<),将string对象的内容插入到输出流中。例如,可以使用以下方式将一个string对象插入到输出流中:
#include <iostream> #include <string> int main() { std::string str = "Hello, world!"; std::cout << str; // 将str插入到输出流中 return 0; }
流提取操作使用输入流对象(如cin)和提取运算符(>>),将输入流中的内容提取到string对象中。例如,可以使用以下方式将用户输入的内容提取到string对象中:
#include <iostream> #include <string> int main() { std::string str; std::cout << "请输入一个字符串:"; std::cin >> str; // 将用户输入的内容提取到str中 std::cout << "您输入的字符串是:" << str << std::endl; return 0; }
上述代码中,
std::cin >> str;
将用户输入的内容提取到字符串变量str中,并通过std::cout
输出。
实现:
流插入
ostream& operator<<(ostream& out,const string& s)
{
for(auto ch :s)//借用范围for 把string类型的字符串中的字符一个个传入ch中
//再用C++的<<进行传输
{
out << ch;
}
return out;
}
流提取
在进行流提取之前,我们要知道string类的流提取底层模拟实现不能和流插入一样调用C++中的流提取符号,因为C++的默认流提取 >> 底层使用的 cin 是一个遇到空格符号或者回车符号终止提取和读取的函数
所以假设我们需要使用流提取进行连续的读取字符串,那么按照程序员写字符串的习惯,当需要写多个连续的字符串时,大多数程序员会使用空格或者回车作为多个字符串的分割标记。
这也就会导致使用cin的流提取会出现错误!因为cin遇到空格和回车会终止提取!
同时,对于流提取而言,它的本质上是遇到空格和回车就会停下读取,所以流提取一般不用于具有空格符号的字符串
并且流提取还附带覆盖的功能,当一个string类型的变量内部具有字符串且在进行 cin >> "hello" >>s;这种操作时,s字符串内部的东西需要被cin>>从键盘或者其他设备中提取的数据覆盖。
于是,string的流提取模拟实现需要完成,解决连续读取操作,解决覆盖操作,解决遇到空格和回车符号暂停读取的操作
同时,string会进行一种扩容操作,这是流提取的扩容机制,所以为了防止流提取时的频繁扩容,我们需要建立流提取的防止扩容频繁的操作
完整代码:
扩容机制解读:
- 设立了一个足够大的字符数组,用字符数组进行扩容的相关操作。
- 第二个if是表示如果字符串较短,那么将会在字符串的后面加上\0,在交给string 类型的s 并且使用+=符号读取字符串的内容方便string空间的开辟
- 而+= 符号会自动读取数组内部的字符串的大小,开辟相对因的空间,进行空间的一次性开辟操作!
- 其中第一个if表示如果字符串太长的话,那就分几次对string类型对象s的空间进行开辟
- 如果等于127则127放置\0并且将这127个字符给string类型的s 使用+=进行空间的开辟,随后i=0 进入插入的循环,进行第二次轮的插入操作和空间的开辟操作!
getline()
getline()的功能其实和流提取相差不多,不过不同的是getline()可以读取空格符号,所以getline()一般用于读取带有空格符号的字符串。
istream& getline(istream& in,string& s)
{
s.clear();
char ch;
ch = in.get();
while(ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}