文章目录
- 驼峰法命名
- 面试题:写一个简洁版的string
- string成员变量
- 构造函数
- 析构函数
- 拷贝构造函数
- 获取C形式的字符串 c_str
- 赋值重载 operator=
- 简易版代码:
- string的改造 ->支持增删查改
- 接口总览
- string成员变量
- 构造函数
- 交换
- 拷贝构造
- 赋值重载operator=
- 析构函数
- 返回元素个数
- 返回容量
- 遍历:
- operator[]重载
- 迭代器
- 变量定义:
- begin() 和end()
- 范围for
- 容量
- reserve
- resize
- 增
- push_back
- append
- operator+=
- 插入字符
- 插入字符串
- Insert
- **在pos位置插入一个字符**
- 在pos**位置之前**插入字符串
- 简化push_back
- 简化append
- 删
- erase
- npos
- find
- 查找一个字符
- 查找一个字符串
- rfind
- 查找一个字符
- 查找一个字符串
- 比较
- clear()
- empty()
- 流提取和流插入运算符重载
- >>
- <<
- getline
- 总结
- 关于string的深入讨论
- 关于string的深浅拷贝
- string支持一个const char*的构造函数
- String.h
驼峰法命名
1.函数,类名 单词首字母大写
- 例如: PushBack
- 变量 第一个单词小写,后面的单词首字母大写
- 例如: valueOver
在stl,linux源码中,所有都是小写,单词和单词之间用_分割
面试题:写一个简洁版的string
- 1.考察的实现string的四个默认成员函数,深浅拷贝的问题
- 2.可以写传统写法,也可以写现代写法
string成员变量
一个char*类型的指针
构造函数
注意:
- 构造函数的缺省值不能为NULL,查文档也可得知无参时默认的参数时空串
""
- 我们要多开辟一个空间存放\0
//默认参数为空字符串,不能为NULL,不然strlen会报错! 要给空字符串
string(const char* str = "")
:_str(new char[strlen(str)+1]) //要多开辟一个空间用来存放\0
{
strcpy(_str, str);//\0也会拷贝过去
}
析构函数
~string()
{
delete[] _str;//释放_str指向的空间 ->new[] 要和delete[]配合使用
_str = nullptr;//指针置空
}
拷贝构造函数
传统写法
- 为了减少拷贝,传引用
- 思路:开辟和要拷贝的对象的字符串大小一样的空间 ->使用库函数strcpy拷贝字符串
//s2(s1)
string(const string& s)
:_str(new char[strlen(s._str)]+1) //要多开辟一个空间用来存放\0
{
strcpy(_str, s._str);//字符串拷贝
}
现代写法
- 为了减少拷贝,传引用
- 先构造一个临时对象,然后临时对象和this指向的对象 进行字符串交换
//现代写法
string(const string& s)
:_str(nullptr)//要初始化_str,否则为随机值,交换后,tmp._str就指向随机的空间,析构的时候释放非法空间
{
//调用的是构造函数创建tmp,这里的参数是字符串!不是对象
//string tmp(s) ->这样是拷贝构造,s是对象,而s._str才是对象的内容->字符串
string tmp(s._str);
swap(_str, tmp._str);//调用std域里面的swap交换函数,交换两个字符串
}
注意: string tmp(s) ->这样才是拷贝构造,s是对象,s._str是对象的内容-字符串, 如果用的是拷贝构造来构造tmp,就是错误的! 不断的复用拷贝构造,陷入死循环
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6OZM9tJm-1671703499128)( https://mangoimage.oss-cn-guangzhou.aliyuncs.com/image-20220204162145388.png)]
获取C形式的字符串 c_str
返回类型:const char*
//获取C形式的字符串
const char* c_str() const //函数后加const修饰,const的对象也能调用
{
return _str;
}
赋值重载 operator=
传统写法
- 防止自己给自己赋值 比较地址是否一致
- 减少拷贝->传引用
- 为了可以连续赋值,所以要有返回值, 由于*this这块空间出了作用域不销毁,可以传引用返回 (this是局部变量指针,出了作用域就销毁了,是*this不销毁)
//case1:s1 = s2 case2:s1 = s1 case3:s1 = s2 = s3
string& operator=(const string& s)
{
//防止自己给自己赋值 s1=s1,比较地址是否一致
if (this != &s)
{
delete[] _str;//先释放原来空间的内容
_str = new char[strlen(s._str) + 1]; //多开辟一个空间保存\0
strcpy(_str, s._str);
}
return *this;
}
注意:由于不会对对象s进行修改,所以参数可以传引用 + 用const修饰
现代写法
注意:此时的参数是传值,且不能加const修饰!
原因:要跟原对象进行交换,所以不能传引用,否则导致原对象改变! 也不能加const,否则不能被修改
// 现代写法
//s1 = s2 ->先用s2拷贝构造一个对象s,s和s2就有相同大小的空间和值,然后再拿s和s1交换
string& operator=(string s)
{
swap(_str, s._str);//s是临时对象,出了作用域就销毁
return *this;
}
由于s是临时对象,出了作用域自动调用析构函数销毁
如果这里是自己给自己赋值,会导致s1的本身空间地址发生改变,因为二者指向的空间发生了交换
这里调用swap函数,这里不仅需要引用#include<algorithm>
函数,还需要指定类域 std::swap(tmp1,tmp2)
当然,我们也可以选择自己实现:
//注意!!这里要传引用!!,否则临时对象tmp析构,会导致交换后的空间也被释放
void swap(char*& str)
{
char* tmp = str;
str = _str;
_str = tmp;
}
//函数内部调用swap只需传一个参数:
swap(tmp._str);
简易版代码:
#include<string.h>
#include<algorithm>
namespace Mango
{
class string
{
public:
string(const char* str = "")
:_str(new char[strlen(str)+1])
{
strcpy(_str, str);
}
~string()
{
delete[] _str;
_str = nullptr;
}
//传统写法拷贝构造
string(const string& s)
{
_str = new char[strlen(s._str) + 1];
strcpy(_str, s._str);
}
/*
//现代写法的拷贝构造函数
string(const string s)
{
_str = nullptr;
string tmp(s._str);
swap(tmp._str);
}
*/
void swap(char*& str)
{
char* tmp = str;
str = _str;
_str = tmp;
}
const char* c_str() const
{
return _str;
}
string& operator=(const string& s)//为了支持连续赋值,所以返回引用
{
if (this != &s)
{
delete[] _str;
_str = new char[strlen(s._str) + 1];
strcpy(_str,s._str);
}
return *this;
}
//现代写法
/*string& operator=(string s)
{
swap(s._str);
return *this;
}
*/
private:
char* _str;
};
}
string的改造 ->支持增删查改
接口总览
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;
//不包含上面的文件,下面会报错
namespace Mango
{
//模拟实现string类
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
//默认成员函数
string(const char* str = ""); //构造函数
string(const string& s); //拷贝构造函数
string& operator=(const string& s); //赋值运算符重载函数
~string(); //析构函数
//迭代器相关函数
iterator begin();
iterator end();
const_iterator begin()const;
const_iterator end()const;
//容量和大小相关函数
size_t size();
size_t capacity();
void reserve(size_t n);
void resize(size_t n, char ch = '\0');
bool empty()const;
//修改字符串相关函数
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
string& insert(size_t pos, char ch);
string& insert(size_t pos, const char* str);
string& erase(size_t pos, size_t len);
void clear();
void swap(string& s);
const char* c_str()const;
//访问字符串相关函数
char& operator[](size_t i);
const char& operator[](size_t i)const;
size_t find(char ch, size_t pos = 0)const;
size_t find(const char* str, size_t pos = 0)const;
size_t rfind(char ch, size_t pos = npos)const;
size_t rfind(const char* str, size_t pos = 0)const;
//关系运算符重载函数
bool operator>(const string& s)const;
bool operator>=(const string& s)const;
bool operator<(const string& s)const;
bool operator<=(const string& s)const;
bool operator==(const string& s)const;
bool operator!=(const string& s)const;
private:
char* _str; //存储字符串
size_t _size; //记录字符串当前的有效长度
size_t _capacity; //记录字符串当前的容量
static const size_t npos; //静态成员变量(整型最大值)
};
//静态成员变量在类外初始化
const size_t string::npos = -1;
//<<和>>运算符重载函数
istream& operator>>(istream& in, string& s);
ostream& operator<<(ostream& out, const string& s);
istream& getline(istream& in, string& s);
}
string :管理字符串的数组,可以增删查改 , 字符串数组的结尾有\0
string成员变量
private:
char* _str;//指向动态开辟的数组
size_t _size;//字符串有效字符个数
size_t _capacity;//容量
构造函数
\0不是有效字符是标识字符串结尾的字符
_capacity
表示的时能存储有效字符的个数,多开辟的一个空间是给\0的
//默认参数为空字符串,不能为NULL,不然strlen会报错! 要给空字符串
string(const char* str = "")
{
_size = strlen(str);//如果没传参数,默认str为空串,大小为0
_capacity = _size;
_str = new char[_capacity + 1];//多开辟一个空间给\0
strcpy(_str, str);//字符串拷贝,\0也会被拷贝
}
交换
注意:先去局部域去找swap函数,所以要指定域, ::
域限定符 左边没有写,默认在全局域中找
//s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
传引用是为了减少拷贝,因为传值传参会调用拷贝构造
//C++98,c++11增加了右值引用的移动语义,优化了swap函数模板
::swap(s1,s2);//调用库的
s1.swap(s2);//自己写的 this->swap(s2)
拷贝构造
容量和size都可以不初始化,但是_str要初始化为空指针,不然就是随机值!交换的时候就会指针非法访问!
注意:拷贝构造和赋值重载两个函数,如果我们不写,编译器会帮我们自动生成一份,对呀内置类型完成的时浅拷贝,对于自定义类型会调用它自己的拷贝构造函数,如果使用默认的拷贝构造就会出现问题:
s2(s1)
如果是浅拷贝,s2和s1指向同一块空间,更改s1的内容会导致s2也受影响,析构的时候,会对这块空间析构两次
所以我们需要实现深拷贝,传统写法:动态开辟空间,把数据拷贝过来 或者现代写法:先构造一个对象,然后交换
传统写法:
//拷贝构造 s2(s1)
string(const string& s)
:_size(strlen(s._str))//写法2:_size(s._size)
,_capacity(s._capacity)
{
_str = new char[_capacity+1];//多开辟一个空间存放\0
strcpy(_str,s._str);
}
现代写法:
复用含参数的构造函数构造对象tmp,然后交换两个对象
- tmp是临时对象,出了作用域调用析构函数销毁, 如果tmp指向一个随机空间,析构的时候会崩溃
- new/malloc出来的空间才能被释放,但是释放空指针没有问题
free(nullptr);//没问题
//s2(s1)
string(const string& s)
:_str(nullptr),_capacity(0),_size(0) //要初始化_str,不然就是随机值
{
string tmp(s._str);//先用s构造一个临时对象 注意:这里的参数是字符串!!!
swap(tmp); //this->swap(tmp)
}
由于修改对象s,所以可以传引用!
赋值重载operator=
传统写法: 开辟新空间,把数据拷贝过来
同样的,如果我们不实现,默认生成的就是浅拷贝
所以我们需要自己实现一份深拷贝,先释放旧空间,然后开辟新空间,把数据拷贝过来
- 注意:要防止自己给自己赋值,如:
s1=s1
, 这样会导致错误,把原空间销毁了 - 有可能是连续赋值,所以要有返回值,由于*this这块空间出了作用域不销毁,可以传引用返回
//可能存在Bug的代码
string& operator=(const string& s)
{
if(this != &s)
{
delete[] _str;//先释放旧空间
_str = new char[s._capacity+1];//开辟和s一样大小的空间
strcpy(_str,s_str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
上述可能存在的问题是什么?
new空间可能会失败,会直接抛出异常,而我们之前先把_str的空间释放了,所以就会导致_str变为野指针,所以可以稍微换一种写法
- 先用临时变量接收这块空间的地址,然后拷贝内容到这块空间上,然后再处理
string& operator=(const string& s)
{
if(this != &s)
{
char* tmp = new char[s._capacity+1];//为\0也要预留空间
strcpy(tmp,s._str);
delete[] _str;//释放原空间
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
现代写法: 创建一个对象出来进行交换
- 防止自己给自己赋值 比较地址是否一致
- 由于*this这块空间出了作用域不销毁,可以传引用返回 (this是局部变量地址(对象的地址),出了作用域就销毁了,而*this是对象,出了作用域不销毁)
- 此处是传值传参! 不可以传引用,否则会对原对象进行修改!
/*写法1:依旧传引用,但是在内部构造临时对象交换
string& operator=(const string& s)
{
if(this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
*/
//写法2:传值,然后直接交换
string& operator=(string s)
{
//防止自己给自己赋值
if(this != &s)
{
swap(s); //this->swap(s)
}
return *this;
}
此时参数的s是局部变量,出了作用域会调用析构函数销毁
如果此时不防止自己给自己赋值是否可行?
此时虽然不会发生误销毁原空间的情况,但是会导致地址发生改变
析构函数
~string()
{
delete[] _str;// 释放数组空间
_str = nullptr;//指针置空
_size = 0;
_capacity = 0;
}
返回元素个数
元素个数不可能为负数 所以返回类型为size_t
size_t size() const
{
return _size;
}
加const修饰函数,这样普通对象和const对象都能调用
返回容量
容量不可能为负数 所以返回类型为size_t
size_t capacity() const
{
return _capacity;
}
加const修饰函数,这样普通对象和const对象都能调用
遍历:
at()作用和operator[]类似 ,不同点是:at()越界抛异常,[]越界直接报错
operator[]重载
返回第i个位置的字符
普通对象 :可读可写
返回下标为i的字符的引用,出了作用域空间还在->用引用返回
//operator[]
//可读可写,所以返回引用
char& operator[](size_t i)
{
assert(i < _size);//保证i的合法性
return _str[i];
}
const对象:只读
出了作用域空间还在->用引用返回
//只读
const char operator[](size_t i) const
{
assert(i < _size);//保证i的合法性
return _str[i];
}
迭代器
分为const迭代器和普通迭代器, const对象调用函数,优先选择最匹配的const成员函数
变量定义:
public:
typedef char* iterator;
typedef const char* const_iterator;
begin() 和end()
begin() :_str位置 end() :_str+_size位置
//普通迭代器 - 普通对象调用
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
//const迭代器 -const对象调用
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
有了迭代器自然就支持了范围for了
迭代器遍历对象
void test()
{
string s1 = "hello world";
string::iterator it = s1.begin();//调用string的迭代器
while (it != s1.end())
{
cout << *it << " ";
it++;
}
}
范围for
看起来很神奇,但是原理很简单,范围for会被编译器替换成迭代器形式,也就是说范围for是有迭代器支持的
for(auto ch:s1)
{
cout<<ch<<" ";//ch本质就是s的每一个字符
}
注意:迭代器的begin和end函数名要小写!不然使用范围for会报错
容量
容量为n,实际空间为n+1个,因为有一个空间是用来存放\0的, _size是有效字符个数,当_size == _capacity 空间就满了
reserve
开辟空间 ->只改变capacity 不改变size
- 由于容量不为负数,所以参数类型:size_t
- 先开辟n+1个空间,然后把原空间的内容拷贝到新空间,再释放原空间,指针指向新空间
//开空间 capacity变,size不变
void reserve(size_t n)
{
//判断是否是扩容
if (n > _capacity)
{
char* tmp = new char[n + 1];//多开辟一个空间存放\0
//strcpy(tmp, _str);//把原内容拷贝到新空间,使用strcpy可能存在问题我!
//把_str的内容拷贝到tmp,共拷贝_size+1个字节
strncpy(tmp,_str,_size+1);//+1是为了把原空间的最后位置的\0也拷贝
delete[] _str;//释放原空间
_str = tmp;//_str指向新空间
_capacity = n;//容量改为n _size不变
}
}
注意:不能使用strcpy函数,因为如果有效字符包含\0,则会发生错误! 所以应该使用strncpy,直接根据有效字符的个数拷贝
resize
开辟空间 ->改变capacity && 改变size
默认用\0初始化
if(n<_size)
{
_size = n;//减少有效字符个数为n个
_str[_size] = '\0';//提前终止
}
针对case2和case3:可以写成一种情况: 都需要填充字符
//开空间+初始化 capacity和size都要改变
//默认填充字符为\0
void resize(size_t n, char val = '\0')
{
//case1
if (n < _size)
{
_size = n;//减少有效字符个数为n个
_str[_size] = '\0';//提前终止
}
else
{
//判断是否是增容 -针对case3
if (n > _capacity)
{
reserve(n);//扩容成n个空间,然后下面再填充数据
}
//case2和case3
//填数据,从原字符串\0位置填,填到字符个数为n个
for (size_t i = _size; i < n; i++)
{
_str[i] = val;
}
_str[n] = '\0';//最后在下标为n位置放\0
_size = n;//字符个数变为n个
}
}
增
push_back
要注意最后要存放\0
//插入字符-尾插
void push_back(char ch)
{
//先判断容量够不够
if (_size == _capacity)
{
//两倍扩容
//如果一开始构造的是空串,_capacity = 0,*2之后还是0,reserve(0),没有开辟空间,然后下面对_size位置访问->就是非法访问,err
// reserve(_capacity * 2); ->err,
reserve(_capacity==0?4:_capacity*2);
}
//插入字符 + 处理\0
_str[_size] = ch;//在原来\0位置插入字符
_str[_size + 1] = '\0';//在后面放\0
_size++;//有效字符+1
//上面三行代码也可以简写为 : _str[_size] = ch ,_str[++_size] = '\0'
}
append
- 不宜两倍扩容,因为两倍扩容也不知道够不够,直接把容量扩成插入之后的总长度
//尾插入字符串 -> 相当于在_size位置往后插入,可以复用insert函数
void append(const char* str)
{
//计算插入之后总长度
size_t len = _size + strlen(str);//原有长度+插入字符串长度
//如果len=_capacity刚好能存放,有\0
if (len > _capacity)
{
//扩容
reserve(len);//复用reserve函数,reserve内部处理了\0,把\0也拷贝到新空间了
}
strcpy(_str + _size, str);//在原字符串末尾(\0位置)插入字符串,str中的\0也会被拷贝
_size = len;//长度变为插入后总长度
}
如果使用strcat(追加函数):需要找到原字符串\0位置,然后再插入,需要遍历前面的字符,效率低.
此处可以直接算出\0位置在哪里,用strcpy更高效
operator+=
插入字符
复用push_back函数
返回插入之后的对象,出了作用域还在!所以可以返回引用
//s1+='x'
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
插入字符串
复用append函数
返回插入之后的对象,出了作用域还在!所以可以返回引用
//s1+="xxxx"
string& operator+=(const char* str)
{
append(str);
return *this;
}
Insert
在pos位置插入一个字符
- 检查pos位置的合法性
- 检查容量够不够
- 把pos位置及其后面的数据往后移动 //pos位置的字符也要往后移动
- 在pos位置插入字符
- 有效字符++
- 返回插入之后的对象,由于出了作用域不销毁,返回引用
Bug1: 若最初为空串:reserve(0) ,没有开辟空间出来,但是后序却访问了_str[pos]位置->非法访问
//Insert
string& insert(size_t pos, char ch)
{
//检查pos的合法性
//可以在_size位置插入(原来字符串的\0位置)
assert(pos<=_size);
//先检查容量
if (_size == _capacity)
{
reserve(_capacity==0?4:_capacity*2);
}
//解决办法1:使用指针
/*
char* end = _str + _size;//指向的是\0的位置
//从后往前移动数据
while (end >= _str + pos)
{
*(end + 1) = *end;
--end;
}
*/
/*解决办法2:强转pos的类型为int
int end = _size;//最后一个有效字符的下一个位置,即\0位置
while(end>=(int)pos)
{
_str[end+1]=_str[end];
--end;
}
*/
//解决办法3:让end和pos不能相等
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];//从后往前拷贝
--end;
}
_str[pos] = ch;//把字符插入到pos位置
_size++;//数据个数+1
//返回插入数据后的字符串
return *this;
}
在pos位置之前插入字符串
- 检查pos位置的合法性
- 检查插入之后的容量够不够
- 把pos位置及其后面的数据往后移动len个位置(len:插入字符串的长度)
- 在pos位置开始往后拷贝字符串,注意:不能用strcpy!!! 因为strcpy会拷贝\0
- 有效字符+len
- 返回插入之后的对象,由于出了作用域不销毁,返回引用
移动数据时,需要注意上面insert的Bug2情况!
//在pos位置插入字符串
string& insert(size_t pos, const char* str)
{
//保证pos位置的合法性
//可以在_size位置插入,
assert(pos <= _size);
size_t len = strlen(str);
//插入字符串之后容量是否够
//如果len + _size = _capacity刚好能放满,因为_capacity多开了一个空间存放\0
if (len + _size > _capacity)
{
//扩容 -开辟 原来字符串长度+插入字符串长度 个空间
reserve(_size+len);
}
//挪动数据,从\0位置开始移动
char* end = _str + _size;
//pos位置的字符也要往后移动,尽管是在最后一个位置插入,也能保证插入后最后一个位置是\0,
while (end >= _str + pos)
{
*(end + len) = *end;//每个字符往后移动len个位置
--end;
}
//插入字符串
//strcpy(_str+pos,str)//不能用strcpy,因为strcpy会把插入字符串的\0也拷贝过去,可能导致提前结束
//这里我们要根据字节数拷贝
strncpy(_str + pos, str, len);//从_str+pos位置开始把str拷贝过去,只拷贝str中的len个字符(不含\0)
_size += len;//有效字符个数+len
return *this;
}
注意:插入字符串的时候使用strncpy,不能使用strcpy,否则会将待插入的字符串后面的’\0’也插入到字符串中,
简化push_back
void push_back(char ch)
{
insert(_size,ch);//尾插一个字符 ->在_size位置插入一个字符
}
简化append
void append(const char* str)
{
insert(_size,str);//尾插一个字符串 ->在_size位置插入一个字符串
}
删
erase
从pos位置开始删除len个字符
情况分析:
情况1:删除位置后面的字符个数 < 要删除的个数
情况2:删除位置后面的字符个数 >= 要删除的个数
//从pos位置开始删除len个字符
//len的缺省值为npos,size_t类型,其值为42亿多,字符串没有这么长
//如果不传len,即删除pos位置后面的所有字符
string& erase(size_t pos, size_t len = npos)
{
//pos不能为\0位置,保证pos的合法性
assert(pos < _size);
size_t sz = _size - pos;//计算pos位置后面剩余的字符个数
//case1:剩余的字符长度<=要删的长度 ->后面的全删完
if (len >= sz) //len=npos的时候,也会进入这里
{
//把pos位置搞成\0,后面的直接忽略
_str[pos] = '\0';
_size = pos;//有效字符变成pos个
}
//case2:剩余的字符长度大于要删的长度
else
{
//把_str+pos+len后面的字符往前覆盖到_str+pos位置
strcpy(_str+pos, _str + pos + len);
_size -= len;//删去了len个字符
}
return *this;
}
npos
静态成员变量->在类里面声明,在类外面定义赋值! 可以不加static
find
查找一个字符
从pos位置开始查找字符,找到了返回下标,没有找到返回npos
//默认从0位置开始找
size_t find(char ch, size_t pos = 0)
{
//不能在\0位置开始往后找
assert(pos < _size);
//从pos位置遍历查找
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
//找不到
return npos;
}
查找一个字符串
从pos位置开始查找字符串,找到了返回下标,没有找到返回npos
- 查找子串,复用strstr函数
//默认从0位置开始找
size_t find(const char* str, size_t pos = 0)
{
//不能在\0位置开始往后找
assert(pos < _size);
//从pos位置开始找子串str首次出现的位置,找到了返回指针
const char* ret = strstr(_str + pos, str);
if (ret != nullptr)
{
//找到了,要返回下标
//指针-指针就是相距的距离 ==> 距离起始位置的下标
return ret - _str;
}
else
{
//找不到
return npos;
}
}
rfind
和find基本一致,只不过是从后往前找
思路:
先把字符串进行逆序, 然后复用find的逻辑
查找pos位置的字符/字符串 ->复用find函数,找逆置之后的字符串的pos'
位置
注意:最后要返回的是pos位置
查找一个字符
-
首先我们需要用对象拷贝构造一个临时对象tmp,因为我们并不希望调用rfind函数后原对象被修改
-
将tmp对象的字符串逆置,然后将所给pos镜像对称一下再调用find函数
-
将从find函数接收到的返回值镜像对称一下作为rfind函数的返回值返回即可,
//默认从npos位置开始找
//从后往前找
size_t rfind(char ch, size_t pos = npos) const
{
string tmp(*this); //拷贝构造
reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的字符串
//pos大于字符串有效长度
if (pos >= _size)
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为pos'位置
size_t ret = tmp.find(ch, pos); //复用find函数
if (ret != npos)
return _size - 1 - ret; //找到了,返回pos位置
else
return npos; //没找到,返回npos
}
注:rfind函数规定,当所给的pos大于等于字符串的有效长度时,看作所给pos为字符串最后一个字符的下标,
查找一个字符串
-
用对象拷贝构造一个临时对象tmp,然后将tmp对象的C字符串逆置,
-
需要拷贝一份待查找的字符串,将其逆置,
-
将所给pos对称一下,改为
pos’
位置再调用find函数,
注意:通过find函数得到pos'
位置后,得到的是待查找字符串的最后一个字符在对象C字符串中的位置
而我们需要返回的是待查找字符串在对象C字符串中的第一个字符的位置
所以还需做进一步调整后才能作为rfind函数的返回值返回
//默认从npos位置开始找
size_t rfind(const char* str, size_t pos = npos) const
{
string tmp(*this); //拷贝构造
reverse(tmp.begin(), tmp.end()); //调用reverse逆置tmp对象的字符串
size_t len = strlen(str); //待查找的字符串的长度
char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串),多给一个空间是给\0的
strcpy(arr, str); //拷贝str给arr
size_t left = 0, right = len - 1; //设置左右指针
//逆置字符串arr
while (left < right)
{
::swap(arr[left], arr[right]);//使用的是std全局域的swap函数,
left++;
right--;
}
if (pos >= _size) //所给pos大于字符串有效长度
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为镜像对称后的位置pos'
size_t ret = tmp.find(arr, pos); //复用find函数从pos'位置开始找字符串arr
delete[] arr; //销毁arr指向的空间,避免内存泄漏
if (ret != npos)
return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
else
return npos; //没找到,返回npos
}
比较
比较操作只需写两个,其他的复用即可
比较函数可以重载为类的成员函数也可也重载为全局的函数, 重载为成员函数的好处是:不必借助友元就可以访问类的成员,如果写在全局,如果要访问类的私有/保护成员,就要在类种写成友元函数
//字符串比较定义为全局的比较好,看着不别扭,要注意定义的顺序,因为要复用函数!
//可以使用内联->直接在调用的地方展开,效率高
inline bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) <0;
}
inline bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str())== 0;
}
inline bool operator<=(const string& s1, const string& s2)
{
return s1 < s2 || s1 == s2;
}
inline bool operator>(const string& s1, const string& s2)
{
return !(s1 <= s2);
}
inline bool operator>=(const string& s1, const string& s2)
{
return !(s1 < s2);
}
inline bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
如果我们想不使用strcmp函数,我们也可以自己遍历两个字符串比较字符
bool operator<(const string& s1, const string& s2)
{
size_t i = 0;
size_t j = 0;
while (i < s1.size() && j < s2.size())
{
//挨个字符比较
if (s1[i] < s2[j])
{
return true;
}
else if (s1[i] > s2[j])
{
return false;
}
else //s1[i] == s2[j],比较下一个字符
{
i++;
j++;
}
}
//有一个字符串遍历结束了 ||两个字符串都遍历结束了
return s1.size() < s2.size() ? true : false;
}
如果写在类里面:只需要传一个参数,因为第一个参数默认是this指针
加const修饰函数,这样普通对象和const对象都能调用
//>运算符重载
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
//==运算符重载
bool operator==(const string& s)const
{
return strcmp(_str, s._str) == 0;//比较字符串是否相同
}
//>=运算符重载
bool operator>=(const string& s)const
{
return (*this > s) || (*this == s);
}
//<运算符重载
bool operator<(const string& s)const
{
//return strcmp(_str, s._str) < 0;
return !(*this >= s);//复用>= (*this)->operator=(s) ,*this是string对象,this是地址
}
//<=运算符重载
bool operator<=(const string& s)const
{
//return (*this < s) || (*this == s);
return !(*this > s);//(*this)->operator>(s) ,*this是string对象,this是地址
}
//!=运算符重载
bool operator!=(const string& s)const
{
return !(*this == s); //(*this)->operator==(s) ,*this是string对象,this是地址
}
clear()
清空已有内容
void clear()
{
_size = 0;
_str[0] = '\0';
}
empty()
判断字符串是否为空
有空间但是没有数据 -> 也是空
#include<string>
int main()
{
string s1;
s1.reserve(10);//只开空间
cout << s1.empty() << endl;//1
return 0;
}
//判空
bool empty()
{
//判断方式1:return _size == 0;
return strcmp(_str, "") == 0;//判断字符串是否是空串
}
注意:两个字符串相比较不能用 == ,要使用strcmp函数
也可以直接判断_size是不是0
流提取和流插入运算符重载
注意:下面两个函数重载的函数都是写在类外的
原因:如果写在类内部,则函数的第一个成员默认是this指针, 流对象和对象抢占左操作数的位置,调用的时候就会看起来很奇怪, 所以要重载成全局的函数
问:需要重载成友元函数吗?
看该函数是否需要访问私有成员
>>
重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入,输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到空格或是’\n’便停止读取
标准库里面也是先清空再写入的:
err代码:
原因:如果原来对象的字符串中有内容,就会在原来的内容上增加,就不符合逻辑
>>
会自动跳过空格和换行,所以不能使用>> ->使用get
//in是cin的别名,返回引用是为了支持连续赋值: cin >> s1 >> s2
istream& operator>>(istream& in, string& s)
{
//先读取一个字符
char ch;
in >> ch;
//读取字符插入,读取到空格或是’\n’便停止读取,
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;//继续读取下一个字符
}
return in;
}
正确写法: cin.get()可以用来接收字符 要清空原有内容
cin是流对象,get是它的方法
istream& operator>>(istream& in, string& s)
{
//先清空已有内容,否则会出错
s.clear();
char ch;
ch = in.get();//类似getchar(),读取一个字符
//读取字符插入,读取到空格或是’\n’便停止读取
//注意:用的是&& 遇到空格或者\n都停止!!!
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();//继续读取下一个字符
}
return in;
}
<<
重载<<运算符是为了让string对象能够像内置类型一样使用<<运算符直接输出打印,实现时我们可以直接使用范围for对对象进行遍历即可,
注意:s对象不修改!所以可以传引用和const修饰 out就是cout的别名,为了可以输出多个对象(支持连续输出),所以要返回out的引用
//out是cout的别名,返回引用是为了支持连续输出: cout << s1<<s2<<endl;
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
/*//我们关注的是size,而不是\0
for(size_t i = 0;i<s.size();i++)
{
out << s[i];
}
*/
return out;
}
注意:不能直接使用out<<s.c_str()<<endl;
或者 out << s._str<<endl;
的方式打印
我们可以使用标准库的string进行验证:
因为以字符串形式输出,关注的是\0
,而我们输出关注的是_size
,
getline
getline函数用于读取一行含有空格的字符串,实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符,
istream& getline(istream& in, string& s)
{
//先清空已有内容
s.clear();
char ch;
ch = in.get();//类似getchar(),读取一个字符
//读取字符插入,读取到空格或是’\n’便停止读取,
while (ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
总结
string是一个管理字符数组的类,要求这个字符数组结尾用\0标识
1.拷贝构造和赋值重载实现深拷贝
2.增删查找的相关接口(跟顺序表类似)
3.重载了一些常见的运算符 如: > < >> << []
4.迭代器
具体要不要加const要看接口的功能性质
1.只读接口函数 -> 加const
2.只写接口函数 ->不加const
3.可读可写接口函数 ->分为加const版本和不加const版本
关于string的深入讨论
C++标准只规定了string要实现的接口功能,具体如何实现,看各个库的实现人自己决定的
- 1.vs系列的编译器,由微软工程师实现
- 2.gcc/g++GNU的c & c++编译器 由开源组织实现的
- 3.Clang编译器,Clang是一个C语言,C++,Objective-C语言的轻量级编译器
虽然它们的结构不一样,实现也就不一样,但是整体增删查改的逻辑大同小异
关于string的深浅拷贝
计数为1时,才是独享这块空间!
vs下的深拷贝:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mS46X4kU-1671703499146)( https://mangoimage.oss-cn-guangzhou.aliyuncs.com/image-20220205112840504.png)]
一开始两个对象占据的是不同一块空间, 二者不相影响
Linux版本下的深拷贝 ->写时拷贝
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vcf54Isi-1671703499146)( https://mangoimage.oss-cn-guangzhou.aliyuncs.com/image-20220205112902496.png)]
一开始二者占据的是同一块空间, 更改某一个对象时,二者的地址发生改变.
string支持一个const char*的构造函数
Mango::string s2 = "hello";//先构造一个临时对象"hello",然后拿这个临时对象拷贝构造s2, 连续的构造,会被编译器优化,直接用"hello"构造s2
如果不想发生这种隐式类型转化:加explicit
关键字
//构造函数
explicit string(const char* str = "")
{
_size = strlen(str);//如果没传参数,str为空串,大小为0
_capacity = _size;
_str = new char[_capacity + 1];//多开辟一个空间给\0
strcpy(_str, str);//字符串拷贝,\0也会被拷贝
}
String.h
- 为了防止命名冲突 ->写在自己定义的命名空间内
- 整个模拟实现的代码都是写在.h文件下的, 不可以.h放声明, .cpp放定义 因为模板不支持分离编译
- string标准库中提供了一个swap交换函数,而全局std域也提供了一个swap的模板函数, 二者都是对两个对象的成员进行交换
- 而string类中的swap仅仅是对3个成员变量进行交换,效率更高
- 如果选择全局域std中的swap,则要进行3次深拷贝(拷贝构造+赋值重载)
#include<iostream>
#include<assert.h>
#include<string.h>
using namespace std;
#pragma warning(disable:26495)
namespace Mango
{
//模拟实现string类
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
//默认成员函数
//构造函数
string(const char* str = "")
:_size(strlen(str))
,_capacity(_size)
{
_str = new char[_capacity + 1];//为\0开辟空间
strcpy(_str, str);
}
//拷贝构造函数
//s2(s1)
//现代写法:
/*
string(const string& s)
:_str(nullptr)
{
string tmp(s._str);
swap(tmp);
}
*/
//传统写法:
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
//赋值运算符重载函数
//s1 =s2
//
//传统写法:
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
//现代写法:
/*
string& operator=(const string& s)
{
if(this != &s)
{
string tmp(s._str);//调用构造函数,构造临时对象
swap(tmp);
}
return *this;
}
*/
//析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//迭代器相关函数
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//容量和大小相关函数
size_t size()
{
return _size;
}
size_t capacity()
{
return _capacity;
}
//开空间 ->只修改_capacity 不修改_size
void reserve(size_t n)
{
//增容才处理
if (n > _capacity)
{
char* tmp = new char[n + 1];//多开一个空间给\0
//strcpy(tmp, _str);//不建议使用strcpy,建议使用strncpy按字节拷贝
strncpy(tmp, _str, _size + 1);
_capacity = n;
_str = tmp;
}
}
void resize(size_t n, char ch = '\0')
{
//根据n的大小决定怎么调整
if (n < _size)
{
_str[n] = '\0';//直接缩小到n位置
_size = n;
}
else
{
//需要填充字符
if (n > _capacity)
{
reserve(n);
}
//从_size位置填充到n位置
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
}
bool empty() const
{
return _size == 0;
}
//修改字符串相关函数
void push_back(char ch)
{
//判断是否需要增容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//插入字符 + 处理\0
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* str)
{
size_t len = _size + strlen(str);//插入后的总长度
if (len > _capacity)
{
reserve(len);//扩容成len个空间
}
//在尾部追加
strcpy(_str + _size, str);
_size = len;
}
string& operator+=(char ch)
{
//insert(_size,ch);
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
//insert(_size,str);
append(str);
return *this;
}
string& insert(size_t pos, char ch)
{
assert(pos <=_size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//[pos,_size]的字符往后移动,\0也后移
char* end = _str + _size;
while (end >= _str + pos)
{
*(end+1) = *end ;
end--;
}
//插入到pos位置
_str[pos] = ch;
_size++;
return *this;
}
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (len+_size > _capacity)
{
reserve(len + _size);
}
char* end = _str + _size;
while (end >= _str + pos)
{
//所有字符往后挪len个长度
*(end + len) = *end;
end--;
}
//在pos位置插入该字符串 -> strcpy
strncpy(_str + pos, str, len);//从_str+pos位置开始把str拷贝过去,只拷贝len个字符(不含\0)
_size += len;//修改字符串长度
return *this;
}
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
size_t sz = _size - len;//剩余字符个数
//从pos位置要删除的长度>剩余字符个数
if (len >= sz)
{
_str[pos] = '\0';
_size = pos;
}
else
{
//把_str + _pos+len后面的字符往前拷贝覆盖
strcpy(_str + pos, _str + pos + len);
}
return *this;
}
void clear()
{
_size = 0;
_str[_size] = '\0';
}
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
const char* c_str()const
{
return _str;
}
//访问字符串相关函数
char& operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& operator[](size_t i)const
{
assert(i < _size);
return _str[i];
}
size_t find(char ch, size_t pos = 0)const
{
assert(pos < _size);
//遍历查找
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
//找字符串
size_t find(const char* str, size_t pos = 0)const
{
assert(pos < _size);
const char* ret = strstr(_str+pos, str);
if (ret)
{
return ret - _str;
}
else
{
return npos;
}
}
//默认从npos位置开始找
//从后往前找
size_t rfind(char ch, size_t pos = npos) const
{
string tmp(*this); //拷贝构造
reverse(tmp.begin(), tmp.end()); //调用reverse逆置对象tmp的字符串
//pos大于字符串有效长度
if (pos >= _size)
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为pos'位置
size_t ret = tmp.find(ch, pos); //复用find函数
if (ret != npos)
return _size - 1 - ret; //找到了,返回pos位置
else
return npos; //没找到,返回npos
}
//默认从npos位置开始找
size_t rfind(const char* str, size_t pos = npos) const
{
string tmp(*this); //拷贝构造
reverse(tmp.begin(), tmp.end()); //调用reverse逆置tmp对象的字符串
size_t len = strlen(str); //待查找的字符串的长度
char* arr = new char[len + 1]; //开辟arr字符串(用于拷贝str字符串),多给一个空间是给\0的
strcpy(arr, str); //拷贝str给arr
size_t left = 0, right = len - 1; //设置左右指针
//逆置字符串arr
while (left < right)
{
::swap(arr[left], arr[right]);//使用的是std全局域的swap函数,
left++;
right--;
}
if (pos >= _size) //所给pos大于字符串有效长度
{
pos = _size - 1; //重新设置pos为字符串最后一个字符的下标
}
pos = _size - 1 - pos; //将pos改为镜像对称后的位置pos'
size_t ret = tmp.find(arr, pos); //复用find函数从pos'位置开始找字符串arr
delete[] arr; //销毁arr指向的空间,避免内存泄漏
if (ret != npos)
return _size - ret - len; //找到了,返回ret镜像对称后再调整的位置
else
return npos; //没找到,返回npos
}
//关系运算符重载函数
bool operator>(const string& s)const
{
return strcmp(_str, s._str) > 0;
}
bool operator>=(const string& s)const
{
//*this就是string对象,两个string对象比较
return ((*this) > s) || ((*this) == s);
}
bool operator<(const string& s)const
{
return strcmp(_str, s._str) < 0;
}
bool operator<=(const string& s)const
{
return (*this) < s || (*this) == s;
}
bool operator==(const string& s)const
{
return strcmp(s._str, _str) == 0;
}
bool operator!=(const string& s)const
{
return !((*this) == s);
}
private:
char* _str; //存储字符串
size_t _size; //记录字符串当前的有效长度
size_t _capacity; //记录字符串当前的容量
static const size_t npos; //静态成员变量(整型最大值)
};
//静态成员变量在类外初始化
const size_t string::npos = -1;
//<<和>>运算符重载函数
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();//先读取一个字符
while (ch != '\n' && ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
ostream& operator<<(ostream& out, const string& s)
{
//范围for遍历输出即可,本质是迭代器
for (auto ch : s)
{
out << ch ;
}
/*
for(int i = 0;i<_size;i++)
{
cout << s[i];
}
*/
return out;
}
istream& getline(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();//先读取一个字符
while (ch != '\n' )
{
s += ch;
ch = in.get();
}
return in;
}
//测试反向查找
void testString8()
{
string tmp("Mango Hello world");
cout << tmp.rfind('e') << endl;
cout << tmp.rfind("He") << endl;
}
//测试现代写法的成员函数
void testString1()
{
string s0;
string s1("Always");
string s2(s1);
cout << s2.c_str() << endl;
string s3("more than words");
s3 = s1;
cout << s3.c_str() << endl;
s3 = s3;
}
//测试流插入流提取运算符重载
void testString7()
{
//string s;
string s("Hello Mango");
cin >> s;
cout << s << endl;
// 不能以字符串形式输出,测试标准库
string s1("more than");
s1 += '\0';
s1 += "words";
cout << s1 << endl;
cout << s1.c_str() << endl;
}
// 测试比较大小运算符重载
void testString6()
{
string s1("abcd");
string s2("abcd");
cout << (s1 <= s2) << endl;
string s3("abcd");
string s4("abcde");
cout << (s3 <= s4) << endl;
string s5("abcde");
string s6("abcd");
cout << (s5 <= s6) << endl;
}
// 测试insert和erase
void testString5()
{
string s(" Mango Hello");
s.insert(0, "Lemon");
cout << s.c_str() << endl;
s.insert(5, '!');
cout << s.c_str() << endl;
s.erase(0, 7);
cout << s.c_str() << endl;
s.erase(6);
cout << s.c_str() << endl;
}
// 测试查找
void testString4()
{
string s("Mango");
cout << s.find('m') << endl;
cout << s.find("max") << endl;
}
// 测试resize
void testString3()
{
string s("Mango"); // capacity - 12
s.resize(5);
cout << s.c_str() << endl;
s.resize(7, '!');
cout << s.c_str() << endl;
s.resize(20, '~');
cout << s.c_str() << endl;
}
// 测试尾插字符及字符串push_back/append,同时测试reserve
void testString2()
{
string s("more than words");
s.push_back('~');
s.push_back(' ');
cout << s.c_str() << endl;
s.append("zhabuduodele");
cout << s.c_str() << endl;
s += '~';
s += "yiyandingzhen";
cout << s.c_str() << endl;
}
}
/*
bool operator<(const string& s1, const string& s2)
{
size_t i = 0;
size_t j = 0;
while (i < s1.size() && j < s2.size())
{
//挨个字符比较
if (s1[i] < s2[j])
{
return true;
}
else if (s1[i] > s2[j])
{
return false;
}
else //s1[i] == s2[j],比较下一个字符
{
i++;
j++;
}
}
//有一个字符串遍历结束了 ||两个字符串都遍历结束了
return s1.size() < s2.size() ? true : false;
}
*/