各位读者老爷好,俺最近在学习string的一些知识。为了更好的了解string的结构,俺模拟实现了一个丐版string,有兴趣的老爷不妨垂阅!!!
目录
我们要明白,代码的实现方法有一万种,只要能实现特定功能就行。例如string类的实现,不同的平台下实现的方法也会有所区别,但是实现的string类一定要满足string文档的要求。俺下面模拟实现的string亦是如此。
俺在上篇博客介绍过,string是一个由类模板实例化而来的类,是用来管理字符串的。其底层实现大致就是字符顺序表。
那么俺模拟实现string,是不是要从类模板开始搞起呢?当然不是,俺可没有这个本事。俺直接定义string类就好。并且string的接口众多,俺只模拟实现部分常用接口噢噢!!
1.string类的定义
namespace HD
{
class string
{
char* _str = nullptr;
size_t _capacity = 0;
size_t _size = 0;
public:
typedef char* iterator;
typedef const char* const_iterator;
static const size_t npos;
//string()
// :_str(new char[1]{'\0'})
// ,_capacity(0)
// ,_size(0)
//{
//}
//string(const char* s)
//{
// _size = strlen(s);
// _capacity = _size;
// _str = new char[_capacity+1];
// strcpy(_str, s);
//}
string(const char* s = "")//以上两构造合二为一
{
_size = strlen(s);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, s);
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str()const
{
return _str;
}
size_t size()const
{
return _size;
}
char& operator[](size_t pos)
{
assert(pos <= _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos <= _size);
return _str[pos];
}
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
size_t capacity()const
{
return _capacity;
}
string(const string& str)//拷贝构造函数
{
/////////////////写法一/////////////
/*_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;*/
/////////////////写法二/////////////
string tmp(str.c_str());
swap(tmp);
}
void reserve(size_t n = 0);
void push_back(char c);
string& append(const char* s);
string& operator+= (const char* s);
string& operator+=(char c);
string& insert(size_t pos, size_t n, char c);
string& insert(size_t pos, const char* s);
string& erase(size_t pos = 0, size_t len = npos);
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) const;
string& operator=(const string& str);
//string& operator=(const string str);
void clear();
void swap(string& str);
};
}
对于这个string类的定义如上。
1.俺们知道string类底层是字符顺序表,所以俺定义3个private成员变量_str、_capacity、_size来实现,并且给这3个成员变量缺省值,这3个缺省值是很有意义的,意义的体现会在下面的讲解可见。
2.俺将一些成员函数(接口)的声明和定义全部放到string类当中,则默认当成内联函数处理;还有一些成员函数(接口)的声明和定义分离,只将这些成员函数的声明放到string类当中。
3.为了防止与命名空间std的string(也就是STL的string)产生冲突,俺实现的string使用自己命名空间(如HD)封装起来。
PS:以下所说的string若没有特殊声明,都默认指俺模拟实现的string。若指STL的string,俺会用红色字体标注string,如string。
2.模拟实现成员函数接口
2.1.constructor(构造函数)
对于这个接口,string内重载了很多个函数。俺暂时就模拟实现两个,如下:
1.string()
这个函数的模拟实现如下,俺将其声明和定义都放到string类中:
string()
:_str(new char[1]{'\0'})
,_capacity(0)
,_size(0)
{
}
这里俺使用初始化列表来初始化。易知这是一个构造空的string对象的,那么当然将_capacity和_size初始化成0;但是对于_str,俺将其指向堆区动态开辟的一个字节的空间,这块空间存储字符'\0'。
为什么_str不用nullptr来初始化呢?其实大有意义,让子弹再飞一会,原因俺下面介绍。
3.string (const char* s)
这个函数的模拟实现如下,俺将其声明和定义都放到string类中:
string(const char* s)
{
_size = strlen(s);
_capacity = _size;
_str = new char[_capacity+1];
strcpy(_str, s);
}
这里俺在函数体内初始化。俺们知道这个构造函数的功能是用C-string来构造string类对象。那么我们开好空间(记得多开一个空间存放'\0'),将C_string(也就是s)拷贝到空间上,_size和_capacity 初始化为C_string的字节数(长度)就好。
2.2.destructor(析构函数)
这个接口就是析构函数,string类中申请了资源,俺们必须显示写析构函数,不然会造成资源的泄露。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
若不显示写析构函数,_str指向的空间就泄露了。
2.3.c_str
这个接口功能就是返回C形式的字符串,也就是返回底层的_str。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:
const char* c_str()const
{
return _str;
}
这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数。
俺们来使用以下自己模拟实现的接口。事先要先知道的是本工程包含3个文件:
……
string.h:存放string类的定义和非成员函数的声明;
……
string.cpp:存放string类部分成员函数的定义、静态成员变量的定义和非成员函数的定义;
……
test.cpp:存放测试代码,来测试咱们模拟实现的string的功能;
……
俺们到现在介绍了几个接口的模拟实现了,咱们在test.cpp中试试:
#include"string.h" namespace HD { void test1() { string S1; string S2("hello world"); cout << S1.c_str() << endl << S2.c_str() << endl; } } int main() { HD::test1(); return 0; }
运行结果:
有几个点需要明确:
……
1.为什么test1函数需要用命名空间HD封装起来?
因为若是不用命名空间HD封装起来,string类实例化对象S1、S2会调用string的构造函数接口,且c_str接口的调用亦是调用string的接口,这样子就违背了我们测试模拟实现的接口的初衷。有关命名空间和编译器访问规则的介绍可以看[C++]C++入门1.0。
……
2.主函数中调用test1函数为什么要指定命名空间HD域?
因为若是不指定命名空间HD域,编译器会访问不到test1函数。编译器的访问规则介绍可以看[C++]C++入门1.0。
……
3.string()函数的定义中,_str不用nullptr来初始化呢?
若是用nullptr来初始化_str,当空string对象调用c_str接口时就返回nullptr,当我们对这个返回值进行操作时就有可能访问到空指针,程序就会崩溃。
2.4.size
这个接口这个接口返回字符串的长度。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:
size_t size()const
{
return _size;
}
这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数。
在test.cpp中测试一下:
#include"string.h"
namespace HD
{
void test2()
{
string S1;
string S2("hello world");
cout << S1.size() << endl << S2.size() << endl;
}
}
int main()
{
HD::test2();
return 0;
}
运行结果:
2.5.operator[]
本接口返回对字符串中位置pos处的字符的引用。且string中本接口重载了2个函数分别供普通对象和const对象使用。俺也模拟实现2个函数,并将其声明和定义都放到string类中:
char& operator[](size_t pos)
{
assert(pos <= _size);
return _str[pos];
}
const char& operator[](size_t pos)const
{
assert(pos <= _size);
return _str[pos];
}
在test.cpp中测试一下:
#include"string.h"
namespace HD
{
void test3()
{
string S1("nbo");
for (int i = 0; i < S1.size(); i++)
{
S1[i] -= 1;
cout << S1[i];
}
cout << endl;
const string S2("hello world");
for (int i = 0; i < S2.size(); i++)
{
cout << S2[i];
}
}
}
int main()
{
HD::test3();
return 0;
}
对于char& operator[](size_t pos)这个函数,这个函数是一个对this指针指向对象的成员变量只进行读访问的函数,可是我们不能将其实现成const成员函数,为什么呢?
因为俺们实现了const char& operator[](size_t pos)const这个函数供const对象调用,若是还将char& operator[](size_t pos)实现成const成员函数的话,无法与const char& operator[](size_t pos)构成函数重载!
2.6.begin、end
有一个问题:基于范围的for循环的方式,对所有容器都适用。那么到现在为止俺们模拟实现的string适用于基于范围的for循环吗?
俺们在test.cpp试试:
#include"string.h"
namespace HD
{
void test4()
{
string S1("hello world");
for (auto ch : S1)
{
cout << ch;
}
cout << endl;
}
}
int main()
{
HD::test4();
return 0;
}
发现行不通的,错误列表有一堆报错:
到现在为止模拟实现的string不适用基于范围的for循环的原因也很简单,因为范围for的底层很简单,容器遍历实际就是替换为迭代器。咱们模拟实现的string还没有模拟实现迭代器,也没有模拟实现迭代器相关接口,当然不适用了。
那么如何模拟实现迭代器呢?
迭代器的实现很复杂,其实迭代器本质就是模拟指针的行为啊,不管迭代器是用什么方法实现的,都希望迭代器能像指针般使用达到访问容器的目的。
那么针对string的模拟实现来说,不用搞那么复杂,我们正好可以返璞归真,利用string类的底层是字符顺序表这个特点,俺们就可以用指针模拟实现迭代器,因为本身就可以用指针来访问顺序表啊!
正向迭代器的模拟实现如下,对应的迭代器属于对应容器的类域,所以俺将其声明放在string类中:
typedef char* iterator;
typedef const char* const_iterator;
俺就不模拟实现反向迭代器了,反向迭代器就不是用指针可以模拟实现的了,复杂的很!
模拟实现了正向迭代器,俺们就可以模拟实现正向迭代器相关的接口了,以下接口的声明和定义都放到string类中:
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin()const
{
return _str;
}
const_iterator end()const
{
return _str + _size;
}
这些接口的模拟实现好像不用解释什么,根据接口的作用很容易就能写出来啊。
模拟实现了正向迭代器和正向迭代器相关的接口,俺们在test.cpp中测试一下:
#include"string.h"
namespace HD
{
void test4()
{
string S1("hello world");
for (auto ch : S1)
{
cout << ch;
}
cout << endl;
const string S2("God is a girl");
string::const_iterator cit = S2.begin();
while (cit != S2.end())
{
cout << *cit;
cit++;
}
cout << endl;
}
}
int main()
{
HD::test4();
return 0;
}
运行结果没问题的,基于范围的for循环也能正常使用了:
2.7.capacity
这个接口这个接口返回字符串的容量。这个接口的模拟实现如下,俺将其声明和定义都放到string类中:
size_t capacity()const
{
return _capacity;
}
这是一个对this指针指向对象的成员变量只进行读访问的函数,所以我们将其实现成const成员函数,且这样子普通对象和const对象都可调用这个函数(这点以下不再赘述)。
在test.cpp中测试一下:
#include"string.h"
namespace HD
{
void test5()
{
string S1;
string S2("hello world");
cout << S1.capacity() << endl << S2.capacity() << endl;
}
}
int main()
{
HD::test5();
return 0;
}
运行结果:
2.8.reserve
本接口功能是为string预留空间,不改变有效元素个数。俺模拟实现这个接口不搞那么复杂,当扩容才使其生效。模拟实现本成员函数采用声明和定义分离的写法:
string类(再次说明string的定义放在了string.h中)中声明:
void reserve(size_t n = 0);
string.cpp中定义:
#include"string.h"
namespace HD
{
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* p = new char[n + 1];
strcpy(p, _str);
delete[] _str;
_str = p;
_capacity = n;
}
}
}
1.缺省参数不能在函数声明和定义中同时出现,当函数声明和定义分离时在声明给缺省参数 。
2.别忘记使用命名空间HD封装起来。
3.该函数声明和定义