为什么要使用string类
string就是字符串的意思,其实我们在C语言阶段就经常接触字符串,那个时候我们对于字符串进行处理都是使用C语言库中的str函数来对字符串进行处理,当我们需要对我们的字符串进行增删查改的时候,这个时候就需要我们自己使用str这些函数来对字符串进行处理了,我们在处理这些函数的时候对于字符串的访问也需要注意使用的规范避免安全问题,我们这样使用str是可以实现我们对字符串的管理的,但是我们这样的操作并不方便,也并不安全;
如果我们可以有一个直接实现了增删查改的工具,我们只需要输入我们需要进行的操作,这个工具就可以帮我们进行数据的更改的话,我们的操作肯定方便有安全;而我们c++中的string类是对字符串这样的数据的一种集合式管理的类,对我们字符串的增删查改操作还有字符串的数据进行封装,优化我们对字符串的操作,也使得我们的操作更加安全;这就是我们要使用string类的原因;
了解string类
我们的string类其实也是我们string头文件中类的实例化,string类是basic_string类模板的一种实例化;basic_string就是类模板,这也意味着我们basic_string适用于很多不同的类型,但是这个类模板不是对字符串进行增删查改的吗,那不就只有char类型了吗?其实char类型也是分不同种类的,因为我们世界的语言分了非常多的种类,char类型一开始是只负责英语字符的所以char类型只有一个字节我们的ascll码表也只需要存储26个英文字符和其他的一些英文符号一共128个字符就好了;
可是当编程语言开始发展各个国家都开始学习编程那么每个国家有不同的语言,为了让编程更好的发展,就要设计不同的字符类型来装下别的国家的字符,就比如说我们中国,我们中国的汉字成千上万,一定是一个字节装不下的,所以我们需要更大的字符类型来装下我们的汉字字符;所以就有了如下图这样的:
图片是一个c++文档里面包含了我们C++库的参考
这是它的网址 cplusplus.com
这样的不同的类型的字符串类的实例化
我们了解了string类也是basic_string类模板的一种实例化,有了这一概念后,我们就大致可以理解世界上的语言是怎么在计算机中存储的了;
接下来让我们进一步了解string类是如何对我们的char类型进行增删查改的;我们首先学习一下他的这些接口:
string的接口(部分)
下面这些接口可以参照我的代码实现来理解如果理解不了可以先到cplusplus.com/reference/string/string/,这个官网上查看权威一些的解释,这样配合理解才是最好的
1.成员函数:
上图中这些成员一般都是在我们的类实例化对象时就会自动调用的,他们的功能就是给我们的类中的数据进行初始化,如果类对象被销毁它里面的数据也会通过析构函数自动调用;通过下面的模拟实现我们也可以明白我们是如何操作的;
下面是我对这些函数的模拟实现
class string
{
public:
string(const char* str = "")//构造函数
:_size(strlen(str))//将我们的字符串长度赋值给size
{
if (_size == 0)
{
_capacity = 3;//赋初值
}
else
{
_capacity = _size;
}
char* tmp = new char[_capacity + 1];
assert(tmp);
_str = tmp;
strcpy(_str, str);
}
string(const string& a)//拷贝构造函数
:_size(a._size),
_capacity(a._capacity)
{
char* tmp = new char[_capacity + 1];
assert(tmp);
_str = tmp;
strcpy(_str, a._str);
}
~string()//析构函数
{
_capacity = 0;
_size = 0;
delete[]_str;
}
string& operator =(const string&a)//赋值运算符重载
{
char* tmp = new char[_capacity + 1];
assert(tmp);
_str = tmp;
strcpy(_str, a._str);
_size = a._size;
_capacity = a._capacity;
return *this;
}
private:
char* _str;//存储字符串数据
size_t _capacity;//类中可存储字符串的容量不够就扩容
size_t _size;//类中字符串的字符个数
}
2.容量
下面是我们对上面接口的函数实现
//获取我们string对象中的数据接口
const char*c_str()const//C_str接口
{
return _str;
}
const size_t size()const//size接口
{
return _size;
}
const size_t capactiy()const//capacity接口
{
return _capacity;
}
void reserve(size_t n)//扩容
{
if (_capacity < n)
{
char* tmp = new char[n+1];//这里需要注意开空间要开n+1个最后放我们的‘/0’字符
memcpy(tmp, _str,_size+1);
delete[]_str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char num = '\0')//初始化空间为num
{
if (n<_size)//当n小于_szie时会发生截断
{
_size = n;
_str[n] = '\0';
}
else//当n大于_size时会初始化n后面到_size的部分
{
if (n > _capacity)//空间不够则扩容
{
reserve(n);
}
for (int i = _size; i < n; i++)
{
_str[i] = num;
}
_str[n] = '\0';
_size = n;
_capacity = n;
}
}
void clear()//清理
{
_size = 0;
_str[0] = '\0';
}
3.数据访问
模拟实现:
//[]操作符
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos < _size);
return _str[pos];
}
4.迭代器
迭代器是用来遍历的它是容器中的通用遍历方式因为在我们之后的学习中遇到链表树那些结构无法使用for那种方式遍历所以发明出来了迭代器这种通用的遍历方式,使得我们所有的容器都可以遍历;
模拟实现:
//迭代器
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;
}
5.增删查改
模拟实现:
//插入——增
string& insert(size_t pos,char ch)//增一个字符
{
assert(pos <= _size);
if (_capacity < _size + 1)
{
reserve(_capacity * 2);
}
/*for (int end = _size-1; end > pos; end--)
{
_str[end + 1] = _str[end];
}
_str[pos + 1] = _str[pos];*/
//下面是优化一下的挪动
for (int end = _size+1; end > pos; end--)
{
_str[end] = _str[end - 1];
}
_str[pos] = ch;
++_size;//别忘了_size要变大;
return *this;
}
string& insert(size_t pos, const char* str)//增一段字符串
{
assert(pos <= _size);
int len = strlen(str);
if (_capacity < _size + len)
{
reserve(_size +len);
}
for (int end = _size +len; end > pos+len-1; end--)
{
_str[end] = _str[end - len];
}
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
void push_back(char ch)//尾插一个字符
{
insert(_size, ch);
}
void append(const char* str)//尾插一段字符
{
insert(_size, str);
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
//删除--删
string& erase(size_t pos=0,size_t len= -1)
{
assert(pos < _size);
if (pos+len < _size)
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
else if (pos + len >= _size||len==-1)
{
_str[pos] = '\0';
_size = pos;
}
return *this;
}
//查找对应字符——查
size_t find(char ch, size_t pos=0)// 查一个字符
{
assert(pos < _size);
for (int i = pos; i < _size; i++)
{
if (ch == _str[i])
{
return i;
}
}
return -1;
}
size_t find(const char* str, size_t pos = 0)//查找一个字符串
{
assert(pos < _size);
char* p = strstr(_str + pos, str);//其实不应该使用这个函数,因为当我们的_str中有'/0'字符时就无法找到‘/0’后面的匹配字符串了
if (p == nullptr)
{
return -1;
}
else
{
return p - _str;
}
}
6.流提取和流插入
模拟实现:
istream& operator>>(istream& in,string& a)
{
char buf[200] = { '\0' };
int i = 0;
a.clear();
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buf[i++] = ch;
ch = in.get();
if (i == 199)
{
buf[i] = '\0';
i = 0;
a += buf;
}
}
if (i != 0)
{
buf[i] = '\0';
a += buf;
}
return in;
}
ostream& operator<<(ostream& out, const string& a)
{
for (int i = 0; i < a.size(); i++)
{
out << a[i];
}
return out;
}
7.运算符重载
模拟实现:
bool operator>(const string& a)const
{
return (strcmp(_str, a.c_str())>0);
}
bool operator<(const string& a)const
{
return (strcmp(_str, a.c_str()) < 0);
}
bool operator>=(const string& a)const
{
return (strcmp(_str, a.c_str()) > 0)|| (strcmp(_str, a.c_str()) == 0);
}
bool operator<=(const string& a)const
{
return (strcmp(_str, a.c_str()) < 0)|| (strcmp(_str, a.c_str()) == 0);
}
bool operator==(const string& a, const string&b)
{
return (strcmp(b.c_str(), a.c_str()) == 0);
}
string类的接口繁多冗余,其实很多地方都重复了,我们只要了解了上面的这些接口对于字符串的处理问题就已经足够了;
注意
在不同的编译器下对于string的底层实现是不同的但是处理的结果是一样的,就像我们刚刚自己的模拟实现一样,它并不是很好的实现,但是它也实现了我们的字符串的增删查改;实现不同,但接口相同,这样也保证了我们代码的可移植性;
如需我的完整的string类的模拟实现代码可以去我的码云上查看: