𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇:Solitary_walk
⸝⋆ ━━━┓
- 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑
目录:
一:C语言里面的字符串
二:标准库里面的string
三:string常用的接口
四:对string 这个类的模拟实现
1:C语言里面的字符串
2:标准库里面的string
3. )string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
3:string常用的接口
3.1对string 对象常用到的构造函数
常用到的构造函数:多为这4个
使用示范:
3.2string 对象的容量的操作
1)size () 使用
简言之就是:size () 这个函数只统计字符串的有效个数(\0之前的字符)
2)resize() 和 reserve () 使用以及对比
通俗的理解就是:resize ( )这个函数可以进行扩容(并默认支持指定 /0 来进行初始化初始化)也可以进行缩容
运行结果:
reserve () :这个函数也可以进行空间的开辟,但是不支持初始化(一般当提前大概知道当前对象的大小的时候可以进行空间开辟,避免底层扩容的消耗,代价过大)
3)capacity()
capacity():这个函数是以字节作为单位返回当前指向对象被开辟的空间大小,注意capacity有效空间的大小并不受当前size()的影响
4)clear ()
简言之就是:把当前的有效的字符进行删除
3.3 对string 对象的访问以及遍历操作
1) operator [ ]
operator [ ] : 返回下标为pos 对应位置的字符
2) begin ()+ end( )
注意:begin 指向当前字符的第一个位置,end 指向最后一个有效字符的下一个位置,范围是左闭右开
使用示范:
3) rebegin( ) reend( )
使用示范:
4) 范围for
3.4 对string 对象的修改操作
3.4.1) push_back() append( )
push_back: 在原来字符串的后面追加一个字符
使用示范:
3.4.2) operator +=
对于以上追加一个字符和一个字符串的过于复杂了,有没有一个函数就可以搞定的???
3.4.3) find() 和 pos() substr() getline( )
find () 这个函数 的返回值:若是存在返回第一个字符出现的对应下标位置否则返回npos
使用示范:
对于getline( )这个函数结合具体的例题更有助于理解
对于这个题其实并不难理解,关键就是如何处理输入中的空格或者是 \0 ,这个时候使用getline () 即可,默认遇到缓冲区里面的空格或者是 \0并不结束读取
#include <iostream>
#include <string>
using namespace std;
int main()
{
string str;
//cin >> str;
getline(cin, str);
//对于scnaf ()和cin 进行输入的时候:当有多个数据进行输入的时候,编译器会自动把空格或者是换行作为数据分割开的依据
int len = 0;
size_t pos = str.rfind(' ');
if (pos != string::npos)//已经找到
{
len = str.size() - (pos + 1);
cout << len << endl;
}
else
cout<< str.size()<<endl;//就是只有整个单词
}
3.4.5) vs下的string深度理解
在VS下:一个string 类的对象 大小是28个字节,包括默认一个16个字节的数组,一个维护_size 的变量,一个维护_capacity的变量,和维护堆上的一个指针变量(默认32平台)总大小 = 16 + 3*4 = 28
注意: 对于VS而言,\0不算做有效字符,所以_capacity的大小是15个字节
3.5对string 非类的成员函数
4:对string 这个类的模拟实现
4.1 有参以及无参的构造函数模拟
首先需要自己定义一个类:string (我为了避免与库里面的string 冲突,把自己定义的这个string 放在 Y 这个命名空间里面)
string 这个类的定义
namespace Y
{
class string
{
public:
private:
size_t _size;
size_t _capacity;
const char* _str;
};
}
构造函数的实现
namespace Y
{
class string
{
public:
//无参的构造函数
string()
:_size(0)
, _capacity(_size)
, _str(new char[1]{ '\0' }) // 开一个空间并进行初始化
{}
//有参的构造函数
string(const char* s = "")
:_size(strlen(s))
,_capacity(_size)
{
_str = new char[_size+1];
strcpy(_str, s);
}
private:
size_t _size;
size_t _capacity;
char* _str;
};
}
注意有参构造函数 的参数类型必须的const char * 类型,所以说是不能传 '\0'
在对_str开空间的时候要多开一个是为了存放 \0
4.2 析构函数模拟
~string()//析构函数
{
delete[] _str;
_size = _capacity = 0;
}
4.3 push_back() append() operator+=() 模拟
push_back() append() operator+=() 这三个函数都是在原来字符串的基础上进行尾插,只不过是尾插一个字符还是尾插一个字符串的区别(对这三个函数的具体理解见前面的分析)
push_back() 模拟
进行尾插的时候:需要考虑是否进行扩容以及对_size要进行改变
void reserve(size_t n)
{
if (n > _capacity) // 扩容条件:扩容之后的空间必须是大于当前的空间容量
{
char* tmp = new char[n + 1];
//对原来数据进行拷贝
strcpy(tmp, _str);
delete []_str;//_str 是一个数组
_str = tmp;
_capacity = n;
}
}
void push_back(const char ch = '\0')
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
append() 模拟
void append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
reserve(len + _size);//注意这里不能单纯的以2倍扩容(当size= 4,len= 80,有问题)
}
strcpy(_str + _size, str);
_size += len;
//_str[_size] = '\0'; //err 因为strcpy拷贝的时候会自动拷贝\0过去
}
operator +=()模拟
其实就是对push_back() 以及 append() 函数的封装
string& operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
4.4 insert ()模拟
错误使用:当对字符串下标为0 的位置插入字符或者是字符串的时候,程序就有问题了:
分析:这是因为发生了整形提升,因为end 类型是size_t 当end = -1是进入死循环导致end 下标的越界最终程序出现问题
解决:
1) 对pos 进行类型强转
2)改变 end 所指向的位置
4.5 erase 模拟
erase介绍见前序
指定删除序列的n个字符从pos位置开始,(注意默认pos是从下标为0的时候,len默认是整个字符串)
代码:
void erase(size_t pos , size_t len = npos)
{
//1:特殊情况处理 (直接对_size,以及\0改变)2:正常情况(数据覆盖)
if (len >= _size || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
//正常删除
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos] = _str[end];
end++;
pos++;
}
_size -= len;
}
}
4.6 流插入的重载(<<)
为了更好的对类成员的访问(有的类成员可能设置成private,protected)这里把流插入函数写成友元函数:注意并不是所有 的 流插入函数都写成友元函数,写成友元函数只是更好的对类成员的访问(这个小小的知识点可能是面试官的灵魂拷问哟)
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s._size; i++)// i< _size 错误:
{
cout << s._str[i];
}
return out;
}
4.7 流提取的重载(>>)
istream& operator>>(istream& in, string& s)
{
s.reserve(128);//避免频繁开空间
char ch = in.get();
//char buff[129];//多开一个为\0存储
size_t i = 0;
while (ch !='\n' && ch!= ' ')//从缓冲区读取数据的时候,默认遇到\n 或者空格读取结束
{
s += ch;
ch = in.get();
}
cout << s.size() << endl;
cout << s.capacity() << endl;
//问题:当输入字符个数过少的时候,会造成空间 空间的浪费,而且是不可回收的
//解决:提前整个数组,这样即使输入字符个数很少程序结束的时候也会归还系统
return in;
}
运行结果:
自习分析:发现我们的代码有很大的优化空间,当我们输入的字符个数过少 的时候会造成所开空间的浪费,而且是不可回收的,那岂不是有待优化对于咱们的代码?
代码:
istream& operator>>(istream& in, string& s)
{
//s.reserve(128);//避免频繁开空间
char ch = in.get();
char buff[129];//多开一个为\0存储
size_t i = 0;
while (ch !='\n' && ch!= ' ')//从缓冲区读取数据的时候,默认遇到\n 或者空格读取结束
{
//s += ch;
if (i == 128)
{
buff[i] = '\0';//这一段读取结束,开始下一段读取
s += buff;
i = 0;
}
buff[i++] = ch;//读取的每一个字符放到buff这个数组,并让下标++
//s += ch;
ch = in.get();
}
if (i != 0) //可能存在当前buff里面只有一部分的数据
{
buff[i] = '\0';//一定要置空,否则出现随机值
s += buff;
}
cout << s.size() << endl;
cout << s.capacity() << endl;
//问题:当输入字符个数过少的时候,会造成空间 空间的浪费,而且是不可回收的
//解决:提前整个数组,这样即使输入字符个数很少程序结束的时候也会归还系统
return in;
}
运行结果:
4.8 find ()模拟
对一个字符的查找
size_t find(char ch, size_t pos = 0)const
{
size_t find = 0;
while (find <_size)
{
if (_str[find] == ch)
{
return find;
}
find++;
}
return npos;
}
对一个字符串的查找
size_t find(const char* str, size_t pos = 0)const
{
//找一个子字符串,借助strstr函数
char* find = strstr(_str + pos, str);
if (find != nullptr)
{
return find - _str;// 返回找到的第一个字符所对应下标(指针相减 = 个数)
}
else
return npos;//未找到
}
在这里有一个关键的知识点:就是如何把指针转换成对应 的元素个数
指针与指针相减即可
4.9 substr() 模拟
代码:
string substr(size_t pos = 0, size_t len = 0)
{
size_t end = len+pos;//结束的下标
if (pos + len >= _size || pos == end) //特殊情况处理:要取的字符串可能超出当前的有效字符串 的长度,需要进行修改
{
len = _size - pos;//实际取到的有效字符
end = _size;
}
string sub("hello");
sub.reserve(15);//提前进行空间的开辟
while (pos < end)
{
sub += _str[pos];
pos++;
}
return sub;
}
4.10 resize()模拟
分析:
resize() 这个函数有双重功能:一个是缩容;另一个是扩容(默认\0进行初始化)
void resize(size_t n,char ch = '\0')
{
if (n < _size)//缩容
{
_str[n] = ch;
_size = n;
}
else //扩容,默认\0
{
reserve(n);
_size += n;
size_t i = 0;
while (i < n)
{
_str[i++] = ch;
}
}
//不要忘了最后置空
_str[_size] = '\0';
}
4.11 reserve()模拟
void reserve(size_t n)
{
if (n > _capacity) // 扩容条件:扩容之后的空间必须是大于当前的空间容量
{
char* tmp = new char[n + 1];
//对原来数据进行拷贝
strcpy(tmp, _str);
delete []_str;//_str 是一个数组
_str = tmp;
_capacity = n;
}
}
本质上讲:用realloc()函数和用new进行扩容其实没有质的区别,只不过用new,delete等操作符可以支持自定义类型
注意这里是进行异地扩容,不要忘了进行数据 对比以及旧空间的释放
4.12 size() 模拟
size_t size() const
{
return _size;
}
4.13capacity() 模拟
ize_t capacity()
{
return _capacity;
}
4.14 迭代器相关的模拟
4.15 比较关系的运算符的重载
在这里简单的实现以下几个比较关系操作符的书写,其余进行复用,还是挺香的
bool operator>(const string& s2) const
{
//借助strcmp()函数一次进行比较返回int 类型的
//return strcmp(*this, s2._str) > 0; //err 指代不明确
return strcmp(_str, s2._str) > 0;//只要是在类里面都可以进行对成员变量的访问,不分私有还是公有
}
bool operator==(const string& s2) const
{
return strcmp(_str, s2._str) == 0;
}
CV一下
//接下来比较关系运算符的虫重载直接进行复用即可
bool operator>=(const string& s2) const
{
//return (operator>(s2) || operator==(s2));
return (*this > s2 || *this == s2);
}
bool operator<(const string& s2) const
{
return (!operator>=(s2));
}
bool operator<=(const string& s2) const
{
return (!operator>(s2));
}
bool operator !=(const string& s2)
{
return !(operator==(s2));
}
4.16赋值运算符的重载
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//传统写法
string operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._size + 1];
delete _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//现代写法
// s2 = s1
string operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
其实本质上没有什么不同,最终编译器还是进行传统写法的调用只不过用现代的写法我们自己是省去了不少事(简言之,就是自己不用搬太多的砖了,有个小弟帮自己干点,就可以减轻自己的负担)
4.17拷贝构造函数的重载
传统写法:
//传统拷贝构造函数的写法
//s2(s1)
string(const string& s)
{
_str = new char[_capacity + 1];
//数据拷贝
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
现代写法:
// 现代写法的拷贝构造函数
// s2(s1)
string(const string& s)
: _size(0)
,_str(nullptr)//为什么要对_str置空(不置空的话,此时_str指向的数据是随机值,也就是说此时的_str是野指针):方便后面交换之后,tmp指向的空间为空,这时在调用析构函数的时候不会崩溃,因为只有动态开辟出来的空间才可以进行释放
, _capacity(0)
{
//我在实现的过程中面临的问题:程序出现野指针(没有对tmp 进行初始化)
//string tmp(s);//err
string tmp(s._str);//将s1里面的字符串初始化tmp
swap(tmp);
}
结语:
以上就是关于string相关函数应用以及模拟实现,相信不少老铁们看到这里,已经实属不易了,毕竟大大浪淘沙,最后终能淘的自己的“金银珠宝”。关于前期阶段的理解是很重要的,只要是前期自己掌握了七八成,相信到这里你也是可以看懂滴,希望各位老铁们看到这里都能有所收获,当然我的此篇博客也存在多多的不足之处,欢迎各位大佬们随时指正,当然后期也会不断更新,希望各位大佬们阔以多多支持!