这里写目录标题
- 前言
- 准备工作
- 构造函数
- 析构函数
- 迭代器的实现
- 插入数据有关的函数实现
- reserve
- push_back
- operator+=
- append
- insert
- erase
- find
- resize
- [ ]
- clear
- >>
- >>
- 新式拷贝构造函数
- 新式赋值重载
前言
在前面的文章里我们学习了c++中string的用法,那么这篇文章我们将带着大家学习如何来模拟实现string这个类,当然我们这里的实现只是为了带着大家来更好的了解这个类,并不是完整的复刻一份相同的类出来,所以我们这里就只实现string类中的主要的常用的功能,那么在开始阅读下面内容之前,大家可以看看下面两篇文章来先了解了解string类的用法以:
string的介绍(上)
string的介绍(下)
准备工作
首先为了防止命名冲突,我得创建一个命名空间,在这个命名空间里面来实现我们的类,那这里我们就称这个空间为:YCF,然后在这个命名空间里面创建一个类,该类类名为string,那么上述的代码如下:
namespace YCF
{
class ycf
{
public:
private:
};
}
因为这个类处理的对象是字符串,要对字符串做增删查改的话,我们这里得创建三个变量来描述这里字符串的关键信息,那这三个变量就分别是char类型的指针 _str,记录字符串结尾的位置 _size,和记录当前对象容量的_capacity,我们将这三个成员变量放到private里面以防被使用者不经意间修改了,那么我们这里的代码就如下:
namespace YCF
{
class string
{
public:
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
构造函数
对于类来说构造函数是非常重要的,我们这里就先来实现用一个字符串来初始化一个类的情形,因为传参传过来的参数是一个字符串而字符串具有常性,所以我们这里在接收的时候就得用const char*类型的参数来进行接收
string(const char* _str)
{
}
因为传过来的内容是字符串,而这个字符串放的地方是常量区,该区域的内容是不能被改变的,所以我们这里就不能简单的将str的值复制成_str,而是得我们亲自开辟一个空间将字符串的内容拷贝到该空间里面去,然后再将size和capacity的值赋值为该字符串的长度,那么这里capacity的意思是能够容纳多少个有效字符,所以当我们申请空间的时候就得申请capacity+1个大小的空间而不是capacity个大小的空间,之所以这么做是为了方便我们后面的计算,那么这里我们的代码就如下:
string(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
那么这里为了方便我们查看这个对象中的字符串内容,我们就可以来实现一个c_str函数,通过这个函数来返回对象中的字符指针,再通过字符指针来打印字符串的内容,那么c_str函数的实现就非常的简单,直接在函数里面返回类中的str指针就行,但是这里的返回值大家要注意一下,我们得加个const以防止使用者通过c_str函数来修改对象中字符串的内容,并且我们还要防止this指针所导致的权限放大的问题所以得在括号的后面加上const,以用来修饰this执政,那么该函数的实现就如下:
const char* c_str() const
{
return _str;
}
有了c_str函数我们就可以来测试第一个构造函数写的是否是对的,测试的代码就如下:
#include"string.h"
using namespace std;
void test1()
{
YCF::string s1("hello world");
cout << s1.c_str() << endl;
}
int main()
{
test1();
return 0;
}
该代码的运行结果为:
这里成功的打印出来了初始化的内容,所以该函数的实现大致是真确的,为了确保完全正确我们就得观察另外两个变量的值,那么这里我们就可以根据c_str函数的原理再来实现size函数和capacity函数,其实现的代码如下:
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
测试代码的修改如下:
void test1()
{
YCF::string s1("hello world");
cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
cout <<"对象中字符串的长度为:" << s1.size() << endl;
cout <<"对象的容量为:"<< s1.capacity() << endl;
}
该代码的运行结果如下:
我们可以看到这里的有效字符确实是有11个,构造函数将_capacity的值复制为和_size的值一摸一样所以这里打印出来的两个变量的值都为11,那么这就说明我们的构造函数实现的没有问题,当然构造函数的类型肯定是不止一个的,我们这里可以写一个无参的构造函数,既然无参的话那么这个构造函数的结果就是对象中的字符串只有一个\0其他内容都没有了,所以在这个构造函数里面我们就只用给他申请1个字节的空间用来存放\0,然后将_size和_capaciy的值初始化为0就可以了,那么这里的代码就如下:
string()
:_size(0)
, _capacity(0)
{
_str = new char[1] {'\0'};
}
该构造函数的测试代码如下:
void test1()
{
YCF::string s1("hello world");
cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
cout <<"对象中字符串的长度为:" << s1.size() << endl;
cout <<"对象的容量为:"<< s1.capacity() << endl;
YCF::string s2;
cout << "对象中字符串的内容为:" << s2.c_str() << endl;
cout << "对象中字符串的长度为:" << s2.size() << endl;
cout << "对象的容量为:" << s2.capacity() << endl;
}
代码的运行结果为:
符合我们的预期,但是大家有没有发现一个可以简化的地方,无参构造函数的结果是_size和_capacity的值都等于0字符串中的内容也只有\0,而我们知道如果一个字符串中只有\0的话strlen的结果也是0,如果strlen的结果为0的话那么第一个构造函数里面的_capacity和_size不就也为0了嘛!如果源字符串的内容为空字符串的话那strcpy的结果不就也为空字符串了嘛?所以这里我们能不能用第一个构造函数来代替这个无参的构造函数呢?答案是可以的,既然不传参数也能调用第一种构造函数的话,我们这里就得给该函数添加一个缺省值上去,因为无参构造函数的内容为空字符串,所以我们这里的缺省值就可以给他一个空字符串,那么该函数的声名就是这样:string(const char* str="")
将第二个构造函数去掉,再运行上面的测试代码我们可以看到这里的运行结果是一样的,那么这就是我们构造函数的内容,希望大家可以理解。
析构函数
构造函数里面是通过new来申请的动态空间,那么我们析构函数要干的事情就是将申请的动态空间进行释放,前面我们学过c++释放动态空间的函数是delete,那这里我们就可以用该函数来达到目的,那么析构函数对应的实现就如下:
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
那这里我们测试该函数的方法就是通过调试来看当该对象的生命周期结束时,对象中的内容会发生什么样的变化?那么测试的代码就如下:
YCF::string* test2()
{
YCF::string s1("hello world");
cout << s1.c_str() << endl;
return &s1;
}
int main()
{
const YCF::string*p1= test2();
return 0;
}
我们开启调试,当编译器执行完构造函数之后对象中的内容变为:
然后我们就打印这个对象中的字符串,并将这个对象的地址作为test2函数的返回值返回main函数,在main函数里面我们就创建一个该类型的指针用来查看此对象经历析构函数之后的内容:
那么这里我们就可以看到该对象中的所有成员变量的内容全部都初始化为了0,所以这就说明析构函数的实现是正确的没有问题。
迭代器的实现
我们说迭代器是一个像指针的东西,他可能是指针也可能不是指针,但是在我们string类里面迭代器他就是通过指针来实现的,所以我们这里的iterator就是char*的typedef的命名重载:
typedef char* iterator;
跟迭代器有关的函数有begin和end,这两个函数的功能的就分别是返回字符串开头的位置和字符串结尾的位置,那么这两个函数就可以直接通过对象中的_size来实现,begin就是直接返回_str,而end就是返回_str+_size
返回的类型就是iterator,那代码的实现就如下:
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
我们可以通过下面的代码来进行一下测试:
void test3()
{
YCF::string s1("hello world");
YCF::string::iterator it1=s1.begin();
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
}
该代码的运行结果如下:
那么这就是迭代器的实现,看到这里我们顺便来聊聊范围for的实现,大家第一次看到这个东西的时候一定非常的好奇这个范围for是如何实现的?那么这里我们就可以通过下面的代码再来理解一下范围for:
void test3()
{
YCF::string s1("hello world");
YCF::string::iterator it1=s1.begin();
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
cout << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
}
我们首先是通过迭代器来将字符串进行循环打印,然后再用范围for再来实现循环打印,每打印一个字符时再加一个空格,那么该代码的运行结果如下:
那么这里大家有没有想过一个问题:我们类中没有实现范围for那他是如何来运行的呢?有人会说是c++帮我们实现的,可是如果是c++实现的范围的话,那他又是如何来识别自定义类型的呢?所以这里的范围for并不是别人帮我们实现的,而是我们自己通过迭代器来实现的,也就是说范围for只是一个虚伪的外表,真正运行的代码其实还是迭代器,那我们这里如何来证明呢?我们将这个类中的begin函数的名字改成Begin再来运行一下这个代码看看会发生什么,首先常规的迭代器是没有任何问题的:
YCF::string s1("hello world");
YCF::string::iterator it1=s1.Begin();
while (it1 != s1.end())
{
cout << *it1;
it1++;
}
cout << endl;
这段代码的运行结果也是正常的:
但是范围for的代码却不正常了:
YCF::string s1("hello world");
//YCF::string::iterator it1=s1.Begin();
//while (it1 != s1.end())
//{
// cout << *it1;
// it1++;
//}
//cout << endl;
for (auto ch : s1)
{
cout << ch << " ";
}
他报出来许多错误:
那这说明了什么?是不是就可以验证范围for的底层就是通过迭代器来实现的,当这段代码编译完之后范围for对应的代码就会替换成迭代器对应的代码而且这个替换他还是死的,一旦我们修改了begin函数或者其他有关的函数他就会报错,大家可以将这里替换与宏联系起来一起理解,那以上就是有关迭代器的实现希望大家能够理解。
插入数据有关的函数实现
reserve
既然要插入数据,那么这里我们就得先来实现一下扩容函数,首先这个函数接受的参数就是一个无符号整型:
void reserve(size_t n)
因为c++中没有像c语言一样有扩容函数realloc,所以我们这里采用的扩容策略就是再创建一个新的空间出来再将原来老空间中的内容复制到新空间里面去,然后释放原来的老空间将对象中的_str指针指向新的空间,最后再修改_capacity的值,那么下面就是对应代码的实现:
void reserve(size_t n)
{
char* tmp = new char[n + 1];//这里的加1是给\0留空间
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
但是这里并没有结束,学过之前的数据结构我们应该能够知道一件事就是:面对数据容量的变化我们采取的策略是只扩容不缩容,所以在执行上述函数体之前我们得先用一个if函数来进行判断,如果你给的n大于_capacity的值的话我们才执行上述代码,否则我们什么都不执行:
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];//这里的加1是给\0留空间
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
那么这就是reserve函数的实现,希望大家能够理解。
push_back
sting库中的push_back每次调用只能尾插一个字符,所以该函数就只有一个参数用来接收你要插入的字符:
void push_back(char ch)
那么在执行这个尾插函数之前我们得先来做一个判断:当对象中的容量不够用的时候调用reserve函数来进行扩容然后再将数据插入,这里的扩容我们就扩成原来容量的两倍:
void push_back(char ch)
{
if (_capacity == _size)
{
reserve(ch * 2);
}
}
然后我们再来实现数据的插入,把_size看作下标他指向的是字符串的结尾也就是\0,那么这里就可以直接将下标为_size的元素修改成我们要尾插的元素,然后让_size的值+1再让下标为_size的元素赋值为\0以免字符串没有结束的标志,那么这里我们的代码就如下:
void push_back(char ch)
{
if (_capacity == _size)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
该函数实现完之后,我们就可以写一段来测试这里函数实现的正确性:
void test1()
{
YCF::string s1("hello world");
cout <<"对象中字符串的内容为:" << s1.c_str() << endl;
cout <<"对象中字符串的长度为:" << s1.size() << endl;
cout <<"对象的容量为:"<< s1.capacity() << endl;
YCF::string s2;
cout << "对象中字符串的内容为:" << s2.c_str() << endl;
cout << "对象中字符串的长度为:" << s2.size() << endl;
cout << "对象的容量为:" << s2.capacity() << endl;
}
这段代码的运行结果为:
我们可以看到字符串的结尾确实增加了两个字符,而且字符串的长度和容量也发生了改变,长度变成了13容量变成了22,但是这能够说明该函数的实现是完全正确的吗?我们再来看看下面的测试代码:
void test4()
{
YCF::string s1("hello world");
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
s1.push_back('x');
s1.push_back('x');
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
YCF::string s2;
cout << "字符串的内容为:" << s2.c_str() << endl;
cout << "字符串的长度为:" << s2.size() << endl;
cout << "字符串的容量为:" << s2.capacity() << endl;
s2.push_back('x');
s2.push_back('x');
s2.push_back('x');
cout << "字符串的内容为:" << s2.c_str() << endl;
cout << "字符串的长度为:" << s2.size() << endl;
cout << "字符串的容量为:" << s2.capacity() << endl;
}
我们将代码运行起来就可以发现这里报错了:
这里报错的原因就是因为我们拿空字符串来初始化对象,这个对象中的_capacity等于0,而扩容的时侯我们是拿2*capacity进行扩容,capacity的值等于0那扩容之后的结果不还是0了嘛,那不就没有开辟新的空间了嘛,那还谈啥插入数据呢,对吧!所以这里的问题就出在了_capacity上,所以当我们插入数据扩容的时候我们就得先来判断_capacity是否为0,如果为0的话我们就将它的值赋值为4,如果不为0我们就将它的值乘以2倍,那么修改后的代码就如下:
void push_back(char ch)
{
if (_capacity == _size)
{
size_t _newcapacity = _capacity == 0 ? 4: 2 * _capacity;
reserve(_newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
我们再运行上面的代码就不会报错了:
operator+=
有了前面的push_back我们就可以偷个懒来实现operator+=中的一个形式就是尾插一个字符,该函数的声明是这样的:
string& operator+=(char ch)
函数体中就是调用push_back函数来实现尾插,然后返回*this来结束该函数:
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
我们来看看上面的实现能否通过下面的测试代码:
void test5()
{
YCF::string s2;
s2 += 'Y';
s2 += 'C';
s2 += 'F';
cout << "字符串的内容为:" << s2.c_str() << endl;
cout << "字符串的长度为:" << s2.size() << endl;
cout << "字符串的容量为:" << s2.capacity() << endl;
}
这段代码的运行结果如下:
我们可以看到这里的字符串内容,长度和容量都是正确的,那么这就说明我们的代码实现的是正确的。当然该函数还有一种形式就是往后加上一个字符串,那这种形式我们就可以通过append函数来实现:
string& operator+=(char* str)
{
append(str);
return *this;
}
那append函数如何来实现呢?这里我们就可以继续往下看:
append
这个函数的功能就是在字符串的尾部插入另一个字符串,那么该函数的声明就如下:
void append(const char* str)
实现这个函数的第一步就是要判断该对象是否需要扩容以及扩容多大个空间,有了上面的经验有些小伙伴就说啊当_size的值等于_capacity的值时就要扩容,扩到原来的_capacity的两倍,可这样实现是对的吗?很明显不是的,因为我们尾插的是一个字符串而不是一个字符,当_size的值不等于_capacity的值时候,你尾插一个字符串容量也可能不够,而且你扩容两倍也不见得会够,比如说对象中有2个字符,此时的容量为4,可这时我要插入长度为100 的字符串,你觉得扩容两倍够吗?答案很明显是不够的,所以我们这里判断是否需要扩容的条件就是当
_size+strlen()大于_capacity时我们就进行扩容,扩容的大小就是_size+strlen(),那么下面就是我们第一步的代码:
void append(const char* str)
{
if (_size + strlen(str)>_capacity)
{
reserve(_size + strlen(str));
}
}
将空间的问题解决之后,我们下面干的事情就是要插入数据,这里的插入数据我们就可以使用strcpy函数,第一个参数放目的位置,第二个参数放源字符串的位置,那么这里的目标位置就是_str+_size指向了对象中的字符串的结尾,源字符串就是str,将数据插入完之后我们就要更改_size使其加上strlen(str)的值,那么下面就是完整的代码:
void append(const char* str)
{
if (_size + strlen(str)>_capacity)
{
reserve(_size + strlen(str));
}
strcpy(_str + _size, str);
_size += strlen(str);
}
我们用下面的代码来测试一下这个函数的正确性:
void test6()
{
YCF::string s1("hello world ");
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
s1.append("hello c++");
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
这段代码的运行结果如下:
符合我们的预期,那么这就是该函数的实现。
insert
相较于前面的插入函数,insert函数能够实现在任意位置进行插入数据,所以这个函数在实现的时候就会多出一个参数pos用来表示数据插入的位置,这里的插入可以只插入单个字符也可以插入一个字符串,所以该函数的实现有两种不同的形式风别如下:
string& insert(size_t pos, char ch)
string& insert(size_t pos,const char* str)
首先来实现第一种任意位置的插入形式插入单个字符,那么这种插入首先干的第一件事就是判断是否需要扩容,如果_size的值等于_capacity的值的话我们就进行扩容其大小为原来大小的两倍:
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(2 * _capacity);
}
}
将空间开辟完之后我们要干的事情就是将pos位置往后的数据全部往后挪动一个位置,然后再将要插入的数据放到pos位置上去,那么这里的挪动数据我们就得创建一个变量end,将他的值复制为字符串末尾的下标,也就是_size的值,然后再创建一个while循环在循环里面将end位置的元素复制到end+1位置上去,再将end的值减1,那么这就是循环所干的内容这个循环结束的条件就是当end的值小于pos的时候我们就结束循环,最后再将元素插入到pos位置上去并使_size的值加1:
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
++_size;
}
那么这就是该函数的具体实现,我们来用下面的代码来测试一下这个函数的正确性:
void test7()
{
YCF::string s1("hello world");
s1.insert(3, 'x');
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
这个代码的运行结果如下:
通过运行的结果来看这里代码的实现是正确的,但是我们再用下面的代码来测试的话获取就会出现一些问题:
void test7()
{
YCF::string s1("hello world");
s1.insert(3, 'x');
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
s1.insert(0, 'x');
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
我们发现这里在执行到第二次插入数据的时候发生了崩溃,崩溃的原因就是死循环导致的,当我们想在0位置插入数据的时候end的值为0,pos的值也为0,而这两个元素的类型都是无符号整型并且while循环结束的条件是end<pos,那pos等于0,end为无符号整型的话那这个条件不就永远不可能成立了嘛!所以这就是我们崩溃的原因,当然这里有小伙伴会说将end的类型改成int不就可以变成负数了吗?但是大家得记得一个东西叫整型提升,当end为int类型,pos为无符号整型时,表示式end<pos会发生整型提升将end的类型变成无符号整型,依然无法解决问题,所以我们这里解决问题的方法就是将end的类型改为int然后在表达中加入强制类型转换依次来解决整型提升的问题:
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(2 * _capacity);
}
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
end--;
}
_str[pos] = ch;
return *this;
}
我们再运行一下上面的测试代码就可以看到运行结果正常了:
当然上面的只是一种解决方法,另外一种方法就是将end的值赋值为_size+1,然后将循环体内部的
_str[end + 1] = _str[end];
改成_str[end ] = _str[end-1];
这样我们循环继续的条件就变成了end>pos当pos为0,end也为0的时候我们就会停止循环并且还将数据全部都挪动完了,那么修改后的代码就如下:
string& insert(size_t pos, char ch)
{
if (_size == _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size+1;
while (end > pos)
{
_str[end] = _str[end-1];
end--;
}
_str[pos] = ch;
++_size;
return *this;
}
上面的测试代码运行的结果如下:
结果是对的,那么这就说明我们函数的实现是正确的没有问题,接下来我们再来看看另外一种形式的实现。相较于插入字符的形式,插入字符串形式的最大区别就在于我们要把pos位置往后数据挪动strlen个长度而不是一个长度,所以我们这里的代码大致实现是一样的,首先创建一个变脸len来记录插入字符串的长度,然后再来判断是否需要扩容?这里的判断条件就是_size+len>capacity,如果成立的话我们这里就得进行扩容,将容量扩容成_size+len,然后就是挪数据将pos位置往后的数据全部都往后挪动len个长度:
string& insert(size_t pos, char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];
end--;
}
}
数据挪动完之后我们要干的事情就是将这个字符串的数据拷贝到挪动出来的位置,那么这里的拷贝我们就得用到strcpy函数,目标位置为_size+pos,源字符串位置为_str,拷贝完之后我们就要将_size的值加上len,那么我们的代码就如下:
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];
end--;
}
strcpy(_str + pos, str);
_size += len;
return *this;
}
我们来用下面的代码测试一下上面代码的正确性:
void test8()
{
YCF::string s1("hello world");
s1.insert(5, "ycf");
cout << "字符串的内容为:" << s1.c_str() << endl;
}
我们将这个代码运行一下就可以看到这里出了问题:
这里打印到ycf就不打印了,这是为什么呢?我们知道字符串是以\0来作为结尾,而这里打印到ycf就不打印了说明我们这里在插入数据数据的时候顺带的将源字符串ycf后面的\0也插入进去了,所以要解决这个问题的话我们就不能使用strcpy函数,而得使用strncpy函数,将len的值传给这个函数的第三个参数,那么这里修改后的代码实现就如下:
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size;
while (end >= pos)
{
_str[end + len] = _str[end];
end--;
}
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
上面的测试代码运行的结果也就正常了:
但是这里的代码依然不是完美的,因为上面的代码在面对插入位置为0的情况时,依然会发生死循环的所以我们这里依然会有两种不同的解决方案,第一种就是将end的类型改成int,然后在表达式里面进行强制类型转换,那么这里的代码就如下:
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
end--;
}
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
有了上面的经验有些小伙伴们就说第二种修改的方法我知道怎么干了,将end的值改成_size+1,将循环体内的
_str[end + len] = _str[end];
修改成
_str[end] = _str[end-end];
就完成了第二种修改,其完整的形式如下:
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len;
while (end > pos)
{
_str[end] = _str[end-len];
end--;
}
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
然后将下面的测试代码运行一下发现运行结果确实是对的:
void test8()
{
YCF::string s1("hello world");
s1.insert(0, "ycf");
cout << "字符串的内容为:" << s1.c_str() << endl;
}
我们可以看到这里的打印结果是真确的,但是代码的实现一定是正确的吗?while循环结束的条件是end大于pos,而while的循环体里面干的事情是_str[end] = _str[end-len];
假如len的值为6,pos的值为0的话,那end等于1 或者 2 3 4 5 的时候这个循环体里面不就发生越界的行为了嘛,虽然这里不会报错,但是当插入的字符串长度非常长的话这里就会拷贝很多不需要的内容,这不就间接的影响了效率嘛,那为什么会出现这种情况呢?我们可以通过下面的图片来发现一下问题所在:
首先这是我们的数组,我们想在下标为2的位置插入字符串abcd,那么这里pos的值就为2,len的值就为4,end的值为_size+len也就等于14所以此时这些值对应的位置就如下:
每次循环都将end-len对应的值复制到end位置上再让end的值减一,那么第一次循环的结果就如下:
第二次循环的结果如下:
那么这样不停的循环,当end-len等于pos位置的时候,所有的数据都挪动到了对应的位置:
但是循环结束的条件是end小于pos,所以这里还会将end的值减一继续执行循环:
那么从这里开始我们就做了一些无用的拷贝导致了效率的降低,那有些小伙伴就说啊:我们将循环的条件修改一下,当end-len小于pos的时候结束循环:while(end-len>=pos)
但是这样修改的话不就又会造成一个问题吗?当pos等0的时候end-len也等于0,而end和len都是无符号整型,所以这个表达式的结果也会是无符号整型,也就不可能出现负数所以该修改方法是不对的,我们要想解决上面的问题右边表达式的结果就不能出现0,所以我们得将目光看向前面的end
当end等于pos+len的时候他就已经完成了最后一个数据的拷贝,所以当end等于或者小于pos+len-1的时候循环得结束,所以while循环的判断条件就得变为: while (end > pos+len-1)
那么完整的代码就如下:
string& insert(size_t pos, const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size+len;
while (end > pos+len-1)
{
_str[end] = _str[end-len];
end--;
}
strncpy(_str + pos, str,len);
_size += len;
return *this;
}
我们再来测试一下代码:
void test8()
{
YCF::string s1("hello world");
s1.insert(0, "ycf");
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
运行的结果是正常的:
这里那么这就是我们第二种形式的完整实现,希望大家能够理解。
erase
erase函数的作用就是删除对象中的数据,这里的参数形式就是给定删除的位置以及要从该位置删除多少个字符,所以该函数的声明为:
string& erase(size_t pos, size_t len = npos)
我们这里给len函数一个缺省值,当我们想将pos往后的所有字符全部都删除的话,我们就可以在调用该函数的时候只传一个参数给pos,len参数就可以使用其缺省值,接下来我们就得实现函数体,首先得判断一下这里的删除是往后的部分删除还是往后的全部删除,当len的长度等于pos或者len的长度加上pos的值大于等于字符串的长度的话我们我们就可以认为这里的是往后的全部删除,那么对于这种情况我们的做法就是直接将pos位置的元素变为\0这样在输出内容的时候就不会输出\0往后的内容,然后再将size的值进行修改将其别为pos,下面就是该情况的实现:
string& erase(size_t pos, size_t len)
{
if (pos + len >= _size||len==npos)
{
_str[pos] = '\0';
_size = pos ;
}
else
{
}
}
第二种情况就是部分的删除,那么对于这种情况我们要干的第一件事情就是先将后面没有删除的数据向前挪动,我们来看看下面的图:
假设pos的位置为2,要往后删除3个字符所以这里的2 3 4 都会被删除,将5往后的元素往前挪动,而5的下标为pos+len,那么我们这里就创建两个变量begin和end,将begin的值初始化为pos,将end的值初始化为pos+len,每次循环就将begin对应的值赋值到end对应的值上,然后再将begin和end的值加一,当end的值大于_size的值时我们就结束循环,那么我们这里的代码就如下:
string& erase(size_t pos, size_t len)
{
if (pos + len >= _size||len==npos)
{
_str[pos] = '\0';
_size = pos ;
}
else
{
size_t begin = pos;
size_t end = pos+len;
while (end<=_size)
{
_str[begin] = _str[end];
begin++;
end++;
}
_size -= len;
}
return *this;
}
当然我们这里的循环用的就非常的麻烦我们可以直接通过strcpy来实现这里循环的功能:
string& erase(size_t pos, size_t len)
{
if (pos + len >= _size||len==npos)
{
_str[pos] = '\0';
_size = pos ;
}
else
{
//size_t begin = pos;
//size_t end = pos+len;
//while (end<=_size)
//{
// _str[begin] = _str[end];
// begin++;
// end++;
//}
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
我们来看看下面的测试代码:
void test9()
{
YCF::string s1("hello world");
s1.erase(2, 3);
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
YCF::string s2("hello world");
s2.erase(4);
cout << "字符串的内容为:" << s2.c_str() << endl;
cout << "字符串的长度为:" << s2.size() << endl;
cout << "字符串的容量为:" << s2.capacity() << endl;
}
我们将这里的代码运行一下就可以发现这里的代码实现是真确的:
find
这个函数我们分为两种情况,第一个是在字符串中指定位置中寻找字符,第二种就是在字符串的指定位置中寻找子字符串,我们这里首先来实现第一种情况,该情况的声明如下:
size_t find(char ch,size_t pos=0)
因为这里是寻找字符所以我们可以直接通过循环遍历的方式来实现该函数,代码如下:
size_t find(char ch,size_t pos=0)
{
assert(pos<_size);
int i = pos;
while (i < _size)
{
if (_str[i] == ch)
{
return i;
}
i++;
}
return -1;
}
测试代码如下:
void test10()
{
YCF::string s1("hello world");
cout << s1.find('o',5)<<endl;
cout << s1.find('x');
}
运行结果如下:
第二种形式就是在字符串中寻找子串,该形式的函数声明如下:
size_t find(char* str ,size_t pos = 0)
该函数的实现其实就用不着我们出手,因为c语言的库中就提供了与该功能相同的函数strstr
str1是源字符串,str2是我们要查找的字串,所以我们这里就先判断一下pos的位置是否合理,然后我们再调用strstr函数,创建一个变量来接收这个函数的返回值,根据这个变量再来做出具体的判断,strstr函数如果找到了就会返回子字符串第一次出现的位置,如果没有找到就返回空指针,那么我们这里就会用if else语句来对这里做出判断,那么代码就如下:
size_t find(const char* str, size_t pos=0)
{
assert(pos < _size);
const char* p = strstr(_str, str);
if (p == NULL)
{
return npos;
}
else
{
return p - _str;
}
}
我们的测试代码就如下:
void test10()
{
YCF::string s1("hello world");
cout << s1.find('o',5)<<endl;
cout << s1.find('x')<<endl;
cout << s1.find("llo");
}
这个代码的运行结果就如下:
那么这么看的话我们这里的函数实现是正确的。
resize
这个函数功能就是调节对象中字符串的长度,这里我们得分三种情况,第一个是当len的值小于_size的时候,当len的长度大于_size小于_capacity的时候,当len的值大于_capacity的时候,那么当我们需要增长字符串的长度的时候,我们就得往这个字符串中添加字符,那么这个字符可以由操作者提供,当操作者不提供的时候就由库默认提供,那么这里的函数声明就如下:
void resize(size_t len, char ch = '0')
这里我们可以将上面的三种情况分为两种,因为我们实现的reserve函数它不会进行缩容,所以我们可以将len的值小于_size分为一种,将其他情况分为另一种,那么这里我们就可以使用if else语句,其判断情况如下:
void resize(size_t len, char ch = '0')
{
if (len < _size)
{
}
else
{
}
}
对于len小于_size的情况我们就可以直接将len对应位置的元素赋值为’\0’,然后将_size的值修改为len:
void resize(size_t len, char ch = '0')
{
if (len < _size)
{
_str[len] = '\0';
_size = len;
}
else
{
}
}
对于两外的两种情况我们可以先使用reserve函数将其容量修改成len,因为reserve函数不会进行缩容,所以就算len小于_capacity的时候我们也可以使用reserve函数,将容量处理完之后我们就可以使用while循环就刚刚扩容出来的新空间全部初始化为使用者给的字符,然后在字符串的结尾添加一个\0用来结束字符串,最后再将_size 的值赋值为len,下面是对应的代码的实现:
void resize(size_t len, char ch = '0')
{
if (len < _size)
{
_str[len] = '\0';
_size = len;
}
else
{
reserve(len);
size_t i = _size;
while (i < len)
{
_str[i] = ch;
i++;
}
_str[i] = '\0';
_size = len;
}
}
下面是该函数的测试代码:
void test11()
{
YCF::string s1("hello world");
s1.resize(20, 'x');
YCF::string s2("hello world");
s2.resize(4);
cout << "字符串的内容为:" << s1.c_str() << endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
cout << "字符串的内容为:" << s2.c_str() << endl;
cout << "字符串的长度为:" << s2.size() << endl;
cout << "字符串的容量为:" << s2.capacity() << endl;
}
运行的结果如下:
那么该函数的实现就是真确的。
[ ]
这里就是对[ ]操作符进行重载用来得到对应位置的数据,并且可以通过该操作符来修改对应的数据,那么这里我们就是通过引用返回的方式来进行实现,其对应的代码如下:
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
我们上面是可读可写的版本,但是为了应对一些其他的情况我们这里还得实现一个其他的版本就是只读的版本,所以我们这里就得添加两个const,其代码如下:
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
clear
这个函数的功能就是将对象的内容全部都清空,但是并不会将容量缩小,所以实现这个函数的时候就可以直接通过修改_size的值来实现将原始的数据清空,那这里的代码就如下:
void clear()
{
_size = 0;
_str[0] = '\0';
}
>>
这里我们要实现的就是流提取,这个函数的实现就非常的简单我们直接利用循环遍历的方式将对象中的每个元素全部都打印出来,首先为了符合我们平时的使用规律我们这里得在函数外来实现这个函数,然后在类中使用友元来获取对象中的数据,那么这里我们的函数声明就是这样:
ostream& operator<<(ostream& out, const string& s)
在类中我们也得声明一下这个函数,并且得在声明的开头加一个friend用来表示有缘:
friend ostream& operator<<(ostream& out, const string& s);
然后在函数体里面我们就通过循环和[ ]的形式来获取每一个元素并将其打印出来,那么这里就非常的简单直接通过for循环来实现:
ostream& operator<<(ostream& out, const string& s)
{
for (size_t i = 0; i < s.size(); ++i)
{
out << s[i];
}
return out;
}
我们可以用下面的代码来测试一下:
void test12()
{
YCF::string s1("hello world");
cout << s1;
}
int main()
{
test12();
return 0;
}
其运行的结果如下:
那么我们这里的运行结果就是正确的。
>>
对于这个函数的重载我们想要实现的功能就是将原来的内容清空将原来然后再将新的数据插入到对象里面,那么我们这里实现该函数的时候就可以先使用clear函数将内容全部清空,然后再使用while循环和+=将使用者给的内容一个一个的输入到对象里面,因为这里要对参数s进行修改,在函数声明的时候我们不要对参数加上const来进行修饰,所以该函数的声明就如下:
istream& operator>>(istream& in, string& s)
因为该函数需要用到类里面的数据,所以我们这里还得将该函数在类中声明为友元的形式:
friend istream& operator>>(istream& in, string& s);
因为我们平时在使用>>的时候他是不会接受空格字符和换行字符的,所以我们这里就可以将这两个字符作为循环结束的条件放到while循环里面,因为库中的>>它本身也不接收空格和换行符,所以这里在接收字符的时候我们就得使用get函数,那么这里我们的代码就如下:
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch!=' '&&ch!='\n')
{
s += ch;
ch = in.get();
}
return in;
}
我们可以通过下面的代码来测试一下该函数实现的真确性:
void test12()
{
YCF::string s1("hello world");
cin >> s1;
cout << s1<<endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
我们将这段代码运行一下并在输入栏中输入下面的数据:
按一下回车就可以看到这里输出了这样的数据:
那么这里就说明我们的函数的实现是正确的没有问题,但是这里有个点需要我们优化一下就是,我们这里是不停的调用+=来插入数据而+=函数是通过push_back函数实现的,使用这个函数会进行扩容,每次都会将容量扩大成两倍,所以当我们要插入一个非常长的字符串的时候,采用这种方式会进行很多次扩容,而不停的扩容会导致效率的降低,所以为了将这里的效率提高一点我们这里得对其进行改进,我们先创建一个数组将这个数组的容量赋值为128,那么每次读取数据的时候我们就先不要使用+=来插入数据而是先将内容放到数组里面,等数组满了之后我们再使用+=函数将数组的内容一下子全部尾插到对象里面,然后将数组清空再将剩余的字符串继续往数组里面插入,这样我们就减少了扩容的次数,从而提高了效率,那么上面的所述的代码第一步如下先创建了数组通过循环将数据插入到数组里面:
istream& operator>>(istream& in, string& s)
{
s.clear();
int i = 0;
char arr[128] = { 0 };
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
arr[i] = ch;
i++;
ch = in.get();
}
}
然后我们再添加一个if语句,当i的值等于128的时候我们就将数组的内容尾插到对象里面,然后将i的值赋值为0:
istream& operator>>(istream& in, string& s)
{
s.clear();
int i = 0;
char arr[128] = { 0 };
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
s += arr;
i = 0;
}
arr[i] = ch;
i++;
ch = in.get();
}
}
当这里的循环结束的时候,数组里面还可能会有数据没有被尾插到对象里面,所以在循环结束之后我们还得加一个判断语句,当i大于0的时候我们就得将i对应的位置的元素赋值为\0,然后再将数据尾插到对象里面,那么我们完整的代码就如下:
istream& operator>>(istream& in, string& s)
{
s.clear();
int i = 0;
char arr[128] = { 0 };
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
if (i == 127)
{
s += arr;
i = 0;
}
arr[i] = ch;
i++;
ch = in.get();
}
if (i > 0)
{
arr[i] = '\0';
s += arr;
}
return in;
}
我们来看看下面的测试代码:
void test12()
{
YCF::string s1("hello world");
cin >> s1;
cout << s1<<endl;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
我们往输入栏中输入多个数据:
再按一下回车:
我们这里的输入输出的值是一样的,所以这里我们的函数实现就是对的没有问题。
新式拷贝构造函数
我们首先来看看原来的拷贝构造函数的写法:
string(const string& s)
{
_str = new char[s._capacity + 1];
_capacity = s._capacity;
_size = s._size;
strcpy(_str, s._str);
}
这种写法就是先申请一个动态的空间,再将_capacity和_size的值赋值为对象s中的_capacity和_size的值,然后再将对象s中的数据拷贝到申请的动态空间里面,那么这是传统写法,我们的现代写法就是利用构造函数先用传过来的对象创建一个临时对象,再用swap函数将本对象的数据与临时对象的数据进行一下交换,这样在该函数结束的时候调用析构函数,就不会将有用的数据进行删除,但是为了保证它不会删除其他的随机数据我们得先使用初始化列表给本对象一个空值,避免析构出错,那么这里的代码就如下:
string(const string& s)
:_str(nullptr)
, _capacity(0)
, _size(0)
{
string tmp(s._str);
swap(_str, tmp._str);
swap(_capacity, tmp._capacity);
swap(_size, tmp._size);
}
我们可以用下面的代码测试一下:
void test13()
{
YCF::string s1("hello world");
YCF::string s2(s1);
cout << s2;
cout << "字符串的长度为:" << s1.size() << endl;
cout << "字符串的容量为:" << s1.capacity() << endl;
}
}
我们将这个代码运行一下看一下这个代码执行的结果为:
那么这个代码的运行结果就是对的,当然我们这里的使用了三次swap看上去有点臃肿,我们可以将这三个swap写到我们自己创建的一个swap函数里面比如说这样:
void swap(string& s)
{
std::swap(_str, s. _str);
std::swap(_capacity, s. _capacity);
std::swap(_size, s. _size);
}
这里在使用swap函数的时候得在前面加上一个std用来指明用的哪个命名空间的swap函数以免发生命名冲突,但是我们这里用了三次swap函数交换了不同的值,有小伙伴知道库中的swap函数是可以直接交换类的对象的,但是库中的swap是这样实现的:
template <class T> void swap ( T& a, T& b )
{
T c(a); a=b; b=c;
}
我们调用swap函数传过去的是一个string的对象的话,他这里就会进行三次深拷贝,一次拷贝构造,两次赋值重载,所以这样使用会大大的降低使用的效率,而且我们是在拷贝构造函数调用的swap函数,在swap函数里面他又会调用该对象的拷贝构造这不就矛盾了,所以得分三次写那么完整的代码就是这样:
void swap(string& s)
{
std::swap(_str, s. _str);
std::swap(_capacity, s. _capacity);
std::swap(_size, s. _size);
}
string(const string& s)
:_str(nullptr)
, _capacity(0)
, _size(0)
{
string tmp(s._str);
swap(tmp);
}
新式赋值重载
有了上面的经验我们这里的赋值重载也可以采用同样的方法来进行升级,首先使用拷贝构造创建一个临时对象,然后将临时对象的数据与你要赋值的对象的数据进行交换,这样就完成了赋值重载,那么这里的代码就是这样:
string& operator=(string& s)
{
string tmp(s);
swap(tmp);
return *this;
}
测试的代码就是这样:
void test14()
{
YCF::string s1("hello world");
YCF::string s2;
s2 = s1;
cout << s2;
}
代码运行的结果如下:
这样我们这里的代码实现就是正确的,当然我们这里可以再进行一次简化,我们在这里可以不使用引用而是直接用形参,因为实参在传递给形参的时候会经历一次临时拷贝,调用的也是拷贝构造,所以我们这里就可以再进一步简化,其代码如下:
string& operator=(string s)
{
swap(s);
return *this;
}
上面就是我们的全部内容,希望大家能够理解。