本文大致分为两个方面来为小伙伴们介绍C++中的string类,分别是string中常用接口的使用与模拟实现。文章若有不足之处,欢迎大家给出指正!
首先为什么要学习string这一类呢?
在C语言中,大家都知道字符串是一以\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留深可能还会越界访问。因此也有了管理字符的string这一类的出现。
接下来,先给大家看下string类的文本介绍(字数过多,可直接看总结)
1. 字符串是表示字符序列的类
2. 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作 单字节字符字符串的设计特性。
3. string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信 息,请参阅basic_string)。
4. string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_traits 和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
5. 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个 类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
1.string是表示字符串的字符类串
2.该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作
3.string再底层实际是:basic_string模版类的别名,typedef basic_string string;
4.不能操作多字节或者变长字符的序列
注意:在使用string类时,必须包含#include头文件以及using namespace std;
(construct)函数名称 | 功能说明 |
构造空的string对象类对象,即空字符串 | |
用C-string来构造string类对象 | |
string(size_t n.char c) | string类对象中包含n个字符c |
拷贝构造函数 |
void text_string()
{
string str1; //无参的构造函数
string str2("hello world"); //带参的构造函数
string str3(str2); //拷贝构造函数
}
函数名称 | 功能说明 |
返回字符串有效字符长度 | |
length() | 返回字符串有效字符长度 |
capacity() | 返回空间总大小 |
检测字符串释放为空串,是返回true,否则返回false | |
clear() | 清空有效字符 |
reserve() | 为字符串预留空间 |
resize() | 将有效字符的个数改成n个,多出的空间用字符c填充 |
注意:
1. size()与length()方法底层实现原理完全相同,引入size()的原因是为了与其他容器的接口保持一 致,一般情况下基本都是用size()。
2. clear()只是将string中有效字符清空,不改变底层空间大小。
3. resize(size_t n) 与 resize(size_t n, char c)都是将字符串中有效字符个数改变到n个,不同的是当字 符个数增多时:resize(n)用0来填充多出的元素空间,resize(size_t n, char c)用字符c来填充多出的 元素空间。注意:resize在改变元素个数时,如果是将元素个数增多,可能会改变底层容量的大 小,如果是将元素个数减少,底层空间总大小不变。
4. reserve(size_t res_arg=0):为string预留空间,不改变有效元素个数,当reserve的参数小于 string的底层空间总大小时,reserver不会改变容量大小。
函数名称 | 功能说明 |
operator[] | 返回pos位置的字符 |
begin()+end() | 获得首字符/尾字符的迭代器 |
rbegin()+rend() | 获取首字符/尾字符的迭代器【反向】 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
void text_string1()
{
//遍历string的三种方法
string s1("hello world");
//方法1->operator[]
for (size_t i = 0; i < s1.size(); ++i)
{
cout << s1[i];
}
cout << endl;
//方法二->迭代器
auto it = s1.begin();
while (it != s1.end())
{
cout << *it;
++it;
}
cout << endl;
//方法三->范围for
for (auto& it : s1)
{
cout << it;
}
cout << endl;
}
函数名称 | 功能说明 |
push_back() | 在字符串后尾差字符c |
append() | 在字符串后追加一个字符串 |
operator+=() | 在字符串后追加字符串str |
c_str() | 返回C格式字符串 |
find() | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind() | 从字符串pos位置开始往前找字符c,返回该字符在字符串中位置 |
substr() | 在str中从pos位置开始,截取n个字符,然后将其返回 |
注意:
1. 在string尾部追加字符时,s.push_back(c) / s.append(1, c) / s += 'c'三种的实现方式差不多,一般 情况下string类的+=操作用的比较多,+=操作不仅可以连接单个字符,还可以连接字符串。
2. 对string操作时,如果能够大概预估到放多少字符,可以先通过reserve把空间预留好。
函数 | 功能说明 |
operator+ | 尽量少用,因为传值返回,导致深拷贝效率低 |
operator>> | 输入运算符重载 |
operator<< | 输出运算符重载 |
getline | 获取一行字符串 |
relational operator | 大小比较 |
(1)成员变量:对于一般的string类,它的成员变量有三个,分别是指向存储字符的数组的指针,string的有效大小,string的容量
(2)构造函数:一般来说,我们会实现三种构造函数,分别是无参的构造函数,带参的构造函数以及拷贝构造函数,但是对于无参的构造函数我们可以将其与带参的构造函数合并,即在带参的构造函数加上缺省值,即可实现合二为一的效果
具体代码如下:
namespace mine
{
class string
{
public:
//构造函数
string(const char* str="")
{
//开空间
_size = strlen(str);
_capacity = _size;
_arr = new char[_capacity+1];
//拷贝数据
memcpy(_arr, str,_size+1);
}
string(const string& str)
{
_size = str._size;
_capacity = str._capacity;
_arr = new char[_capacity + 1];
memcpy(_arr, str._arr,str._size+1);
}
private:
//成员变量
char* _arr;
size_t _size;
size_t _capacity;
}
}
在能够创立一个基本的string对象后,那我们该如何去拿到这个对象的数据呢,这就需要迭代器了。对于不同的容器,虽然它们都配有迭代器,在使用的方法上也大致相同。但是各自迭代器的底层实现却不一定相同。
由于我们要通过迭代器实现拿取string类中的数据并且能够修改它,所以这个迭代器应该是能够前后移动并能够访问到string的数据,那么我们很自然而然就想到char *的指针是不是就能达到这样的一个效果,所以对于string来说,指针是天然的迭代器。指针++,--就能到达下一个字符或者上一个字符,解引用就能访问到该字符,如果不是const对象,就还能对其进行修改。想到这里,string迭代器的实现就差不多了。
在实现完普通对象和const对象迭代器后,接下来就是与迭代器密切相关的begin()和end()两个接口,通过这两个接口我们就能够拿到string对象第一个位置和结束位置的迭代器了。对于前者,我们直接返回字符数组的指针就可以了,而后者则是字符数组的指针加上对象中有效字符的个数,即_size,就可以得到结束位置的迭代器。
具体实现如下:
//迭代器定义
typedef char* iterator; //普通对象
typedef const char* const_iterator; //const对象
iterator begin()
{
return _arr;
}
iterator end()
{
return _arr + _size;
}
const_iterator begin() const
{
return _arr;
}
const_iterator end() const
{
return _arr + _size;
}
在创健完一个string对象后,那么我们该如何往里面添加数据呢?这就定然离不开下面这几个接口。
(1)push_back()——尾插一个字符
(2)append()——尾插一个字符串
(3)insert()——任意位置插入一个字符/一个字符串
(4)reserve()——扩容
上面所述的前三个接口就是string类常用的三个插入数据的接口了,他们插入的逻辑也大同小异。首先,在插入数据前我们都应该检查当前对象的一个容量是否足够,如果不够就需要调用reserve函数进行扩容,可以扩容到刚刚好的空间也可以是二倍扩容,根据实际需要书写即可。其次就是数据的插入,最后就是成员变量_size的改变,当然别忘记在插入有效数据后,还要在末尾加上‘\0’奥。
具体实现如下:
//增
//扩容
void reserve(size_t capacity)
{
if (capacity > _capacity)
{
char* tmp=new char[capacity + 1];
memcpy(tmp, _arr,_size+1);
delete[] _arr;
_arr = tmp;
_capacity = capacity;
}
}
//尾插一个字符
void push_back(char ch)
{
//检查容量
if (_size == _capacity)
{
//扩容
reserve(_capacity = 0 ? 4 : 2 * _capacity);
}
//插入数据
_arr[_size] = ch;
_size++;
_arr[_size] = '\0';
}
//尾插一个字符串
void append(const char* str)
{
//检查容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容
reserve(_size + len);
}
//插入数据
for (size_t i = 0; i < len; i++)
{
_arr[_size++] = str[i];
}
_arr[_size] = '\0';
}
//任意位置插入一个字符
void insert(size_t pos, size_t n, char ch)
{
///判断位置合法性
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动数据
int end = _size+n;
int left = _size;
while (end >=pos+n)
{
_arr[end] = _arr[left];
--end;
--left;
}
//插入数据
for (size_t i = 0; i < n; ++i)
{
_arr[pos] = ch;
++pos;
}
_size += n;
}
//任意位置插入一个字符串
void insert(size_t pos, const char* str)
{
//检查容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
int end = _size + len;
int left = _size;
while (end >= pos + len)
{
_arr[end] = _arr[left];
--end;
--left;
}
//插入数据
for (size_t i = 0; i < len; ++i)
{
_arr[pos] = str[i];
++pos;
}
_size += len;
}
当我们实现完数据的插入后,如果我们遇到不想要的数据,是不是还要删除数据呀,接下来就是erase()接口的实现啦!
对于erase()这一接口,首先它有两个形参,分别是指定删除的位置pos,和删除的数据长度len,如果没有指定删除的长度,那么该函数就会将pos位置及以后得数据全部删除。在了解完形参之后,那么实现的思路是什么样呢?第一步就是检查pos位置的合法性,避免pos过大导致越界访问。第二步就是删除数据,在删除这一步我们会面临两种情况,一种是将pos位置及以后位置的数据全部删除,另一则是删除pos及其以后的一部分数据。对于前者我们只需要将‘\0’放于pos位置并将_size修改即可。而后者则需要先将指定的数据删除后,在将后面的数据向前移动。
具体实现代码如下:
//删
void erase(size_t pos,size_t len=npos)
{
assert(pos < _size);
if (pos + len > _size || len==npos)
{
_arr[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos; i <= _size-len; ++i)
{
_arr[i] = _arr[i + len ];
}
_size = pos;
}
}
当一个string对象放在我们的面前时,如果我们想要去查找数据的时候呢,就需要用到find()这一接口啦!
如果是查找一个字符,我们就只需要遍历这个string对象就可以了;如果是查找一个字符串,我们就需要将string对象与所给字符串进行匹配,可以用到strstr()这个函数来帮我们实现这效果。
具体代码如下:
//查找
size_t find(char ch,size_t pos=0)
{
assert(pos < _size);
for(size_t i = pos; i < _size;i++)
{
if (_arr[i] == ch) return i;
}
return npos;
}
size_t find(const char* str, size_t pos=0)
{
assert(pos < _size);
const char* ptr = strstr(_arr + pos, str);
if (ptr)
{
return ptr - _arr;
}
else
{
return npos;
}
}
有时候当我们会遇到两个string对象进行大小的比较的场景。在了解过C语言字符串的比较逻辑的小伙伴就会知道两个字符串的比较并不是比较两个的长短,而是从头遍历字符,根据ascll码值的大小来判断两个字符串的大小,只有的当两个字符串的长度相等,并且其中每个相应位置字符对应的ascll码值相等才能说明两个字符串相等。
那么string类其实比较的逻辑与两个字符串的比较逻辑相似,不过由于string对象毕竟不是字符串,而是管理字符的类,因此string的比较的结束标志并不是'\0',而是比到_size位置截止,因此我们就需要用memcmp函数来进行string的比较而不是strcmp,这是与以往字符串比较的一个比较大的不同点。
具体实现如下:
bool operator<(const string& str)const
{
int ret = memcmp(_arr, str._arr, _size < str._size ? _size : str._size);
return ret == 0 ? _size < str._size : ret < 0;
}
bool operator==(const string& str)const
{
return (_size == str._size && memcmp(_arr, str._arr, _size < str._size ?
_size : str._size)==0);
}
bool operator>(const string& str)const
{
return !(*this < str || *this == str);
}
bool operator>=(const string& str)const
{
return !(*this < str );
}
bool operator<=(const string& str)const
{
return !(*this > str);
}
由于string类是自定义类型,那么流插入来说就无法识别这一类型,也就是编译器不知道该如何处理stirng对象。同样的流提取也面临这一情况。因此我们就不得不对流插入和流提取进行运算符重载。
具体代码如下:
ostream& operator<<(ostream& out, const string& str)
{
for (auto ch : str)
{
cout << ch;
}
return out;
}
istream& operator>>(istream& in, string& str)
{
//清空数据
str.clear();
//处理缓冲前面的空格或者换行
char ch = in.get();
while (ch== ' ' || ch == '\n')
{
ch = in.get();
}
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 128)
{
buff[i] = '\0';
str += buff;
i = 0;
}
ch = in.get();
}
while (i > 0)
{
buff[i] = '\0';
str += buff;
return in;
}
return in;
}
除了上面的接口以外,博主还实现了其他的接口,就不一一而述啦,大家可以通过下面整个的string类来观看,相信以小伙伴们的水平应该不难理解。
#include <assert.h>
//string
namespace mine
{
class string
{
public:
//迭代器重定义
typedef char* iterator;
typedef const char* const_iterator;
//构造函数
string(const char* str="")
{
//开空间
_size = strlen(str);
_capacity = _size;
_arr = new char[_capacity+1];
//拷贝数据
memcpy(_arr, str,_size+1);
}
string(const string& str)
{
_size = str._size;
_capacity = str._capacity;
_arr = new char[_capacity + 1];
memcpy(_arr, str._arr,str._size+1);
}
//提供下标访问
char& operator[](size_t pos)
{
//检查pos合法性
assert(pos < _size);
return _arr[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _arr[pos];
}
//提供C接口
const char* c_str() const
{
return _arr;
}
//提供存储数据个数
size_t size() const
{
return _size;
}
//迭代器
iterator begin()
{
return _arr;
}
iterator end()
{
return _arr + _size;
}
const_iterator begin() const
{
return _arr;
}
const_iterator end() const
{
return _arr + _size;
}
//增
void reserve(size_t capacity)
{
if (capacity > _capacity)
{
char* tmp=new char[capacity + 1];
memcpy(tmp, _arr,_size+1);
delete[] _arr;
_arr = tmp;
_capacity = capacity;
}
}
void push_back(char ch)
{
//检查容量
if (_size == _capacity)
{
//扩容
reserve(_capacity = 0 ? 4 : 2 * _capacity);
}
//插入数据
_arr[_size] = ch;
_size++;
_arr[_size] = '\0';
}
void append(const char* str)
{
//检查容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
//扩容
reserve(_size + len);
}
//插入数据
for (size_t i = 0; i < len; i++)
{
_arr[_size++] = str[i];
}
_arr[_size] = '\0';
}
string& operator +=(char ch)
{
push_back(ch);
return *this;
}
string& operator +=(const char* str)
{
append(str);
return *this;
}
//void insert(size_t pos, size_t n,char ch)
//{
// ///判断位置合法性
// if (_size + n > _capacity)
// {
// reserve(_size + n);
// }
// //挪动数据
// size_t end = _size;
// while (end >= pos && end!=npos)
// {
// _arr[end + n] = _arr[end];
// --end;
// }
// //插入数据
// for (size_t i = 0; i < n; ++i)
// {
// _arr[pos] = ch;
// ++pos;
// }
// _size += n;
//
//}
//void insert(size_t pos, size_t n, char ch)
//{
// ///判断位置合法性
// if (_size + n > _capacity)
// {
// reserve(_size + n);
// }
// //挪动数据
// int end = _size;
// while (end >= (int)pos && end != npos)
// {
// _arr[end + n] = _arr[end];
// --end;
// }
// //插入数据
// for (size_t i = 0; i < n; ++i)
// {
// _arr[pos] = ch;
// ++pos;
// }
// _size += n;
//}
void insert(size_t pos, size_t n, char ch)
{
///判断位置合法性
if (_size + n > _capacity)
{
reserve(_size + n);
}
//挪动数据
int end = _size+n;
int left = _size;
while (end >=pos+n)
{
_arr[end] = _arr[left];
--end;
--left;
}
//插入数据
for (size_t i = 0; i < n; ++i)
{
_arr[pos] = ch;
++pos;
}
_size += n;
}
void insert(size_t pos, const char* str)
{
//检查容量
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
int end = _size + len;
int left = _size;
while (end >= pos + len)
{
_arr[end] = _arr[left];
--end;
--left;
}
//插入数据
for (size_t i = 0; i < len; ++i)
{
_arr[pos] = str[i];
++pos;
}
_size += len;
}
//删
void erase(size_t pos,size_t len=npos)
{
assert(pos < _size);
if (pos + len > _size || len==npos)
{
_arr[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos; i <= _size-len; ++i)
{
_arr[i] = _arr[i + len ];
}
_size = pos;
}
}
//查找
size_t find(char ch,size_t pos=0)
{
assert(pos < _size);
for(size_t i = pos; i < _size;i++)
{
if (_arr[i] == ch) return i;
}
return npos;
}
size_t find(const char* str, size_t pos=0)
{
assert(pos < _size);
const char* ptr = strstr(_arr + pos, str);
if (ptr)
{
return ptr - _arr;
}
else
{
return npos;
}
}
string substr(size_t pos=0 ,size_t len=npos)
{
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)
{
n= _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i <pos+n; i++)
{
tmp += _arr[i];
}
return tmp;
}
void resize(size_t n, char ch = '\0')
{
//n<=size
if (n <= _size)
{
_arr[n] = '\0';
_size = n;
}
//n>size
else
{
//扩容
reserve(n);
//填数据
for (size_t i = _size; i < n; ++i)
{
_arr[i] = ch;
}
_arr[n] = '\0';
_size = n;
}
}
//版本1
/* bool operator<(const string& str)const
{
size_t pos1 = 0;
size_t pos2 = 0;
while (pos1 < _size && pos2 < str._size)
{
if (_arr[pos1] < str._arr[pos2])
{
return true;
}
if (_arr[pos1] > str._arr[pos2])
{
return false;
}
pos1++;
pos2++;
}
return _size < str._size ? true : false;
}*/
/*bool operator==(const string& str)const
{
size_t pos1 = 0;
size_t pos2 = 0;
while (pos1 < _size && pos2 < str._size)
{
if (_arr[pos1] == str._arr[pos2])
{
return true;
}
else
return false;
pos1++;
pos2++;
}
return _size == str._size ? true : false;
}*/
//版本2
bool operator<(const string& str)const
{
int ret = memcmp(_arr, str._arr, _size < str._size ? _size : str._size);
return ret == 0 ? _size < str._size : ret < 0;
}
bool operator==(const string& str)const
{
return (_size == str._size && memcmp(_arr, str._arr, _size < str._size ? _size : str._size)==0);
}
bool operator>(const string& str)const
{
return !(*this < str || *this == str);
}
bool operator>=(const string& str)const
{
return !(*this < str );
}
bool operator<=(const string& str)const
{
return !(*this > str);
}
void swap(string str)
{
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
std::swap(_arr, str._arr);
}
//赋值
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
void clear()
{
_arr[0] = '\0';
_size = 0;
}
//析构函数
~string()
{
delete[] _arr;
_arr = nullptr;
_size = _capacity = 0;
}
private:
//成员变量
char* _arr;
size_t _size;
size_t _capacity;
public:
static size_t npos;
};
size_t string::npos = -1;
ostream& operator<<(ostream& out, const string& str)
{
for (auto ch : str)
{
cout << ch;
}
return out;
}
istream& operator>>(istream& in, string& str)
{
//清空数据
str.clear();
//处理缓冲前面的空格或者换行
char ch = in.get();
while (ch== ' ' || ch == '\n')
{
ch = in.get();
}
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 128)
{
buff[i] = '\0';
str += buff;
i = 0;
}
ch = in.get();
}
while (i > 0)
{
buff[i] = '\0';
str += buff;
return in;
}
return in;
}
}
四、结语
到此为止,关于string对象的讲解就告一段落了,至于其他的内容,小伙伴们敬请期待呀!
关注我 _麦麦_分享更多干货:
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下期见!
注:为string中重要函数
因篇幅问题不能全部显示,请点此查看更多更全内容