目录
前言
常见接口模拟实现
默认成员函数
1.构造函数
2.析构函数
3.拷贝构造函数
4.赋值运算符重载
迭代器
简单接口
1.size()
2.c_str()
3.clear()
操作符、运算符重载
1.操作符[]
2.运算符==
3.运算符>
扩容接口
1.reserve()
2.resize()
增删查改接口
1.push_back()
2.append()
3.运算符+=
4.insert()
5.erase()
6.find()
7.substr()
流插入&流提取
后记
我们知道,熟练使用STL是c++程序员的必备技能之一,今天我们来了解STL中的string类,即字符串类型,与c语言中的字符串类似,c++中只是对此做了封装,以及一些接口,基础的知识点这里不再赘述,全部接口功能包括但不限于默认成员函数、运算符操作符重载、reserve、push、pop、insert等等,更多接口介绍参考cplusplus.com/reference/string/string/ ,下面介绍一些常见或者主要接口的实现。
如下图可知,库中string构造函数很多,适合接受不同参数列表的初始化方式,我们实现一种参数列表全缺省的构造函数,可以满足构造基本要求:
①传c类字符串就是用此字符串给类型初始化;
②不传就是用空字符串初始化对象,
注意:初始化时_size和_capacity都是初始化为字符串的长度,而new申请空间时多申请一个用来存放'\0'。
代码:
String(const char* str = "")
{
_size = strlen(str); //初始化列表有依赖关系时建议不用初始化列表,在大括号内初始化
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
析构函数在库中只有下面一种,无参数无返回值,符合特性,我们知道,此类涉及申请内存,所以在析构函数中一定要释放内存,将变量置0,代码如下:
代码:
~String()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
我们知道,拷贝构造函数是构造函数的一种,与普通构造函数写法类型,这是一种传统写法,代码如下,但是,这里讲一种现代写法,也是一种的复用的方法,就是定义一个临时对象,调用其自己的构造函数初始化,之后将初始化的成员交换给目标对象,而临时对象会自动调用析构函数释放,完美地将目标对象初始化完成。
代码:
//现代写法(老板思维)
String(const String& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
String tmp(s._str);
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
//传统写法
//String(const String& s)
//{
// _size = s._size;
// _capacity = _size;
// _str = new char[_capacity + 1];
// strcpy(_str, s._str);
//}
库中的赋值运算重载如图,这里实现第一种,传统写法就是赋值_sieze、_capacity之后开空间传元素,考虑还用一下现代写法,去定一个临时对象调用拷贝构造初始化,再将其成员交换给目标对象,目标对象完美完成其初始化。
注意:考虑自己给自己赋值的情况
代码:
//现代写法(老板思维)
String& operator=(const String& s)
{
if (&s != this)
{
String tmp(s);
/*delete[] _str;
_str = nullptr;*/ //无需在此释放,交换给tmp后,出了作用域自动调用析构清理
swap(_str, tmp._str);
swap(_size, tmp._size);
swap(_capacity, tmp._capacity);
}
return *this;
}
//传统写法
//String& operator=(const String& s)
//{
// if (&s == this) //注意:自己给自己赋值的情况
// return *this;
// _size = s._size;
// _capacity = _size;
// /*delete[] _str; //先释放的话万一new失败了就导致释放但未开辟空间成功
// _str = new char[_capacity + 1];*/
//
// char* tmp = new char[_capacity + 1]; //所以先开空间,开成功了再释放原空间
// delete[] _str;
// _str = tmp;
// strcpy(_str, s._str);
// return *this;
//}
迭代器有四种,这里实现两种以及其const型迭代器,string的迭代器就是原生指针(其他类型不一定),所以实现很容易,代码如下。
代码:
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;
}
size()就是获取字符串字符个数,很简单不过多赘述,设置成const成员函数,是因为可以接受普通对象,也可以接受const对象。
代码:
size_t size() const
{
return _size;
}
c_str()是获取c类型字符串,有一些场合可能需要传递指针型字符串,但是其是私有成员不能直接访问,此接口可以实现访问。
代码:
char* c_str() const
{
return _str;
}
clear()是清除字符串中字符,使其成为空串。
代码:
void clear()
{
_str[0] = '\0';
_size = 0;
}
操作符[]重载是很重要的重载,实现字符串对象可以像数组一样使用[]加下标访问,代码实现也很简单,注意普通对象和const对象权限不同,需要分开写。
代码:
char& operator[](size_t pos) //普通对象,[]访问元素可读可写
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const //const对象,[]访问元素只可读
{
assert(pos < _size);
return _str[pos];
}
看过笔者上篇文章(http://t.csdn.cn/xT4nq)可以知道,先重载==、>,其他关系运算符可以复用它们方便的实现出来,不多赘述,不理解的可以翻翻看。
代码:
bool operator==(const String& s) const
{
return strcmp(_str, s._str) == 0;
}
参考==运算符介绍。
代码:
bool operator>(const String& s) const
{
return strcmp(_str, s._str) > 0;
}
reverse()是预留空间,但不初始化,即只改变_capacity,传进想要预留的字符个数n,比_capacity大就会释放空间重新申请并拷贝元素。
注意:为string申请空间一定要多申请一个空间放'\0'
代码:
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
resize()也是预留空间,并且初始化,即改变_capacity和_size,不传需要初始化的字符则默认是'\0',实现过程比reserve()多考虑一个赋值情况即可。
代码:
void resize(size_t n, char ch = '\0')
{
if (n > _size)
{
reserve(n + 1);
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
else
{
_str[n] = '\0';
_size = n; //只需要改变size,不需要改变capacity
}
}
push_back()是在字符串结尾插入指定字符,首先检查是否需要扩容,需要扩容则要看是不是刚初始化的空字符串,空字符串默认先给4个地址空间,不是空字符串则扩容空间至二倍。
注意:
①莫忘_size加一;
②莫忘加'\0'在结尾。
代码:
void push_back(char ch)
{
if (_size == _capacity)
reserve(_capacity ? _capacity * 2 : 4);
_str[_size] = ch;
_str[_size + 1] = '\0';
_size++;
}
append()是在字符串尾部插入一个字符串,实现逻辑很简单,可以直接复用push_back,但避免重复向操作系统申请空间,提前预留好空间用来插入字符,代码如下。
代码:
void append(const char* str)
{
size_t i = 0;
size_t n = strlen(str);
if (_size + n > _capacity)
reserve(_size + n); //要尽量少的重复申请空间
for (i = 0; i < n; i++)
{
push_back(str[i]);
}
}
+=运算符重载则是在字符串尾部既可以插入字符,也可以插入字符串,调用上面俩函数即可。
代码:
String& operator+=(char ch)
{
push_back(ch);
return *this;
}
String& operator+=(const char* str)
{
append(str);
return *this;
}
上面的插入操作只能作用在字符串尾部,而insert()函数是指定位置插入字符或者字符串,极端位置可以复用上面函数,普通位置需要移动元素,经历过c语言的学习,实现此功能并不难。
注意:代码中的标记点处是向后移动元素,注意移动过程中对下标的控制,不要越界访问。
代码:
String& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
reserve(_capacity ? _capacity * 2 : 4);
if (pos == _size)
push_back(ch);
else
{
size_t end = _size + 1;
while (end > pos) //标记点
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
return *this;
}
String& insert(size_t pos, const char* str)
{
size_t i = 0;
size_t n = strlen(str);
if (_size + n > _capacity)
reserve(_size + n);
if (pos == _size)
append(str);
else
{
size_t end = _size + n;
while (end >= pos + n) //标记点
{
_str[end] = _str[end - n];
end--;
}
for (i = 0; i < n; i++)
{
_str[i + pos] = str[i];
}
_size += n;
}
return *this;
}
erase()则是删除指定位置指定长度的字符串,比插入的实现要简单,其中可以调用strcpy函数,npos是size_t类型的最大值,作为长度缺省值,实现不指定长度时默认是删除至结尾,注意删除完以后莫忘改变_size变量,其他并无难度。
代码:
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (_size - pos <= len)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
find()是查找指定字符或字符串第一次出现的位置下标,找不到返回npos,即无符号整型最大值,查找字符从头向后遍历查找即可,查找字符串调用strstr函数,注意此函数找到了是返回字符串地址,返回空是没找到,而find()是需要返回下标,注意控制。
代码:
size_t find(char ch, size_t pos = 0) const
{
assert(pos < _size);
size_t i = 0;
for (i = pos; i < _size; i++)
{
if (_str[i] == ch)
return i;
}
return npos;
}
size_t find(const char* substr, size_t pos = 0) const
{
assert(substr);
assert(pos < _size);
char* ptr = strstr(_str + pos, substr);
if (ptr == nullptr)
return npos;
return ptr - _str;
}
substr()是提取指定位置指定长度的子串,形成返回string类对象,复用+=运算符简化了实现逻辑,大体实现逻辑与其他函数并无不同,不多赘述。
代码:
String substr(size_t pos, size_t len = npos)
{
assert(pos < _size);
String tmp;
if (_size - pos <= len)
{
tmp += (_str + pos);
}
else
{
for (size_t i = 0; i < len; i++)
{
tmp += _str[pos + i];
}
}
return tmp;
}
在上一篇文章Date类型的简单实现中,使用友元的方式将流插入、流提取编写成了全局函数,因为当时是因为需要访问私有成员变量,那么这里我们发现,可以不需要访问私有成员变量,所以这里不在需要成为友元。
对于流插入,定义一个临时数组,大小固定为32个元素的地址空间,循环从键盘获取字符插入,当临时数组满了之后,就整体插入到对象中,当获取到空格或者回车就停止,符合流插入的特性。
注意:
①临时数组没满时就遇到空格或回车的情况;
②这里使用istream类的get函数获取字符,不仅可以接收到普通字符也可以收到空格和回车,用以判断,而不直接使用cin<<,是因为cin输入过程中遇到空格或回车就停止了,接收不到它们。
代码:
istream& operator>>(istream& in, String& s) //无需成为友元函数,因为不需要访问私有成员
{
s.clear();
const size_t N = 32;
char tmp[N];
size_t i = 0;
char ch;
ch = in.get();
while (ch != ' ' && ch != '\n')
{
tmp[i++] = ch;
if (i == N - 1)
{
tmp[i] = '\0';
s += tmp;
i = 0;
}
ch = in.get();
}
tmp[i] = '\0';
s += tmp;
return in;
}
ostream& operator<<(ostream& out, const String& s) //无需成为友元函数,因为不需要访问私有成员
{
for (size_t i = 0; i < s.size(); i++)
{
out << s[i];
}
return out;
}
今天介绍了一些string常见常用接口的使用及底层实现,希望对于以上接口的使用可以熟练掌握,对于不常见的可以了解,知道有这样一个函数,在后面需要使用的时候,也可以查阅文档即时了解一下(cplusplus.com/reference/string/string/),对于以上接口的使用或者实现不懂的地方,欢迎可以在评论区评论或者私我,一起学习进步哦!