嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!
我的博客:yuanManGan
目录
本章来模拟实现一下string类,不是按照模板实现,而是按照容易理解的实现。
string的成员变量
我们的string类本质还是字符数组,但我们可以动态开辟,用_str字符指针来指向数组,我们还需要知道数组的空间大小,以及有效字符个数,跟之前实现的顺序表有点类似,但这里用类来实现。
我们将string放在一个命名空间里面,以防和库里的冲突。
namespace refrain
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
成员变量
c_str和size( ),capacity( )
这里为了方便打印,我们先实现这个返回c类型的字符串,就是j将_str返回,随便也实现另外俩个成员变量的返回
我们将声明与定义分离,写在不同的文件里。
const char* string::c_str() const
{
return _str;
}
size_t string::capacity() const
{
return _capacity;
}
size_t string::size() const
{
return _size;
}
默认成员函数:
string的默认构造
无参构造:
_size 和_capacity好处理,都是0但_str应该初始化为什么,是空指针还是什么,不如看看库里面是怎么实现的?
库里面是'\0'那我们就按照它的来实现吧!那就意味着我们一开始就得开一个'\0'的空间。但我们的capacity和size不要记录这个'\0'的空间。
string()
:_str(new char[1]{'\0'})
,_size(0)
,_capacity(0)
{ }
带参构造:
我们带参构造就将传入的参数直接拷贝过去就好了。
string(const char* str)
:_str(new char[strlen(str) + 1])
,_size(strlen(str))
,_capacity(strlen(str))
{
memcpy(_str, str, _size + 1);
}
看看这种写法,用了三次strlen时间成本大大提高了,我们可不可以在初始化列表先将_size初始化,然后复用_size呢?
我们试试:
这里为什么_str没有创建空间呢?我们回忆一下初始化列表是按照怎么顺序,对是按照变量声明的顺序,我们先声明的_str,但此时_size还未初始化,_size的值看编译器实现,这里vs将_size初始化为了0,所以只有一个空间。
那有同学就要说了,那我们将声明顺序改一下能不能实现呢,我们试试。
ok了,但这样真的好吗,你这不是自己给自己埋雷吗,万一别人不知道,在这里乱改一下,那怎么办?
我们可以考虑只在初始化列表初始化_size让_str和_capacity走初始化函数。
依旧ok。
还有个问题,我们可不可以给缺省值,就不用写默认无参构造了?
最终版本:
string(const char* str = "")
:_size(strlen(str))
{
_str = new char[_size + 1];
_capacity = _size;
memcpy(_str, str, _size + 1);
}
string的析构函数:
这个就简单了,但要判断一下,如果_str为空就不能析构
~string()
{
if (_str)
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
}
string 的拷贝构造:
//传统写法
string::string(const string& s)
{
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s._size + 1);
_size = s._size;
_capacity = s._capacity;
}
赋值运算符重载:
// s1 = s2
string& string::operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
尾插相关操作
string 的reserve(扩容)
扩容是一个会频繁调用的操作,所以我们先来实现一下这个操作。
这是库里实现的。
void reserve(size_t n);
先在string.h写个声明,在string.cpp里实现这个函数。我们要将容量扩容到n,如果n>capacity,就直接新创建一块空间然后拷贝,有人说不能用relloc吗,我的建议是不要使用,因为relloc扩容扩的空间大了,也是重新创建一块空间进行扩容,然后拷贝。
那如果n <capacity呢,我们看编译器,可能缩容,但一般不缩容,我们就不实现这个了,缩容是典型的以时间换空间的案例。
void string::reserve(size_t n)
{
if (n > _capacity)
{
//注意这里是n+1给'\0'留一点空间
char* tmp = new char[n + 1];
//要判断_str是否为nullptr对空指针解引用要报错
if (_str)
{
memcpy(tmp, _str, _size + 1);
delete[] _str;
}
_str = tmp;
_capacity = n;
}
}
string的push_back
加入函数得先判断一下是否需要扩容,当_size == _capacity 时需要扩容。然后将最后一个字符改成要加入的值,再将_size++最后将最后一个位置弄成'\0'
void string::push_back(char ch)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
string的append(追加字符串)
这里的扩容逻辑就得考虑一下了,如果我们插入的字符串的长度是len,如果len + _size > _capaticy时才会扩容,我们是扩二倍,还是len + _size 呢,如果给多少扩多少时,我们会面临一个问题:就是如果我们频繁扩小字符串,就会频繁扩容;如果我们扩二倍,如果我们扩容的字符串很大,len + _size > 2 * _capacity就出现了一个很严重的问题,我存的值不见了,就好比你去银行存了几百万,结果一查就省几十万了,谁还敢存钱在你们银行。
我们这里就得分类讨论一下了,如果len+_size > 2*capacity,我们就扩容到len +_size,没有就扩到2倍。
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newcapacity);
}
memcpy(_str + _size, str, len + 1);
_size += len;
}
string重载运算符+=
实现了push_back和append实现+=运算符就易如反掌了,只需要复用加重载就完成了。
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
char& string::operator[](size_t i)
{
return _str[i];
}
const char& string::operator[](size_t i) const
{
return _str[i];
}
我们再来实现一下string的遍历吧
string的遍历:
重载[ ]:
这个实现很简单。直接返回*(_size + i);
char& string::operator[](size_t i)
{
assert(i < _size);
return _str[i];
}
const char& string::operator[](size_t i) const
{
assert(i < _size);
return _str[i];
}
迭代器:
这里实现迭代器就使用原生指针了,但底层实现不一定是原生指针,可能是其他的主要看编译器想怎么实现。
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}
string::const_iterator string::begin() const
{
return _str;
}
string::const_iterator string::end() const
{
return _str + _size;
}
范围for:
实现了迭代器就实现了范围for,范围for的实质就是替换为迭代器。
string 在任意位置插入删除
insert
insert有很多个版本,我们就实现其中比较实用的两个吧,第二个就实现插入一个吧
在pos位置插入一个字符:
就将pos位置之后的字符全部向后挪一步,然后将pos位置改成插入的值。
void string::insert(size_t pos, char ch)
{
assert(pos < _size);
//判断是否需要扩容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + 1;
while (pos < end)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size++;
}
在任意位置插入字符串
void string::insert(size_t pos, const char* str)
{
assert(pos < _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t newcapacity = _size + len > 2 * _capacity ? _size + len : 2 * _capacity;
reserve(newcapacity);
}
size_t end = _size + len;
while (end >= pos + len)
{
_str[end] = _str[end - len];
end--;
}
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = str[i];
}
_size += len;
}
erase
任意位置删除n个字符
这里得分类讨论一下,如果删的字符个数过多就等于把后面全删了,这种情况比较好处理,当我们第二个参数不传时,默认删完,那我们该咋实现呢?对给缺省值给你npos我们得先定义一下npos,定义成const静态成员变量
private:
size_t _size;
size_t _capacity;
char* _str;
static const size_t npos = -1;
在c++这里可以这样给缺省值哦。
erase函数就成这样了。
void erase(size_t pos, size_t len = npos);
注意声明和定义不能同时给缺省值。
当删不完的时候
我们可以使用c语言的库函数memmove来移动数据(也是懒得实现了)
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
//删完
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
memmove(_str + pos, _str + pos + len, _size - (pos + len) + 1);
_size -= len;
}
}
string中的查找和裁剪
find:
我们也是简单实现这两个哦
最后一个好实现直接遍历一遍即可:
size_t string::find(char ch, size_t pos) const
{
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch) return i;
}
return npos;
}
从第pos位置开始查找字符串sub返回最先的找到的下标
这里我们直接调用库函数里面的strstr来查找。
size_t string::find(const char* sub, size_t pos) const
{
assert(pos < _size);
const char* p = strstr(_str + pos, sub);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
substr:
创建一个string类型的ret,直接+=
string string::substr(size_t pos, size_t len)const
{
assert(pos < _size);
if (len > _size - pos)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0; i < len; i++)
{
ret += _str[pos + i];
}
return ret;
}
补充拷贝构造和赋值运算符重载的现代写法:
swap
我们先来实现一下swap函数,有人就要问了,库里面不是有swap函数吗
看看库里面的swap
template <class T> void swap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
看看这里是创建了一个c对象拷贝a对象,然后再赋值交换,要付出的代价有点太大了 。我们在string这个类中仅仅需要交换一下指针和_size 和_capacity就行了。
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
注意这里的函数里面swap函数必须要制定std空间,不然会认为自己调用自己导致无限递归。
但我们学习C++的有两种人,一种是只了解怎么使用string的,一种是像我们这样深入学习string库,了解底层原理,他们并不知道那种更高效,为了避免这种情况发生,我们编译器会自动调用string库里面的swap函数,无论你是下面那种代码:
swap(s1, s2);
s1.swap(s2);
然后我们来实现一下构造函数:
我们可以将传入的对象先默认构造一份然后交换给this
拷贝构造
string::string(const string& s)
{
string tmp(s_str);
swap(tmp);
}
但当我们实现以下操作时得到的不是我们想要的答案
void test_string01()
{
string s1("hello world");
s1 += '\0';
s1 += "xxxxxx";
string s2(s1);
cout << s1 << endl;
cout << s2 << endl;
}
为什么打印不了后面的呢?
问题出在了我们进入拷贝构造后,要将目标字符串默认构造一份,此时的默认构造除了问题,其中计算_size时只会计数到'\0',会导致出现问题。
那我们咋解决呢?
在string中可以用迭代区间构造,需要使用模版,这里为什么要使用模版呢?有人说直接用string里面的迭代器不就好了。我们不只是可以使用string的迭代器,还可以用其他容器的迭代器。
迭代区间构造
template <class InputIterator>
string(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
我们将拷贝构造改成这样就ok了。
string::string(const string& s)
{
string tmp(s.begin(),s.end());
swap(tmp);
}
重载赋值运算符
赋值运算符也是同样的思路
string& string::operator=(const string& s)
{
string tmp(s.begin(), s.end());
swap(s);
return *this;
}
还有一种更简单的写法
string& string::operator=(string tmp)
{
swap(tmp);
return *this;
}
我们这里自己传值传参,传值传参调用构造函数, 然后直接交换,返回*this,出作用于,tmp直接销毁。
流插入流提取操作符的重载
cout
要将该重载定义在string类外。
这个实现就很简单直接打印就行
ostream& operator<<(ostream& os,const string& s)
{
for (auto& ch : s)
{
os << ch;
}
return os;
}
看这种情况,我们打印s1的c_str( )时出现了我们不想要的结果,这是为什么呢,c_str()返回的是c类型的字符串,而c类型的字符串它以'\0'为结尾,只要发现的'\0'就返回。
cin
我们先来简单实现一个我们都爱犯的错误的代码
istream& operator>>(istream& is, string& s)
{
char ch; is >> ch;
s += ch;
while (ch != '\0' && ch != '\n')
{
is >> ch;
s += ch;
}
return is;
}
我们发现为什么一直得不到结果呢?因为我们的流输入操作,以空格或者换行为间隔,读取下一个,输入流(如键盘、文件)不会直接读取到 '\0'
('\0'
是字符串的结束符,不是输入字符)。
那我们该怎么解决呢?
c++io流中里面有一个get函数用来读取单个字符
istream& operator>>(istream& is, string& s)
{
char ch; is.get(ch);
s += ch;
while (ch != '\0' && ch != '\n')
{
is.get(ch);
s += ch;
}
return is;
}
这里还存在一些问题就是,要把之前的数据清除掉。
这又得写个clear函数了
简单实现一下。
clear
void string::clear()
{
_size = 0;
_str[_size] = '\0';
}
这里就不实现缩容了,没必要。
istream& operator>>(istream& is, string& s)
{
s.clear();
char ch; is.get(ch);
s += ch;
while (ch != '\0' && ch != '\n')
{
is.get(ch);
s += ch;
}
return is;
}
最后一个小问题,我们如果频繁输入小的数据,就又会频繁扩容的问题出现,那又该怎么解决了,我们都不知道我们要输入多少的字符,也不能提前扩容。
我们可以实现一个内存池,比如开个255空间大小的内存池,当输入的小于255时就放在内存池中。
实现如下:
istream& operator>>(istream& is, string& s)
{
s.clear();
char buff[256];
size_t i = 0;
char ch = is.get();
while (ch != '\0' && ch != '\n')
{
buff[i++] = ch;
ch = is.get();
if (i == 255)
{
buff[i] = '\0';
s += buff;
i = 0;
}
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return is;
}
over!感谢观看!