【C++】 string底层封装的模拟实现
目录
- 前情提要
- Member functions —— 成员函数
- 构造函数
- 拷贝构造函数
- 赋值运算符重载
- 析构函数
- Element access —— 元素访问
- Iterator —— 迭代器
- Capacity —— 容量
- size
- capacity
- clear
- empty
- reserve
- resize
- Modifiers —— 修改器
- push_back
- append
- operator+=(char ch)
- operator+=(const char* s)
- 在pos位置插入n个字符
- 删除pos位置的n个元素
- swap
- String Operations —— 字符串操作
- 从pos位置开始找指定的字符
- 从pos位置开始找指定的字符串(找子串)
- 从pos位置开始取len个有效字符(取子串)
- 流插入和流提取
- 流插入
- 流提取
- swap函数解析
- 源码
前情提要
因为我们接下来实现的类和和库中std命名空间的string类的类名相同,所以我们为了防止冲突,用一个bit命名的命名空间解决这个问题
namespace bit
{
class string {
public:
//...
private:
size_t _size;
size_t _capacity;
char* _str;
};
}
- 接下去呢,就在测试的test3.cpp中包含一下这个头文件,此时我们才可以在自己实现的类中去调用一些库函数
#include <iostream>
#include <assert.h>
using namespace std;
#include "string.h"
Member functions —— 成员函数
构造函数
- 这里我们把构造函数的声明放在string.h的头文件中,然后用分文件编写的设计在stirng.cpp中实现构造函数的源代码
string::string()
:_str(new char[1] {'\0'})
, _size(0)
, _capacity(0)
{
}
- 我们利用构造函数的初始化列表来初始化类的3个成员变量,_str表示存储的string的内存空间,_size表示string对象的长度,_capacity表示这个string 对象的占用空间是多少个字节
或许有的兄弟会有疑问,为什么初始化_str的时候,开辟_str的空间要用 new char[1]而不是new char,这是因为我们string的数据存储都是连续存储在一起的,用\0标识结束位置,如果用new char那么单独在各个不连续的空间并不能让每个数据后面都有\0,所以我们new char[]连续的空间,存储在一起,在这块连续的空间后放上\0。
- 然后我们立即来测试一下,因为我们自己实现的 string类 是包含在了命名空间bit中的,那么我们在使用这个类的时候就要使用到 域作用限定符::
bit::string s1;
接下来有了无参的构造函数,我们再重载一个有参的字符串构造函数
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
- 先说一下为什么初始化列表我值初始化了_size
- 看到了吗?不是,哥们,怎么_size都还没初始化就去把_str初始化了,这个时候问题就不来了吗,开的空间大小就是随机的一个值。
- 那为什么会出现这个情况呢?
- 这个我在类和对象的文章里面讲过,初始化列表要按照类中成员变量的声明顺序来初始化,很明显_str这个变量比_size这个变量先声明,所以这里就会出现先初始化_str变量需要用_size,但是_size还没有初始化的问题
综上,这就是为什么在初始化列表初始只初始化_size的原因,这样保证一个变量的顺序一定是正确的。其余的变量在函数体里面初始化, 当然第一个无参构造也可以这么写,我这里提出来这个写法就是想说明注意这个问题。
拷贝构造函数
- 在类和对象的文章中,我讲过,一个对象拷贝给另一个对象,拷贝构造函数对于内置类型不做处理,对于自定义类型那么就要调用那个被拷贝对象的默认构造函数来拷贝给另一个对象。那么这个时候就会发生浅拷贝的问题。
- 所谓的浅拷贝,就是如上面一样, s2是s1的拷贝,但是这种拷贝他不会让s1指向原来的空间,s2指向新开辟的空间,浅拷贝只是把s1中3个成员变量的值拷贝过去,并不会执行开空间操作,这时候两个对象的_str的成员变量都指向一个空间,就会导致其中一个对象已经析构了这块空间,另一个对象销毁的时候又对这块已经被析构了的空间造成二次析构,这相当于就是内存泄露,一块空间已经还给操作系统了,我们还通过一个指针来访问这块空间,这时候编译器就会报错了。
- 所以我们就要自定义来实现一个拷贝构造函数解决,两个指针指向同一块空间造成两次析构的问题。
string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
- 这段代码,我们就单独给_str开了和被拷贝对象一样大的空间,然后把被拷贝对象的数据拷贝到_str中,完成了深拷贝
赋值运算符重载
对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个
- 但是呢默认生成的这个也会造成一个 浅拷贝 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1和s3就会指向一块同一块空间,此时便造成了下面这些问题
1.又会造成上面拷贝构造的问题,两次析构
2.因为他们指向同一块空间,修改一个对象就会影响另一个对象
3.原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题
string& string::operator = (string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
}
- 和拷贝构造一样,但是多了的是我们要把被赋值的对象的_str内存释放了,不然那块空间就没人管了,就会导致内存泄露。
- 再补充一点这里为啥要判断this != &s ,就是自己给自己赋值,也许大部分人不会这么做,但是不一定有人会用错赋值,就像汽车的车窗防夹功能一样,没人会主动让车窗夹,但是总有些特殊情况。
下面我们来写一个现代的懒人写法
string& string::operator = (string s)
{
if (this != &s)
{
swap(s);
return *this;
}
}
- 是不是非常的简洁,这个就是用了一个swap函数来解决,来现在我们就来详细说一下这个swap函数是怎么完成赋值拷贝的。
- 这就完美的完成了我们s1得到了s2的数据,又把原来s1的空间释放了
- 附上swap的源代码,就是和我们模拟的一样,改变了_str的指向
string::string(const string& s)
{
string tmp(s.c_str());
swap(tmp);
}
- 那么拷贝构造也可以利用这个swap函数来完成拷贝给this对象的问题
析构函数
最后的话就是析构函数这一块,前面在调试的过程中我们已经看到很多遍了,此处不再细述
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
Element access —— 元素访问
- 嘿嘿,这个接口可是非常的好用,让我们string可以像数组一样使用,爽得很。
char& string::operator[](size_t pos)
{
assert(pos >= 0 && pos < _size);
return _str[pos];
}
- 因为我们string底层封装的用来存储数据的变量就是一个char*的_str指针,我们在C语言阶段说过,指针是可以像数组那样使用的,所以直接返回_str的pos位置元素就行了,另外注意pos一定是在有效的范围内,所以断言一下
// 可读不可写
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
- 这个版本就是提供给我们const的string对象使用的
那现在有人问,所以这玩意实现来有什么用?不急,请看VCR
- 是不是很轻松就拿到了string对象中的每个数据
- 看到没,可以像数组一样更改sting对象的数据
所以非常的方便,const对象一样用法,就不展示了。
Iterator —— 迭代器
- 当然,除了[]可以访问我们的string对象的数据,迭代器也可以
-而对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的
typedef char* iterator;
typedef const char* const_iterator;
- 这里的话我就实现一下最常用的【begin】和【end】,首位的话就是_str所指向的这个位置,而末位的话则是_str + _size所指向的这个位置
iterator begin()
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
- const迭代器就是照葫芦画瓢了,只需要重载就行了
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
Capacity —— 容量
size
- 首先是size, 这个接口非常的简单,就是统计一下元素个数就行了. 因为不会改变this变量,所以加上一个 const
size_t size() const
{
return _size;
}
capacity
- 对于 capacity() 也是同样的道理
size_t capacity() const
{
return _capacity;
}
clear
- 这个注意只清理元素个数,不会清理内存空间。我们直接在_str[0]这个位置放上一个\0即可,并且再去修改一下它的_size = 0即可
void clear()
{
_str[0] = '\0';
_size = 0;
}
empty
- 对于 empty() 来说呢就是对象中没有数据,那么使用0 == _size即可
bool empty() const
{
return 0 == _size;
}
reserve
- 当我们构造函数初始化开辟的空间内存不够的时候,我们就要进行扩容。
// 扩容(修改_capacity)
void reserve(size_t newCapacity = 0)
{
// 当新容量大于旧容量的时候,就开空间
if (newCapacity > _capacity)
{
// 1.以给定的容量开出一块新空间
char* tmp = new char[newCapacity + 1];
// 2.将原本的数据先拷贝过来
strcpy(tmp, _str);
// 3.释放旧空间的数据
delete[] _str;
// 4.让_str指向新空间
_str = tmp;
// 5.更新容量大小
_capacity = newCapacity;
}
}
resize
1.如果n小于_size,那么就把元素个数调整到n就行
2.如果n大于_size小于_capacity,那么把元素个数调整到n,但是不扩容,把多的内存初始为\0
3.如果这个 n > _size 的话,我们便要选择去进行扩容了
void string::resize(size_t n, char ch)
{
if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
}
else
{
_size = n;
_str[n] = '\0';
}
}
Modifiers —— 修改器
push_back
- 首先明确一点,我们只要是要一直插入数据,就需要足够大的空间,需要空间我们要动态的扩容,这里我们设置默认空间4, 每次扩容两倍
- 最后扩容完,我们就可以安心插入数据了,但是要特别注意在尾插完加上一个\0
void string::push_back(char ch)
{
//插入之前,检查是否需要扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
append
- 接下来我们要追加的是一个字符串,首先我们要先计算出这个字符串的长度,然后在我们原有的元素个数上加上这个字符串的长度,如果总长度大于我们的内存再扩容到满足装下这个字符串的长度,否则两倍扩容即可,有效利用空间。
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len >= _capacity)
{
int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;
if (NewCapacity < _size + len)
{
NewCapacity = _size + len;
}
reserve(NewCapacity);
}
int j = 0;
for (size_t i = _size; i <= _size + len; i++)
{
_str[i] = str[j++];
}
_size += len;
}
operator+=(char ch)
- 首先的话是去【+=】一个字符,这里我们直接复用前面的push_back()接口即可,最后因为【+=】改变的是自身,所以我们return *this,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
operator+=(const char* s)
- 而对于【+=】一个字符串,我们则是去复用前面的append()即可
string& operator+=(const char* s)
{
append(s);
return *this;
}
在pos位置插入n个字符
void string::insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n >= _capacity)
{
int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;
if (NewCapacity < _size + n)
{
NewCapacity = _size + n;
}
reserve(NewCapacity);
}
int j = _size;
for (size_t i = _size + n; i >= pos + n ; i--)
{
_str[i] = _str[j--];
}
j = 0;
for (size_t i = pos; i <pos + n; i++)
{
_str[i] = ch;
}
_size += n;
}
- 首先计算我们插入n个字符后的长度,如果大于内存,我们就需要扩容。然后
删除pos位置的n个元素
void string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
int j = pos;
if (len != npos)
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[j++] = _str[i];
}
_size -= len;
}
else
{
_str[pos + 1] = '\0';
_size = pos + 1;
}
}
其实就是从pos + len 个位置的元素开始往前覆盖,就行,然后减少长度,在遍历的时候因为我们是遍历到长度的位置就不会遍历到长度之外的字符,然后后面插入的时候也能覆盖
swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
String Operations —— 字符串操作
从pos位置开始找指定的字符
- 从0开始遍历到长度的位置查找有没有指定的字符,如果有返回下标,没有返回npos
size_t find(char ch, size_t pos) const
{
assert(pos <= _size);
for (size_t i = 0; i < _size; i++)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
- npos是一个在const静态成员变量的无符号整数,用于标识没查找到指定的内容,原则来说静态成员变量应该在类外初始化,因为他要给类的所以对象使用,可是这里在缺省参数的位置初始化,这个位置原本是留给初始化列表中没初始化成功的,但是这是一个例外.条件必须是const常量 并且整形的数据才能这样写
从pos位置开始找指定的字符串(找子串)
- 这可以使用strstr子串查找函数,如果返回指针为空就没有,否则会返回子串的起始地址,如果我们想要得到子串在_str中的指针距离起始位置指针-指针即可
size_t find(const char* s, size_t pos) const
{
assert(pos < _size);
char* tmp = strstr(_str, s);
if (tmp)
{
// 指针相减即为距离
return tmp - _str;
}
return npos;
}
从pos位置开始取len个有效字符(取子串)
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > (_size -= pos))
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i <= len; i++)
{
sub += _str[pos + i];
}
return sub;
}
- 如果截取的长度大于_size - = pos,从pos开始的总长度,最多只能截取到 _size - pos,后面就复用sub += 尾插了
流插入和流提取
流插入
- 如果不想写s1 << cout这种就需要吧this这个在类中固定第一个位置的参数放在后面去,这样我们就不能重载在类中,重载在全局中
ostream& operator<<(ostream& os, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
os << s[i];
}
return os;
}
流提取
istream& operator>>(istream& is, string& s)
{
s.clear(); // 对原来的字符串清理,重新输入
char ch;
ch = is.get();
char buffer[256];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buffer[i++] = ch;
if (i == 255)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = is.get();
}
if (i > 0)
{
buffer[i] = '\0';
s += buffer;
}
return is;
}
- 1.这里输入ch一定要用流提取的get函数,不然cin 和scanf会直接忽略空格和\n, 不会写入到ch里面导致不能结束,get就不会忽略
2.这里用buffer数组的原因是:尽量减少扩容的次数和空间浪费的情况
3.所以我们使用一个数组buffer暂存里面,如果buffer存储满了再扩容,这时候只需要扩容一次,而且buffer是在栈上开辟的,是可以这个函数结束就销毁了
swap函数解析
- 为啥要自己写一个成员函数swap?可以看一下算法库swap的过程
源码
string.h
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<assert.h>
using namespace std;
namespace bit
{
class string
{
public:
typedef char* iterator;
typedef const char* const_iterator;
string();
string(const char* str);
string(const string& s);
~string();
void reserve(size_t n);
iterator begin();
iterator end();
char& operator[](size_t pos);
const char& operator[](size_t pos) const;
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void insert(size_t pos, size_t n, char ch);
void erase(size_t pos, size_t len = npos);
void clear();
void swap(string& s);
size_t size() const;
const_iterator begin() const;
const_iterator end() const;
void push_back(char ch);
void append(const char* str);
void resize(size_t n, char ch = '\0');
size_t find(char c, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string& operator+=(char ch);
string& operator+=(const char* str);
string& operator = (string s);
string substr(size_t pos, size_t len = npos);
const char* c_str() const
{
return _str;
}
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
public:
static const size_t npos = -1;
};
ostream& operator<<(ostream& os, const string& s);
istream& operator>>(istream& is, string& s);
istream& getline(istream& is, string& s, char delim = '#');
void swap(string& s1, string& s2);
}
string.cpp
#include"String.h"
#include<iostream>
using namespace std;
namespace bit
{
typedef char* iterator;
typedef const char* const_iterator;
string::string()
:_str(new char[1] {'\0'}) //不能初始化为null实现c_str的时候防止cout对空指针解引用
, _size(0)
, _capacity(0)
{
}
string::string(const char* str)
:_size(strlen(str))
{
_capacity = _size;
_str = new char[_size + 1];
strcpy(_str, str);
}
//传统写法
//string::string (const string& s)
//{
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _size = s._size;
// _capacity = s._capacity;
//}
string::string(const string& s)
{
string tmp(s.c_str());
swap(tmp);
}
string::~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
void string:: reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
iterator string::begin()
{
return _str;
}
char& string::operator[](size_t pos)
{
assert(pos >= 0 && pos < _size);
return _str[pos];
}
void string::resize(size_t n, char ch)
{
if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
}
else
{
_size = n;
_str[n] = '\0';
}
}
const char& string::operator[](size_t pos) const
{
assert(pos >= 0 && pos <= _size);
return _str[pos];
}
size_t string::size() const
{
return _size;
}
iterator string::end()
{
return _str + _size;
}
const_iterator string::begin() const
{
return _str;
}
const_iterator string::end() const
{
return _str + _size;
}
void string::push_back(char ch)
{
//插入之前,检查是否需要扩容
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (_size + len >= _capacity)
{
int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;
if (NewCapacity < _size + len)
{
NewCapacity = _size + len;
}
reserve(NewCapacity);
}
int j = 0;
for (size_t i = _size; i <= _size + len; i++)
{
_str[i] = str[j++];
}
_size += len;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
for (int i = _size + 1; i > pos; i--)
{
_str[i] = _str[i - 1];
}
_str[pos] = ch;
_size++;
_str[_size] = '\0';
}
void string::insert(size_t pos, size_t n, char ch)
{
assert(pos <= _size);
if (_size + n >= _capacity)
{
int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;
if (NewCapacity < _size + n)
{
NewCapacity = _size + n;
}
reserve(NewCapacity);
}
int j = _size;
for (size_t i = _size + n; i >= pos + n ; i--)
{
_str[i] = _str[j--];
}
j = 0;
for (size_t i = pos; i <pos + n; i++)
{
_str[i] = ch;
}
_size += n;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
int len = strlen(str);
if (_size + len >= _capacity)
{
int NewCapacity = _capacity == 0 ? 4 : 2 * _capacity;
if (NewCapacity < _size + len)
{
NewCapacity = _size + len;
}
reserve(NewCapacity);
}
int j = _size;
for (int i = _size + len ; i >= pos + len; i--)
{
_str[i] = _str[j--];
}
j = 0;
for (int i = pos; i < pos + len; i++)
{
_str[i] = str[j++];
}
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos <= _size);
int j = pos;
if (len != npos)
{
for (size_t i = pos + len; i <= _size; i++)
{
_str[j++] = _str[i];
}
_size -= len;
}
else
{
_str[pos + 1] = '\0';
_size = pos + 1;
}
}
void string::clear()
{
_size = 0;
_str[0] = '\0';
}
size_t string::find(char c, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
const char* ptr = strstr(_str + pos, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
/*string& string::operator = (string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
}*/
string& string::operator = (string s)
{
if (this != &s)
{
swap(s);
return *this;
}
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (len > (_size -= pos))
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = 0; i <= len; i++)
{
sub += _str[pos + i];
}
return sub;
}
ostream& operator<<(ostream& os, const string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
os << s[i];
}
return os;
}
istream& operator>>(istream& is, string& s)
{
s.clear(); // 对原来的字符串清理,重新输入
char ch;
ch = is.get();
char buffer[256];
int i = 0;
while (ch != ' ' && ch != '\n')
{
buffer[i++] = ch;
if (i == 255)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = is.get();
}
if (i > 0)
{
buffer[i] = '\0';
s += buffer;
}
return is;
}
istream& getline(istream& is, string& s, char delim)
{
s.clear(); // 对原来的字符串清理,重新输入
char ch;
ch = is.get();
char buffer[256];
int i = 0;
while ( ch != delim)
{
buffer[i++] = ch;
if (i == 255)
{
buffer[i] = '\0';
s += buffer;
i = 0;
}
ch = is.get();
}
if (i > 0)
{
buffer[i] = '\0';
s += buffer;
}
return is;
}
void string::swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
void swap(string& s1, string& s2)
{
s1.swap(s2);
}
}
这就是stirng的底层模拟实现,看完去实现一下吧