𝙉𝙞𝙘𝙚!!👏🏻‧✧̣̥̇‧✦👏🏻‧✧̣̥̇‧✦ 👏🏻‧✧̣̥̇:Solitary_walk
⸝⋆ ━━━┓
- 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑
WeChat_20240806081335
目录:
思维导图
一: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 () 使用以及对比
相同点:都可以对当前对象进行扩容
不同点:扩容之后空间释放进行初始化;是否影响数据 的有效个数;reserve()当参数小于有效数
据个数的时候,此函数啥也不干
通俗的理解就是:resize ( )这个函数可以进行扩容(并默认支持指定 /0 来进行初始化)也可以
进行缩容,注意只是影响size,但不会影响capacity
运行结果:
reserve () :这个函数也可以进行空间的开辟,但是不支持初始化(一般当提前大概知道当前对
象的大小的时候可以进行空间开辟,避免底层扩容的消耗,代价过大)
3)capacity()
capacity():这个函数是以字节作为单位返回当前指向对象被开辟的空间大小,注意capacity有
效空间的大小并不受当前size()的影响
4)clear ()
简言之就是:把当前的有效的字符进行删除,只会影响size
3.3 对string 对象的访问以及遍历操作
1) 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过去
}
注意:使用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 ()模拟
分析:是否进行扩容,数据挪动的位置;下标位置的判断
初次对insert() 进行模拟的时候,不少友友们都会掉坑吧~~~
分析:当对字符串下标为0 的位置插入字符或者是字符串的时候,程序就有问题
这是因为发生了类型提升:有符号类型会默认提升为无符号的类型
因为end 类型是size_t 当end = -1是进入死循环导致end 下标的越界,最终程序出现问题
最优解,就是end 指向\0 所在位置的下一个
解决:
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 错误:
{
//out << s._str << endl;
//这样写有问题,会看成char* 类型,可能在还没有遇到字符串结束的\0自动停止,因为中途可能有\0
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';//,注意必须置为\0
s += buff;
i = 0;//这一段读取结束,开始下一段读取
}
buff[i++] = ch;//读取的每一个字符放到buff这个数组,并让下标++
ch = in.get();
}
if (i != 0) //可能存在当前buff里面只有一部分的数据
{
buff[i] = '\0';//一定要置空,否则下面对s进行字符串尾插的时候,不知道追加到哪里结束
s += buff;
}
//问题:当输入字符个数过少的时候,会造成空间 空间的浪费,而且是不可回收的
//解决:提前整个数组,这样即使输入字符个数很少程序结束的时候也会归还系统
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() 这个函数有双重功能:一个是缩容(只会影响size大小,不影响capacity);另一个是扩
容(默认\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 迭代器相关的模拟
暂时可以把begin end理解成指针(单本质上并不是指针)
begin:指向数据的起始位置
end:指向最后一个数据的下一个位置
注意:迭代器的范围是左闭右开的
//暂时可以把迭代器理解成指针
iterator begin()
{
return _str;//返回第一个数据的位置
}
iterator end()
{
return _str + _size;// 返回最后一个数据的下一个位置
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
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赋值运算符的重载
传统写法
//传统写法
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这个对象进行浅拷贝的(值拷贝),在未经任何处理的时候,编译器默认对自定
义类型的对象进行浅拷贝的,当s2这个对象调用析构函数的时候,_str所指向的空间就归还了系
统;当s1调用析构函数的时候,s1这个对象_str所指向的空间就已经是随机的了,再进行释放的时
候就出现了 野指针的问题,所以此时程序崩溃(注意:先构造的对象后析构)
浅拷贝带来的问题:
析构函数调用多次;一个对象的修改会影响其他指向当前数据的对象
图解:
解决:
深拷贝:s2这个对象拷贝构造s1这个对象的本质只是进行数据的拷贝,并不是让s2对象的_str和s1
这个对象里面 _str都指向相同的空间,所以就需要自己进行空间的申请
传统写法:
//传统拷贝构造函数的写法
//s2(s1)
string(const string& s)
{
_str = new char[_capacity + 1];
//数据拷贝
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
现代写法:
// 现代写法的拷贝构造函数
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_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);
}
在这里有个小小的问题:为什么那个现代写法里面的_str要进行置空???
分析:
对于delete 而言,当释放的空间为空是没有问题的,若不及时置空,那么此时的_str就会指
向随机的数据(空间不是明确的),那么在调用swap()函数的时候,tmp 所指向的_str就会指向随
机数据,在进行资源释放的时候,就出现野指针的问题,导致程序崩溃
结语:
以上就是关于string相关函数应用以及模拟实现,相信不少老铁们看到这里,已经实属不易了,毕
竟大浪淘沙,最后终能淘的自己的“金银珠宝”。关于前期阶段的理解是很重要的,只要是前期自
己掌握了七八成,相信到这里你也是可以看懂滴,希望各位老铁们看到这里都能有所收获,当然我
的此篇博客也存在多多的不足之处,欢迎各位大佬们随时指正,当然后期也会不断更新,希望各位
大佬们阔以多多支持!