string的使用及模拟实现
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
string类的形式出现在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char,char_traits,allocator> string;
- 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
string的常用接口
1.string类对象的构造函数
string s1;
string s2("hello world");
string s3(s2);
基本常用的构造函数有三种:
1.string():构造空的string类对象,即空字符串
2.string(const char* s):用C语言的string来构造string类对象
3.string(const string&s) :拷贝构造
4.string(size_t n, char c) :string类对象中包含n个字符c(不常用)
2.string类对象的容量操作
1.获取字符串有效元素个数
string s1("hello");
cout << s1.size() << endl; 推荐
cout << s1.length() << endl;
2.获取当前容量
cout << s1.capacity() << endl;
实际容量是16,因为还有个\0
3.清空字符串 clear
s1.clear();
但是容量并未清理,只是清理了size
4.判断字符串是否为空 empty
4.判断字符串是否为空 empty
检测字符串是否为空串,是返回true,否则返回false
cout << s1.empty() << endl;
//清空字符串
s1.clear();
cout << s1.empty() << endl;
5.reserve请求更改容量
5.reserve
此函数对字符串长度没有影响,也不能更改其内容。
如果n大于当前字符串容量,则该函数将容器的容量增加到n个字符(或更大),小于当前容量则不执行操作
string s2("hello");
s2.reserve(100);
6.resize将有效字符的个数改成n个,多出的空间用字符c填充
6.resize 将有效字符的个数改成n个,多出的空间用字符c填充
若更改空间容量之前有字符串,则不会更改原来的
若之前是空,则会把所有开辟的空间初始化给的字符
string s2("hello");
s2.reserve(100);
s2.resize(1000,'x');
若比当前容量小,则会删除数据(size)
3. string类对象的访问及遍历操作
1.返回pos指定下标位置 operator[ ]
1.返回pos指定下标位置 operator[ ]
有两个重载
1.返回值和形参都可修改—————— char& operator[] (size_t pos)
2.返回值和形参都加const修饰———— const char& operator[] (size_t pos) const;
string s1("hello world");
for(size_t i = 0;i < s1.size();i++)
{
cout << s1.operator[](i) << " ";
s1.operator[](i)++;
}
cout << endl;
for(size_t i = 0;i < s1.size();i++)
{
cout << s1.operator[](i) << " ";
}
2.迭代器 begin、end
2.迭代器 begin、end
begin获取第一个字符位置的迭代器
end获取最后一个字符下一个位置的迭代器
暂时可以理解为指向首字符的一个指针和指向\0位置的一个指针
string s1("hello world");
string::iterator it = s1.begin();
while(it != s1.end())
{
(*it)++;
it++;
}
cout << endl;
it = s1.begin();
while(it != s1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
3.反向迭代器 rbegin、 rend
3.反向迭代器 rbegin、 rend
rbegin获取最后一个字符位置的迭代器
rend获取第一个字符的前一个位置的迭代器
string s1("hello world");
string::reverse_iterator rit = s1.rbegin();
while(rit != s1.rend())
{
cout << *rit++ << " ";
}
cout << endl;
4.范围for
自动往后迭代,自动判断结束 (自动根据获取的类型获取范围和位置)
注意:C++11以上才支持这种写法
可以看成是反回下标位置的引用,就可以更改读取位置本身的数据了
for(auto& e: s1)
{
e += 1;
}
for(auto e : s1)
{
cout << e << " "
}
cout << endl;
注意:
迭代器属于内嵌在类中的
迭代器相比operator[ ]确实是没优势,但是operator[ ]只有数组能用,而其他结构如链表用迭代器就很便利了,所以是迭代器是通用的方式
C++11中还有cbegin cend 代表是const修饰的,但是普通版本也可以自动识别const修饰的
4. string类对象的修改操作
1.push_back在字符串后尾插字符c
1.push_back 在字符串后尾插字符c
string s1("hello world");
s1.push_back('a');
2.append 在字符串后追加一个字符串
2.append 在字符串后追加一个字符串
string s1("hello world");
s1.push_back('a');
s1.append("ni hao");
3.operator+=在字符串后追加字符串str
3.operator+= 在字符串后追加字符串str
string s1("hello world");
s1+=' ';
s1+="ni hao";
4.c_str返回C语言格式的字符串(字符串的首地址,char*)
4.c_str 返回C语言格式的字符串(字符串的首地址,char*)
5.find查找 ,substr取指定位置+指定长度的字符串内容
5.find查找 ,substr取指定位置+指定长度的字符串内容
pos是要取的位置,不给值默认从0开始
len是要取得长度,不给值默认有多少取多少
共有4种:默认都是从pos位置开始查找
(1)查找一个string类对象的字符串
(2)查找一个字符串
(3)查找字符串的一部分,n就是要查找的长度
(4)查找一个字符
返回值:如果找到就返回第一个匹配的位置(字符串的第一个字符的位置),没找到就返回npos
npos是 unsigned int类型的,默认是-1,换成无符号整型就是一个很大的数
find+substr组合应用
例如:分别取出一个网址的协议名,域名,路径
string url("https://fanyi.baidu.com/#en/zh/ERROR%3A%20%20%20%20PREPROCESSOR%3A%20MACROS%20TOO%20NESTED");
取协议
size_t pos1 = url.find(':');
string protocol = url.substr(0,pos1-1);
取域名
size_t pos2 = url.find('/',pos1+3);
string domain = url.substr(pos1+3,pos2 - (pos1+3));
取资源
string uri = url.substr(pos2+1);
除了find还有rfind,是从后往前找就不做介绍了
6.头插 insert、删除erase
string s1("hello world");
s1.insert(0,1,'x'); 在指定位置,指定插入个数,指定要插入的 字符
s1.insert(s1.begin(),'y'); 给迭代器位置,指定要插入的字符
s1.insert(1,"test"); 在指定位置,插入字符串
s1.erase(0,1); 头删
s1.erase(s1.size()-1,1); 尾删
s1.erase(3); 从第三个位置往后删直到结束
头删,头插效率很低,因为要把数据整体往后挪,所以尽量不要用
另外一些常见功能
operator>> 输入运算符重载
operator<< 输出运算符重载
getline 获取一行字符串
relational operators 大小比较
string模拟实现
class My_string
{
public:
private:
char* _str; 字符串
size_t _capacity; 容量
size_t _size; 下标/字符个数
static const size_t _nops;
};
static const Hx::My_string::_nops = -1;
1.基础功能
构造函数
char* strcpy(char* temp1,const char* temp2)
{
assert(temp2);
char* ret = temp1;
while(*temp1++ = *temp2++)
{}
return ret;
}
int strlen(const char* temp)
{
assert(temp);
int count = 0;
while(temp[count])
{
count++;
}
return count;
}
My_string(const char* str = "")
:_str(new char [strlen(str) + 1])
,_size(strlen(str))
,_capacity(_size)
{
strcpy(_str,str);
}
"" 或 "\0"都代表空字符串,只存一个\0
nullptr NULL都通过不了,且\0等同于NULL
构造函数在申请内存的时候要多申请一个,是留给\0的
析构函数
~My_string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
析构的时候需要释放申请的动态内存,并把指针置空
拷贝构造
拷贝构造传统写法
My_string(const My_string& s)
:_str(new char[s._capacity+1])
,_size(s._size)
,_capacity(_size)
{
strcpy(_str,s._str);
}
拷贝构造现代写法
void My_swap(My_string& s)
{
swap(_str,s._str);
swap(_capacity,s._capacity);
swap(_size,s._size);
}
My_string(const My_string& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
My_string temp(s.c_str);
My_swap(temp);
}
拷贝构造涉及到深拷贝:对动态内存中的数据拷贝,但不能把地址也拷贝,否则会导致,一块动态内存能被多个对象改变,且析构多次
传统写法:
(1)先用被拷贝的对象容量值给即将要实例化的对象开辟空间
(2)再把size、capacity全都拷贝
(3)把被拷贝对象的字符串,拷贝给即将实例化的对象(可以用strcpy、也可以遍历)
现代写法:
(1)用被拷贝对象的字符串构造一个临时对象,这样就能自动计算出size、capacity
(2)需要写一个类域内部应用的swap函数接口,再利用库函数中的swap把成员函数分别交换
C++中的swap源码
为什么不用C++库函数中的swap直接交换对象,因为这里只是对成员变量交换,而用库中的代价很大,传递的是两个类对象, 会完成三次深拷贝,库函数的swap应尽量对内置变量使用
赋值重载
传统写法
My_string& operator=(const My_string& s)
{
if(this != &s)
{
delete[] _str;
_str = new char[s._capacity+1];// 或strlen(s._str)+1
_size = s._size;
_capacity = s._capacity;
strcpy(_str,s._str);
}
return *this;
}
改进1
My_string& operator=( My_string& s)
{
if(this != &s)
{
char* temp = new char[s._capacity+1];
strcpy(temp,s._str);
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = temp;
}
return *this;
}
改进2
My_string& operator=( My_string& s)
{
if(this != &s)
{
My_string temp(s);
My_swap(temp);
}
}
改进3
My_string& operator=( My_string s)
{
My_swap(s);
return *this;
}
测试功能
赋值重载需要注意的是,若赋值的目标对象与源对象的长度和容量相同,是可以直接cpy字符串的,但是若出现一大一小的情况就需要额外考虑了
大赋小:需要增容
小赋大:需要缩小
所以它们都需要,释放目标动态内存,用源对象容量大小新开辟一块空间给目标空间
再把size、capacity、字符串复制到目标对象
传统写法:
(1)先创建一个临时指针,接收新开辟的动态内存(用源对象的capacity+1)
(2)拷贝字符串到新的动态内存中
(3)释放目标对象的动态内存
(4)把新动态内存地址、源对象的capacity、size赋给目标对象
(5)利用隐藏this与传递进来的对象别名判断是否自己给自己赋值
注意:要先用临时变量去开辟空间,切记不要 先释放,若释放失败则会抛异常,不会执行后续操作了,数据也就丢了,而先开辟,开辟失败时原来保存的数据不会收到影响
现代写法:
(1)传参设置为传值,传值等于是用传进来的值拷贝构造的临时对象;
(2)用临时对象与目标对象的引用交换,临时对象在调用结束时会销毁并调用析构函数,就把交换给的动态内存释放掉了
2.访问遍历操作
获取size
size_t size() const
{
return _size;
}
size的位置是\0的位置,因为数组下标是0开始的
c_str 按C格式读取字符串
const char* c_str() const
{
return _str;
}
C语言中,读取一个字符串只需要他的首地址
下标引用符[ ]重载
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
begin、end 迭代器
迭代器是获取首位、末尾位置地址的,关键字:iterator、const_iterator
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str+_size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str+_size;
}
测试功能
3.容量操作
reserve 为字符串预留空间
void reserve(size_t n)
{
if(n > _capacity)
{
char* temp = new char[n+1];
strcpy(temp,_str);
delete[] _str;
_capacity = n;
_str = temp;
}
}
给对象指定大小,若n比原有的容量大则增容,重新申请容量为n大小的内存,并把原字符串拷贝,再更新capacity的数值;若n比原有的容量小,则不执行任何操作
增容时注意深拷贝的问题,先申请后释放,因为有可能申请失败
resize 将有效字符的个数该成n个,多出的空间用字符c填充
void resize(size_t n,char ch = '\0')
{
if(n <= _size)
{
_size = n;
_str[_size] = '\0';
}
else
{
if(n > _capacity)
{
reserve(n);
}
memset(_str+_size,ch,n-_size);
_size = n;
_str[_size] = '\0';
}
}
分三种情况:
(1)n小于capacity,则只需要把size的位置缩小为n就行了
(2)n等于capacity,不执行任何操作(可以把小于和等于合为一种情况操作)
(3)n大于capacity,那就需要扩容了,先深拷贝原来的动态内存的字符串,在将size到n之间全置为ch,最后改变size的位置并注意不要丢失\0
clear 清除
void clear()
{
assert(_size);
_size = 0;
}
清除只需要把size置0,并把0位置改为\0即可
empty 判断空字符串
bool empty() const
{
return _size == 0;
}
判断size是否为0即可
4.增删查改
push_back 、append 尾插
void push_back(char ch)
{
if( _size == _capacity )
{
reserve(_capacity == 0 ? 4 : _capacity*2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
void append(const char* s)
{
int len = strlen(s);
if( _size+len > _capacity )
{
reserve(_capacity == 0 ? 4 : _size+len);
}
strcpy(_str+_size,s);
_size += len;
}
尾插入一个字符:
(1)要先进行增容判断(可以复用reserve)
(2)再把这一个字符拷贝上去,size增1
注意strcpy的起始位置是size的位置
尾插入一个字符串:
与插入字符大同小异
(1)需要先算出字符串长度+当前长度size,判断是否增容
(2)用strcpy把字符串拷贝上去
(3)size更新,更新的长度为 size+计算的字符串长度(不需要单独设置\0,因为会把\0拷贝的)
+=操作符重载
My_string operator+=(char ch)
{
push_back(ch);
return *this;
}
My_string operator+=(const char* s)
{
append(s);
return *this;
}
+=只是空壳,重载复用的尾插字符和字符串
find 查找
char* my_strstr(const char* temp1,const char* temp2)
{
assert(temp1 && temp2);
char* t1 = temp1;
char* t2 = temp2;
char* ret = t1;
while(*t1 && *t2)
{
ret = t1; //用来记录比较前的首地址位置。如果字符串元素都相等,就返回比较的第一个元素首地址
while(*t1 == *t2) //只要不相等就退出
{
t1++;
t2++;
if('\0' == *t2) //当源字符串已经为\0时,证明相等,则退出
return ret;
if('\0' == *t1) //当目标字符串遍历出\0,就没必要再缩进剩余几个字符进行比较了,直接退出
return NULL;
}
t2 = temp2; //若比较不相等,则源字符串复位,等待下次比较
t1 = ret; //遍历指针回到第一次比较的位置
t1++; //证明上一次比较,此位置比较不出来相等,就缩一位
}
return NULL;
}
size_t find(char ch)
{
for(size_t i = 0;i < _size;i++)
{
if(_str[i] == ch)
return i;
}
return _nops;
}
size_t find(const char* s,size_t pos = 0)
{
char* ptr = strstr(_str,s); 并返回在_str中第一次匹配的地址
if(ptr)
return ptr-_str;
return _npos;
}
默认从0开始找
查找一个字符:直接遍历查找,或者用迭代器,并返回找的的下标
查找一个字符串:复用strstr,返回找到的首字符的下标
若没找到反回的都是npos(size_t 类型的,值是-1,也就是无符号整型最大值)
insert 指定下标插入
My_string& insert(size_t pos,char ch)
{
assert(pos <= _size);
if(_size == _capacity)
{
reserve(_capacity == 0? 4 : _capacity*2);
}
/* size_t end = _size;
while(end >= pos)
{
_str[end+1] = _str[end];
--end;
} 无符号整型在变为-1时是会变为正数的最大值
int end = _size;
while(end >= pos) 把end变为int,但是在比较时会发生算术转换,还是会转换为unsigned int
{
_str[end+1] = _str[end];
--end;
}
*/
size_t end = _size;
while(end >= pos)
{
_str[end+1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
return *this;
}
My_string& insert(size_t pos,char* s)
{
assert(pos <= _size);
size_t len = strlen(s);
if(_size+len > _capacity)
{
reserve(_capacity == 0? 4 : _size+len);
}
size_t end = _size + len;
while( end-len >= pos )
{
_str[end] = _str[end - len];
end--;
}
strncpy(_str+pos,s,len); //不可以使用strcpy
_size += len;
return *this;
}
前插入一个字符:
(1)先进行增容判断
(2)在把插入位置到结尾整体往后挪1个位置(利用下引用符、迭代器都可以)
(3)插入字符
(4)更新size
前插入一个字符串:
(1)先计算要插入的字符串长度
(2)计算的长度+当前size 判断是否增容
(3)先把pos到结尾的字符串往后挪,与挪字符类似,字符是挪1,字符串是挪字符串长度(类似希尔排序中的 +gap预排序)
(4)strcnpy把字符串插入到对象的字符串中,不能用strcpy,会把\0也拷贝的(注意起始位置是pos位置)
(5)更新size(size+计算的长度)
erase 指定删除
指定删除的位置及个数
My_string& erase(size_t pos = 0,size_t len = _nops)
{
assert(pos < _size);
if(len == _npos || len + pos >= _size)
{
_size = pos;
_str[pos] = '\0';
}
else
{
strcpy(_str + pos,_str + pos + len);
_size -= len;
}
return *this;
}
分三种情况:
(1)全删(pos = 0),直接在初始位置置0
(2)从中途开始删,删到最后,也是直接在pos位置置\0
(3)删一部分,没超过size,从pos位置整体往前挪len个,更新size
5.string类非成员函数
比较大小
之前日期类练习时的运算符都重载成成员,这里的运算符就重载成全局的来练习
int strcmp(const char* temp1,const char* temp2)
{
assert(temp1 && temp2);
while(*temp1 && *temp2)
{
if(*temp1 != *temp2)
{
return *temp1 - *temp2;
}
temp1++;
temp2++;
}
return *temp1 - *temp2;
}
bool operator<(const My_string& s1,const My_string& s2)
{
size_t i = 0;
while( i < s1.size() && i < s2.size() )
{
if(s1[i] < s2[i])
return true;
else if(s1[i] > s2[i])
return false;
else
i++;
}
if(i == s1.size() && i < s2.size())
return true;
else
return false;
}
或直接strcmp(s1.c_str(),s2.c_str());
bool operator==(const My_string& s1,const My_string& s2)
{
return strcmp(s1.c_str(),s2.c_str()) == 0;
}
bool operator!=(const My_string& s1,const My_string& s2)
{
return !(s1 == s2);
}
bool operator<=(const My_string& s1,const My_string& s2)
{
return (s1 == s2) || (s1 < s2);
}
bool operator>=(const My_string& s1,const My_string& s2)
{
return !(s1 < s2);
}
bool operator>(const My_string& s1,const My_string& s2)
{
return !(s1 <= s2);
}
+加运算符重载
My_string operator+(const My_string& s,const char ch)
{
My_string temp = s;
temp += ch;
return temp;
}
My_string operator+(const My_string& s,const char* str)
{
My_string temp = s;
temp += str;
return temp;
}
流插入
ostream& operator<<(ostream& out,My_string& s)
{
for(size_t i = 0;i < s.size();i++) z这个是不管中间是否是\0,都输出,直到结尾
{
out << s[i] ;
}
out << endl;
return out;
}
out << s.c_str() << endl; error 这个格式是遇到\0结束
这里需要的是无论中途是否有\0都要读取到size位置
流提取
istream& operator>>(const istream& in,My_string& s)
{
char ch = in.get(); 每次获取一个字符
while(ch != ' ' || ch != '\n') 当出现换行和空格结束
{
s += ch; 每次插入一个字符
ch = in.get(); 更新字符
}
return in;
}
和C语言输入字符串类似scanf(“%s”)可输入字符串,不可中途用空格和回车
get就是每次接收一个字符,当读取到空格或者换行就退出