文章目录
- 前言
- 成员变量
- 成员函数
- 构造函数
- 拷贝构造函数
- 浅拷贝
- 深拷贝
- 拷贝构造函数实现
- 析构函数
- 赋值重载
- 空间管理函数
- 元素访问
- 元素修改
- 字符串运算
- 流提取 & 流插入
- 流提取
- 流插入
- 迭代器
- begin & end
- rbegin & rend
- 总结
前言
模拟实现不是为了写得和库里面一样好。而是为了更好的了解底层,从而能够更熟练的使用这些类,同时也能学习大佬们的代码风格。
在学习这章之前,需要对类与对象有一定的知识基础,如果对类与对象有些生疏的话,可以看看这篇文章:《类与对象》
string 类是 C++标准库中的一个重要类,用于表示字符串。
以下是一些关于 string 类的主要特点:
- 动态存储:可以自动管理内存,根据字符串的实际长度动态分配和释放内存。
- 丰富的操作:提供了很多方便的操作方法,如字符串连接、查找、比较、提取子串等。
- 高效性:在性能上表现较好,适用于各种字符串处理场景。
成员变量
在正式模拟实现之前,我们得要先确定我们要实现的类中的成员变量由哪些类型构成:
- 首先需要有一个字符指针,用于存储字符串。
- 再定义一个变量,用来记录字符串的有效个数。
- 再定义一个变量,用来记录该字符指针能存储的有效字符个数大小
- 再定义一个全局变量
npos
,来完成以下操作:npos
是一个静态成员常量值,具有size_t
类型元素的最大可能值。- 当在
string
的成员函数中作为len
(或sublen
)参数的值时,该值表示“直到字符串结束”。 - 作为返回值,它通常用于表示没有匹配。
- 该常量定义为
-1
,由于size_t
是无符号整型,因此它是该类型的最大可表示值。
class string
{private:char* _str = nullptr; // 字符指针, 字符串用于存储字符串size_t _size = 0; // 用于记录该字符串有几个有效字符(即字符串的长度)size_t _capacity = 0; // 用于记录该字符指针能存储的有效字符个数大小// Member constants:const static size_t npos = -1;
};
成员函数
构造函数
空字符串构造函数(默认构造函数)
构造一个长度为0个字符的空字符串。
string(const char* str = ""){size_t size = strlen(str);_size = _capacity = size;_str = new char[_capacity + 1];strcpy(_str, str);}
拷贝构造函数
这里需要先介绍两个概念:
浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
就像一个家庭中有两个孩子,但父母只买了一份玩具,两个孩子愿意一块玩,则万事大吉,万一不想分享就你争我夺,玩具损坏。
深拷贝
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。父母给每个孩子都买一份玩具,各自玩各自的就不会有问题了。
拷贝构造函数实现
构造一个str的副本,这里因为涉及到资源管理的问题,所以拷贝构造时,我们需要重新创建一个空间,来存储str的内容。否则如果直接浅拷贝的话,会导致一下两个问题:
- 对其中一个字符串改变时,另一个字符串也会跟着被改变(因为它们管理的是同一片空间)
- 在调用析构函数时,同一份资源被释放两次
string(const string& str){_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}
string
构造函数不止有这两个,如果想要去实现其他的,也不是很困难。只是这两个构造函数,在大部分的情况下就够用了。
析构函数
对于这些参与空间资源分配的类,我们都要自己写析构函数,否则很可能导致内存泄漏。
~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}
赋值重载
为字符串赋一个新值,替换其当前内容(如果是两个相等的字符串就没必要相互赋值了)。
string& operator=(const string& str){if (str != *this){delete[] _str;_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}return *this;}
这里还有一种写法,我们可以复用拷贝构造函数:
string& operator=(const string& str){if (str != this->_str){string tmp = str;swap(tmp);}return *this;}
当然这里的swap函数是需要我们自己实现的:
void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}
这里我复用了std空间里面的swap函数,将string类中的成员变量都交换掉。
空间管理函数
这里只模拟实现我勾选的这些函数。
实现这些功能并不复杂,代码如下:
size_t capacity() const{return _capacity;}size_t size() const{return _size;}void reserve(size_t n = 0){assert(_str);// 我实现的是只考虑扩容的,不考虑缩容if (_capacity < n){char* tmp = new char[n + 1]; // 开辟空间strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){assert(_str);// 不考虑缩容的情况if (n > _capacity){reserve(n);for (size_t i = _size;i < n;++i)_str[i] = c;_str[_size = n] = '\0';}}void clear(){_str[_size = 0] = '\0';}bool empty() const{return _size == 0;}
元素访问
要实现[]
重载并不复杂,但是需要写两个,一个是可读可写的,一个是只读的:
char& operator[](size_t n){assert(n >= 0);assert(n < _size);return _str[n];}const char& operator[](size_t n) const{assert(n >= 0);assert(n < _size);return _str[n];}
元素修改
这里有些函数我是重载了几个版本的,具体实现细节我写进注释里了
void push_back(char c){assert(_str);// 先判断是否需要扩容if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}_str[_size] = c;_str[++_size] = '\0'; // 增加字符以后不要忘记将_size增加}string& append(const string& str){assert(_str && str._str);// 先判断是否需要扩容size_t len = strlen(str._str);if (_size + len >= _capacity){size_t newcapacity = _size + len + 10; // 每次扩容都多增加是个容积的容错reserve(newcapacity);}memmove(_str + _size, str._str, len);_str[_size += len] = '\0';return *this;}string& operator+=(char c){push_back(c);return *this;}string& operator+=(const string& str){append(str);return *this;}string& insert(size_t pos, const string& str){assert(pos <= _size);size_t len = strlen(str._str);if (_size + len >= _capacity)reserve(_size + len);size_t end = _size + len;while (end - len + 1 > pos){_str[end] = _str[end - len];--end;}memmove(_str + pos, str._str, len);_size += len;return *this;}string& insert(size_t pos, char c){assert(pos <= _size);if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;++_size;return *this;}string& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[end - len] = _str[end];++end;}_size -= len;}return *this;}void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}
字符串运算
c_str
和data
的功能都是一样的。这两个函数是为了能够让string类兼容C语言中的一些函数。比如:我们使用C语言中的fopen
函数打开一个文件名(文件名是一个常量字符串),而假设我们使用string
类来管理文件名,这是,就需要这两个函数来将string
类转换成常量字符串了。
const char* c_str(){return _str;}const char* data(){return _str;}size_t find(const string& str, size_t pos = 0) const{char* tmp = strstr(_str + pos, str._str);if (tmp == nullptr) // 查找子串失败return npos;return tmp - _str; // 返回两个指针之间的间隔距离,即表示找到的子串的起始位置}size_t find(char c, size_t pos = 0) const{for (size_t i = 0;i < _size;++i)if (_str[i] == c) return i;return npos;}size_t rfind(char c, size_t pos = 0) const{for (size_t i = _size - 1;i != npos;--i)if (_str[i] == c) return i;return npos;}string substr(size_t pos = 0, size_t len = npos) const{assert(pos < _size);string str;if (len == npos || pos + len >= _size)len = _size;for (size_t i = pos;i < len;++i)str += _str[i];return str;}
流提取 & 流插入
流提取
因为在STL中string
类的对象是可以直接使用cout
打印出来的,如果我们也想让我们自己实现的string
能够直接打印出来,就需要重载一下<<
操作符,具体操作如下:
std::ostream& operator<<(std::ostream& out, const string& str)
{for (auto& e : str)out << e;return out;
}
流插入
STL中string
类也是支持cin
插入数据的,如果我们也想让我们自己实现的string
能够使用cin
插入数据,就需要重载一下>>
操作符,具体操作如下:
std::istream& operator>>(std::istream& in, string& str)
{str.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){str += ch;ch = in.get();}return in;
}
我们需要先将原string
对象中的资源清理一下,再插入。这里是利用了istream
类中的get()
函数从输入缓冲区获取我们输入的字符,然后再使用我们在上面重载的+=
操作符,来将我们输入的一组数据插入到一个string
对象中。
但这里存在一个问题:
就是如果我们每次读取一个字符,再用+=
这个操作符时,假设输入缓冲区中的数据量很大,那么必定存在很多次扩容,扩容还是会有一定的消耗的,所以我们使用一个字符数组来充当缓冲区,现将字符读入到这个数组中,待数组满了以后,我们再将这个数组中的数据插入到string
对象中,这样就能有效的降低扩容的次数了
std::istream& operator>>(std::istream& in, string& str)
{str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != ' ' && ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;
}
这里还需要重载一个函数getline()
获取一行字符串。因为>>
操作符,遇到空格或者回车就结束读取了,如果我们需要读取一行字符串(里面可能含有空格),这是就需要使用getline()
了
std::istream& getline(std::istream& in, string& str)
{str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;
}
迭代器
迭代器的设计很巧妙的,不同的容器都能够通过相同的方式去访问,这就是迭代器的强大之处。下面就先通过string
的迭代器带大家先了解一下迭代器,随着后面模拟实现其他容器,迭代器的结构也会相应的发生变化。
begin & end
这里只模拟实现我标记出来的那些,C++11出的那几个迭代器,主要是为了区分普通迭代器(iterator)和常量迭代器(const_iterator),但其实,我们用上面的那些就能够涵盖普通迭代器与常量迭代器了(因为普通迭代器和常量迭代器是可以重载的)
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; // 返回字符串的最后一个字符的下一个位置}
因为string
的底层空间是连续的,所以我们可以直接使用原生指针来定义为迭代器。
当然,使用范围for的前提是需要满足满足迭代器begin
和end
,才能满足范围for
这里我只是将begin
改为Begin
,就不能支持范围for了
rbegin & rend
再说反向迭代器之前,需要具备适配器模式的知识,如果不是很了解可以先看看这篇文章:适配器模式
解释一下为什么要具有适配器模式的知识再往下看:
如果我们只是单纯的完成string类的反向迭代器的功能,其实就可以直接复制一遍正向迭代器的代码,改一下就能实现这个功能了。
但STL中有很多容器,每个容器的底层实现都不一样,所以C++中的迭代器主要有以下几种类型:
- 输入迭代器:只读,只能顺序向前移动。
- 输出迭代器:只写,只能顺序向前移动。
- 前向迭代器:可读写,能顺序向前移动。
- 双向迭代器:可读写,能顺序向前和向后移动。
- 随机访问迭代器:可读写,能跳跃式地访问元素。
有了适配器模式,我们写一份代码。就能让与该迭代器模式相似的容器中的反向迭代器复用这里的代码(比如,string
和vector
的迭代器都是支持随机访问的,所以我们写一份能够支持随机访问的反向迭代器的代码,就能使string
和vector
都能够复用)
这里使用了三个模板参数,目的如下:
在使用迭代器时,我们难免会使用->
(结构体指针访问成员)操作符,或者使用*
(解引用)操作符(关于这两个操作符的重载,在模拟实现list章节会重点讲,因为在string
和vector
的底层物理结构是连续的空间,我们用原生指针就能当做它的迭代器,所以不需要重载这两个操作符。但是我们在这里实现的反向迭代器不是原生指针,而是用一个结构体封装起来的结构,从上面实现可以看出。所以需要重载这两个操作符。这里只要先知道,重载->
操作符的返回值是char*
或者const char*
,重载*
操作符的返回值是char&
或者const char&
)。
因为 const char&
是只读的,而 char&
可读可写,同理char*
可以改变所指向的内容,const char*
不能改变所指向的内容。所以我们需要将两个迭代器区分开来,而我们嵌套三个模板参数,就可以用后面两个模板参数来区分了。这样,我们就只用写一份代码,const和非const的迭代器就都能使用这一份代码了。我在这里用一个图给大家展示一下:
但是请注意:在string中,我们并不会用到->
操作符,因为->
操作符是自定义类型的指针对其成员的一个访问操作符,string类不会去存一个自定义类型,因为在后面模拟实现vector类的时候,反向迭代器我也会使用这段代码,所以这里我设计成三个模板参数。(实际上char&
或const char&
是不用传递过来的,但为了能够使用这串代码传过来也一样的)
再来看看实现部分,这里我是模拟库里面实现了一个对称的结构:
所以我们在对迭代器进行解引用操作时,是对当前位置的前一个位置解引用(此时迭代器的位置不发生改变,所以我们需要创建一个迭代器用于记录当前位置的前一个位置,再对该位置解引用)。
而对于前置++/–,和后置++/–的实现,是利用正向迭代器向相反方向移动实现的:
具体一些操作实现如下:
template<class Iterator, class Reference, class Point>struct __random_reverse_iterator{typedef __random_reverse_iterator self;__random_reverse_iterator(const Iterator& it):_cur(it){}Reference operator*(){Iterator tmp = _cur;return *(--tmp);}Point operator->(){return &(this->operator*());}self& operator++(){--_cur;return *this;}self operator++(int){self tmp = *this;--_cur;return tmp;}self& operator--(){++_cur;return *this;}self operator--(int){self tmp = *this;++_cur;return tmp;}self& operator+=(int nums){_cur -= nums;return *this;}self& operator-=(int nums){_cur += nums;return *this;}bool operator!=(const self& it){return _cur != it._cur;}bool operator==(const self& it){return _cur == it._cur;}private:Iterator _cur;};// 下面是我们实现的string类(只含反向迭代器部分)
class string
{
public:typedef __random_reverse_iterator<iterator, char&, char*> reverse_iterator;typedef __random_reverse_iterator<const_iterator, const char&, const char*> const_reverse_iterator;reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}const_reverse_iterator rbegin() const{return const_reverse_iterator(end());}const_reverse_iterator rend() const{return const_reverse_iterator(begin());}
};
这里我并没有将这个反向迭代器的所用功能实现完,但是基本的框架也是有了。
总结
我在这里将我模拟实现string类的源码附在下面,供大家参考一下如何封装的(因为是在学习阶段,所以封装做得可能不是太好,以下只是我个人的理解):
#include <cstring>
#include <cassert>namespace hyt
{template<class Iterator, class Reference, class Point>struct __random_reverse_iterator{typedef __random_reverse_iterator self;__random_reverse_iterator(const Iterator& it):_cur(it){}Reference operator*(){Iterator tmp = _cur;return *(--tmp);}Point operator->(){return &(this->operator*());}self& operator++(){--_cur;return *this;}self operator++(int){self tmp = *this;--_cur;return tmp;}self& operator--(){++_cur;return *this;}self operator--(int){self tmp = *this;++_cur;return tmp;}self& operator+=(int nums){_cur -= nums;return *this;}self& operator-=(int nums){_cur += nums;return *this;}bool operator!=(const self& it){return _cur != it._cur;}bool operator==(const self& it){return _cur == it._cur;}private:Iterator _cur;};class string{public: // 声明友元函数friend bool operator==(const string& lhs, const string& rhs);friend bool operator!=(const string& lhs, const string& rhs);friend bool operator>(const string& lhs, const string& rhs);friend bool operator>=(const string& lhs, const string& rhs);friend bool operator<(const string& lhs, const string& rhs);friend bool operator<=(const string& lhs, const string& rhs);public: // 迭代器typedef char* iterator;typedef const char* const_iterator;typedef __random_reverse_iterator<iterator, char&, char*> reverse_iterator;typedef __random_reverse_iterator<const_iterator, const char&, const char*> const_reverse_iterator;// 普通迭代器iterator begin(){return _str;}iterator end(){return _str + _size;}// const迭代器const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}// 反向普通迭代器reverse_iterator rbegin(){return reverse_iterator(end());}reverse_iterator rend(){return reverse_iterator(begin());}// 反向const迭代器const_reverse_iterator rbegin() const{return const_reverse_iterator(end());}const_reverse_iterator rend() const{return const_reverse_iterator(begin());}public:// 构造函数string(const char* str = ""){size_t size = strlen(str);_size = _capacity = size;_str = new char[_capacity + 1];strcpy(_str, str);}// 拷贝构造string(const string& str){_str = new char[str._capacity + 1];strcpy(_str, str._str);_size = str._size;_capacity = str._capacity;}// 赋值操作符重载string& operator=(const string& str){if (str != this->_str){string tmp = str;swap(tmp);}return *this;}// 析构~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}// Capacity:size_t capacity() const{return _capacity;}size_t size() const{return _size;}void reserve(size_t n = 0){assert(_str);// 我实现的是只考虑扩容的,不考虑缩容if (_capacity < n){char* tmp = new char[n + 1]; // 开辟空间strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char c = '\0'){assert(_str);// 不考虑缩容的情况if (n > _capacity){reserve(n);for (size_t i = _size;i < n;++i)_str[i] = c;_str[_size = n] = '\0';}}void clear(){_str[_size = 0] = '\0';}bool empty() const{return _size == 0;}// String operations:const char* c_str(){return _str;}const char* data(){return _str;}size_t find(const string& str, size_t pos = 0) const{char* tmp = strstr(_str + pos, str._str);if (tmp == nullptr) // 查找子串失败return npos;return tmp - _str; // 返回两个指针之间的间隔距离,即表示找到的子串的起始位置}size_t find(char c, size_t pos = 0) const{for (size_t i = 0;i < _size;++i)if (_str[i] == c) return i;return npos;}size_t rfind(char c, size_t pos = 0) const{for (size_t i = _size - 1;i != npos;--i)if (_str[i] == c) return i;return npos;}string substr(size_t pos = 0, size_t len = npos) const{assert(pos < _size);string str;if (len == npos || pos + len >= _size)len = _size;for (size_t i = pos;i < len;++i)str += _str[i];return str;}// Element access:char& operator[](size_t n){assert(n >= 0);assert(n < _size);return _str[n];}const char& operator[](size_t n) const{assert(n >= 0);assert(n < _size);return _str[n];}//Modifiers:void push_back(char c){assert(_str);// 先判断是否需要扩容if (_size == _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}_str[_size] = c;_str[++_size] = '\0'; // 增加字符以后不要忘记将_size增加}string& append(const string& str){assert(_str && str._str);// 先判断是否需要扩容size_t len = strlen(str._str);if (_size + len >= _capacity){size_t newcapacity = _size + len + 10; // 每次扩容都多增加是个容积的容错reserve(newcapacity);}memmove(_str + _size, str._str, len);_str[_size += len] = '\0';return *this;}string& operator+=(char c){push_back(c);return *this;}string& operator+=(const string& str){append(str);return *this;}string& insert(size_t pos, const string& str){assert(pos <= _size);size_t len = strlen(str._str);if (_size + len >= _capacity)reserve(_size + len);size_t end = _size + len;while (end - len + 1 > pos){_str[end] = _str[end - len];--end;}memmove(_str + pos, str._str, len);_size += len;return *this;}string& insert(size_t pos, char c){assert(pos <= _size);if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 10 : 2 * _capacity;reserve(newcapacity);}size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = c;++_size;return *this;}string& erase(size_t pos = 0, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{size_t end = pos + len;while (end <= _size){_str[end - len] = _str[end];++end;}_size -= len;}return *this;}void swap(string& str){std::swap(_str, str._str);std::swap(_size, str._size);std::swap(_capacity, str._capacity);}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;// Member constants:const static size_t npos = -1;};bool operator==(const string& lhs, const string& rhs){return (!(strcmp(lhs._str, rhs._str)) && lhs._size == rhs._size);}bool operator!=(const string& lhs, const string& rhs){return !(lhs == rhs);}bool operator>(const string& lhs, const string& rhs){return strcmp(lhs._str, rhs._str) > 0;}bool operator>=(const string& lhs, const string& rhs){return (lhs > rhs || lhs == rhs);}bool operator<(const string& lhs, const string& rhs){return !(lhs >= rhs);}bool operator<=(const string& lhs, const string& rhs){return (lhs < rhs || lhs == rhs);}// 重载 流插入 & 流提取std::ostream& operator<<(std::ostream& out, const string& str){for (auto& e : str)out << e;return out;}std::istream& operator>>(std::istream& in, string& str){str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != ' ' && ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;}std::istream& getline(std::istream& in, string& str){str.clear();char buffer[128] = {};int count = 0;for (char ch = in.get();ch != '\n';ch = in.get()){buffer[count++] = ch;if (count == 127){str += buffer;count = 0;}}if (count > 0){buffer[count] = '\0';str += buffer;}return in;}
}