C++: string的模拟实现

C++: string的模拟实现

  • 一.前置说明
    • 1.模拟实现string容器的目的
    • 2.我们要实现的大致框架
  • 二.默认成员函数
    • 1.构造函数
    • 2.拷贝构造函数
      • 1.传统写法
      • 2.现代写法
    • 3.析构函数
    • 4.赋值运算符重载
      • 1.传统写法
      • 2.现代写法
  • 三.遍历和访问
    • 1.operator[]运算符重载
    • 2.iterator迭代器
  • 四.容量相关函数
    • 1.size,capacity
    • 2.reserve
    • 3.resize
  • 五.尾插
    • 1.push_back
    • 2.append
    • 3.operator+=运算符重载
  • 六.指定位置插入删除
    • 1.insert
      • 1.插入一个字符的版本
      • 2. 插入一个字符串的版本
    • 2.erase
  • 七.查找,交换,截取操作
    • 1.find
    • 2.swap
    • 3.substr
  • 八.几个小函数
    • 1.c_str
    • 2.clear
    • 3.empty
  • 九.流插入和流提取
    • 1.<<流插入
    • 2.>>流提取
  • 十.完整代码
    • 1.my_string.h:
    • 2.my_string.cpp
    • 3.test.cpp

一.前置说明

注意:本文会用到strcpy,strstr,strncpy,strlen这几个函数
我会说明它们的功能和用法
如果大家想要彻底了解这几个函数
可以看一下我之前的博客:
征服C语言字符串函数(超详细讲解,干货满满)

1.模拟实现string容器的目的

在这里插入图片描述
比如说leetcode字符串相加这道题
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
既然string的模拟实现对我们这么重要
那就让我们一起踏上string的模拟实现之旅吧

2.我们要实现的大致框架

我们经过string容器的使用的学习之后
我们知道string容器其实就是一个顺序表,只不过这个顺序表里面存储的都是char类型的数据
而且末尾有一个隐含的’\0’
因此我们的string容器可以这样定义

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
namespace wzs
{class string{public://nposstatic size_t npos;private:char* _str;//这就是string容器中的那个字符数组int _size;int _capacity;};//static size_t string::npos = -1;//errsize_t string::npos = -1;//yes
}

下面就是我们要实现的框架

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <iostream>
using namespace std;
#include <assert.h>
namespace wzs
{class string{public://nposstatic size_t npos;//1.全缺省的构造函数string(const char* str = "");~string();//2.拷贝构造函数string(const string& s);//3.赋值运算符重载string& operator=(const string& s);//4.返回C风格的字符串const char* c_str() const;//5.iteratortypedef char* iterator;iterator begin();iterator end();typedef const char* const_iterator;const_iterator begin() const;const_iterator end() const;//6.流插入运算符重载//friend ostream& operator<<(ostream& out, const string& s);//7.operator[]重载char& operator[](int index);const char& operator[](int index) const;//8.size() capacity()const int size() const;const int capacity() const;//9..reserve:这里不允许缩容void reserve(int capacity);//10.push_backvoid push_back(const char c);//11.appendvoid append(const char* str);//12.+=string& operator+=(const char c);string& operator+=(const char* str);//13.insertvoid insert(size_t pos, const char c);void insert(size_t pos, const char* str);//14.erasevoid erase(size_t pos = 0, size_t len = npos);//15.resizevoid resize(size_t n, char c = '\0');//16.swapvoid swap(string& s);//17.findsize_t find(const string& s, size_t pos = 0) const;size_t find(const char c, size_t pos = 0) const;//18.substrstring substr(size_t pos = 0, size_t len = npos)const;//19.clearvoid clear();//20.emptybool empty()const;//21.>>//friend istream& operator>>(istream& in, string& s);private:char* _str;int _size;int _capacity;};//static size_t string::npos = -1;//errsize_t string::npos = -1;//yesostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);
}

下面我们就逐个完成这些函数

大家会发现,我这里把流插入和流提取的友元给注释掉了
其实对于string类来说,流插入和流提取是不需要在类内用友元的
因为:
首先,友元是一种突破封装的方式,会增加耦合度,所以并不建议使用
其次,我们使用友元是因为我们想要访问类内的私有数据
可是对于string类来说
它的私有数据:size,capacity,_str都已经对外提供接口了
因此流插入和流提取只需要调用那些接口即可
无需使用友元来访问类内私有数据了

二.默认成员函数

1.构造函数

//1.全缺省的构造函数
string(const char* str = "")
{//1.申请空间int len = strlen(str);_str = new char[len + 1];//2.拷贝数据strcpy(_str, str);//3.修改_size和_capacity_size = len;_capacity = len;
}

在这里我们实现的是一个全缺省的构造函数
注意几点:
1.strlen求字符串长度时不会算上’\0’,因此我们开空间时要开len+1大小,因为我们还要在末尾存储’\0’
2.这里的

strcpy(_str,str);
会把str字符串中的从首元素开始一直延伸到'\0'为止的数据拷贝给_str
'\0'也会被拷贝进去!!!

在这里插入图片描述
对于这个构造函数:
其实就是
1.申请一个空间
2.拷贝数据
3.修改_size和_capacity

2.拷贝构造函数

1.传统写法

注意:string中的_str是开辟在堆区的
所以我们要实现深拷贝,否则会造成同一空间多次释放等问题

//2.拷贝构造函数
string(const string& s)
{//1.申请空间int len = s._size;_str = new char[len + 1];//2.拷贝数据strcpy(_str, s._str);//3.修改_size和_capacity_size = len;_capacity = len;
}

跟刚才的构造函数基本相同

2.现代写法

string(const string& s):_str(nullptr),_size(0),_capacity(0)
{string tmp(s._str);swap(tmp);//this->swap(tmp)
}

使用s._str调用构造函数来构造tmp
然后再去把tmp和*this交换

非常巧妙

3.析构函数

~string()
{//释放空间delete[] _str;_str=nullptr;//重置_size和_capacity_size = 0;_capacity = 0;
}

1.释放空间
2.重置_size和_capacity

4.赋值运算符重载

1.传统写法

//3.赋值运算符重载
string& operator=(const string& s)
{if (this != &s)//这里是为了防止自己给自己拷贝的冗余操作{//1.开辟一块新空间int len = s._size;char* tmp = new char[len + 1];//2.拷贝数据  把s._str的数据拷贝给tmpstrcpy(tmp, s._str);//3.释放原有空间delete[] _str;_str = tmp;tmp = nullptr;//4.修改_size和_capacity_size = len;_capacity = len;}return *this;
}

其实就是
1.开辟一块新空间
2.拷贝数据
3.释放原有空间
4.修改_size和_capacity

2.现代写法

//现代写法1
string& operator = (const string& s)
{if (this != &s){//调用拷贝构造函数,然后交换string tmp(s);swap(tmp);}return *this;
}
//现代写法2
//直接传值传参,然后交换
string& operator = (string s)
{swap(s);return *this;
}

三.遍历和访问

1.operator[]运算符重载

//7.operator[]重载
//同STL库中的operator[]重载:有两个重载版本
//一个没有被const修饰
//另一个是被const修饰的
char& operator[](int index)
{//STL中的string允许[]访问最后的'\0'assert(index >= 0 && index <= _size);return _str[index];
}
const char& operator[](int index) const
{//STL中的string允许[]访问最后的'\0'assert(index >= 0 && index <= _size);return _str[index];
}

就是直接返回对应位置的引用的即可

2.iterator迭代器

我们在C++ 带你吃透string容器的使用末尾提到了iterator迭代器
这里就不赘述了

同STL库,iterator有两个版本:const和非const
//5.iterator
typedef char* iterator;
iterator begin()
{return _str;
}
iterator end()
{return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{return _str;
}
const_iterator end() const
{return _str + _size;
}

四.容量相关函数

1.size,capacity

因为size和capacity对于类外是只读属性,因此用const修饰

//8.size() capacity()
const int size() const
{return _size;
}
const int capacity() const
{return _capacity;
}

2.reserve

我们在这里实现的版本是不允许缩容的
能不能缩容取决于编译器的具体实现
这一点我们这篇博客: C++ 带你吃透string容器的使用也提到过

//9.reserve:这里不允许缩容
void reserve(int capacity)
{//只有需要扩容时才会进行扩容if (_capacity < capacity){//1.开辟新空间char* tmp = new char[capacity + 1];//2.将原有数据拷贝至新空间strcpy(tmp, _str);//3.释放原有空间delete[] _str;//4.将_str指向新空间_str = tmp;tmp = nullptr;//5.修改_capacity_capacity = capacity;}
}

1.开辟新空间
2.将原有数据拷贝至新空间
3.释放原有空间
4.将_str指向新空间
5.修改_capacity

3.resize

这里我复用了erase和push_back
大家可以最后再来看这个resize,因为这个resize不会再其他接口中调用,最后看也无妨

//15.resize
//这里给字符设置了缺省参数'\0',将两个版本合二为一
void resize(size_t n, char c = '\0')
{//1.n<_size  :  删除多余字符,修改_size,但不修改_capacityif (n < _size){erase(n);//erase负责修改_size}//2._size<=n<=_capacity  :  尾插字符c直到_size == nelse if (n <= _capacity){//尾插数据while (_size < n){push_back(c);//push_back负责修改_size}}//3.n>_capacity:  需要reserveelse{//扩容:将容量扩为nreserve(n);//reserve负责修改_capacity//尾插数据while (_size < n){push_back(c);//push_back负责修改_size}}
}

分为三种情况:
在这里插入图片描述

五.尾插

1.push_back

//10.push_back
void push_back(const char c)
{//1.容量不够了的话:扩容if(_capacity==_size){int newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//2.尾插_str[_size] = c;//3.修改_size_size++;//4.不要忘了末尾的'\0'_str[_size] = '\0';
}

1.容量不够了的话:扩容
2.尾插
3.修改_size
4.不要忘了末尾的’\0’

2.append

//11.append
void append(const char* str)
{//1.容量不够的话: 扩容int len = strlen(str);int newcapacity = _size + len;//如果容量够,reserve不会执行任何语句(具体原因请看reserve函数)reserve(newcapacity);//2.拷贝数据strcpy(_str + _size, str);//3.修改_size_size += len;
}

1.容量不够的话: 扩容
2.拷贝数据
3.修改_size

注意:
_str+_size就指向了_str的末尾(‘\0’)
然后strcpy从_str末尾(‘\0’)开始进行拷贝

问题来了:
能不能用strcat呢?
不建议用
因为strcat使用时会先遍历目标字符串找到’\0’
然后再去进行拷贝
这样的话就多了一个遍历目标字符串的消耗
而我们直接_str+_size就找到目标字符串末尾(‘\0’)了

可以看出:当我们掌握了一些库函数的底层实现之后就是可以优化掉很多不必要的消耗

3.operator+=运算符重载

直接复用push_back和append即可
也是两个版本

//12.+=
string& operator+=(const char c)
{push_back(c);return *this;
}
string& operator+=(const char* str)
{append(str);return *this;
}

六.指定位置插入删除

1.insert

1.插入一个字符的版本

注意:下面就有坑了
大家看一下下面这个代码有什么问题?
提示:
1.while循环位置的错误
2.而且这个错误只有当我进行头插的时候才会出现

void insert(size_t pos, const char c)
{assert(pos >= 0 && pos <= _size);//1.容量不够的话要扩容if (_capacity == _size){int newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//2.将[pos,_size]的数据往后挪动//这里把_size位置的数据(也就是'\0')也往后挪动是为了//让我们最后的时候就不用单独再去在末尾添加'\0'了//因为'\0'已经被我们挪动到最后了size_t end = _size;while (end >= pos){_str[end + 1] = _str[end];end--;}//3.在pos位置插入字符c_str[pos]=c;//4.修改_size_size++;
}

1.容量不够的话要扩容
2.将[pos,_size]的数据往后挪动
3.在pos位置插入字符c
4.修改_size

下面我们来调试看一下
在这里插入图片描述
一开始时大家可以重点看_str的变化(理解数据后移的过程)
等到end快要减为0时再去看end的变化
在这里插入图片描述
因为end是size_t类型(也就是无符号整数)
永远大于等于0
当pos==0时,while(end>=pos)永远都成立
因此陷入死循环

那么怎么办呢?
第一种解决方案:强制类型转换

//把[pos,_size]的数据右移
//解决方案1:强制类型转换
size_t end = _size;
while ((int)end >= (int)pos)
{_str[end + 1] = _str[end];end--;
}

此时insert就正常运行了
在这里插入图片描述
尽管我end还是会减为-1时变为42亿多
但是while循环中我end强制类型转换转为了int类型(其实是产生了int类型的临时变量,然后用临时变量去比较的)
所以真实的比较场景是while(-1>=0),此时就退出while循环了

不过还有一个小点:
如果我这样写呢? 能不能顺利运行呢?

int end = _size;
while (end >= pos)
{_str[end + 1] = _str[end];end--;
}

在这里插入图片描述
答案是:还是不行
因为int类型的end跟size_t类型的pos比较时
int类型会进行整形提升产生size_t类型的临时变量,然后用临时变量跟pos去比较
也就是size_t类型的-1,-2,-3…跟size_t类型的pos去比
我们都知道size_t类型的-1,-2,-3那可是无符号整型的前几个最大值啊
肯定比我这个pos大嘛
所以还是死循环

只能强制类型转换吗?
当然不是,条条大路通罗马
还可以这样挪动数据

//13.insert
void insert(size_t pos, const char c)
{assert(pos >= 0 && pos <= _size);if (_capacity == _size){int newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//把[pos,_size]的数据右移//解决方案2size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = c;_size++;
}

此时当end减为0时就已经挪动完数据了
并且while循环也可以正常退出
在这里插入图片描述
我们在这里就采用第二种解决方案啦
大家也可以用一下第一种解决方案

2. 插入一个字符串的版本

跟插入字符的操作差不多
只不过挪动的步长不是1而是字符串的长度

void insert(size_t pos, const char* str)
{assert(pos >= 0 && pos <= _size);//1.容量不够的话要扩容int len = strlen(str);int newcapacity = _size + len;reserve(newcapacity);//2.将[pos,_size]的数据往后挪动len个位置int end = _size + 1;while (end > pos){_str[end + len - 1] = _str[end - 1];end--;}//3.在pos位置插入字符串的前len个字符(也就是不插入'\0')strncpy(_str + pos, str, len);//使用strncpy,而不能使用strcpy//因为我们不要拷贝'\0'//而strcpy是拷贝到'\0'才结束//4.修改_size_size += len;
}

1.容量不够的话要扩容
2.将[pos,_size]的数据往后挪动len个位置
3.在pos位置插入字符串的前len个字符(也就是不插入’\0’)
4.修改_size

strncpy就是可以指定拷贝n个字符
而不是跟strcpy一样直接拷贝到’\0’
在这里插入图片描述

2.erase

//14.erase
void erase(size_t pos = 0, size_t len = npos)
{assert(pos >= 0 && pos <= _size);//1.判断要删除的长度和[pos,_size)长度的大小//要删除的长度大于等于[pos,_size)的长度//此时直接将pos位置置为'\0'并且修改_size即可if (len >= _size - pos){_str[pos] = '\0';_size = pos;}//要删除的长度小于[pos,_size)的长度//那么把[pos+len,_size]的数据往前移动len位置来覆盖我们要删除的字符//然后修改_size即可else{strcpy(_str + pos, _str + pos + len);_size -= len;}
}

1.判断要删除的长度和[pos,_size)长度的大小
2.如果要删除的长度大于等于[pos,_size)的长度
那么直接将pos位置置为’\0’并且修改_size即可
3.如果要删除的长度小于[pos,_size)的长度
那么把[pos+len,_size]的数据往前移动len位置来覆盖我们要删除的字符
并且修改_size即可

注意:这里if语句中要使用len>=_size-pos
而不能使用len+pos>=_size
因为如果len为npos的话
那么len+pos就会溢出
会出现一些意想不到的错误
比如这样:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最后的结果:
在这里插入图片描述
我们注意到len+pos溢出之后变为了1
而我们的pos是2
因此:
strcpy(_str + pos, _str + pos + len);
就是把1位置的数据往2位置去拷贝,直到遇到’\0’为止
这就搞得非常荒诞了,因此这里使用的是len>=_size-pos

使用len>=_size-pos的话
因为len本身是npos
所以就会执行if语句的那个分支,而不是进入else执行strcpy导致出现千奇百怪的错误
在这里插入图片描述

七.查找,交换,截取操作

1.find

跟STL库一样,我们实现两个版本
查找字符串和查找字符

//17.find
size_t find(const char c, size_t pos = 0) const
{assert(pos >= 0 && pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;
}

这个查找字符就很简单了,遍历一遍即可

但是这个查找字符串我们要用到strstr字符串匹配函数
在这里插入图片描述

size_t find(const string& s, size_t pos = 0) const
{assert(pos >= 0 && pos < _size);char* index = strstr(_str + pos, s._str);if (index == nullptr){return npos;}else{return index - _str;}
}

唯一需要注意的一点就是:
返回空指针的话代表没有查找到,需要加一个if判断
如果查找到了
返回index-_str即可(指针-指针的问题,是C语言的基础知识)

2.swap

//16.swap
void swap(string& s)
{//这里调用的是std(标准库中的那个swap函数)std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

这里的swap的实现其实就是
交换指针,交换_size和_capacity
这样做的好处是无需再去拷贝string了,而是直接交换指针即可,大大优化了swap的效率
可见这个设计是非常巧妙的

3.substr

大家看一下这个代码有没有什么问题

string& substr(size_t pos = 0, size_t len = npos)const
{assert(pos >= 0 && pos < _size);string s;//1.判断要截取的长度和[pos,_size)长度的大小//要截取的长度大于[pos,_size)的长度  ->  那么修改len方便后续形成子串if (len >= _size - pos){len = _size - pos;}//2.插入数据形成子串for (size_t i = 0; i < len; i++){s += _str[i + pos];}//3.返回子串return s;
}

1.判断要截取的长度和[pos,_size)长度的大小 如果大了,那么就修改len
2.插入数据形成子串
3.返回子串

其实这个代码是有问题的,只不过并不是我们实现逻辑的问题
而是这个引用做返回值的问题
在这里插入图片描述
在这里插入图片描述

因为我们的这个s串是substr函数中的局部变量
出了作用域后会自动销毁(调用析构函数)
但是因为我们是引用作为返回值
因此s1.substr(1,7)这个对象就是我们刚才析构后的s串
然后用这个已经析构了的串给s2进行拷贝构造
在这里插入图片描述
于是拷贝构造时
strcpy访问s._str导致出现了对空指针的解引用操作
因此报错

怎么办呢?
把引用做返回值改为值做返回值
就好了
但是好的前提一定是你的拷贝构造,赋值运算符重载都是用的深拷贝,而不是浅拷贝
否则就会出现同一空间多次释放的错误
在这里插入图片描述
下面我把拷贝构造函数给干掉
那么就会用编译器默认形成的拷贝构造函数(默认形成的是浅拷贝)
在这里插入图片描述
此时,尽管你是值拷贝,但是你是浅拷贝
也就是说返回是我指向已经析构后的空间的那个指针
因此在s2析构的时候会对那个空间再次进行析构

也就是发生了同一空间多次释放的问题
也就是野指针的问题

下面我把拷贝构造函数恢复

string substr(size_t pos = 0, size_t len = npos)const
{assert(pos >= 0 && pos < _size);string s;//1.判断要截取的长度和[pos,_size)长度的大小//要截取的长度大于[pos,_size)的长度  ->  那么修改len方便后续形成子串if (len >= _size - pos){len = _size - pos;}//2.插入数据形成子串for (size_t i = 0; i < len; i++){s += _str[i + pos];}//3.返回子串return s;
}

此时就正常运行了
在这里插入图片描述
整个流程是:
s串返回值进行拷贝构造形成临时变量
然后返回那个临时变量
然后再由那个临时变量给这个s2进行拷贝构造

关于引用的知识点:
C++入门-引用
深浅拷贝的知识点
C++类和对象中(构造函数,析构函数,拷贝构造函数)详解

八.几个小函数

1.c_str

//4.返回C风格的字符串
const char* c_str() const
{return _str;
}

2.clear

//19.clear
void clear()
{_str[0] = '\0';//关于这里为什么要把第一个字符置为'\0'//是因为我们要保证//此时即使使用cout<<c_str()来打印string对象时也不会跟使用流插入打印string对象产生歧义//因为使用cout<<c_str()是打印到'\0'才会停止//而流插入是按照size来打印的_size = 0;
}

3.empty

//20.empty
bool empty()const
{return _size == 0;
}

九.流插入和流提取

对于流插入和流提取我们之前就在日期类接触了。
不能重载成成员,因为会和this指针抢占第一个位置。
所以需要定义成全局的
不过无需使用友元,因为可以调用接口来访问类内数据

1.<<流插入

按照_size打印

ostream& operator<<(ostream& out, const string& s)
{for (int i = 0; i < s.size(); i++){out << s[i];}return out;
}

对于<<和c_str()的区别:<<按照size进行打印,跟\0没有关系,而c_str()遇到\0结束
在这里插入图片描述
因此推荐使用<<

2.>>流提取

cin和scanf一样,都是读到’ ‘或者’\n’后就会停止
因此我们需要一个字符一个字符地读取

所以使用get()函数来读取字符
第一个版本:

istream& operator>>(istream& in, string& s)
{
//注意:使用流提取时,会覆盖原有数据
//因此需要先clear一下s.clear();char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;
}

但是第一个版本有一个缺陷:
当我们想要读取的数据特别特别多的时候
就会频繁扩容,造成不必要的麻烦
因此我们可以设置一个"缓冲区"似的数组
第二个版本:

istream& operator>>(istream& in, string& s)
{s.clear();char tmp[128] = { '\0' };char ch = in.get();size_t index = 0;while (ch != ' ' && ch != '\n'){if(index == 127){s += tmp;index = 0;}tmp[index++] = ch;ch = in.get();}if (index >= 0){tmp[index] = '\0';s += tmp;}return in;
}

十.完整代码

1.my_string.h:

#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include <string>
#include <iostream>
using namespace std;
#include <assert.h>
//小函数定义在.h里面,弄成内联函数
namespace wzs
{class string{public://nposstatic size_t npos;//1.全缺省的构造函数string(const char* str = "");~string();//2.拷贝构造函数string(const string& s);//18.substrstring substr(size_t pos = 0, size_t len = npos)const;//3.赋值运算符重载string& operator=(string s);//4.返回C风格的字符串const char* c_str() const{return _str;}//5.iteratortypedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}//6.流插入运算符重载//friend ostream& operator<<(ostream& out, const string& s);//7.operator[]重载char& operator[](int index);const char& operator[](int index) const;//8.size() capacity()const int size() const{return _size;}const int capacity() const{return _capacity;}//9..reserve:这里不允许缩容void reserve(int capacity);//10.push_backvoid push_back(const char c);//11.appendvoid append(const char* str);//12.+=string& operator+=(const char c);string& operator+=(const char* str);//13.insertvoid insert(size_t pos, const char c);void insert(size_t pos, const char* str);//14.erasevoid erase(size_t pos = 0, size_t len = npos);//15.resizevoid resize(size_t n, char c = '\0');//16.swapvoid swap(string& s);//17.findsize_t find(const string& s, size_t pos = 0) const;size_t find(const char c, size_t pos = 0) const;//19.clearvoid clear(){_str[0] = '\0';_size = 0;}//20.emptybool empty()const{return _size == 0;}//21.>>//friend istream& operator>>(istream& in, string& s);private:char* _str;int _size;int _capacity;};ostream& operator<<(ostream& out, const string& s);istream& operator>>(istream& in, string& s);void test1();void test2();void test3();void test4();void test5();void test6();void test7();void test8();void test9();void test10();void test11();void test12();void test13();void test14();void test15();void test16();void test17();
}

2.my_string.cpp

#include "my_string.h"
namespace wzs
{//1.全缺省的构造函数string::string(const char* str){int len = strlen(str);_str = new char[len + 1];strcpy(_str, str);_size = len;_capacity = len;}string::~string(){delete[] _str;_str = nullptr;_size = 0;_capacity = 0;}//2.拷贝构造函数/*string(const string& s){int len = s._size;_str = new char[len + 1];strcpy(_str, s._str);_size = len;_capacity = len;}*///拷贝构造的现代写法//有些编译器上有问题//因为如果_str没有被初始化为nullptr//那么就完了,因为tmp除了这个构造函数就会调用析构//所以我们可以先给缺省值或者初始化列表来让这个_str初始化为nullptrstring::string(const string& s):_str(nullptr), _size(0), _capacity(0){//先构造string tmp(s.c_str());//再交换swap(tmp);}//18.substrstring string::substr(size_t pos, size_t len)const{assert(pos >= 0 && pos < _size);string s;if (len >= _size - pos){len = _size - pos;}for (size_t i = 0; i < len; i++){s += _str[i + pos];}return s;}//3.赋值运算符重载/*string& operator=(const string& s){if (this != &s){int len = s._size;char* tmp = new char[len + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;tmp = nullptr;_size = len;_capacity = len;}return *this;}*///赋值运算符重载的现代写法1/*string& operator=(const string& s){if (this != &s){//先拷贝构造string tmp(s);//再交换swap(tmp);return *this;}}*///赋值运算符重载的现代写法2//直接传值调用//传参的时候值传参  ->  调用拷贝构造函数传参string& string::operator=(string s){//没有必要判断是否是自己给自己赋值//因为这个对象已经拷贝出来了,覆水已经难收swap(s);//*this原来的空间是这个形参s释放的return *this;}//7.operator[]重载char& string::operator[](int index){//stl中的string允许[]访问最后的'\0'assert(index >= 0 && index <= _size);return _str[index];}const char& string::operator[](int index) const{//stl中的string允许[]访问最后的'\0'assert(index >= 0 && index <= _size);return _str[index];}//9..reserve:这里不允许缩容void string::reserve(int capacity){if (_capacity < capacity){char* tmp = new char[capacity + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;tmp = nullptr;_capacity = capacity;}}//10.push_backvoid string::push_back(const char c){if (_capacity == _size){int newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = c;_size++;_str[_size] = '\0';}//11.appendvoid string::append(const char* str){int len = strlen(str);int newcapacity = _size + len;reserve(newcapacity);strcpy(_str + _size, str);_size += len;}//12.+=string& string::operator+=(const char c){push_back(c);return *this;}string& string::operator+=(const char* str){append(str);return *this;}//13.insertvoid string::insert(size_t pos, const char c){assert(pos >= 0 && pos <= _size);if (_capacity == _size){int newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}//把[pos,_size]的数据右移//size_t end = _size;//while (end >= pos)//{//	_str[end + 1] = _str[end];//	end--;//}//解决方案1:强制类型转换//int end = _size;//while (end >= pos)//{//	_str[end + 1] = _str[end];//	end--;//}//解决方案2size_t end = _size + 1;while (end > pos){_str[end] = _str[end - 1];end--;}_str[pos] = c;_size++;}void string::insert(size_t pos, const char* str){assert(pos >= 0 && pos <= _size);int len = strlen(str);int newcapacity = _size + len;reserve(newcapacity);//[pos,_size]的数据右移len位int end = _size + 1;while (end > pos){_str[end + len - 1] = _str[end - 1];end--;}strncpy(_str + pos, str, len);//使用strncpy,而不能使用strcpy//因为我们不要拷贝'\0'//而strcpy是拷贝到'\0'才结束_size += len;}//14.erasevoid string::erase(size_t pos, size_t len){assert(pos >= 0 && pos <= _size);//要删除的长度大于[pos,_size)的长度if (len >= _size - pos)//if (len + pos >= _size){_str[pos] = '\0';_size = pos;}//把[pos+len,_size]的数据往前移动len位置else{strcpy(_str + pos, _str + pos + len);_size -= len;}}//15.resizevoid string::resize(size_t n, char c){//1.n<_size  :  删除多余字符,修改_size,但不修改_capacityif (n < _size){erase(n);//erase负责修改_size}//2._size<=n<=_capacity  :  尾插字符c直到_size == nelse if (n <= _capacity){while (_size < n){push_back(c);//push_back负责修改_size}}//3.n>_capacity:  需要reserveelse{reserve(n);//reserve负责修改_capacitywhile (_size < n){push_back(c);//push_back负责修改_size}}}//16.swapvoid string::swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}//17.findsize_t string::find(const string& s, size_t pos) const{assert(pos >= 0 && pos < _size);char* index = strstr(_str + pos, s._str);if (index == nullptr){return npos;}else{return index - _str;}}size_t string::find(const char c, size_t pos) const{assert(pos >= 0 && pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == c){return i;}}return npos;}//static size_t string::npos = -1;//errsize_t string::npos = -1;//yesostream& operator<<(ostream& out, const string& s){for (int i = 0; i < s.size(); i++){out << s[i];}return out;}istream& operator>>(istream& in, string& s){//库里的流提取输入的时候会覆盖之前的旧内容!!//运行看一下//因此要clears.clear();//clear一般不会释放空间,就是把第一个元素置为'\0'并且把size置为0//如果clear不把第一个元素置为'\0'//那么>>和c_str就不一样了!!!!//因为>>是按size打印,c_str打印到'\0'之前为止char tmp[128] = { '\0' };//get是istream类型调用的成员函数char ch = in.get();//get()是C++的,getchar()是C语言的,C++和C语言的缓冲区是不一样的,(C++是可以设计成不兼容C语言的)////尽管C++是兼容C语言的,不过尽量还是用C++的size_t index = 0;//getline其实是当ch遇到'\n'之后才会停止while循环!!!!while (ch != ' ' && ch != '\n'){//index是下一个要插入数据的下标//注意:这里是127时就要放s里面并清空//因为tmp的最后一个数据一定要保证是'\0'!!!!!!if (index == 127){s += tmp;index = 0;}tmp[index++] = ch;ch = in.get();}if (index >= 0){tmp[index] = '\0';s += tmp;}return in;}
}

3.test.cpp

#include "my_string.h"
namespace wzs
{void test1(){const string s1("hello world");string s2;cout << s1.c_str() << endl;cout << s2.c_str() << endl;string s3(s1);cout << s3.c_str() << endl;s2 = s1;cout << s2.c_str() << endl;}void test2(){//string s2 = "hello iterator";//string::iterator it = s2.begin();//while (it != s2.end())//注意:我们使用iterator访问和遍历时要注意左闭右开使用[begin,end)//{//	cout << *it << " ";//这里可以暂时理解为像是指针解引用的用法一样//	it++;//这里可以暂时理解为像是指针自增(也就是后移)的用法一样//}//cout << endl;//cout << s2 << endl;string s2 = "hello iterator";string::iterator it = s2.begin();while (it != s2.end())//注意:我们使用iterator访问和遍历时要注意左闭右开使用[begin,end){*it += 1;//(*it)++;这样也可以,不过不要忘了加小括号(运算符优先级的问题)cout << *it << " ";//这里可以暂时理解为像是指针解引用的用法一样it++;//这里可以暂时理解为像是指针自增(也就是后移)的用法一样}cout << endl;cout << s2 << endl;}void test3(){std::string s("hello world");char c = s[s.size()];cout << s[s.size()] << endl;}void test4(){string s1("hello world");s1.reserve(100);}void test5(){string s1;int old_capacity = s1.capacity();cout << old_capacity << endl;cout << s1 << endl;for (int i = 0; i < 100; i++){s1.push_back('w');//将'w'这个字符尾插进入s1当中if (old_capacity != s1.capacity()){cout << s1.capacity() << endl;old_capacity = s1.capacity();}}cout << s1 << endl;}void test6(){string s("hello world");/*s.push_back('2');s.push_back('3');s.append(" 1124");*/s += "2";s += '3';s += " 1124";cout << s << endl;}void test7(){string s("[hello world]");s.insert(0, 'w');//从0位置头插一个字符:'w'cout << s << endl;}void test8(){string s("0123456789");s.erase(3, 4);//从3号下标位置开始删除4个字符,也就是删除了3456cout << s << endl;s.erase(2);//默认从2号下标开始的删除所有字符cout << s << endl;s.erase();//默认删除所有字符cout << s << endl;}void test9(){//string s1("hello world");1.n<size//s1.resize(4);//cout << s1 << endl;//string s1("hello world");//2.size<n<capacity//cout << s1.capacity() << endl;s1.resize(13,'q');//s1.resize(30, '2');//cout << s1;string s1;s1.resize(10, 'w');cout << s1 << endl;}void test10(){string s1("123");string s2("456");s1.swap(s2);cout << s1 << endl;cout << s2 << endl;}void test11(){string s1("abcd 1234 xxxx");size_t index1 = s1.find("cd", 1);//从1号下标开始查找字符串cdcout << index1 << endl;size_t index3 = s1.find(' ', 1);//从1号下标开始查找字符' 'cout << index3 << endl;//查不到的情况:size_t index4 = s1.find("abcd", 1);//从1号下标开始查找字符串abcd  ->  查不到,返回nposcout << index4 << endl;//无符号整形最大值}void test12(){string s1("hello world");string s2 = s1.substr(1, 7);cout << s2 << endl;}void test13(){string s("http://www.baidu.com/index.html?name=mo&age=25#dowell");string substr1, substr2, substr3;//我们的目标是://substr1:http//substr2:www.baidu.com//substr3:index.html?name=mo&age=25#dowellsize_t pos1 = s.find(':', 0);//从0下标开始出发查找':'substr1 = s.substr(0, pos1 - 0);//[0,pos1):左闭右开的区间:长度是右区间-左区间 也就是pos1-0size_t pos2 = s.find('/', pos1 + 3);//pos1位置此时是':'  我们下一次要从pos1+3的位置开始查找 也就是第一个'w'的位置substr2 = s.substr(pos1 + 3, pos2 - (pos1 + 3));//[pos1+3,pos2):这个区间内的子串substr3 = s.substr(pos2 + 1);//从pos2+1开始一直截取到最后即可//substr传值返回,是临时变量,临时变量具有常性cout << substr1 << endl;cout << substr2 << endl;cout << substr3 << endl;}void test14(){//char ch1, ch2;//cin >> ch1 >> ch2;//因为输入多个值时,' '和'\n'默认是分割符,不是有效数据!!!!!!//scanf也是拿不到的string s1, s2, s3;cin >> s1 >> s2 >> s3;cout << s1 << endl << s2 << endl << s3 << endl;}void test15(){string s1("012345");cout << s1 << endl;cout << s1.c_str() << endl;s1.insert(2, '\0');cout << s1 << endl;cout << s1.c_str() << endl;}void test16(){string s1("hello world");cout << s1 << endl;cout << s1.c_str() << endl;//此时clear没有把第一个元素置为'\0's1.clear();cout << s1 << endl;cout << s1.c_str() << endl;}//整形转字符串:to_string//stoi:字符串转整形void test17(){//整形转字符串:to_stringstd::string s = std::to_string(123);//stoi:字符串转整形int i = stoi(s);cout << s << endl;cout << i << endl;}
}

以上就是C++: string的模拟实现的全部内容,希望能对大家有所帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/185568.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ssm+vue的公司安全生产考试系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的公司安全生产考试系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结…

探索前端设计的新境界——介绍IVueUI工具助力Vue页面设计

在快速发展的前端领域&#xff0c;Vue.js作为一款渐进式JavaScript框架&#xff0c;一直备受开发者喜爱。然而&#xff0c;在Vue前端开发的旅程中&#xff0c;页面设计常常是一个不可避免的挑战。今天&#xff0c;我要向大家介绍一款令Vue前端开发者受益匪浅的工具——www.ivue…

Python文件操作

目录 一.文件的编码二.文件的读取三.文件的写入四.文件的追加五.文件操作综合案例 一.文件的编码 编码就是一种规则集合&#xff0c;记录了内容和二进制进行相互转换的逻辑。最常见的是UTF-8编码计算机只认识0和1&#xff0c;所以需要将内容翻译成0和1才能保存在计算机中。同时…

“大+小模型”赋能油气行业高质量发展

近日&#xff0c;中国石油石化科技创新大会暨新技术成果展在北京盛大举行&#xff0c;九章云极DataCanvas公司携油气行业一站式AI综合解决方案重磅亮相&#xff0c;充分展示了公司助推油气行业实现AI规模化应用深厚的AI技术实力和领先的AI应用水准&#xff0c;赢得了行业专家和…

spring boot整合Jasypt实现配置加密

文章目录 目录 文章目录 前言 一、Jasypt是什么&#xff1f; 二、使用步骤 1.引入 2.测试使用 3.结果 总结 前言 一、Jasypt是什么&#xff1f; Jasypt&#xff08;Java Simplified Encryption&#xff09;是一个Java库&#xff0c;提供了一种简单的加密解密方式&#xff0c…

热门话题解析:pytest测试用例顺序问题解决方案!

前言 上一篇文章我们讲了在pytest中测试用例的命名规则&#xff0c;那么在pytest中又是以怎样的顺序执行测试用例的呢&#xff1f; 在unittest框架中&#xff0c;默认按照ACSII码的顺序加载测试用例并执行&#xff0c;顺序为&#xff1a;09、AZ、a~z&#xff0c;测试目录、测…

Codeforces Round 906 (Div. 2)(D推公式 E1分类讨论区间 E2 dp+线段树)

A - Doremys Paint 3 推公式得 b1b3b5b7.... b2b4b6b8... 所以如果只有一个数或者两个数且数量差小于等于1即可 #include<bits/stdc.h> using namespace std; const int N 2e510,mod1000003; #define int long long typedef long long LL; typedef pair<int, in…

第三方实验室LIMS管理系统源码,asp.net LIMS源码

LIMS实验室信息管理系统源码 LIMS系统的功能根据实验室的规模和任务而有所不同&#xff0c;其系统主要功能包括:系统维护、基础数据编码管理&#xff0c;样品管理、数据管理、报告管理、报表打印、实验材料管理、设备管理等。它可以取代传统的手工管理模式而给检测实验室带来巨…

java获取第n次出现字符串前后面字符串,如:截取第二个逗号后面的数据

java获取第n次出现字符串前后面字符串&#xff0c;如&#xff1a;截取第二个逗号后面的数据 方法&#xff1a; /*** 获取指定第几位字符串后面字符串&#xff0c;如&#xff1a;截取第二个逗号后面的数据** param str:要处理的字符串* param mediumStr&#xff1a;根据截取的媒…

时间序列异常检测14篇顶会论文合集,附必备工具和数据集

今天来聊聊一个在量化交易、网络安全检测、自动驾驶汽车和大型工业设备的日常维护等领域都有重要作用的研究主题&#xff1a;时间序列异常检测。 时间序列异常检测是一种在时间序列数据中识别和标识与预期模式、趋势或行为不符的异常点或事件的技术。鉴于它如此广泛的应用范围…

18、串口通信

串口介绍 串口是一种应用十分广泛的通讯接口&#xff0c;串口成本低、容易使用、通信线路简单&#xff0c;可实现两个设备的互相通信。 单片机的串口可以使单片机与单片机&#xff0c;单片机与电脑、单片机与各式各样的模块互相通信&#xff0c;极大的扩展了单片机的应用范围&…

MySQL InnoDB Cluster

MySQL InnoDB Cluster 一、InnoDB Cluster 基本概述 MySQL InnoDB Cluster 为 MySQL 提供了一个完整的高可用解决方案。通过使用 MySQL Shell 提供的 AdminAPI,你可以轻松地配置和管理一组至少由3个MySQL服务器实例组成的 InnoDB 集群。 InnoDB 集群中的每个 MySQL 服务器实例…

JRT和检验共用的打印层实现

之前对接的打印和导出是C#实现的&#xff0c;如果要完全Java化就需要用Java把打印元素绘制协议用Java实现&#xff0c;这次介绍实现主体搭建&#xff0c;最终使JRT达到完全信创和跨平台目标。到这篇后&#xff0c;所有的Java难题都解决完毕&#xff0c;几天到几周之内就可以把打…

(二进制、八进制、十进制、十六进制)的进制转换

整型有4种进制形式&#xff1a; 1.十进制&#xff1a; 都是以0-9这九个数字组成&#xff0c;不能以0开头。 2.二进制&#xff1a; 由0和1两个数字组成。 3.八进制&#xff1a; 由0-7数字组成&#xff0c;为了区分与其他进制的数字区别&#xff0c;开头都是以0开始。 4.十六进制…

聚类算法的算法原理

聚类算法是机器学习中常用的一种无监督学习方法&#xff0c;其主要目标是将数据集划分为具有相似特征的组或簇。这种算法在数据挖掘、模式识别、社交网络分析等领域有着广泛的应用。聚类算法的核心思想是通过计算数据点之间的相似度或距离&#xff0c;将相似的数据点聚集在一起…

WordPress 粘贴图片上传插件

找了很久&#xff0c;发现一款不错的插件&#xff0c;允许我们直接粘贴图片文件并且上传到媒体库。以前的插件上传后媒体库不会显示&#xff0c;这个要显示。 启用后编辑器会有一个图标&#xff0c;如果开启&#xff0c;那么久可以截图后直接粘贴了。 学习资料源代码&#xf…

TR转发路由器测评—云企业网实现跨地域跨VPC的网络互通测评实战【阿里云产品测评】

文章目录 一.转发路由器 Transit Router 测评1.1 准备阶段1.2 本文测评收获1.3 什么是云企业网实例、转发路由器实例和云数据传输服务 二.使用云企业网实现跨地域跨VPC的网络互通2.2 **测试连通性**2.3 网络拓扑如下&#xff1a; 心得&#xff1a;总结&#xff1a; 声明&#x…

基于SpringBoot房屋租赁系统

摘 要 社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大人民群众的喜爱&#xff0c;也逐渐进入了每个用户的使用。互联网具有便利性&#xff0c;速度快&#xff0c;效率高&#xff0c;成本低等优点。 因此&#xff0c;构建符…

JSON.stringify方法详解 后端接受JSON数据格式

1、方法定义&#xff1a;JSON.stringify(value, replacer, space) 参数说明&#xff1a; value&#xff1a;js对象 replacer&#xff1a;替换对象&#xff0c;可以是一个方法、对象或数组&#xff0c;将value按照替换规则展示。 space&#xff1a;填充参数&#xff0c;可以是数…

Python排序算法大比拼:快速排序 VS 归并排序

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 快速排序和归并排序是两种常见的排序算法&#xff0c;在Python中有着重要的应用。本文将深入探讨这两种算法的原理和实现&#xff0c;并提供丰富的示例代码来说明它们的工作方式。 快速排序算法 def quicksort…