目录
前言
关于容器的学习我认为不应该仅仅只是了解其用法,还要了解其底层是如何实现的,这对我们使用STL库中的容器有很大的帮助,可以帮助我们提高开发效率;
一、hpp文件
在一个项目中,我们要追求程序与程序之间高内聚,低耦合;所以我们需要对我们这个string进行封装,我们将string封装在一个hpp文件中,也有人直接写成h文件,所谓hpp文件即其中又具有声明,也有定义;我们将声明与定义写在同一个文件中;
二、需要做什么
在进行string的模拟实现之前,我们需要清楚,我们需要实现哪一些函数接口,我们需要做什么,明确我们自己的目的,我的目的是实现string类中一些比较常用的接口,不将所有接口一一实现,太过于冗余;
三、开干
1、命名污染问题
首先我们为了不和标准库中的string发生命名冲突,我们将我们自己string类封装在我们设定的命名空间内;其中我们实现string与我们之前实现的顺序表很相似;
namespace MySpace
{
class string
{
public:
private:
// 指向字符串的指针
char* _str;
// 字符串的大小(不包括\0)
int _size;
// string的容量(不包括\0)
int _capacity;
};
}
2、构造函数与析构函数
string类中有许多构造函数,这里就选择其中一部分来进行实现;
// 构造函数
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 4 : _size;
// 注意需要加1,我们算出的capacity没有计算\0
char* tmp = new char[_capacity + 1];
strcpy(tmp, str);
_str = tmp;
}
// 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
一定要注意\0的问题,细节决定成败,这里的size和capacity都没有计算\0,故在new的时候,我们需要+1;
3、拷贝构造
注意,当我们实现拷贝构造时,拷贝构造必须是深拷贝,不然会出现不可预估的结果;
// 拷贝构造(必须深拷贝)
string(const string& str)
{
_capacity = str._capacity;
_size = str._size;
char* tmp = new char[_capacity + 1];
strcpy(tmp, str._str);
_str = tmp;
}
4、容量相关成员函数
首先,我们实现以下这四个较为容易实现的成员函数,也没有什么细节,较为容易;在实现一些不改变对象的成员函数时,需加上const修饰;
// capacity
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
bool empty() const
{
return _size == 0;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
我们再来实现reserve与resize,实现下列函数时需要注意reserve与resize的特性,当他们处理不同大小的值时做出的动作;
void reserve(int n)
{
// n小于等于capacity时什么都不做,不会缩容
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(int n, char ch = '\0')
{
if (n < _size)
{
_str[n] = '\0';
_size = n;
}
else
{
if (n > _capacity)
{
reserve(n);
}
memset(_str + _size, ch, n - _size);
_str[n] = '\0';
}
}
5、修改相关成员函数
以下为给string类进行插入的函数,特别需要注意的是,pos是一位size_t类型,也就是无符号整型,因此在判断的时候要特别注意,会不会由0减到一个非常大的数;
// modify
string& append(const string& str)
{
// 是否需要扩容
if (_size + strlen(str._str) > _capacity)
{
// 切记这里不能二倍扩,因为二倍扩可能还是不够
int newcapacity = _size + strlen(str._str);
reserve(newcapacity);
}
strcpy(_str + _size, str._str);
_size += strlen(str._str);
return *this;
}
string& push_back(const char ch)
{
// 是否需要扩容
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
_size += 1;
_str[_size] = '\0';
return *this;
}
string& operator+=(const string& str)
{
append(str);
return *this;
}
string& operator+=(const char ch)
{
push_back(ch);
return *this;
}
string& insert(size_t pos, const string& str)
{
int len = strlen(str._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._str, len);
_size += len;
_str[_size] = '\0';
return *this;
}
string& insert(size_t pos, const char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
int end = _size;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = ch;
_size += 1;
_str[_size] = '\0';
return *this;
}
在实现erase之前,我们首先补充一个特殊的语法,之前我们在介绍静态成员变量时,我们说过,静态成员函数要在类内声明,在外部定义,因为静态成员变量不属于某个类实例化出的对象,而属于整个类,因此只能在类外定义,但如果我们给这个静态成员用const修饰起来,便可以在类内定义;在string类中,便有一个const静态成员变量npos;
private:
char* _str;
int _size;
int _capacity;
const static size_t npos = -1;
// 并且这个特殊的常静态成员只能是整型家族的
// const static double ndpos = 1.1; // err
const static long long dlpos = 10; // 正确
注意:非整型家族常静态成员变量只能在类内声明,类外定义!!
string& erase(size_t pos, size_t len = npos)
{
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
这里正式介绍为啥函数库中有一个swap,这里为啥还重载一个特殊的swap,以下为string中swap的实现;
// string 中
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
// algorithm 中
template<class T>
void swap(T& x, T& y)
{
T tmp = x;
x = y;
y = tmp;
}
当我们用第一种swap时,我们仅仅只需要拷贝一个字符指针类型的数据,而我们使用第二个swap时,我们需要拷贝一整个string,消耗大了很多,因此string中实现了一个swap成员函数;
6、迭代器
实际上,string类中的迭代器有许多实现的方式,在GCC编译环境中,迭代器实际上就是一个字符指针,而VS使用的MSVC环境中,会对string类迭代器进行一个更为复杂的封装;本文采用GCC的实现方式;
// 迭代器
// 简单的进行类型重定义即可
typedef char* iterator;
typedef const char* const_iterator;
// 普通对象调用的迭代器
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
// 重载const对象调用的迭代器
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
有了上述的迭代器我们也可以实现用迭代器进行构造,插入,删除等成员函数的重载;
7、运算符重载
当然,string中也有许多运算符重载,我们也可以一一模拟实现,其中我们可以通过多次复用的思想,让我们的代码更精简;
// 运算符
// 比较运算符
bool operator>(const string& str) const
{
return strcmp(_str, str._str) > 0;
}
bool operator==(const string& str) const
{
return strcmp(_str, str._str) == 0;
}
bool operator!=(const string& str) const
{
return !(*this != str);
}
bool operator>=(const string& str) const
{
return (*this > str) || (*this == str);
}
bool operator<(const string& str) const
{
return !(*this >= str);
}
bool operator<=(const string& str) const
{
return !(*this > str);
}
// []重载,使其可以像数组一样访问
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];
}
8、其他类成员函数
其他类型成员函数我们挑选了如下几个来实现;
// other func
const char* c_str() const
{
return _str;
}
size_t find(const string& str, size_t pos = 0)
{
const char* p = strstr(_str + pos, str._str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
9、非成员函数
对于非成员函数,这里采用设置为友元函数的方式,来访问类内私有成员; 这里需要注意的是对于用重载流插入打印与用c_str指针来打印的区别,重载流插入的打印是按照_size的大小来打印,而c_str是遇到\0结束打印,如下图所示例子
// 类内声明友元
friend ostream& operator<<(ostream& out, string& str);
friend istream& operator>>(istream& in, string& str);
// 类外实现定义
ostream& operator<<(ostream& out, string& str)
{
out << str._str << endl;
return out;
}
istream& operator>>(istream& in, string& str)
{
// 清空数据
str.clear();
// 设置缓冲区,减少扩容次数
char buf[128];
int i = 0;
// 获取字符
char ch = in.get();
while (ch != ' ' && ch != '\n')
{
buf[i++] = ch;
if (i == 127)
{
buf[i] = '\0';
i = 0;
str += buf;
}
ch = in.get();
}
if (i != 0)
{
buf[i] = '\0';
str += buf;
}
return in;
}
}
三、本文源码
为了缩短本文篇幅,所有源码都存放在了gitee中,链接如下