创作初心:在加深个人对知识系统理解的同时希望可以帮助到更多需要的同学
😄柯一梦的专栏系列
🚀柯一梦的Gitee主页
🛠️柯一梦主页详情
座右铭:心向深耕,不问阶序;汗沃其根,花自满枝。
今天我们来自主复现一个String类:
1.命名的注意事项
因为我们要自己实现一个名叫String的类,可能和库里面的String出现命名冲突,所以我们一般使用命名空间。但是测试文件,函数实现文件在声明、使用函数的时候,只能包含两个域:一个是命名空间域,一个是类域......这未免有些太麻烦了,我们最好在测试文件和函数实现文件中都包含一个命名空间!!!(多个文件可以共用一个命名空间,编译器最后会把他们合在一起)。
注意:我们为什么会命名冲突呢?
我们可以包含string的头文件,并且展开std(因为string类里面的函数实现都在std里),但是我们在自己写string的时候,就会和标准库里面的string混淆。所以就要在多个文件里面使用同一个命名空间把我们自己写的string包起来。我们在命名空间里面使用我们自己写的string的时候,一些东西比如<<,它会先在我们的命名空间里面匹配合适的,如果找不到合适的,就在命名空间外的std里面找
2.构造函数
**细节1:**成员变量我写的是char* _str,我在写构造函数的时候,接收的参数是const char* str,我想把这个参数拷贝给_str,我不能单纯地把参数的地址赋值给_str(这样的话就是浅拷贝,会相互污染,而且),而是要开辟一个新的字符数组,然后把字符数组的地址传给他,然后再把内容拷贝一下。
还有一个细节:在拷贝的时候我们可能会有两个不同的选择,strcpy和memcpy。但是这两个有什么区别呢?
- char* strcpy(char* dest, const char* src);strcpy的处理对象一般是以/0结尾的字符串,所以他的终止条件非常单一,就是遇到/0。而且strcpy会从dest的起始位置开始覆盖,直到复制完src的'\0'为止。
- void* memcpy(void* dest, const void* src, size_t n);memcpy的处理对象可以是字符串,也可以是结构体、数组。但是memcpy可以传入参数n,来控制拷贝的src的长短,并且是严格执行,并不会因为/0就停止。
- 值得一提的是,dest是指针,也就是说可以从一串字符串或者数组的中间开始
- 有的同学还会问:我们为什么让_str直接指向str呢?因为我们的目的是把str里面的东西拷贝给_str。因为str指向的地方是一个常量串,我们就没办法修改了。
**细节2:**在解决完_str的赋值问题以后,我们再来解决一个小问题:
复制代码
string::string(const char* str)
:_str(new char[strlen(str)+1])
,_size(strlen(str))
,_capacity(strlen(str))
{
strcpy(_str, str);
}
const char* string::c_str() const
{
return _str;
}
strlen和sizeof有区别:sizeof是在编译的时候运行的,strlen是在运行的时候调用的。所以在这里我们相当于调用了三次函数。有什么解决办法吗?
- 有的同学会说用赋值好的_size去赋值_str和_capacity:我们要牢记,c++的编译器在初始化自定义类型的时候,是按照声明顺序去初始化的,所以我们必须把_size的位置调到前面去,但是这样一来代码的耦合度就会很高......这不是我们想要的。
- 我们可以利用编译器先走初始化列表这个功能,先把_size赋值了,然后再在函数体里面对其余的两个成员变量初始化。
细节3: 无参的构造函数和带参的构造函数可以合并,怎么搞呢?我们可以把带参数的构造函数给带上一个缺省值,但是这个缺省值我们给的也很讲究不能给空指针,因为空指针的话strlen会去访问,导致程序出错,所以最好的解决方法就是给'\0'。但是这句话里面有一个错误,因为我没传入的参数是一个const char*,是一个字符串地址,如果我们单单给一个'\0'编译器会报错,因为这不是一个字符串。
3.析构函数、size函数、c_str函数、[]运算符重载、iterator的实现
析构函数、size函数、c_str函数都很简单就是直接返回类里面的东西
复制代码
string::~string()
{
delete[]_str;
_str = nullptr;//?
_size = _capacity = 0;
}
size_t string::size()
{
return _size;
}
const char* string::c_str() const
{
return _str;
}
\]运算符重载我们要写两个,一个是不可修改内容的(不需要使用const修改this指针,和引用返回值),一个是可修改内容的(前面的反过来)
const char& string::operator[](size_t i) const
{
assert(i >= 0 && i < _size);
return _str[i];//返回的是什么类型,主要是看函数的头,而不是说你堂而皇之的写一个 &_str[i],这是不对的
}
char& string::operator[](size_t i)
{
assert(i >= 0 && i < _size);
return _str[i];
}
* iterator的实现也特别简单,我们的string的底层是一个字符数组,所以iterator也就被简化成了一个字符指针,原生指针只是string的一种实现方式。因为iterator是一个类型,所以我们需要重命名一个类型,也就是typedef char\* iterator。此外,在我们实现了iterator之后呢,for(auto ch:s1)这个遍历也可以使用了,因为遍历for的底层就是去调用迭代器。
* 因为有const修饰的string对象,所以我们就不能再使用简单的iterator了,我们需要用const_iterator,同时参数传递的时候this指针也要用const修饰。
string::iterator string::begin()
{
return _str;
}
string::iterator string::end()
{
return _str + _size;
}

## 4.三个函数push_back、append、operator+=、reserve
### 4.1push_back与reserve的实现
在数据结构中,只要是涉及了插入相关的知识的时候,我们首先要做的就是检查内存是否还够用。再检查的时候分两种情况:1、_capacity = 0,也就是一个字符都还没有插入的时候,这个时候我们不能盲目的使用_capacity\*2。2、有插入的时候,这个时候我们就可以使用_capacity\*2了,在使用完_capacity\*2以后呢,我们得到了一个新的内存大小,这个时候我们怎么进行扩容呢?不仅是吧_capacity的数值改变一下,更重要的是_str所指向的空间......这个时候我们需要封装一个函数,叫作reserve!!!
我们先来实现一下reserve
void string::reserve(size_t n)
{
if (n > _capacity)//保证他至少不缩容
{
char* tmp = new char[n+1];//为什么我们要+1呢?我们期望存n个有效字符,但是'\0'也是要存里面的
memcpy(tmp, _str, _size+1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
有三个小细节:
1. 我们在开空间的时候,要先用一个新指针指向开辟的空间
2. 所开的空间应该比传过来的参数n多一个位置
3. 我们在使用memcpy的时候,我们要拷贝的字符个数应该是实际字符数+1,因为要把\\0拷贝进去
4. 首元素地址+数组里面的实际元素个数='\\0'的位置
push_back的实现
void string::push_back(const char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
几个小细节:
1. 我们确定要开辟的空间的时候,要先考虑_capacity是否为0,因为如果为0的话,2\*_capacity就没有意义了
2. 我们在尾插的时候,数组名+_size就表示\\0的位置
3. _size记得++
4. 字符数组的最后记得放入'\\0'
### 4.2append的实现
append是尾插一个字符串,所以我们接收的参数就是const char\* ch(以防是一个常量字符串)
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)//因为括号里面的_size,len,_capacity都不包含'\0',所以运算的时候都不需要+1
{
size_t newcapacity = _capacity * 2 > (_size + len) ? _capacity * 2 : (_size + len);
reserve(newcapacity);//这里为什么传的是newcapacity而不是newcapacity+1呢?因为我们在实现reserve的时候已经考虑了\0,开辟空间的时候已经多开一个了
}
memcpy(_str + _size, str, len + 1);
_size+=len;
}
几个小细节:
1. 我们还是要确定capacity的大小,如果_capacity\<_size+len,那么比较_capacity\*2与其的大小关系,如果是小的字符串,就可以直接扩二倍,如果是大的,就只开那么大就好了
2. memcpy的时候,要拷贝len+1个值,因为要把\\0拷进去
3. _size记得变更数值
### 4.3operator+=的实现
operator+=既可以添加字符串,也可以添加一个字符。其实底层就是一个对push_back和append的封装
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
唯一值得注意的就是他的返回值了,应该是对象本身的引用
## 5.流输入运算符重载
流输出运算符重载有三种方式:
> 1.
>
> ostream& operator<<(ostream& out, const string& s)
> {
> out< return out;
> }
>
> 2.
>
> ostream& operator<<(ostream& out, const string& s)
> {
> for (auto ch : s)
> {
> out << ch;
> }
> return out;
> }
>
> 3.
>
> ostream& operator<<(ostream& out, const string& s)
> {
> for (size_t i = 0;i < s.size();i++)
> {
> out << s[i];
> }
> return out;
> }
几个小细节:
1. 第一种输出方式有一个陷阱,就是如果字符串里面有'\\0'那么就会中途终止,因为c_str是把string对象转换成const char\*来进行输出。
2. 我们应该使用迭代器或者\[\]字符索引的方式来进行输出
3. 有的人在写reserve的时候使用的是strcpy,这个函数我们之前已经说过是遇到'\\0'就停止,当内存不够的时候,我们就会扩容,在赋值原字符串到新字符串的时候,如果原字符串中间有'\\0'就会停止,所以中间的那一段内存就空出来了,会导致乱码,出现烫烫烫......或者屯屯屯......
4. append的时候可以使用strcpy,因为常量字符串中间不会有'\\0'
## 6.insert和erase的实现
### 6.1insert的实现极为恶心!!!!!超级多小细节
我们先实现单个字符的插入
void string::insert(char ch,size_t pos)
{
assert(pos <= _size);//因为size_t不会为负数,所以我们在判断的时候,可以取巧
//判断这一段是否扩容我们直接调用push_back的逻辑
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
//在挪动数据的时候我们要从后往前挪
size_t end = _size+1;//把'\0'的位置给end
while (end > pos)
{
_str[end] = _str[end-1];//这一段极其重要,我们把'\0'的位置给了end,我们要把\0带着一起往后挪动
--end;
}
_str[pos] = ch;
++_size;
}
直接点出小细节:
1. 我们在判断pos是否越界的时候,结合他的类型是size_t,所以只需要让其小于_size即可
2. 从普通逻辑来讲,移动方式决定了判别方式,也决定了赋值方式:也就是说end+1 = end的话end就赋值为'\\0'的位置,其次判别方式也就顺理成章的变成了end\>=pos(但是其中有一个巨大的坑,如果是头插的话,将无法判别end和pos的大小,这是由他们的类型决定的);所以这一块我们只能end\>pos,那么逻辑也就只有一种就是end赋值为'\\0'后面的那个字符位置,这样一来就不会出现越界
3. 接上一个话题,有的人说在第一种方法下,我们把end改为int不就好了吗?end在变成0的时候-1=-1,这样就可以-1\ _capacity)//因为括号里面的_size,len,_capacity都不包含'\0',所以运算的时候都不需要+1
{
size_t newcapacity = _capacity * 2 > (_size + len) ? _capacity * 2 : (_size + len);
reserve(newcapacity);//这里为什么传的是newcapacity而不是newcapacity+1呢?因为我们在实现reserve的时候已经考虑了\0,开辟空间的时候已经多开一个了
}
size_t end = _size + 1;
while (end > pos)
{
_str[end - 1 + len] = _str[end - 1];
--end;
}
memcpy(_str + pos, str, len);
_size += len;
}
### 6.2erase的实现也很麻烦,非常的绕
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);//要删除的数据大于后面的字符
if (len == npos||len>=(_size-pos))//前闭后开相减就是个数
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t i = pos+len;
memmove(_str + pos, _str + i, _size - i + 1);
_size -= pos;
}
}
实现过程中的一些小细节:
1. 首先肯定是检查pos的越界问题
2. 我们要先分成两类,一个是全删的,一个不是全删的,无论是全删还是删一部分,我们的逻辑都是1、改变_size的大小 2、将_str的_size的位置的元素赋值为'\\0'
3. 如果是删除中间一段的字符串,那么我们就要画图找逻辑,并且避免比较时候由于类型是size_t而产生越界无法比较的问题
4. 我们在删完一部分以后,我们还要把后面的元素移到前面...在这里我们就可以直接使用memmove(这个函数可以自动调整读取的顺序,即使复制区域重叠,也可以完成复制)
## 7.find、substr、拷贝构造函数
### 7.1find的实现:
先看查找一个字符的:
size_t string::find(char ch, size_t pos)const
{
for (size_t i = pos;i < _size;i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;//注意这里的返回值是npos
}
没什么难的,主要是查找失败的时候返回npos
查找一个字符串:
size_t string::find(const char* str, size_t pos)const
{
const char* p1 = strstr(_str + pos, str);
if (p1 == nullptr)
{
return npos;
}
else//指针如何转化为下标?
{
return p1 - _str;
}
}
细节:
1. 我们可以直接使用strstr去查找字符串(这个是c语言里面带的暴力查找)
2. strstr的返回值是一个地址,如何把地址转化为下标呢?其实指针相减就是下标
### 7.2substr的实现:
string string::substr(size_t pos, size_t len)const
{
if (len == npos || pos + len >= _size)
{
len = _size - pos;
}
string ret;
ret.reserve(len);
for (size_t i = 0;i < len;i++)
{
ret += _str[pos + i];
}
return ret;
}
1. substr是返回含一部分的字符串的string;
2. 一开始的逻辑和erase的一样
3. 后面就再定义一个对象,既可以使用+=,也可以使用memcpy对他们进行赋值
4. 最后一个细节就是我们在返回的时候,这里涉及到构造函数,我们要先实现一个构造函数
### 7.3构造函数的实现:
我刚开始写构造函数的时候写出来了一个错误的:
string::string(const string& s)const
{
(*this).reserve(s._capacity);
memcpy(*this,s,_capacity+1);
}
这里面有一个致命的错误,因为(\*this)还没有被初始化,在reserve的函数体内呢,会出现访问_size的时候出现错误(因为是一个野指针)。所以我们必须使用new char\[\]的方式进行初始化
void string::reserve(size_t n)
{
if (n > _capacity)//保证他至少不缩容
{
char* tmp = new char[n+1];//为什么我们要+1呢?我们期望存n个有效字符,但是'\0'也是要存里面的
memcpy(tmp, _str, _size+1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
这个逻辑就是对的
string::string(const string& s)
{
(*this) = new char[s._size + 1];
memcpy(_str, s._str, s._capacity + 1);
_size = s._size;
_capacity = s._capacity;
}
## 8.逻辑运算符\>,=,\>=,\<,\<=,==,!=的实现
### 8.1\<的实现:
先看代码:
bool string::operator <(const string& s)const
{
size_t i1 = 0, i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] > s._str[i2])
{
return false;
}
else if(_str[i1] s._size;//只有这种情况是true,其余都是false
}
细节:
1. 我们在走循环的时候,需要定义两个变量,让他们去走各自的string
2. 这样一来我们就可以在后续的判断大小的时候使用一个取巧的方式
3. 这个取巧的方式就是看i1 i2和各自size的位置关系
### 8.2==的实现
bool string::operator ==(const string& s)const
{
size_t i1 = 0, i2 = 0;
while (i1 < _size && i2 < s._size)
{
if (_str[i1] != s._str[i2])
{
return false;
}
else
{
i1++;
i2++;
}
}
//到这里也是三种情况
//1.hello与hello 2.hellox与hello 3.hello与hellox
return i1 == _size && i2 == s._size;
}
## 9.cin、clear、getline的实现:
### 9.1cin的实现:
我们先看一串没有纠错之前的代码:
istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
}
这串代码的逻辑是我在控制台输入一串代码,中间可以有空格,这个串就被存在缓冲区里面,in\>\>ch就会一直从缓冲区里面读取字符。但是这串代码没考虑到的是空格不会进入ch,不会去比较。而且这段代码是一段死循环,不会返回。因为istream\>\>会一直跳过。
修改以后的代码:
istream& operator>>(istream& in, string& s)
{
char ch = in.get();
//in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
//in >> ch;
ch = in.get();
}
return in;
}
如果string对象之前有值,在cin以后会连带着之前的一起输出,所以我们要再写一个函数clear
clear非常好实现,不需要销毁空间,只需要把0位置置成'\\0',再把size置成0
### 9.2clear的实现:
void string::clear()
{
_str[_size] = '\0';
_size = 0;
}
### 9.3getline的实现:(getline的功能就是把一行里面的东西全部输入string对象里面)
istream& string::getline(istream& in, string& s, char delim)
{
s.clear();
char ch = in.get();
//in >> ch;
while (ch != delim)
{
s += ch;
//in >> ch;
ch = in.get();
}
return in;
}
getline和cin的实现都有缺陷,因为我们只能+=一些字符,所以如果是一个长串的话就会\*2 \*2 \*2......扩容很多次
下面是修改以后的代码:我们使用一个buff作为缓冲
istream& operator>>(istream& in, string& s)
{
s.clear();
char buff[128];
int i = 0;
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
istream& getline(istream& in, string& s, char delim)
{
s.clear();
char buff[128];
int i = 0;
char ch = in.get();
while (ch != delim)
{
buff[i++] = ch;
if (i == 127)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}