目录
你知道在STL库中,string是怎么实现的吗?其实我们也能写!
本篇将手把手实现string类。通过自己的实现,我们能更好地去理解string类的底层原理。
引入
首先,在string.h里把string的框架搭好:
#pragma once
namespace jzy //为了和STL里的string区分,我们把string放进自定义的命名空间里
{
class string
{
public:
private:
char* _str;
size_t _size; //也可以用int,但库里面一般用size_t
size_t _capacity; //_capacity是有效字符的空间数,不包括\0
};
}
构造 | 析构函数
构造函数
构造函数的形参有两种情况,一是有参,二是无参,这两个都要实现。
➡️有参:
string(char* str) //有参:传字符串首元素的地址过来
:_str(new char[strlen(str) + 1]) //记得为末尾的\0开一份空间
, _size(strlen(str))
, _capacity(strlen(str))
{
strcpy(_str, str);
}
然而,这个函数在测试时却报错了:
#include"string.h"
using namespace jzy;
void test1()
{
string s1("abc");
}
int main()
{
test1();
return 0;
}
报错:
这其实是因为:
abc是位于常量区的常量字符串,被const修饰,不可修改。这就是说,权限比较小。
而它要调用的string函数,形参未被const修饰,是可修改的,权限较大。
就像公司的上下级关系,权限小的不可以调用权限大的,它只能调用平级。
所以,string的形参也要被const修饰。
这个故事告诉我们,能用const就尽量用。
➡️修改后的有参:
class string
{
public:
string(const char* str)
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(strlen(str))
{
strcpy(_str, str);
}
➡️无参:
string()
:_str(new char[1]) //尽管无参,仍要为\0开空间
,_size(0)
,_capacity(0)
{
_str[0] = '\0';
}
或者,用缺省值将有参/无参 合二为一:
string(const char* str = "\0") //注:这里不能用'\0',得用"\0",//单引号表示的是单个字符,类型为char而非char*
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(strlen(str))
{
strcpy(_str, str);
}
补:这里的"\0"其实有点画蛇添足。用""就可以了,里面隐含了\0。只要是常量字符串。都暗含了\0,只是看不见。
那这里的"\0"能不能换成nullptr呢?
不能!因为strlen不会检查判空,而是直接访问字符串,直到找到'\0'才会结束。
如果不传参,那默认为nullptr的话,strlen就会访问空指针,使程序崩溃。
析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
返回指针的c_str()
函数c_str()的作用?
"在C语言中,使用printf直接输出string类型的字符串可能会出现乱码。这是因为printf函数的%s格式化符号期望传入一个char类型的参数,而string类型的字符串实际上是一个对象,不是一个字符指针。所以在使用printf输出string类型的字符串时,应该使用s.c_str()方法将string类型转换为char类型。
而在C++中,使用cout输出string类型的字符串是没有问题的。cout对string类型有特殊的处理方式,可以直接输出string类型的字符串。
此外,也可以通过循环遍历string的每个字符,使用printf逐个输出字符,或者使用cout逐个输出字符,都可以得到相同的结果。
需要注意的是,如果没有包含<string>头文件,那么默认情况下是不能使用cout输出string类型的字符串的,此时需要使用c_str()方法将string类型转化为char*类型。"
(源自 c知道)
简单来说,string类是无法直接被cout或者printf输出的,它需要被转化成char类型才可以。那c_str()做的就是这样一个转化的工作。
c_str()返回的是字符串的首字母地址,此地址只读不写,因此要用const来修饰:
char* c_str()const
{
return _str;
}
有了字符串的首元素地址,我们就可以cout输出stirng字符串了。
测试一下:
void test2()
{
string s1("abcdef");
cout << s1.c_str() << endl;
}
求字符大小的size()
size_t size()const //只读不写 就加上const保护
{
return _size;
}
operator[]
operator[]是非常好用的接口,它能把字符串当数组一样使用。这么好用的接口,实现起来其实很简单。
普通对象调用:
char operator[](size_t pos)
{
return *(_str + pos);
}
🤔等等……直接传值返回的话,会有什么弊端吗?
有的!我们无法直接修改字符,来测试一下:
void test3()
{
string s1("abcdef");
cout << ++s1[0] << endl;
}
传值返回s1[0],我们得到的并不是s1里的'a',而是它的拷贝,这导致我们无法修改真正的a。
如果想要对a做修改,那就要传引用返回。
更新版的operator[]:
char& operator[](size_t pos)
{
return *(_str + pos);
}
const对象调用:
我们知道,const权限小,不能调用权限大的函数。因此,要再写一份const版的operator[]函数:
const char& operator[](size_t pos) const
{
return *(_str + pos);
}
经过const的保护,这个版本的operator[]是只读不写的。
迭代器的实现
之前我们对迭代器的认识为“像指针一样的东西”,那它究竟是不是指针呢?两者又有什么关联呢?别急,实现一遍我们就知道了。
iterator:
begin():返回第一个字符的位置。
end():返回最后一个字符的下一个位置。
typedef char* iterator;
iterator& begin() //&可加,也可不加
{
return _str;
}
iterator end() //注意:这里不能加&!
{
return _str + _size;
}
这里说明一下,end()为什么不能加&。因为end()指向的不是最后一个字符,而是它的后一个,也就是\0,所以end()处是开区间,是不能取到的。
测试一下:
void test4()
{
string s1("hello");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << " "; //可读
it++;
}
cout << endl;
for (auto ch : s1)
{
cout << ++ch << " "; //可写
}
}
上面实现的是普通的迭代器,是可读可写的。
现在再实现const_iterator,只读不写的:
const_iterator:
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
范围for
之前我们说过,范围for看起来很高级,实际上底层原理很简单,现在我们就来揭秘一下。
其实,只要写了迭代器,那直接就能用范围for,它甚至不需要你去实现。
现在,我们直接在刚刚实现的迭代器后面,使用范围for:
void test4()
{
string s1("hello");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout <<*it<< " ";
it++;
}
cout << endl;
for (auto ch : s1) //再遍历一遍
{
cout << ch << " ";
}
}
这时因为:范围for语句的底层原理是通过迭代器来实现的。编译器会用迭代器 来替换范围for。
范围for会自动调用 对象的begin()和end()方法 来获取迭代器的起始、结束位置,然后通过迭代器来遍历。
迭代器是一个对象,用于遍历和访问元素。范围for通过迭代器来遍历集合,不用再显式地操作指针,使代码更简洁易读。
可见,范围for的确没啥含金量……
深浅拷贝
浅拷贝的不足
之前我们了解过,浅拷贝就是值拷贝,对于内置类型,是按字节的方式直接拷贝的。对于自定义类型,是调用其拷贝构造函数完成拷贝的。
浅拷贝真的够用吗?
答案当然是否定的。如果有成员变量是指针,那拷贝时,仅仅是复制了指针的值而不复制指针指向的空间。
如图,string的浅拷贝:
可见,俩指针指向同一片空间。这就导致,当其中一个指针释放空间时,另一个指针也受到影响。
所以说,有指针成员时,就需要进行深拷贝了。
实现深拷贝
深拷贝是由我们自己实现的,拷贝指针时,不仅仅是复制值,更是要复制一份空间。
现在我们来实现下string的深拷贝:
string(const string& s)
:_str(new char[strlen(s._str) + 1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str,s._str);
}
可以看到,深拷贝的确是新开了空间:
赋值的深拷贝
其实赋值和刚刚说的拷贝构造是一个道理。很多时候,默认的赋值运算符就够用了,
但当涉及资源管理,如指针,就会出现两个指针指向同一块空间的情况。
来看string赋值的崩溃现场:
void test5()
{
string s1("hello");
string s2("111111111111111111111111111");
s1 = s2;
}
那这种场景就需要实现赋值的深拷贝。
我们来实现一下:
string& operator=(const string& s)
{
delete[] _str; //先释放旧空间
_str = new char[strlen(s._str) + 1]; //再开新空间
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
测试下:
void test5()
{
string s1("hello");
string s2("111111111111111111111111111");
s1 = s2;
}
可见,赋值成功。
❗但是,这并非 赋值运算符 的最终形态。因为还没检测 是否自己给自己赋值。
🚩这里要注意一个点:在实现赋值的深拷贝时,需要检测是否自己给自己赋值。
先来看看如果给自己赋值,会发生什么,
void test5()
{
string s1("hello");
string s2("111111111111111111111111111");
s2 = s2;
}
s2中的字符居然无效了!(被置成了随机值)
这是因为,this 和 形参s 都是s2。一上来this的_str空间就被释放,所以,此时s的 _str空间也被释放了,这俩现在都是随机值。所以,再把s拷给this,就会出现随机值的状况。
所以说,需要检查 是否自己给自己赋值 的情况。
赋值深拷贝的最终形态:
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str; //先释放旧空间
_str = new char[strlen(s._str) + 1]; //再开新空间
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
}
传统写法与现代写法
在stirng这里,我们就要学会写同一个功能的两种写法,即传统写法与现代写法。
现代写法 较传统写法的优势目前可能表现不出来,但等我们学到了vector、list时,现代写法就方便多了。
所以,这两种写法,都是有必要掌握的!
下面就用拷贝构造来举例:
传统写法
传统写法,就是老老实实地打工搬砖:开空间、初始化、拷贝:
string(const string& s)
:_str(new char[strlen(s._str) + 1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str,s._str);
}
现代写法
现代写法,则精明多了。这些累活我不自己干,我雇打工人tmp来干:
string(const string& s)
{
string tmp(s._str); //先构造个tmp
swap(_str, tmp._str); //把tmp交换给我
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
但是这样写,有一个隐患:
this._str未经初始化,里面是随机值。经过swap,把随机值给了tmp. _str。
在delete tmp时,对随机值指向的空间进行释放,可能会引发崩溃。
如果 _str置空的话,delete就不会释放空指针。所以,要给this. _str初始化。
经过改造:
void swap(string&tmp) //这是写在jzy类域里的swap
{
::swap(_str, tmp._str); //用::调用全局的swap函数
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string(const string& s)
:_str(nullptr) //初始化,更安全
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
注:这俩swap不一样,不是函数重载。函数重载的前提是在同一作用域。而这俩swap,一个是类里面的,一个是全局的。
所以说,全局的swap在调用时要加::,不然它会优先去局部域找,找到我们写的那个swap(string&tmp)之后,会认为参数不匹配。
总结一下,现代写法的本质是”拷贝构造新对象+将自己和新对象进行交换“。
练习
现在,在学习了现代写法的思想之后,我们来练习写operator=的现代写法:
void swap(string& tmp)
{
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
return *this;
}
}
增删查改
增容reserve()
reserve”保留“:开若干个空间,先保留在那里,即增容。
void reserve(size_t size)
{
if (size > _capacity) //先检查下要不要增容
{
char* p = new char[size + 1]; //开新空间(为\0多开一个空间)
strcpy(p, _str); //拷数据
delete[] _str; //释放旧空间
_str = p;
_capacity = size;
}
}
改变大小+初始化的resize()
刚刚的reserve是处理容量,现在的resize是处理大小的。
resize能将size变大 / 变小(容量不变),size变大的同时 还能顺便给初始化了。
库里面的resize分两种:给初始值 和 不给初始值的。我们一会实现,就用缺省值的方式 合二为一。
string& resize(size_t n, char c = '\0') //n是新的_size,ch是初始化的值
{
if (n < _size) //小 就截断
{
_str[n] = '0';
_size = n;
}
else
{
if (n > _capacity) //大 就扩容
{
reserve(n);
}
memset(_str + _size, c, n - _size);
_str[n] = '\0';
_size = n;
}
return *this;
}
尾插字符push_back()
void push_back(const char c)
{
if (_size == _capacity) //先检查要不要扩容
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0'; //别忘了最后得加上'\0'
}
追加字符串append()
void append(const char* ch)
{
//考虑扩容
size_t total_size = _size + strlen(ch); //先判断容量
if(_capacity < total_size)
{
reserve(total_size);
}
strcpy(_str + _size, ch); //直接把ch拷到\0的位置
_size += strlen(ch);
}
贼好用的operator+=
在了解stirng类的方法时,我们就惊叹过,operator+=真的好好用,既能追加字符,又能加字符串。
实际上,追加字符 和 追加字符串 是构成重载的两个函数,现在我们来实现下。
//追加字符
string& operator+=(const char c)
{
push_back(c); //复用了push_back()
return *this;
}
//追加字符串
string& operator+=(const char* ch)
{
append(ch); //复用了append()
return *this;
}
指定位置插 insert()
插字符:
string& insert(size_t pos, char c)
{
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--;
}
//再插入
_str[pos] = c;
_size++;
_str[_size] = '\0';
return *this; //其实这个返回值意义不大
}
插字符串:
string& insert(size_t pos, const char* ch)
{
assert(pos <= _size);
size_t total_size = _size + strlen(ch);
if (_capacity < total_size) //先算出一共需要多少空间,不够就开
{
reserve(total_size);
}
size_t end = _size ; //挪数据
size_t span = strlen(ch);
while (end > pos) //这个循环很容易写错! 不能写end>=pos,减成负数也就是无穷大,很容易越界!
{
_str[end + span] = _str[end];
end--;
}
_str[pos+span] = _str[pos];
strncpy(_str+pos, ch, strlen(ch));
_size += strlen(ch);
return *this;
}
删除erase()
void erase(size_t pos, size_t len = npos)
{
assert(len < _size);
if (len == npos||len>_size-pos) //当删到末尾或者不够删时
{
_str[pos] = '\0';
}
else
{
strcpy(_str + pos, _str + pos + len);
_str[_size - len] = '\0';
}
_size -= len;
}
关于npos:
npos的类型为size_t,它被设为-1,因为size_t表示无符号数,-1在无符号数中表示 最大值。
npos在字符串中,意味着直到字符串的末尾。在容器中,表示不存在的位置。
我们要在类里声明静态的npos,在类外定义:
//类里
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
//类外
size_t string::npos = -1;
或者,在类里这样写:
private:
char* _str;
size_t _size;
size_t _capacity;
const static size_t npos = -1; //const static在C++中是语法的特殊处理,直接可以当成定义初始化
查找find()
查找字符:
size_t find(char c, size_t pos = 0)const //从pos位置开始找c,找到返回下标
{
assert(pos < _size);
for (; pos < _size; pos++)
{
if (_str[pos] == c)
{
return pos;
}
}
return npos;
}
查找字符串:
size_t find(const char* s, size_t pos)const //第一个const一定要加!!
{
assert(pos < _size);
const char* ret = strstr(_str + pos, s);
if (ret == nullptr)
{
return npos;
}
return ret-_str;
}
这里说明一下,关于左操作数的const为什么一定要加。
在测试时,我们给的例子为:s1.find("day", 0),这里的”day“是常量字符串,类型为const char*,而不是char *,
所以左操作数的类型也要严格为const char*,才能与常量字符串匹配。
比大小组合
bool operator>(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) > 0;
}
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator>=(const string& s1, const string& s2)
{
return s1 > s2 || s1 == s2;
}
bool operator<(const string& s1, const string& s2)
{
return !(s1 >= s2);
}
bool operator<=(const string& s1, const string& s2)
{
return !(s1 > s2);
}
bool operator!=(const string& s1, const string& s2)
{
return !(s1 == s2);
}
流插入与流提取
引入
不知道你注意到没有,我们用流插入<<运算符 时,string类型的对象是没法直接输出的。
void test8()
{
string s1("happy");
int a = 4;
cout << a << endl; //√,输出4
cout << s1.c_str() << endl; //√,将string类型转化成char*类型,输出happy
cout << s1 << endl; //×
}
为啥string类的对象不能用<<呢?
我们先厘清几个问题:
1.cout是什么?
cout是iostream中定义的ostream类中的对象。
"iostream一个库,用于输入输出流的操作。ostream是iostream库中的一个类,它是用来进行输出操作的。而cout是ostream类的一个对象,它是在iostream头文件中定义的。"
2.<<是什么?
<<是运算符,它之所以能用在cout上,是因为在iostream中对<<进行了重载。
3.cout<<4是什么?
其实就是对象cout在调用函数operator<<()
相当于cout.operator<<(4)
所以,为啥string类的对象不能用流插入、流提取呢,因为string类型中没有重载operator<<()函数!
现在,我们就来重载一下<<和>>,让string类也能用上!
三点说明:返回值、形参与实现方法
➡️operator<<的返回值类型是什么?
直接说答案:返回cout的类型,即ostream&。
我们通过下面这行代码来说明原因:
cout<<4<<" days"; //连续的流插入
此代码的实质是:
cout.operator<<(4).operator<<("days");
所以说,operator<<返回值为ostream&类型,因为这样可以支持连续的流插入。该返回值又可作为对象,继续调用<<。
➡️形参:>> / <<是双目运算符,且第一个形参(左操作数)必须为cin或者cout,即istream类或ostream类。
说明原因:
cin和cout应该作为左操作数,因为根据我们的理解和使用习惯,我们是将输入的内容存进变量a中:cin>>a; 将a插入输出流中:cout<<a;
这俩都是cin、cout在左,而变量a在右。
如果cin/cout做右操作数,那用起来就是a<<cout / a>>cin,这不符合我们的理解,非常怪异。
所以,可知流插入、流提取的声明为:
ostream& operator<<(ostream& out, const string& s); //流插入
istream& operator>>(istream& in, string& s); //流提取
➡️实现方法:
⭐1.不能用成员函数重载,只能用类外的函数重载。
如果用成员函数重载,那左操作数默认就是this。而刚刚我们说明了,>> / <<的左操作数必须为istream类或ostream类,这就冲突了。
所以不能放在类里实现,得在类外。
2.如果涉及到访问私有成员,那得用友元函数重载。
当需要输入/ 输出类的私有成员时,就将重载的<</ >>函数设为友元,予以访问权限。
流插入的实现
注:这里不需要设为友元函数,因为我们直接就能访问out的数据,没必要再设友元了。
在类外实现:
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
思路很简单:将string的内容依次插入对象out中,再返回out。
流提取的实现
istream& operator>>(istream& in, string& s)
{
char ch;
ch = in.get(); //先获取输入流的首个字符
while ( ch! = ' ' && ch != '\n') //若首字符不是换行符or空格,则进入循环
{
s += ch; //将该字符存入变量s中
ch = in.get(); //继续获取输入流的字符
}
return in;
}
思路:
step1 获取in(输入流)的首字符
step2 当首字符不是换行符/空格,进入循环:存字符进变量、继续获取字符
step3 返回in
注意:这里不能用in>>ch;来获取输入流的字符。因为在cin>>操作符获取不到字符间的空格or换行。
(图源:c知道)
总代码
📖string.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#pragma warning(disable:6031)
#include<iostream>
#include<assert.h>
using std::cin; //为什么不直接using namespace std;?
using std::cout; //因为那样会产生冲突,我定义的string和std里的string,编译器不知道用哪个好
using std::endl;
using std::swap;
using std::ostream;
using std::istream;
namespace jzy //为了和STL里的string区分,我们把string放进自定义的命名空间里
{
class string
{
public:
//构造函数
string(const char* str = "\0")
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(strlen(str))
{
strcpy(_str, str);
}
//析构
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//c_str()
char* c_str()const
{
return _str;
}
//size()
size_t size()const //只读不写 就加上const保护
{
return _size;
}
//operator[]
char& operator[](size_t pos)
{
return *(_str + pos);
}
const char& operator[](size_t pos) const
{
return *(_str + pos);
}
//普通迭代器
typedef char* iterator;
iterator& begin() //&可加,也可不加
{
return _str;
}
iterator end() //注意:这里不能加&!
{
return _str + _size;
}
//const迭代器
typedef const char* const_iterator;
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
//拷贝构造的深拷贝——传统写法
/*string(const string& s)
:_str(new char[strlen(s._str) + 1])
,_size(s._size)
,_capacity(s._capacity)
{
strcpy(_str,s._str);
}*/
//现代写法
void swap(string&tmp)
{
::swap(_str, tmp._str);
::swap(_size, tmp._size);
::swap(_capacity, tmp._capacity);
}
string(const string& s)
:_str(nullptr) //初始化,更安全
,_size(0)
,_capacity(0)
{
string tmp(s._str);
swap(tmp);
}
//赋值——传统写法
//string& operator=(const string& s)
//{
// if (this != &s)
// {
// delete[] _str; //先释放旧空间
// _str = new char[strlen(s._str) + 1]; //再开新空间
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
// return *this;
// }
//}
//赋值——现代写法
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
return *this;
}
}
//增容
void reserve(size_t size)
{
if (size > _capacity) //先检查下要不要增容
{
char* p = new char[size + 1]; //开新空间
strcpy(p, _str); //拷数据
delete[] _str; //释放旧空间
_str = p;
_capacity = size;
}
}
//尾插字符
void push_back(const char c)
{
if (_size == _capacity) //先检查要不要扩容
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = c;
_str[_size] = '\0'; //别忘了最后得加上'\0'
}
//尾插字符串
void append(const char* ch)
{
//考虑扩容
size_t total_size = _size + strlen(ch); //先判断容量
if(_capacity < total_size)
{
reserve(total_size);
}
strcpy(_str + _size, ch); //直接把ch拷到\0的位置
_size += strlen(ch);
}
//operator+=
//追加字符
string& operator+=(const char c)
{
push_back(c); //本质是复用了push_back()
return *this;
}
//追加字符串
string& operator+=(const char* ch)
{
append(ch);
return *this;
}
//指定位置插
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//先把后面的数据往后挪
size_t end = _size;
while (end > pos) //这个看似不起眼的循坏,却很容易写错!
{
_str[end] = _str[end - 1];
end--;
}
//再插入
_str[pos] = c;
_size++;
_str[_size] = '\0';
return *this; //这个返回值意义不大
}
string& insert(size_t pos, const char* ch)
{
assert(pos <= _size);
size_t total_size = _size + strlen(ch); //没引string.h头文件,是怎么用起来strlen的? 因为iostream中包含了stdio.h(其中包含了strlen)
if (_capacity < total_size) //先算出一共需要多少空间,不够就开
{
reserve(total_size);
}
size_t end = _size ; //把后面的数据挨个往后挪
size_t span = strlen(ch);
while (end > pos) //这个循环很容易写错! 不能写end>=pos,变成负数也就是无穷大,很容易越界!
{
_str[end + span] = _str[end];
end--;
}
_str[pos+span] = _str[pos];
strncpy(_str+pos, ch, strlen(ch));
_size += strlen(ch);
return *this;
}
//删字符——初始版
//void erase(size_t pos, size_t len = npos)
//{
// assert(len < _size);
// size_t num = _size - pos - len; //算出要挪几个数
//
// size_t start = pos + len; //从下标为start的数开始挪
// while (num) //挪数据
// {
// _str[pos++] = _str[start++];
// num--;
// }
// _str[_size - len] = '\0';
// _size -= len;
//}
//删字符——进阶版
void erase(size_t pos, size_t len = npos)
{
assert(len < _size);
if (len == npos||len>_size-pos) //当删到末尾或者不够删时
{
_str[pos] = '\0';
_size = strlen(_str);
}
else
{
strcpy(_str + pos, _str + pos + len);
_str[_size - len] = '\0';
_size -= len;
}
}
//查找
size_t find(char c, size_t pos = 0)const //从pos位置开始找c
{
assert(pos < _size);
for (; pos < _size; pos++)
{
if (_str[pos] == c)
{
return pos;
}
}
return npos;
}
size_t find(const char* s, size_t pos)const //第一个const一定要加!!
{
assert(pos < _size);
const char* ret = strstr(_str + pos, s);
if (ret == nullptr)
{
return npos;
}
return ret-_str;
}
private:
char* _str;
size_t _size; //也可以用int,但库里面一般用size_t
size_t _capacity; //_capacity是有效字符的空间数,不包括\0
static size_t npos;
};
size_t string::npos = -1;
//流插入
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
//流提取
istream& operator>>(istream& in, string& s)
{
char ch;
//in >> ch; //cin获取不到数据间的空格or换行
ch = in.get(); //先获取输入的首个字符
while (ch!= ' ' && ch != '\n') //若首字符不是换行符or空格,则进入循环
{
s += ch; //将该字符存入变量s中
ch = in.get(); //继续获取输入的字符
}
return in;
}
}
📖test.cpp
#include"string.h"
using namespace jzy;
void test1()
{
string s1("abc");
string s2;
}
void test2()
{
string s1("abcdef");
cout << s1.c_str() << endl;
}
void test3()
{
string s1("abcdef");
cout << ++s1[0] << endl;
const string s2("abcdef");
cout << s2[0]<< endl;
for (int i = 0; i < s2.size(); i++)
{
cout << s2[i] << " ";
}
}
void test4()
{
/*string s1("hello");
string::iterator it = s1.begin();
while (it != s1.end())
{
cout <<*it<< " ";
it++;
}
cout << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
cout << endl;*/
const string s1("hello");
string::const_iterator it = s1.begin();
//cout << ++s1[0] << " "; //不可修改
for (auto ch : s1)
{
cout << ++ch << " "; //为什么这里可以修改s1的内容?❓
}
}
void test5()
{
string s1("hello");
string s2("111111111111111111111111111");
s2 = s2;
}
void test6()
{
string s1("happy");
string s2(s1);
s1=s2 = "hi";
}
void test7()
{
string s1("happy");
//s1.reserve(100);
s1.push_back('A');
cout << s1.c_str() << endl;
s1.append(" day");
cout<< s1.c_str() << endl;
s1 += 'y';
cout << s1.c_str() << endl;
s1 += " zz";
cout << s1.c_str() << endl;
s1.insert(0, 'k');
cout << s1.c_str() << endl;
s1.insert(4, "kkk");
cout << s1.c_str() << endl;
s1.erase(4, 4);
cout << s1.c_str() << endl;
cout << s1.find('p', 2) << endl;
cout << s1.find("day", 0) << endl;
}
void test8()
{
string s1("happy");
cout << s1 << endl;
string s2;
cin >> s2;
cout << s2 << endl;
}
int main()
{
//test1(); //构造函数
//test2(); // c_str
//test3(); // size() | operator[]
//test4(); //迭代器 | 范围for
//test5(); //深浅拷贝 | 赋值
//test6(); //拷贝构造 / 赋值的现代写法
//test7(); //reserve | push_back | append | operator+= | insert | 删字符 | 查找
test8(); //流插入 | 流提取
return 0;
}