【STL源码剖析】string类模拟实现 了解底层-走进底层-掌握底层【超详细的注释和解释】
博主对大家的话
从今天开始,STL源码剖析的专栏就正式上线了!其实在很多人学习C++过程中,都是只学习一些STL的使用方式,并不了解底层的实现。博主本人认为,这样的学习这样的技术是不深的。如果我们想要熟悉的掌握一门语言,我认为,底层的实现必不能少!
但是,想从0开始模拟实现STL的容器,需要我们熟悉C++的语法,特别是类和对象部分的知识!
博主学习C++到现在,我认为C++类和对象基本语法的学习比任何部分都要重要,而我花在这上面的时间也是最多的!只要搞清楚C++面向对象编程的基本规则,我们才能在STL的世界里游刃有余!
所以我希望大家在学习STL之前,先将数据结构与算法,C++的类和对象部分内容掌握熟悉!
前言
那么这里博主先安利一下一些干货满满的专栏啦!
数据结构专栏:数据结构 这里包含了博主很多的数据结构学习上的总结,每一篇都是超级用心编写的,有兴趣的伙伴们都支持一下吧!
STL源码剖析专栏:STL容器的模拟实现 以STL源码为基础,模拟实现STL的各种容器。从底层开始,一步一步解密STL。掌握底层,才是真正学会STL
算法专栏:算法
力扣刷题专栏:Leetcode
C的深度解剖专栏:C语言的深度解剖
**
实现过程一些要注意的点
因为STL库里面的string
其实是很多年发展出来的类,里面一共有106个成员函数。
在这里面其实并不是每一个我们都要去实现的,我们要去实现的其实是一些重要的,必备的,有代表性的功能。其实关于string
类发展到现在这么久,其实很多人对它有很多不同的评价,这里博主分享一篇文章。
因此,在博主的模拟实现中,只提供了STL中的部分接口,但是这些,对于在初学阶段来说,足够了!
对于实现过程的一些细节问题,博主会在源代码的注释中指出!
STL中string类模拟实现
#pragma once
#include<cstring>
#include<cassert>
#include<iostream>
using namespace std;
//为了区分和库里面的,我们用命名空间包起来
namespace MyString {
class string {
public:
//迭代器和const迭代器
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;
}
//构造
#if false
string()//初始化的时候一定不能给nullptr,因为如果创建一个空对象的时候,去打印
//就会造成空指针的解引用
//或者给全却称
:_str(new char[1]),
_size(0),
_capacity(0)
{
_str[0] = '\0';//给一个空间放'\0'
}
#endif
string(const char* str = "\0")//写""是等价的,因为""会自带'\0'
//:_str(new char[strlen(str) + 1]),
//_size(strlen(str)),
//_capacity(strlen(str)) -- 这样搞用三次strlen() -- 别用初始化列表了
{
//动态开辟空间
size_t len = strlen(str);
_str = new char[len + 1];
_size = len;
_capacity = len;
strcpy(_str, str);
}
//析构
~string() {
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
//拷贝构造(实现一个深拷贝)
//写法1:传统写法
#if 0
string(const string& s)
:_str(new char[s._capacity+1]),
_size(s._size),
_capacity(s._capacity)
{
strcpy(_str, s._str);
}
//赋值重载
string& operator=(const string& s) {
if (this == &s)return *this;//自己给自己赋值 -- 直接避开
//无论什么情况,都选择把原来的空间释放掉+开新的空间+拷贝
char*tmp = new char[s._capacity + 1];//一定要记得+1
//new失败怎么办 -- 这样就会造成拷贝失败,而且原来的string被释放了,所以最好先new再delete原来的
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
//这样写的话,自己给自己赋值就会出问题 -- 因为自己给自己释放了 -- 拷贝自己 -- 拷贝到的就是随机值!
}
#endif
//现代写法
//构造来当打工人
void swap(string& tmp) {
std::swap(_str, tmp._str);
std::swap(_size, tmp._size);
std::swap(_capacity, tmp._capacity);
}
string(const string& s)
: _str(nullptr),
_size(0),
_capacity(0)
{
//因为s里面的东西可能是随机值
//如果换给tmp之后,tmp出了作用域是会析构的 -- 析构随机的东西就会出问题
//析构0或者null是不会出问题的,所以初始化一下
//因为swap要频繁调用,我们这里自己再写一个最好
string tmp(s._str);
swap(tmp);//this->swap(tmp)
}
//写法2:
#if 0
string& operator=(const string& s) {
if (this == &s)return *this;//自己给自己赋值 -- 直接避开
string tmp(s);
swap(tmp);//this和swap换
return *this;
//把原来的s换给tmp之后 -- tmp还需要帮忙打扫s的空间,因为tmp是一个局部对象!
}
#endif
//写法3
string& operator=(string s) {//s就是顶替tmp,s就完成拷贝了,而且是局部对象!
swap(s);
return *this;
}
//在以后复杂数据结构的学习中 -- 现代写法的优势会更大
//size
size_t size()const {
return _size;
}
size_t capacity()const {
return _capacity;
}
//[]重载
char& operator[](size_t pos) {
assert(pos < _size);
return _str[pos];
}
char& operator[](size_t pos) const { //这个是不能写的
assert(pos < _size);
return _str[pos];
}
//兼容C字符串接口
const char* c_str() {
return _str;
}
void reserve(size_t n) {
if (n > _capacity) {
//扩容不缩容
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void resize(size_t n, char ch = '\0') {
//开空间+初始化
if (n > _size) {
//插入数据
reserve(n);
for (size_t i = _size; i < n; i++) {
_str[i] = ch;
}
_str[n] = '\0';
_size = n;
}
else {
//删除数据
_str[n] = '\0';
_size = n;
}
}
void push_back(char ch) {
//不用去写CheckCapacity函数,我们先写reserve这个函数,复用!
#if 0
if (_size==_capacity)
{
//注意:如果一开始是用了构造的缺省值,也就是0,就不能扩二倍!
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';//一定要记得'\0'
#endif
insert(_size, ch);
}
void append(const char* str) {
#if 0
size_t len = strlen(str);
if (_size + len > _capacity)//注意,此时不能扩容二倍了!因为可能不够
{
reserve(_size + len);
}
strcpy(_str + _size, str);//哪个好?
//strcat(_str, str);//这个效率是比较低的 -- 因为要去strcat要去找'\0',持续追加的时候效率非常低!
_size += len;
#endif
insert(_size, str);
}
string& operator+=(char ch) {
push_back(ch);
return *this;
}
string& operator+=(const char* str) {
append(str);
return *this;
}
//insert
string& insert(size_t pos, char ch) {
assert(pos <= _size);
if (_size == _capacity) {
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
//pos==0情况第一种处理方法
#if 0
int end = _size;
while (end >= (int)pos) {
//注意:pos==0的时候会出问题,end--之后会变成-1->整型最大值,所以给pos强转一下,_size改成int就行
_str[end + 1] = _str[end];
--end;
}
#endif
//第二种处理方法(比较推荐)
size_t end = _size + 1;//这些都别改,继续用uint
while (end > pos)//这里写成大于!
{
_str[end] = _str[end - 1];//这里改成-1
--end;
}
_str[pos] = ch;
++_size;
return * this;
}
//实现这些接口一定要很细心很细心!
string& insert(size_t pos, const char* str) {
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity) {
reserve(_size + len);
}
//挪动数据
size_t end = _size + len;//这些都别改,继续用uint
while (end >= pos + len)//这里写成大于!
{
_str[end] = _str[end - len];//这里改成-1
--end;
}
//放数据
strncpy(_str + pos, str, len);//这里用ncpy,不要把'\0'拷贝进去
_size += len;
return *this;
}
//erase
string& erase(size_t pos, size_t len = npos) {
assert(pos < _size);
if (len == npos || pos + len >= _size) {
//相当于pos后面的全部删掉了
_str[pos] = '\0';
_size = pos;
}
else {
//删除部分
//需要挪动数据
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
void clear() {
_str[0] = '\0';
_size = 0;
}
//find系列
#if true
size_t find(char ch, size_t pos = 0) {
assert(pos < _size);
for (size_t i = pos; i < _size; i++) {
if (ch == _str[i]) {
return i;
}
return npos;
}
}
size_t find(const char* sub, size_t pos = 0) {
//strstr -- 暴力
//字符串匹配算法 kmp/bm
assert(pos < _size);
assert(sub);
const char* ptr = strstr(_str + pos, sub);
if (ptr == nullptr) {
return npos;
}
return ptr - _str;
}
//substr -- 从pos位置开始,取len个字符
string substr(size_t pos = 0, size_t len = npos)const {
//不用改变自己,const也行
assert(pos < _size);
size_t realLen = len;
if (len == npos || pos + len > _size) {
//超范围了
//有多少取多少
realLen = _size - pos;
}
string sub;
for (size_t i = 0; i < realLen; ++i) {
sub += _str[pos + i];
}
return sub;
}
#endif
//运算符重载比较系列
#if true
bool operator>(const string& s)const {
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s)const {
return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s)const {
return *this > s || *this == s;
}
bool operator<=(const string& s)const {
return !(*this > s);
}
bool operator<(const string& s)const {
return !(*this >= s);
}
bool operator!=(const string& s)const {
return !(*this == s);
}
#endif
private:
char* _str;
size_t _size;
size_t _capacity;
public:
//这个npos按道理在外面也可以取到才对
const static size_t npos = -1;
};
//流提取和流插入
//不是必须是友元的,因为不一定要访问私有,这里就不用
ostream& operator<<(ostream& out, const string& s) {
for (size_t i = 0; i < s.size(); i++) {
out << s[i];
}
return out;
}
istream& operator>>(istream& in, string& s) {
s.clear();
//输入字符串很长,不断+=,频繁扩容,效率很低,大家可以想办法优化一下
//1.reserve() -- 缺陷:浪费空间
//2.别动string先,先放到一个临时数组里面,因为这个临时数组是在栈上的 -- 出了>>就销毁了
char ch;
ch = in.get();
const size_t N = 32;
char buff[N];
size_t i = 0;
//s.reserve(128);
while (ch != ' ' && ch != '\n') {
buff[i++] = ch;
if (i == N - 1) {//表示满了
buff[i] = '\0';
s += buff;//一批一批加到string里面
i = 0;
}
ch = in.get();
}
//把最后一批加上去
//buff相当于缓冲了
buff[i] = '\0';
s += buff;
return in;
//但是现在会有bug
//如果字符串里面原来有东西的话,会留下
//我们先要清空一下string
}
//size_t string::npos = -1;//在类外面定义
//但是const static变量可以在类里面给缺省,外面就不用写了,这是C++的特例
}
尾声
看到这里,相信大家对string
类的模拟实现已经有一定的了解了!string
的模拟实现,知识我们掌握STL的开始,后面,博主将会给大家带来vector
,list
等等STL容器的模拟实现,持续关注,订阅专栏,点赞收藏都是我创作的最大动力!
( 转载时请注明作者和出处。未经许可,请勿用于商业用途 )
更多文章请访问我的https://blog.csdn.net/Yu_Cblog?type=blog