string常用接口及模拟实现
string相当于前面学的字符串,为了更方便操作,C标准库中提供了一些str系列的库函数。那么下面就来看看这些接口。
一、string模拟实现概括
namespace s
{
class string
{
public:
string(const char* str = "");
string(const string& s);
void swap(string& s);
~string();
string& operator=(const string& s);
///
size_t size() const;
size_t capacity() const;
bool empty() const;
void clear();
void reserve(size_t n);
void resize(size_t n, char c = '\0');
///
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
typedef char* iterator;
iterator begin();
iterator end();
typedef const char* const_iterator;
const_iterator begin() const;
const_iterator end() const;
const char* c_str() const;
size_t find(char c, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
string substr(size_t pos = 0, size_t len = npos);
///
void push_back(char c);
void append(const char* str);
string& operator+=(char c);
string& operator+=(const char* str);
string& insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static const int npos; //静态区变量在类内声明,类外定义
};
const int npos = -1; //类外定义时,不需要再写static关键词
namespace s
{
class string
{
public:
string(const char* str = "");
string(const string& s);
void swap(string& s);
~string();
string& operator=(const string& s);
///
size_t size() const;
size_t capacity() const;
bool empty() const;
void clear();
void reserve(size_t n);
void resize(size_t n, char c = '\0');
///
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
typedef char* iterator;
iterator begin();
iterator end();
typedef const char* const_iterator;
const_iterator begin() const;
const_iterator end() const;
const char* c_str() const;
size_t find(char c, size_t pos = 0) const;
size_t find(const char* s, size_t pos = 0) const;
string substr(size_t pos = 0, size_t len = npos);
///
void push_back(char c);
void append(const char* str);
string& operator+=(char c);
string& operator+=(const char* str);
string& insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
private:
char* _str;
size_t _size;
size_t _capacity;
public:
static const int npos; //静态区变量在类内声明,类外定义
};
const int npos = -1; //类外定义时,不需要再写static关键词
///
bool operator<(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& _cout, const string& s);
istream& operator>>(istream& _cin, string& s);
istream& getline(istream& _cin, string& s);
};
bool operator<(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& _cout, const string& s);
istream& operator>>(istream& _cin, string& s);
istream& getline(istream& _cin, string& s);
};
二、模拟实现函数接口
1、(constructor)函数名称
①.构造函数
//1.模拟实现string(const char* s)构造函数
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size;
//开空间要多开一个存放开空间的数量,后面delete有需求
_str = new char[_capacity + 1];
//开出空间之后,把字符串str拷贝给我们新创建的string
strcpy(_str,str);
}
②.拷贝构造函数
拷贝构造可以分为浅拷贝和深拷贝。我们这里写的是深拷贝,如果不写,系统自动调用的就是浅拷贝,也叫做值拷贝,即按字节进行拷贝。
虽然浅拷贝和深拷贝都拷贝出同样的值,但指向的空间不一样!
如果是浅拷贝,在调用析构函数就会出现问题。因为ptr1和ptr2都指向同一块空间,析构会导致同一块空间被释放两次。与此同时,改变一个指针就会影响到另一个指针。
//2.模拟实现string(const string&s)拷贝构造函数
string(const string& s)
{
//这里与前面不同,前面是个字符串还不是string。所以前面还得先去构造。
//在这里就可以直接访问string里面的私有变量了
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
strcpy(_str, s._str);
}
//那么这里,我们还可以对拷贝构造函数进行一次改进
string(const string& s)
{
//这里注意,我们是构造一个tmp,而不是拷贝一个tmp
//我们传入的参数是一个char*类型的,并不是string类型的,这里要加以区分
string tmp(s._str);
//再把tmp和调用该函数的对象进行调换就可以了
//但是这个swap得我们自己实现,因为在std库中swap函数形参没有两个string类型
//因此我们得自己实现一下swap函数,也很容易实现,交换对象中的成员就可以了
swap(tmp);
} //这就相当于让tmp作为一个傀儡调用构造来接收,再把接收到的数据跟调用对象交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
③.析构函数
//3.模拟实现~string 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
④.赋值运算符重载(以及swap的实现)
这里有个点要注意,大家可以看到代码中并不是直接把s传给_str,而是通过tmp进行的。因为:_str的空间如果小于s,将s拷贝过去就会导致越界。 如果_str的空间大于s,会导致空间浪费。
//4.模拟实现赋值运算符重载
string& operator=(const string& s)
{
//两个string无法直接传输,我们通过临时变量tmp来储存空间以及空间中的数据
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
//我们先释放掉原来的那块空间,以免造成内存泄漏,再把 tmp指向的空间给到_str
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
2、string类对象的容量操作
①.size
返回字符串中字符的个数
//由于这里不改变内部成员变量,建议加const
size_t size() const
{
return _size;
}
②.capacity
返回字符串的容量
//由于这里不改变内部成员变量,建议加const
size_t capacity() const
{
return _capacity;
}
③.empty
判断是否为空字符串
//由于这里不改变内部成员变量,建议加const
bool empty() const
{
if (_size == 0)
return true;
else
return false;
}
④.clear
clear用来清空有效字符
//clear()清理的是size,即有效字符长度,而capacity并没有变。
void clear()
{
_size = 0;
_str[0] = '\0'; //这里不要忘记给\0
}
⑤.reserve
reserve用于扩容
如果n比原来的空间要小,则不发生任何变化。n比原空间大时,才去扩容
void reserve(size_t n)
{
//如果n比原来的空间要小,则不发生任何变化。n比原空间大时,才去扩容
if(n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_size = _capacity = n;
_str[n] = '\0'; //不要忘记给\0
}
}
⑥.resize
用来改变字符串的size值。如果预改变值比原size小,则把size缩小。 如果比size大,则插入字符(可传参,否则默认斜杠0)进去,如果容量都小于预改变值,则会先进行扩容。
void resize(size_t n, char c = '\0')
{
if (n <= _size)
{
_str[n] = '\0';
_size = n; //这里是直接变成n,不是size-n,不要弄错了
}
else
{
reserve(n); //大于容量扩容,小于容量无影响
for (size_t i = _size; i < n; i++)
{
_str[i] = c; //将给定的参数c依次放到_str中
}
_str[n] = '\0'; //依然是记得要放\0在末尾
_size = n; //size也随之改变了,一定不要忘记了!!
}
}
3、string类对象的访问及遍历操作
①.operator[]
string字符串有些像我们的数组,是可以通过下标来访问元素的。
该操作符返回pos位置的字符
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//再写一个const类型的,以便const对象也可以使用
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
//这里不建议只写const的版本的,因为我们可能会对其进行访问修改
②迭代器
string类的迭代器是一个类似于指针的东西,但我们不能把迭代器都认作指针,我们比喻成指针可以方便我们理解!
typedef char* iterator; //这就是迭代器
// begin用来返回字符串的第一个位置
iterator begin()
{
return _str;
}
// end用来返回字符串最后一个位置的下一个位置,即'\0'
iterator end()
{
return _str + _size;
}
//与operator[]类似,我们还需要写一个const类型的函数以便使用
typedef const char* const_iterator; //迭代器
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//这里不建议只写const的版本的,因为我们可能会对其进行访问修改
③范围for
范围for的底层是依据迭代器来的
void test1_string()
{
s::string s1("hello world");
//迭代器的使用
s::string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it; //hello world
it++;
}
cout << endl;
//范围for:从s1的第一个字符开始往后遍历到最后一个字符,ch会自动往后走
for (auto& ch : s1) //加上引用相当于别名,还可以对ch进行修改
{
cout << ch; //hello world
}
cout << endl;
}
④c_str
c_str用于获取c类型的字符串,直接返回字符串即可
const char* c_str() const
{
return _str;
}
//这里建议加上const,写一个就够了,const和非const对象都可以使用
//因为这里只是提取这个字符串,并不会对其修改
⑤find
返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const
{
//要记得断言pos位置
assert(pos < _size);
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const
{
//要记得断言pos位置
assert(pos < _size);
const char* ch = strstr(_str + pos, s); //pos不要掉了
if (ch)
{
return ch - _str; //两指针相减就是间隔
}
return npos;
}
⑥substr
substr用来获取字符串的一段子串,从pos位置开始往后len个字符,注意是从pos位置开始往后数len个字符(强调)
如果len=npos,或者len > _size - pos,则从pos位置往后一直到整个字符串结尾
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
string sub;
if (len == npos || len > _size - pos)
{
for (int i = pos; i < _size; i++)
{
sub += _str[i];
}
}
else
{
for (int i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
4、string类对象的修改操作
①push_back
即顺序表里的尾插,这里为在字符串最后一个位置插入字符
void push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
//这里不能写成if-else,因为下面这几条语句是一定得执行的
_str[_size] = c;
_size++;
_str[_size] = '0'; //依旧不要忘记\0
}
//除此之外,我们也可以复用insert尾插入字符
insert(_size,c);
②append
append用来在字符串的末尾再追加一串字符串
void append(const char* str)
{
size_t len = strlen(str);
/*if (_capacity < _size + len)*/
//这条语句不宜写,
//因为字符串如果很长的话,可能会出现数据溢出的情况
if(_capacity-_size<len) //采用移项的写法
{
reserve(_size + len);
}
// strcpy可以把选定开始拷贝的位置,并且
//会把\0也给拷贝进来,
//因此在结尾处我们就不用再去给\0了。
// 这里也可以使用strcat,因为都是从末尾开始追加字符串,
//同样strcat也会把\0也给拷贝进来。
strcpy(_str + _size, str);
_size += len;
//这里结束之后就不用像之前一样给\0了
//这里也可以使用后文的insert追加字符串来完成
insert(_str, str);
}
③operator+=
该操作符用于在字符或字符串后追加字符或字符串
string& operator+=(char c)
{
push_back(c); //我们直接复用push_back就可以了
return *this;
}
string& operator+=(const char* str)
{
append(str); //我们直接复用append
return *this;
}
④insert
insert的作用是在指定的pos位置往后插入字符或字符串。
拷贝字符:
string& insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
size_t end = _size + 1; //这里还得包含\0的位置,因此+1
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
//这里要注意一个点,下面这个循环是走不通的!!
//假设pos==0,最后一次循环就是_str[1] = _str[0],end==0
//再之后end--变为-1,但我们可以看到pos是size_t类型的,
//属于无符号数,那么循环不会停止,会持续地进行,造成死循环
//解决方法一种是按上面写的方法来,还有一张就是强制转换,譬如下面优化后的版本
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
end--;
}
//优化后:
size_t end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
end--;
}
}
拷贝字符串:
string& insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_capacity - _size < len)
{
reserve(_size + len);
}
int end = _size + len;
while (end > pos + len - 1) //这里-1,是为了end能够顺利走到
{ //我们需要的位置,大家可以结合图理解
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len); //这里使用strncpy,是为了
_size += len; //不把str里的\0拷贝过去
}
⑤erase
erase用来从pos位置往后删除len个字符
如果len=npos或者pos+len>_size,意味着pos后面的字符都要被删除
否则就是包含在_size里面,我们就可以通过移动数据或者strcpy的方式完成
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || len > _size - pos)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
//_str[_size] = '\0'; 这里不需要写这一步,
//因为pos+len<_size,所以\0一直都还在
}
}
5、非成员函数
①关系运算符
关系运算符主要是指<,>,==,<=,>=,!=六种。
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 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);
}
②<<流插入操作符
之所以写这个操作符是因为内置类型可以直接cout,但是我们string不可以
ostream& operator<<(ostream& _cout, const string& s)
{
for (auto& ch : s)
{
_cout << s;
}
return _cout;
}
③>>流提取操作符
istream& operator>>(istream& _cin, string& s)
{
//每次调用都需要通过clear来清除原字符串的数据,这样往后才能插入新的数据
//否则就会导致新老数据叠在一起,达不到预期效果
s.clear();
char ch;
//_cin >> ch; //流提取不能获取空格或换行符
ch = _cin.get(); //所以我们采用get()函数
while (ch != ' ' && ch != '\n') //遇到空格或者换行就退出
{
s += ch;
ch = _cin.get();
}
return _cin;
}
这个写法有缺陷,如果插入数据很大就会导致多次扩容。但这里不建议使用reserve来给定内存,因为如果插入数据很小,就会导致空间浪费。 只要string对象不销毁,那么这些不使用的空间就一直存在,堆积多了就会严重浪费!
所以我们采用一种新方法:通过开辟一个128的数组来解决:
istream& operator>>(istream& _cin, string& s)
{
//每次调用都需要通过clear来清除原字符串的数据,这样往后
//才能插入新的数据,否则就会导致新老数据叠在一起,达不到预期效果
s.clear();
char ch;
//_cin >> ch; //流提取不能获取空格或换行符
ch = _cin.get(); //所以我们采用get()函数
char buff[128];
size_t i = 0;
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127) //这里相当于是buff数组的最后一个位置,
//如果到这个位置了,就把buff数据给到s,并把i重置为0
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = _cin.get();
}
if (i > 0) //这里是i++后,ch不符合条件退出来的,所以
//这个i就相当于是s最后一个位置的下一个位置了,直接插入\0
{
s += buff;
s[i] = '\0';
}
return _cin;
}
④getline
getline与流提取不同的是,getline是获取一行的字符。
也就是说我们可以在上面流提取的代码加以修改,即把遇到空格退出循环的条件去掉,则可以达到getline的效果!
istream& getline(istream& _cin, string& s)
{
s.clear();
char ch;
char buff[128];
ch = _cin.get();
size_t i = 0;
//[0,126] 共127个字符
//我们只需要把遇到空格退出循环的条件去掉就可以了
while (ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[127] = '\0';
s += buff;
i = 0;
}
ch = _cin.get();
}
if (i > 0)
s += buff;
return _cin;
}
制作不易,求点赞关注!❀❀