我们不仅要会使用strng的接口,还要模拟实现,更深地理解strng的底层逻辑。这里我们最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数这些比较核心的接口。
1.准备工作
我们依旧采用声明和定义分离的方式模拟实现string,跟之前模拟实现Stack、顺序表那些是同样的操作,建三个文件,一个头文件string.h,两个源文件test.cpp和string.cpp。
在string.h中用命名空间分隔一下,因为c++库里面也有string,避免冲突。string.h里面写一些会用到的头文件,一个string类,string类的成员变量都是老朋友了,size和capacity也是介绍过的,和模拟实现顺序表差不多的,前面的博文说过,string可以认为是char类型的顺序表。
#include <iostream>
#include <assert.h>
using namespace std;
namespace lyj //用命名空间与库里的string分隔开
{
class string
{
private:
char* _str;
size_t _size;
size_t _capacity;
};
}
2.基础接口
短小而且频繁被调用的函数,就直接放在string类里面,就不做声明和定义分离了。
2.1 无参构造
还是写在string.h里的string类,作为string类的成员函数。
class string
{
public:
string() //无参构造
:_str(new char[1]{'\0'}) //不能是nullptr
,_size(0)
,_capacity(0)
{}
private:
char* _str;
size_t _size;
size_t _capacity;
};
无参构造走初始化列表,一个一个初始化,不传参_size和_capacity都是0,但是_str不可以为nullptr,这里要开辟一个空间,初始化为'\0'。
2.2 带参构造
写在string类里面。带参构造就不要走初始化列表初始化了,因为不是很方便,我们就在函数体里实现。
class string
{
public:
string() //无参构造
:_str(new char[1]{'\0'})
,_size(0)
,_capacity(0)
{}
string(const char* str) //带参构造
{
_size = strlen(str);
_capacity = _size; //capacity大小不包括\0
_str = new char[_capacity + 1]; //开空间时多开一个
strcpy(_str, str);//拷贝
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
传参的时候_size就是传过来的str的大小,_capacity也初始化的和size一样大,切记,_capacity大小是不包括'\0'的,但是我们开空间的时候要多开一个,所以new char的大小是_capacity+1。空间开好之后就把str的数据拷贝到_str去,就初始化好了。
在test.cpp中测试一下带参构造和无参构造。
#include "string.h"
namespace lyj //命名空间保持一致
{
void test1(void)
{
string s1; //不传参
string s2("hello world");//传参
}
}
int main()
{
lyj::test1(); //指定命名空间调用函数
return 0;
}
2.3 无参构造和带参构造结合
两种构造函数可以结合成一个构造函数,如下。
string(const char* str = "") //给缺省值,什么都没有的字符串
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);//拷贝
}
什么都没有的字符串自带\0,strlen求的是\0之前的字符长度,如果不传参,strlen(str)的结果就是缺省值的空字符串大小,为0, _capacity = _size = 0,new的大小为1;如果传参就是带参构造的结果。
在test.cpp中测试一下这个结合的构造函数。
#include "string.h"
namespace lyj //命名空间保持一致
{
void test1(void)
{
string s1; //不传参
string s2("hello world");//传参
}
}
int main()
{
lyj::test1(); //指定命名空间调用函数
return 0;
}
结果是正确的。
2.3 析构函数
写在string类里面。
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
析构函数没啥多说的。
2.5 c_str
写在string类里面。这个接口也算是打印函数,我们还没实现流插入流提取函数,暂时用这个。
const char* c_str()
{
return _str;
}
在test.cpp中测试一下。
namespace lyj //命名空间保持一致
{
void test1(void)
{
string s1; //不传参
string s2("hello world");//传参
//打印
cout << s1.c_str() << endl;
cout << s2.c_str() << endl;
}
}
int main()
{
lyj::test1(); //指定命名空间调用函数
return 0;
}
2.6 size 、capacity 和 operator[]
都写在string类里面。
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
operator提供两个版本,普通对象和const对象。 普通对象返回引用,可以修改,const对象返回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];
}
2.7 拷贝构造
还是写在string.h里的string类,作为string类的成员函数。
//假设用s1拷贝s2,即s2(s1)
string(const string& s)//拷贝构造
{
_str = new char[s._capacity]+ 1; //s2开和s1一样的大小
strcpy(_str, s._str);//拷贝数据
_size = s._size;
_capacity = s._capacity;
}
有资源的申请是深拷贝。
2.8 operator= 赋值
还是写在string.h里的string类,作为string类的成员函数。
//s2 = s1;
string operator=(const string& s) //赋值
{
delete[] _str;
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
return *this;
}
但是这个代码,自己赋值给自己就会出问题。所以我们要做一个改动。
string operator=(const string& s) //赋值
{
if (this != &s) //不是自己给自己赋值时
{
delete[] _str;
_str = new char[s._capacity + 1];
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
3.迭代器和范围for
先实现迭代器。
typedef char* iterator; //给char*换个名字叫iteraator
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;//_str + _size是'\0'的位置
}
但是不可以认为迭代器就是指针,没有这么简单。 只能在string里面用指针这样实现一下。
在test.cpp里测试。
namespace lyj //命名空间保持一致
{
void test1(void)
{
string s1; //不传参
string s2("hello world");//传参
//迭代器遍历
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
}
int main()
{
lyj::test1(); //指定命名空间调用函数
return 0;
}
范围for可以直接用,因为我们说过,范围for其实底层就是迭代器。
如果我们现在把迭代器的实现注释掉,范围for也不能用。
除了普通迭代器,还有const迭代器。
typedef const char* const_iterator; //给const char*换个名字叫const_iteraator
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
到这我们就大概实现了string的大概框架,增删查改的接口我们下次再说,本篇就到这里,拜拜~