【C++】string类的增删改查模拟实现(图例超详细解析!!!)

目录

一、前言

二、string类的模拟实现 

✨前情提要 

✨Member functions —— 成员函数

 ⚡构造函数

 ⚡拷贝构造函数

 ⚡赋值运算符重载

 ⚡析构函数

 ✨Element access —— 元素访问

⚡operator[ ]

⚡Iterator —— 迭代器

 ✨Capacity —— 容量

 ⚡size

 ⚡capacity

 ⚡clear

 ⚡empty

 ⚡reserve

 ⚡resize

 ✨Modifiers —— 修改器

 ⚡push_back

 ⚡append

 ⚡operator+=(char ch)

 ⚡perator+=(const char* s)

 ⚡insert 

 ⚡erase

 ⚡swap

✨String Operations —— 字符串操作 

 ⚡ find --- 寻找一个字符

 ⚡find --- 寻找一个字符串 

 ⚡substr

 ✨非成员函数重载

⚡relational operators 

⚡operator<< 流插入

⚡operator>> 流提取【⭐】 

⚡getline 

 三、string 类的模拟实现整体代码

🥝 string.h

🍇string.cpp

🍍test.cpp

 四、共勉


一、前言

        在经过漫长的类和对象与STL 学习之后,对于 STL中的 string类有了一个基本的认识,如果还有不太了解string 类的老铁,可以看看这篇文章:string类的详解
        本模块呢,我将会带大家一起从 0~1去模拟实现一个STL库中的 string类,当然模拟实现的都是一些常用的接口,以便于让大家更好的巩固之前学习过的 缺省参数、封装、类中的6大默认成员函数等,代码量大概在 600行左右。

二、string类的模拟实现 

✨前情提要 

  • 首先第一点,为了不和库中的string类发生冲突,我们可以包上一个名称为xas_string的命名空间,此时因为作用域的不同,就不会产生冲突了,如果这一块有点忘记的同学可以再去看看 namespace命名空间
#pragma once#include <iostream>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cout;
using std::cin;
using std::endl;// 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域xas_string
namespace xas_string
{class string{public:// .....private:char* _str;         // 指向字符数组的指针size_t _size;       // 字符数组的有效数据的长度size_t _capacity;   // 字符串数组的容量};
}
  • 接下去呢,就在string.cpp中进行定义,在test.cpp中进行测试即可。其中需要包含一下这个头文件,此时我们才可以在自己实现的类中去调用一些库函数
#include "string.h"

✨Member functions —— 成员函数

 ⚡构造函数

好,首先第一个我们要来讲的就是【构造函数】 

  • 首先我们从无参的构造函数开始讲起,看到下面的代码,你是否有想起了 C++初始化列表,我们默认给到 _size 和 _capacity 的大小为,然后给字符数组开了一个大小的空间,并且将其初始化为\0 在string.cpp定义中可以写为:
// 无参构造函数
// 在 xas_string作用域中的string类 的string()函数
xas_string::string::string():_str(new char[1])    , _size(0), _capacity(0)
{_str[0] = '\0';
}
  • 然后我们立即在test.cpp中测试一下,因为我们自己实现的 string类 是包含在了命名空间xas_string中的,那么我们在使用这个类的时候就要使用到 域作用限定符::
xas_string::string s1;
//  测试初始化,与循环打印 迭代器
void test1()
{xas_string::string s1;cout << s1 << endl;
}int main()
{test1();return 0;
}
  •  开始运行,结果出现了报错的现象。这个是什么原因呢?

  •  对于输入输出流运算符,我们后面会讲到,目前,我们先用 c_str()函数来代替输出
const char* c_str() const     // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _str;
}
  •  然后打印一下这个string对象发现是一个空串

  • 有无参,那一定要有带参的,可以看到这里我们在初始化_size的时候先去计算了字符串str的长度,因为_size取的就是到 \0 为止的有效数据个数(不包含\0),那么【strlen】刚好可以起到这个功能
  • 然后在_str这一块,我们为其开出的空间就是 容量的大小 + 1(+1的作用是为'\0'开空间),最后的话还要在把有效的数据拷贝到这块空间中,使用到的是【strcpy】
// 有参构造函数
string(const char* str)    // ""  --- 为空的字符串
{_str = new char[strlen(str) + 1];   // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1_size = strlen(str);_capacity = strlen(str);           // capacity 不包括 '\0'strcpy(_str, str);
}
  • 同样地来进行一个测试

⚡拷贝构造函数

马上,我们就来聊聊有关【拷贝构造函数】的内容 

  • 在 拷贝构造函数详解 中我们有提到过若是一个类在没有显示定义拷贝构造对于内置类型不做处理,而对于自定义类型会去调用 类中默认提供的拷贝构造函数 此时就会造成浅拷贝的问题

  •  浅拷贝:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间。其中一个对象的改动会对另一个对象造成影响。
  • 深拷贝:深拷贝是指源对象与拷贝对象互相独立。其中任何一个对象的改动不会对另外一个对象造成影响。
  • 很明显,我们并不希望拷贝出来的两个对象之间存在相互影响,因此,我们这里需要用到深拷贝。下面提供深拷贝的两种写法: 

 写法一:传统写法

传统写法的思想简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。因为拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是互相独立的。 

//拷贝构造
string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
  • 通过调试再去观察的话,我们可以发现,此时 对象s1 和 对象s2 中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响

 写法二:现代写法

现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。

//拷贝构造
string(const string& s):_str(nullptr), _size(0), _capacity(0)
{string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象
}

 注:swap成员函数的模拟实现在文章的后面。

  •  同样地来进行一个测试

 ⚡赋值运算符重载

 对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个

  • 但是呢默认生成的这个也会造成一个 浅拷贝 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1和s3就会指向一块同一块空间,此时便造成了下面这些问题
  • 在修改其中任何一者时另一者都会发生变化;
  • 在析构的时候就也会造成二次析构的;
  • 原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题

  • 那么此时我们应该自己去开出一块新的空间,将s3里的内容先拷贝到这块空间中来,然后释放掉s1所指向这块空间中的内容,然后再让s1指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间
  • 最后的话不要忘记去修改一下s1的【_size】和【_capacity】,因为大小和容量都发生了改变

  •  下面是具体的代码,学习过 赋值运算符重载 的同学应该不陌生

写法一:传统写法 

// 赋值运算符重载string& operator=(const string& s){if (this != &s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}

 写法二:现代写法 

但是呢,就上面这一种写法并不是最优的,我们来看看下面的这种写法 

  • 很多同学非常地震惊,为何这样子就可以做到【深拷贝】呢?
string& operator=(const string& s)
{if (this != &s) //防止自己给自己赋值{string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象}return *this; //返回左值(支持连续赋值)
}
  • 有关这个swap()函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模板 的时候有提到过库中的这个 swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据
  • 可以看到在我们自己实现的这个swap(string& s) 函数中就去调用了std标准库中的函数然后交换一个string对象的所有成员变量
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}
  • 接下去来解释一下这里的原理,我们在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题

  • 透过上面这个图解读者应该对新的这种拷贝构造有了一定的理解:反正你这个tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果

 ⚡析构函数

最后的话就是析构函数这一块,前面在调试的过程中我们已经看到很多遍了,此处不再细述 

~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}

 ✨Element access —— 元素访问

基本的成员函数我们已经讲完了,string对象也构造出来了,接下去我们来访问一下对象里面的内容吧

⚡operator[ ]

  • 首先最常用的就是这【下标 + [ ]】的形式去进行一个访问,那很简单,我们通过当前所传入的下标值去访问对应的数据即可
  • 下面的话有两种实现形式,一个是可读可写的,一个则是可读不可写
// 可读可写
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
// 可读不可写
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
  • 里面我们就通过循环来访问一下,这里的size()函数我们会在下面讲到

  • 但是呢,如果我在定义这个对象的时候在前面加上一个const的话此时这个对象就具有常性了,在调用operator[]的时候调用的便是 可读不可写 的那一个,所以此刻我们去做一个修改操作的话就会出问题了
const xas_string::string s4("hello C++");

⚡Iterator —— 迭代器

那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了【下标 + []】的形式,我们还可以使用迭代器的形式去做一个遍历

  • 而对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的
  •  string类中的迭代器实际上就是字符指针,只是给字符指针起了一个别名叫iterator而已。 
typedef char* iterator;
typedef const char* const_iterator;

 注:不是所有的迭代器都是指针。

  • 这里的话我就实现一下最常用的【begin】和【end】,首位的话就是_str所指向的这个位置,而末位的话则是_str + _size所指向的这个位置
iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}
  • 实现了普通版本的迭代器之后,我们再来看看常量迭代器。很简单,只需要修改一下返回值,然后在后面加上一个【const成员】,此时就可以构成函数重载了
const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}
  • 首先我们来看一下这个普通的迭代器,成功地遍历了这个string对象

  •  那么对于常对象来说的话,就要使用常量迭代器来进行遍历
// 针对------const 对象的访问// 打印这个字符串  --- 不能修改void print_str(const string& s){for (size_t i = 0; i < s.size(); i++){std::cout << s[i] << " ";}std::cout << std::endl;string::const_iterator it = s.begin();while (it != s.end()){// 内容不能修改std::cout << *it << " ";// 指针可以修改it++;}std::cout << std::endl;}

 ✨Capacity —— 容量

下面六个接口我们一起来看看,然后一同测试

 ⚡size

  • 首先是 size(),这里的话我们直接返回_size即可,因为不会去修改成员变量,所以我们可以加上一个【const成员】(因为它是不可被修改滴)
  • size函数用于获取字符串当前的有效长度(不包括’\0’)
size_t size() const
{return _size;
}

 注意:这里的 const 是用来修饰 this指针滴

 ⚡capacity

  •  capacity函数用于获取字符串当前的容量
size_t capacity() const
{return _capacity;
}

 ⚡clear

  • 对于 clear() 而言就是去清除当前对象的数据,我们直接在_str[0]这个位置放上一个\0即可,并且再去修改一下它的_size = 0即可
  • 不过这个接口来说我们不要去加【const成员】,因为修改了其成员变量_size
void clear()
{_str[0] = '\0';_size = 0;
}

 ⚡empty

  • 对于 empty() 来说呢就是对象中没有数据,那么使用0 == _size即可
bool empty() const
{return 0 == _size;
}

💬 然后我们来测试一下 

 ⚡reserve

 然后我们来看【reserve】扩容

reserve规则: 

  1. 扩容n大于对象当前的capacity时,将capacity扩大到n或大于n
  2. 扩容n小于对象当前的capacity时,什么也不做
  • 很明显,只有当这个 新容量大于旧容量的时候,才会去选择去开空间,这里的扩容逻辑和我们在实现旧版本的拷贝构造函数时类似的:也是先开出一块新的空间(这里主要使用这个newCapacity 去开),然后再将原本的数据拷贝过来,释放旧空间的数据后让_str指向新空间即可。最后的话不要忘了去更新一下容量大小
void reserve(size_t newCapacity)// 扩容(修改_capacity)
{// 当新容量大于旧容量的时候,就开空间if (newCapacity > _capacity){// 1.以给定的容量开出一块新空间char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'// 2.将原本的数据先拷贝过来strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')// 3.释放旧空间的数据delete[] _str;// 4.让_str指向新空间_str = tmp;// 5.更新容量大小_capacity = newCapacity;}
}
  •  注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。

💬 马上来做一个测试

 ⚡resize

 然后我们再来讲讲【resize】,博主觉得下面的这个算法是比较优的,读者可以参考一下

  • 首先我们来分析一下,对于【resize】而言主要对对象中的数据去做一个变化,那就需要去进行分类讨论
  1. 如果这个 newSize < _size 的话,那我们要选择去删除数据
  2. 如果这个 newSize > _size,但是呢 newSize < _capacity 的话,此时要做的就是新增数据但是呢不去做扩容
  3. 如果这个 newSize > _size 并且 newSize > _capacity,我们便要选择去进行扩容了

 当 _size = 10 , _capacity = 15

  • 在分析完了之后,我们立即来实现一下相关的代码。可以看到,一上来我就直接去判断了newSize 是否大于_size,然后在内部又做了一层判断,只有当newSize > _capacity时,才去执行【reserve】的扩容逻辑
  • 如果newSize并没有超过容量大小的话我们要做的事情就是去填充数据,这里用到的是一个内存函数【memset】
    • 我们从_str + _size 的位置开始填充;
    • 填充的个数是newSize - _size个;
    • 填充的内容是c
  • 若是newSize <= _size的话,我们所要做的就是去截取数据,到newSize为止直接设置一个 \0,然后更新一下当前对象的_size大小
// 改变大小
void resize(size_t newSize, char c = '\0')
{// 1.当新的_size比旧的_size来得小的话,则进行删除数据if (newSize > _size){// 只有当新的size比容量还来的大,才去做一个扩容if (newSize > _capacity){reserve(newSize);}// 如果newSize <= _capacity,填充新数据即可memset(_str + _size, c, newSize - _size);}// 如果 newSize <= _size,不考虑扩容和新增数据_size = newSize;_str[newSize] = '\0';
}

 💬 马上我们就来分类测试一下

  • 首先是resize(8),可以看到这里发生了一个数据截断的情况,_size也相对应地发生了一个变化

  •  接下去的话是resize(12),这并没有超过其容量值,但是却超出了_size 大小,所以我们要去做一个增加数据

  •  最后一个则是resize(18),此时的话就需要去走一个扩容逻辑了,并且在扩完容之后还要再进一步去填充数据

 ✨Modifiers —— 修改器

 好,接下去我们来讲讲修改器这一块

⚡push_back

  • 首先第一块的话简单一点,我们去追加一个字符,那首先要考虑到的也是一个扩容逻辑,因为我们是一个字符一个字符去进行插入的,那么当这个_size == _capacity的时候,就要去执行一个扩容的逻辑了,这边的话是运用到了这个三目运算符,若是容量的大小为0的话,默认开个大小为4的空间就可以了;其他的情况都是以2倍的形式去进行扩充
  • 最后在扩完容之后我们就在末尾去增加数据了,因为_size指向的就是 \0 的位置,所以就把字符放在这个位置上就可以了,顺带地记得去后移一下这个_size,再放上一个 \0
// 追加一个字符
void push_back(char ch)
{// 如果数据量大于容量的话,则需要进行扩容if (_size == _capacity){ 	reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}

💬 马上我们就来分类测试一下

 ⚡append

  • 接下去的话是【append】,要追加的是一个字符串,所以我们要先去算出它的长度,接下去判断一下在加上这个长度后是否要去做一个扩容,最后的话还是通过我们熟悉的【memcpy】通过字节的形式一一拷贝到_str + _size的位置(注意拷贝len + 1个,带上最后 \0),最后再把大小_size给增加一下即可
// 追加一个字符串
void append(const char* s)
{int len = strlen(s);	// 获取到待插入字符串的长度// 若是加上len长度后超出容量大小了,那么就需要扩容if (_size + len > _capacity){reserve(_size + len);}// 将字符串拷贝到末尾的_size位置memcpy(_str + _size, s, len + 1);// 大小增加_size += len;
}

 💬 马上我们就来分类测试一下

 读者一定会觉得上面的函数调用太过于冗余,不过没关系,我们还有【+=】

 ⚡operator+=(char ch)

  • 首先的话是去【+=】一个字符,这里我们直接复用前面的push_back()接口即可,最后因为【+=】改变的是自身,所以我们return *this,那么返回一个出了作用域不会销毁的对象,可以采取 引用返回 减少拷贝
string& operator+=(char ch)
{push_back(ch);return *this;
}

⚡perator+=(const char* s)

  • 而对于【+=】一个字符串,我们则是去复用前面的append()即可
string& operator+=(const char* s)
{append(s);return *this;
}

 💬 马上我们就来分类测试一下

⚡insert 

 接下去我们就要来实现一下【insert】这个接口了-- 从 pos 位置 开始插入n 个字符

  • 不过在这之前呢我们先要去声明并初始化一个静态的成员变量npos,它是最大的无符号整数值。但是对于 静态的成员变量 来说我们需要 在类内声明并且在类外进行初始化
// 类内声明
const static size_t npos;
// 类外初始化
const static size_t npos = -1;
  • 首先第一个的话就是要在pos位置插入n个字符
void insert(size_t pos, size_t n, char ch)
  • 因为这里会传入一个pos位置,所以第一步我们就是要去考虑这个pos位置是否合法
assert(pos <= _size);
  • 接下去第二步的话就是去考虑过扩容的问题了,如果_size + n之后的大小大于_capacity的话那就要调用【reserve】接口去实现一个扩容的逻辑了
// 考虑扩容
if (_size + n > _capacity)
{reserve(_size + n);
}
  • 第三步呢并不是直接去插入数据,而是要先给需要插入的n个字符腾出位置。从_size位置开始,让字符以n个单位地从后往前挪即可,若是从前往后挪的话就会造成覆盖的问题

// 挪动数据
size_t end = _size;
while (end >= pos)
{_str[end + n] = _str[end];--end;
}
  • 不过呢,我们在这里还要考虑一种极端的情况,如果这个pos == 0的话,也就是在这个位置开始插入数据,那也就相当于头插,此时需要将全部的数据向后进行挪动,可是呢当这个end超出pos的范围时,也就减到了-1,但是呢这个end的数据类型则是【size_t】,为一个无符号整数,我们知道对于无符号整数来说是不可能为负数的,那么这个时候就会发生一个轮回,变成最大的无符号正数

  • 我们可以来看看当这个end在不断减少直至减到0的时候就会突然变成一个很大的数字,这个其实就是npos的值了,此时就会造成一个死循环,导致程序崩溃
// 字符插入测试
void test6()
{xas_string::string s1("abcdefghijk");s1.insert(0, 3, '#');cout << s1.c_str() << endl;}int main()
{test6();return 0;
}

  • 所以我们应该将无符号该有 有符号类型 size_t ----> int
// 挪动数据int end = _size;while (end >=(int)pos){_str[end + n] = _str[end];--end;}
  • 当这个挪动的逻辑结束后,我们就可以从pos这个位置去插入n个字符了。最后再去更新一下这个_size的大小即可
// 插入n个字符
for (size_t i = 0; i < n; i++)
{_str[pos + i] = ch;
}
_size += n;

 从pos位置开始插入一个字符串

void erase(size_t pos, int len = npos)
  • 对于在【pos位置插入一个字符串】来说,其他逻辑和上面这个接口都是一样,也是要经过 扩容移位放数据 这些操作,只是这里在放数据的时候换成了字符串而言
// 插入字符串
for (size_t i = 0; i < len; i++)
{_str[pos + i] = s[i];
}
_size += len;

整体的代码为: 

void insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}int end = _size;// 向后挪动while (end >= (int)pos){_str[end + len] = _str[end];end--;}// 这里不能用 strcpy 会把 '\0' 拷贝过来strncpy(_str + pos, str, len);_size += len;}

⚡erase

 删除从pos位置开始的len个有效长度字符

  • erase函数的作用是删除字符串任意位置开始的n个字符。删除字符前也需要判断pos的合法性,进行删除操作的时候分两种情况:

 1、pos位置及其之后的有效字符都需要被删除

  • 这时我们只需在pos位置放上’\0’,然后将对象的size更新即可。 

 2、pos位置及其之后的有效字符只需删除一部分。

  •  这时我们可以用后方需要保留的有效字符覆盖前方需要删除的有效字符,此时不用在字符串后方加’\0’,因为在此之前字符串末尾就有’\0’了。

// 删除  -- 从 pos 位置 删除 长度为 len 的字符串 void erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}}

  💬 马上我们就来分类测试一下

 ⚡swap

  • 对于【swap】函数我们在上面已经有讲解过了,此处不再过度赘述
void swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

✨String Operations —— 字符串操作 

然后再来讲讲有关字符串的一些操作 

⚡ find --- 寻找一个字符

  • 这个很简单,就是去遍历一下当前对象中的_str,若是在遍历的过程中发现了字符ch的话就返回这个位置的下标,如果遍历完了还是没有找到的话就返回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;
}

⚡find --- 寻找一个字符串 

  • 我直接使用的是C语言中的库函数 strstr函数详解,这个的话我们在 字符串函数与内存函数解读 的时候也有讲解并模拟过,如果找到了的话就会返回子串第一次出现在主串中的指针。那我们如果要去计算这个指针距离起始位置有多远的话使用指针 - 指针的方式即可。那如果没找到的话我们返回【npos】即可
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;
}

 💬 马上我们就来分类测试一下

 ⚡substr

 上面是去匹配子串,现在我们要将这个子串给取出来,要如何去取呢?

string substr(size_t pos, size_t len = npos)
  • 首先要考虑到的是,如果我们从pos位置开始所要取的子串长度大于剩余的串长,那最多能取到的有效范围也就是从pos位置开始的到末尾的_size结束这段距离,所以当这个所取长度过长的话,我们就要考虑去更新一下取子串长度的有效范围

  • 可以看到,我以这个n作为可取的子串长度,一开始得让其等于传入进来的len长,因为如果这个所取长度没有超出有效范围的话,我们所用的还是len
  • 但是如果呢这个长度超出了有效范围后,我们便要去更新这个n = _size - pos
size_t n = len;
if (len == npos || pos + len > _size)
{// 就算要取再大的长度,也只能取到pos - _size的位置n = _size - pos;
}
  • 那接下去的话我们就可以去取这个子串了,使用循环的方式从pos位置开始取,取【n】个即可,然后追加到这个临时的 string对象 中去,最后呢再将其返回即可,那我们返回一个出了作用域就销毁的临时对象,只能使用【传值返回】,而不能使用【传引用返回】
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{tmp += _str[i];
}
return tmp;

 ✨非成员函数重载

 最后的话再来模拟一些【非成员函数重载】,使用到的也是非常多

⚡relational operators 

① 小于 

//< 运算符重载
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)
{return *this < s || *this == s;
}

④ 大于

bool operator>(const string& s)
{return !(*this <= s);
}

 ⑤ 大于等于

bool operator>=(const string& s)
{return !(*this < s);
}

⑥ 不等于 

bool operator!=(const string& s)
{return !(*this == s);
}

💬 马上我们就来分类测试一下

 ⚡operator<< 流插入

  • 那有认真学习过【类和对象】的话,就可以知道为了不让this所指向的对象默认成为第一个参数的话,我们需要将这个函数实现到类外来,如果要访问类内私有成员的话,就可以使用到【友元】这个东西,不过呢我们不建议使用这个,会破坏类的封装性
// 流插入
ostream& operator<<(ostream& out, const string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
  • 还有一点要提醒的是对于这个流插入来说我们是一定要进行引用返回的,这样就不会去调用拷贝构造了。因为在库中对这个函数是做了一个 防拷贝 的效果,即在后面加上一个= delete

💬 好,那到这里的话,我们是时候来讲讲这个cout << s.c_str() 和 cout << s 的区别了 

  • c的字符数组, 以\0为终止算长度 
  • string不看\0, 以size为终止算长度 

⚡operator>> 流提取【⭐】 

接下去再来看看这个【>>流提取】 

  • 重载>>运算符是为了让string对象能够像内置类型一样使用>>运算符直接输入。输入前我们需要先将对象的C字符串置空,然后从标准输入流读取字符,直到读取到’ ‘或是’\n’便停止读取。
//>>运算符的重载
istream& operator>>(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != ' '&&ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in; //支持连续输入
}

 💬 马上我们就来分类测试一下

⚡getline 

getline函数用于读取一行含有空格的字符串。实现时于>>运算符的重载基本相同,只是当读取到’\n’的时候才停止读取字符。 

//读取一行含有空格的字符串
istream& getline(istream& in, string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in;
}

 💬 马上我们就来分类测试一下

 三、string 类的模拟实现整体代码

🥝 string.h

#pragma once
#include <iostream>
#include <assert.h>
using std::ostream;
using std::istream;
using std::cout;
using std::cin;
using std::endl;// 为了不和 std库 中的 string类 发生冲突,创建我们自己的作用域
namespace xas_string
{class string{public:typedef char* iterator;             // 迭代器某种意义上就是 指针typedef const char* const_iterator; //  默认成员函数string(const char* str = "");       // 有参构造函数string(const string& s);            // 拷贝构造string& operator=(const string& s); // 赋值运算符重载~string();                          // 析构函数//迭代器相关函数iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;//容量和大小相关函数size_t size()  const;                 // 返回目前 字符串的有效字符个数size_t capacity() const;              // 返回目前 字符串的容量void clear();                         // 清空字符串bool empty() const;                   // 判断字符串是否为空void reserve(size_t newCapacity = 0); // 扩容(修改_capacity)void resize(size_t newSize, char c = '\0'); // 改变大小// 修改字符串相关函数void push_back(char ch);               // 追加一个字符void swap(string& s);                  // 交换 --- 交换两个字符串void append(const char* s);            // 追加一个字符串string& operator+=(char ch);           // 追加一个字符string& operator+=(const char* s);     // 追加一个字符串void insert(size_t pos, const char* str); // 插入n个字符void erase(size_t pos, size_t len = npos); // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 // 访问字符串相关函数char& operator[](size_t pos);             // 可读可写const char& operator[](size_t pos) const; // 可读不可写const char* c_str()const;                 // 用 C语言的方式返回size_t find(char ch, size_t pos = 0);     // 寻找字符size_t find(const char* str, size_t pos = 0); // 寻找 字符串string substr(size_t pos = 0, size_t len = npos); // 截取字符串 从某个位置 取 len 个字符// 关系运算符重载bool operator<(const string& s)const;      // < 运算符重载 bool operator==(const string& s)const;     // ==运算符重载bool operator<=(const string& s);          // <=运算符重载bool operator>(const string& s);          // >运算符重载bool operator>=(const string& s);         // >=运算符重载bool operator!=(const string& s);         // !=运算符重载private:char* _str;         // 指向字符数组的指针size_t _size;       // 字符数组的有效数据的长度size_t _capacity;   // 字符串数组的容量const static size_t npos = -1; //静态成员变量(整型最大值)};// 流插入ostream& operator<<(ostream& out, const string& s);//>>运算符的重载istream& operator>>(istream& in, string& s);//读取一行含有空格的字符串istream& getline(istream& in, string& s);void print_str(const string& s);    // const对象的输出 
}

 🍇string.cpp

#include "string.h"// 有参构造函数
xas_string::string::string(const char* str)    // ""  --- 为空的字符串
{_str = new char[strlen(str) + 1];   // strlen 计算的是字符产的长度 ,不计算'\0' 所以要+1_size = strlen(str);_capacity = strlen(str);           // capacity 不包括 '\0'strcpy(_str, str);
}//拷贝构造
xas_string::string::string(const string& s):_str(nullptr), _size(0), _capacity(0)
{string tmp(s._str); //调用构造函数,构造出一个C字符串为s._str的对象swap(tmp); //交换这两个对象
}// 赋值运算符重载
// 传统写法
//xas_string::string& xas_string::string::operator=(const string& s)
//{
//	if (this != &s)
//	{
//		char* tmp = new char[s._capacity + 1];
//		//memcpy(tmp, s._str, s._size + 1);
//		strcpy(tmp, s._str);
//		delete[] _str;
//
//		_str = tmp;
//		_size = s._size;
//		_capacity = s._capacity;
//	}
//	return *this;
//}//现代写法2
xas_string::string& xas_string::string::operator=(const string& s)
{if (this != &s) //防止自己给自己赋值{string tmp(s); //用s拷贝构造出对象tmpswap(tmp); //交换这两个对象}return *this; //返回左值(支持连续赋值)
}// 析构函数
xas_string::string::~string()
{delete[] _str;_str = nullptr;_size = 0;_capacity = 0;
}char& xas_string::string::operator[](size_t pos)        // 可读可写
{assert(pos <= _size);return _str[pos];
}const char& xas_string::string::operator[](size_t pos) const  // 可读不可写
{assert(pos < _size);return _str[pos];
}xas_string::string::iterator xas_string::string::begin()
{return _str;
}xas_string::string::iterator xas_string::string::end()
{return _str + _size;
}xas_string::string::const_iterator xas_string::string::begin() const
{return _str;
}xas_string::string::const_iterator xas_string::string::end() const
{return _str + _size;
}// 针对------const 对象的访问
// 打印这个字符串  --- 不能修改
void xas_string::print_str(const string& s)
{for (size_t i = 0; i < s.size(); i++){std::cout << s[i] << " ";}std::cout << std::endl;string::const_iterator it = s.begin();while (it != s.end()){// 内容不能修改std::cout << *it << " ";// 指针可以修改it++;}std::cout << std::endl;
}void xas_string::string::push_back(char ch)   // 追加一个字符
{// 如果数据量大于容量的话,则需要进行扩容if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size++] = ch;_str[_size] = '\0';
}// 返回目前 字符串的有效字符个数
size_t xas_string::string::size()  const    // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _size;
}size_t  xas_string::string::capacity() const  // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _capacity;
}bool xas_string::string::operator<=(const string& s)          // <=运算符重载
{return *this < s || *this == s;
}// 清空字符串
void xas_string::string::clear()
{_str[0] = '\0';_size = 0;
}
// 判断字符串是否为空
bool xas_string::string::empty() const
{return 0 == _size;
}
void xas_string::string::reserve(size_t newCapacity)// 扩容(修改_capacity)
{// 当新容量大于旧容量的时候,就开空间if (newCapacity > _capacity){// 1.以给定的容量开出一块新空间char* tmp = new char[newCapacity + 1]; //多开一个空间用于存放'\0'// 2.将原本的数据先拷贝过来strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0')// 3.释放旧空间的数据delete[] _str;// 4.让_str指向新空间_str = tmp;// 5.更新容量大小_capacity = newCapacity;}
}void xas_string::string::resize(size_t newSize, char c) // 改变大小
{// 1.当新的_size比旧的_size来得小的话,则进行删除数据if (newSize > _size){// 只有当新的size比容量还来的大,才去做一个扩容if (newSize > _capacity){reserve(newSize);}// 如果newSize <= _capacity,填充新数据即可memset(_str + _size, c, newSize - _size);}// 如果 newSize <= _size,不考虑扩容和新增数据_size = newSize;_str[newSize] = '\0';
}void xas_string::string::append(const char* s)             // 追加一个字符串
{int len = strlen(s);	// 获取到待插入字符串的长度// 若是加上len长度后超出容量大小了,那么就需要扩容if (_size + len > _capacity){reserve(_size + len);}// 将字符串拷贝到末尾的_size位置memcpy(_str + _size, s, len + 1);// 大小增加_size += len;
}xas_string::string& xas_string::string::operator+=(char ch)            // 追加一个字符
{                                      push_back(ch);return *this;
}xas_string::string& xas_string::string::operator+=(const char* s)    // 追加一个字符串
{append(s);return *this;
}void xas_string::string::insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}int end = _size;// 向后挪动while (end >= (int)pos){_str[end + len] = _str[end];end--;}// 这里不能用 strcpy 会把 '\0' 拷贝过来strncpy(_str + pos, str, len);_size += len;}void xas_string::string::erase(size_t pos, size_t len) // 删除  -- 从 pos 位置 删除 长度为 len 的字符串 
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;}else{strcpy(_str + pos, _str + pos + len);_size -= len;}
}size_t xas_string::string::find(char ch, size_t pos)      // 寻找字符
{for (size_t i = 0; i < _size; i++){if (_str[i] == ch){return i;}}return npos;
}size_t xas_string::string::find(const char* str, size_t pos) // 寻找字符串
{const char* ptr = strstr(_str + pos, str);if (ptr == nullptr){return npos;}else{return ptr - _str;}
}
xas_string::string xas_string::string::substr(size_t pos, size_t len ) // 截取字符串 从某个位置 取 len 个字符
{assert(pos < _size);size_t end = pos + len;if (len == npos || pos + len >= _size){end = _size;}string str;str.reserve(end - pos);for (size_t i = pos; i < end; i++){str += _str[i];}return str;
}bool xas_string::string::operator<(const string& s)const       // < 运算符重载 
{return strcmp(_str, s._str) < 0;
}bool xas_string::string::operator==(const string& s)const      //==运算符重载
{return strcmp(_str, s._str) == 0;
}
bool xas_string::string::operator>(const string& s)           // >运算符重载
{return !(*this <= s);
}
bool xas_string::string::operator>=(const string& s)
{return !(*this < s);
}
bool xas_string::string::operator!=(const string& s)
{return !(*this == s);
}// 用 C语言的方式返回
const char* xas_string::string::c_str() const     // 内部不进行修改的文件,可以加上 const 防止权限放大
{return _str;
}// 流插入
ostream& xas_string::operator<<(ostream& out, const xas_string::string& s)
{for (size_t i = 0; i < s.size(); i++){out << s[i];}return out;
}
//>>运算符的重载
istream& xas_string::operator>>(istream& in, xas_string::string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != ' ' && ch != '\n') //当读取到的字符不是空格或'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in; //支持连续输入
}
//读取一行含有空格的字符串
istream& xas_string::getline(istream& in, xas_string::string& s)
{s.clear(); //清空字符串char ch = in.get(); //读取一个字符while (ch != '\n') //当读取到的字符不是'\n'的时候继续读取{s += ch; //将读取到的字符尾插到字符串后面ch = in.get(); //继续读取字符}return in;
}// 交换
void xas_string::string::swap(string& s)
{std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);
}

🍍test.cpp

#include "string.h"//  测试初始化,与循环打印 迭代器
void test1()
{// 初始化测试xas_string::string s1("hello string");cout << s1.c_str() << endl;cout << endl;// 拷贝构造测试xas_string::string s2(s1);cout << s2.c_str() << endl;cout << endl;//赋值运算符重载xas_string::string s3("hello world!");s1 = s3;cout << s1.c_str() << endl;// 引用返回 是可以进行修改的for (size_t i = 0; i < s3.size(); i++){s3[i]++;}cout << s3.c_str() << endl;cout << endl;// 迭代器xas_string::string::iterator it = s1.begin();while (it != s1.end()){std::cout << *it << " ";it++;}std::cout << std::endl;// 范围 forfor (auto ch : s1){std::cout << ch << " ";}cout << endl;cout << endl;// 常量对象print_str(s1);}// 测试容量相关的函数
void test2()
{xas_string::string s1("hello string");cout << s1.size() << endl;cout << s1.capacity() << endl;cout << s1.empty() << endl;s1.clear();cout << s1.empty() << endl;cout << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;s1.reserve(100);cout << s1.size() << endl;cout << s1.capacity() << endl;
}// 测试 resize()函数
void test3()
{xas_string::string s1("abcdefghijk");s1.reserve(15);cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;cout << endl;s1.resize(18,'x');cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;
}// 测试修改函数
void test4()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;cout << endl;cout << "追加字符:xxxxx" << endl;cout << endl;s1.append("xxxxx");cout << s1.c_str() << endl;cout << s1.size() << endl;cout << s1.capacity() << endl;
}void test5()
{xas_string::string s1("abcdefghijk");s1 += 'x';cout << s1.c_str() << endl;s1 += "yyyy";cout << s1.c_str() << endl;
}// 字符插入测试
void test6()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << endl;s1.insert(0,"###");cout << s1.c_str() << endl;}// 字符串删除测试
void test7()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;cout << endl;s1.erase(3, 3);cout << s1.c_str() << endl;}// 访问字符串相关函数测试
void test8()
{xas_string::string s1("abcdefghijk");cout << s1.c_str() << endl;size_t pos1 = s1.find('c', 0);cout << pos1 << endl;size_t pos2 = s1.find("def", 0);cout << pos2 << endl;cout << endl;xas_string::string s2 = s1.substr(5, 5);cout << s2.c_str() << endl;
}// 非成员函数重载
void test9()
{xas_string::string s1("hello string");cout << s1.c_str() << endl;xas_string::string s2("hello world");cout << s2.c_str() << endl;cout << (s1 < s2) << endl;cout << (s1 > s2) << endl;cout << (s1 == s2) << endl;cout << (s1 != s2) << endl;
}// 输入输出流测试
void test10()
{xas_string::string s1("hello");s1 += '\0';s1 += "*******";cout << s1.c_str() << endl;cout << s1 << endl;xas_string::string s2;getline(cin,s2);cout << s2;
}int main()
{test10();return 0;
}

 四、共勉

       以下就是我对 string类的模拟实现 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 C++vector类模拟实现 的理解,请持续关注我哦!!!  

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

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

相关文章

井字棋源码(网络线程池版)

源码链接&#xff1a;game 效果可能没有那么好&#xff0c;大家可以给点建议。 效果展示 game.h #include <stdio.h> #include <stdlib.h> #include <time.h>#define ROW 3 #define COL 3void InitBoard(char board[ROW][COL], int row, int col) {int i…

企业数字化转型,“业务”先行

在当今时代&#xff0c;数字化转型已经成为企业发展的必经之路。数字化转型&#xff0c;简而言之&#xff0c;就是运用数字技术&#xff0c;对企业运营管理的各个环节进行深度改造&#xff0c;以提升企业的运营效率和市场竞争力。据有关机构研究测算&#xff0c;数字化转型可使…

丈母娘眼中“靠谱女婿”职业榜曝光,公务员跌落榜首,新兴职业成宠儿!

正如婆婆和媳妇相处很复杂&#xff0c;丈母娘和女婿亦有着微妙关系&#xff0c;看对眼是“半个儿”&#xff0c;不对付则会成为小两口婚姻的“地雷”&#xff0c;甚至是恋爱路上的“拦路虎”。 近来&#xff0c;最新丈母娘认可的“靠谱女婿”职业榜排行新鲜出炉&#xff0c;备受…

5g工业数采网关是什么?-天拓四方

随着工业4.0时代的到来&#xff0c;数字化、网络化、智能化成为工业发展的新趋势。在这个过程中&#xff0c;5G工业数采网关作为一种关键设备&#xff0c;发挥着越来越重要的作用。本文将详细解析5G工业数采网关是什么&#xff0c;以及它在工业领域中的应用和重要性。 一、5G工…

socket套接字在tcp客户端与tcp服务器之间的通信,以及socket中常用的高效工具epoll

1.socket&#xff08;套接字&#xff09;的概念 Socket是对TCP/IP协议的封装&#xff0c;Socket本身并不是协议&#xff0c;而是一个调用接口&#xff08;API&#xff09;&#xff0c;通过Socket&#xff0c;我们才能使用TCP/IP协议,主要利用三元组【ip地址&#xff0c;协议&am…

【芯片科普】运算放大器用作比较器的注意事项

运算放大器和比较器 比较器和运算放大器电气符号非常相像&#xff0c;都是有反相、同相两个输入端和一个输出端的器件&#xff0c;输出端的输出电压范围一般在供电的轨到轨之间&#xff1b;同时比较器和运算放大器都具有低偏置电压、高增益和高共模抑制比的特点。 图1 运算放…

自由场、半自由场、扩散场

按声场性质可以将声场分为三类&#xff1a;自由声场、半自由声场、扩散声场 分别对应着全消声室&#xff0c;半消声室&#xff0c;混响室 自由声场&#xff1a; 声源在均匀、各向同性媒介中传播时&#xff0c;不计边界影响的声场&#xff0c;此时声场中只有直达声没有反射声。…

测试工程师面试准备(软硬件)

您好&#xff0c;我叫XXX。学历XX&#xff0c;XXX专业毕业。X年X月份毕业&#xff0c;但是去年二月份已经找到工作开始实习了&#xff0c;目前工作一年了&#xff0c;这一年的过程中我主要负责软件的开发和测试和软硬件联调测试工作。具体来说就是&#xff0c;在软件开发完成后…

华为 2024 届实习校园招聘-硬件通⽤/单板开发——第八套

华为 2024 届实习校园招聘-硬件通⽤/单板开发——第八套 部分题目分享&#xff0c;完整版带答案(有答案和解析&#xff0c;答案非官方&#xff0c;未仔细校正&#xff0c;仅供参考&#xff09;&#xff08;共十套&#xff09;获取&#xff08;WX:didadidadidida313&#xff0c…

Qwen1.5微调

引子 由于工作上需要&#xff0c;一直在用Qwen做大模型推理&#xff0c;有个再训练的需求&#xff0c;特此琢磨下Qwen的训练。OK&#xff0c;我们开始吧。 一、安装环境 查看显卡驱动版本 根据官网推荐 OK&#xff0c;docker在手&#xff0c;天下我有。 docker pull qwenll…

rancher-rke2 修改--service-cluster-ip-range

一、场景 因为需要部署新版本的ingress-nginx&#xff0c;而部署ingress-nginx的时候需要使用hostnetowrk以及nodeport的端口为80和443&#xff0c;service-node-port-range 默认为30000开始,部署会报错。 二、产生修改的需求 1、api-servier的配置文件位置 默认是没有的&…

LabVIEW学习记录 - 实时显示时间

LabVIEW操作 - 实时显示时间 在程序框图&#xff0c;选择函数->定时->格式化日期/时间字符串 该函数的使用手册说明&#xff1a; 鼠标选择“格式化日期/时间字符串”->创建->输入控件->输入格式 查看时间代码格式&#xff1a; 编程->定时->获取时间日…

Linux交换空间的创建使用

交换空间&#xff1a; 换出&#xff1a;将内存中不常用&#xff08;冷数据&#xff09;的放去硬盘里 换出&#xff1a;内存要使用这部分数据时&#xff0c;将硬盘的这部分数据放入内存 在内存和硬盘上用来交换数据的空间就是交换空间 创建交换空间的步骤 1.去磁盘上创建一个分…

C语言 | Leetcode C语言题解之第47题全排列II

题目&#xff1a; 题解&#xff1a; int* vis;void backtrack(int* nums, int numSize, int** ans, int* ansSize, int idx, int* perm) {if (idx numSize) {int* tmp malloc(sizeof(int) * numSize);memcpy(tmp, perm, sizeof(int) * numSize);ans[(*ansSize)] tmp;return…

OssUtil工具上传文件

安装、上传、下载 1. 安装ossutil sudo -v ; curl https://gosspublic.alicdn.com/ossutil/install.sh | sudo bash2. 配置ossutil ossutil config3. 验证是否已成功安装ossutil ossutil 如果屏幕中输出ossutil所有支持的命令&#xff0c;表明已成功安装ossutil。# 完整上传 os…

ruoyi-nbcio-plus基于vue3的flowable修正加签与跳转的前端问题

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/ 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a…

idea 中导入的项目maven不自动下载依赖包

导入之后不会自动引入依赖包&#xff0c;如下图&#xff0c;external libraries 下没有依赖 解决方案&#xff1a;重新更新下maven的Local repository 即可

MMSeg搭建自己的网络

配置结构 首先&#xff0c;我们知道MMSeg矿机的配置文件很多&#xff0c;主要结构如下图所示。 在configs/_base_下是模型配置、数据集配置、以及一些其他的常规配置和运行配置&#xff0c;四类。 configs/all_config目录下存放&#xff0c;即是将四种配置聚合在一起的一个总…

产品推荐 | BittWare基于Altera Agilex“M FPGA的lA-860m加速卡

01 产品概述 BittWare的lA-860m是一款Altera Agilex“M系列FPGA卡&#xff0c;针对吞吐量和内存密集型应用进行了优化。M 系列 FPGA 具有广泛的内存层次结构&#xff0c;包括集成高带宽存储器 &#xff08;HBM2e&#xff09; 和硬内存片上网络 &#xff08;NoC&#xff09;&am…

自动化测试超详细总结

简介 软件测试是软件开发过程中一个必不可少的环节。传统的软件测试方式通常是手动测试&#xff0c;即由专业的测试人员通过手动操作软件应用程序来验证其功能和性能。然而&#xff0c;这种方式存在许多缺点&#xff0c;例如时间耗费、测试结果不稳定、测试覆盖率不够高等。 为…