【c++深入系列】:万字string详解(附有sso优化版本的string模拟实现源码)
🔥 本文专栏:c++
🌸作者主页:努力努力再努力wz
💪 今日博客励志语录:
当你想放弃时,想想为什么当初坚持走到了这里
★★★ 本文前置知识:
类和对象(上)
类和对象(中)
类和对象(下)
STL
那么在我的上一篇博客中,我着重讲解了模版,那么有了模版之后,那么c++的设计者在模版的基础上引入创建了STL,那么想必你在学习c++之前也一定听说过STL的鼎鼎大名,那么所谓的STL,就是c++为我们提供的一个算法与数据结构的标准库,那么其中有我们很多常见的数据结构的实现比如顺序表以及链表还有栈和队列等等,那么在以前学习c语言的时候,这些数据结构都需要我们自己来手写实现,但是对于c++来说,那么它就已经帮你把这些工作给完成了,也就是说,你听说过的绝大部分的数据结构,那么c++的标准库中都有对应的实现,那么我们要做的就是站在巨人的肩膀上前行,也就是调用前人为我们写好的数据结构来解决更多复杂的问题,而不需要我们再去浪费时间在自己去造轮子了,那么STL中的各种数据结构有一个更为专业并且形象的名字来称呼,那么便是容器
那么STL除了能够提供容器,那么它还会提供各种针对于相关容器的一些算法,其中最为熟知的便是sort函数也就是排序算法,那么它底层是采取快速排序来实现,并且默认是以升序进行排列,并且能够针对不同的容器来进行排序,除了sort,那么STL还提供了swap函数,swap是交换不同数据类型的任意两个数,那么其实底层采取的就是模版实现,还有返回两个数的较大值的max函数,那么STL中相关的算法都在algorithm头文件中,我在往后的博客中也会依次来介绍
那么STL除了大家最为熟悉的容器以及算法,那么还提供了各种迭代器,那么对于没学习或者没接触过STL的初学者来说,那么可能会对迭代器感到陌生,那么这里迭代器我们可以简单理解为它就是以一种统一接口来遍历各个不同的容器,那么至于底层如何实现,那么我会在往后的博主介绍,那么这里我们主要就是来初步的认识STL
那么STL还会提供分配器以及适配器和函数对象,那么这部分内容就是作为了解即可,那么对于初学者来说,目前还接触不到,那么目前我们学习STL的内容核心是围绕容器以及算法和迭代器这三个内容的学习上,那么有了STL之后,那么你会发现STL对于初学者带来的最大的一个帮组之一就是体现在写算法题上,因为算法题经常会用到各种数据结构比如顺序表以及栈等等,那么如果你用c语言来写算法题的话,那么你第一步肯定得是先实现这个数据结构以及这个数据结构相关操作的函数,那么你还在磨刀的时候,而对于其他用c++的人来写算法题的话,那么此时他们就已经在砍柴了,他们只需要引入STL库中包含特定容器的头文件即可,所以STL的意义不言而喻
那么本文作为STL系列的第一篇博客,那么要介绍的第一个容器便是string,那么string严格意义上来说其实并不属于STL库中的容器中的一种,由于历史原因,那么string的诞生是早于STL库的出现,那么所以就会导致string有一些内容是不符合STL的规范,那么看到下文你便会知道string究竟是哪部分内容不满足STL的要求,那么string为了适应STL库,采取了向下兼容的方式,所以string在广义上是可以属于STL库的,那么了解了string的一个历史背景之后,那么接下来我们就来全方位的来认识string
那么这里推荐一个学习STL很好用的一个网站(这里不是打广告):
https://cplusplus.com/reference/,该网站虽然不是c++的官方文档网站,但是它整个网站的排版以及涉及到的内容都很全面,并且网站稳定并且很干净,没什么广告,那么当我们对于某一个容器,或者容器中的某个成员函数感到陌生的话,那么我们可以打开这个网站,像查字典一样去搜索即可
string的意义
那么在想必读者在好奇string如何使用以及其底层实现之前,那么首先的第一个疑问肯定是为什么要有string
那么我们知道string就是字符串,那么我们现实生活中很多内容比如一个人的姓名以及一个商品的名称,那么这些现实生活中的内容在计算机中的映射就是以字符串的形式存在,那么由于计算机底层只能存储0和1组成的二进制序列而不会存储诸如ABCD这样的字符符号,所以为了表示出字符,那么采取的就是一个编码的方式,那么所谓的编码其实就是将建立一个所谓的映射表,那么将这些特定的字符给映射对应一个特定值的二进制序列,其中最为常见的编码便是ASCII,那么ASCII码将26个英文字符以及一些常见的符号比如‘*’,‘$'等都建立了对应的映射,比如小写的a字符在底层对应的就是值为97的二进制序列,那么你会发现阿斯克码只是解决了美国人关于他们表达所需要的符号的映射关系,而没有包含汉字等其他语言符号的映射,所以之后便有诞生了UTF-8编码等等来解决中文符号的映射的需求,那么对于char类型的数据,那么它采取的就是阿斯克码
那么对于c语言来说,那么它表示一个字符串,采取的就是一个字符数组的形式,那么数组的每一个元素是char类型,其中在最后一个字符的结尾会添加一个\0作为标记,因为c语言的很多针对字符串操作相关的函数会获取到到字符串的起始位置,但是它不知道该字符串的结束位置,所以就以\0来作为结尾位置的标记
而我们有两种方式来存储字符串,那么第一种方式便是定义一个固定大小的静态数组来存储字符串,那么在定义的时候,就需要我们显示的在字符串末尾添加\0,其次我们定义完静态数组之后,那么我们要对该字符串进行比如增删改查等等操作,那么我们只能使用c语言为我们提供的str系列函数,比如你现在有一个字符串拼接的需求,那么此时你可以调用strcat函数来满足这个需求
char *strcat(char *dest, const char *src);
但是对于strcat函数来说,那么它会接收两个参数,第一个参数是指向被拼接的字符串的首元素的字符指针,第二个参数则是指向拼接的字符串的首元素的字符指针,但是strcat函数在设计的时候明显有一个缺陷,因为strcat工作的原理,就是将被拼接的字符串依次拷贝到原字符串的末尾,直到遇到拷贝到拼接字符串的\0位置结束,但是该函数不会检查被拼接字符串剩余空间的容量是否能够容纳拼接的字符串的长度,所以就会有越界的风险,所以相较于strcat函数来说,那么更为安全的函数则是strncat函数
char *strncat(char *dest, const char *src, size_t n);
那么它还会接收第三个参数,也就是指定要拷贝拼接字符串的长度,那么它通过第三个参数来及时截断,避免越界的风险
那么知道c语言如何实现字符串的增功能,那么对于字符串的拷贝,那么这里c语言则是提供了strcpy函数,那么这个函数会接收一个指向拷贝的目标字符串的首元素的的字符指针以及一个指向被拷贝的字符串的首元素的字符指针,那么它的工作原理,就是遍历被拷贝的字符串,然后将拷贝的字符串的字符拷贝到对应位置直到遇到\0结束
同样对于strcpy函数来说,它也有一个明显的缺陷,就是该函数也不会检查目标字符串的容量是否能够完整拷贝新的字符串,所以也会有越界的风险
其次如果我们要对字符串进行分割的话,那么c语言则是提供了strtok函数,那么它接收一个分割的目标字符串以及分隔符字符串,那么这里strtok函数的调用就比其他的str系列的函数比如strcpy以及strcat函数要复杂,而strtok函数的内部会维护一个静态的指针,来保存下一次分割的起始地址,然后从分割的起始地址开始,来对每一个字符进行遍历,如果该字符是分隔符,那么就将该字符设置为\0,然后返回分割的起始地址
所以第一次调用strtok函数的时候就需要我们传一个指向原本字符串的起始地址,然后下一次调用的时候就不需要传指针了,只需传递一个空指针NULL即可,因为内部通过静态的指针保存了下一次要分割的位置,而如果你不熟悉strtok函数的实现原理的话,那么这里如果你调用了strtok函数分割了字符串str1,然后再调用strtok函数去分割字符串str2的话,那么此时第二次调用就会覆盖之前第一次调用的静态指针所保存的str1下一次分割的起始位置的内容,那么你再一次调用strtok函数来分割str1的话,就会导致分割出错
//错误用法
char* token1=strtok(str1,"|");
char* token2=strtok(str2,"|");
token1=strtok(NULL,"|");
其次对于strtok,那么如果你分割的字符串的长度不为1,比如是这种情况:
char* token=strtok(str1,"*|");
那么注意,此时这里strtok函数会将’*‘和’|'视作两个独立的分隔符来处理,如果str1是这种形式的字符串:
"hello*k*k*|woo"
那么此时就会被分割成[hello\0,k\0,k\0,woo\0]这四个部分,那么你本来的需求是想要以",|"整体作为分割符,但是这里strtok的行为则是将每个字符拆分作为单独的分隔符,只要匹配到其中任意一个分割符就进行返回
所以刚才上文讲了我认为c语言中str系列函数中最容易使用出错的三个函数,不仅刚才带你回顾了这三个函数如何使用以及注意的坑,更重要的是我想传达的一个观点,那么就是c语言为我们提供的str系列函数其实是并不完美的,其中是有很多的缺陷的,比如strcat以及strcpy的越界问题,那么就十分考验我们对于这些str函数底层的原理实现的一个理解以及掌握程度,其次我们如果采取的静态数组来存储字符串的话,第一要注意的点是就是得手动设置字符串结尾的\0,如果忘记设置\0,那么后果不用多说,其次对于是静态固定大小的数组的话,那么你还会面临一个问题,比如我添加一个字符串,此时这个该静态数组的容量不够又该怎么办,而静态数组无法进行扩容,而如果你采取的是动态数组的话,那么c语言的str系列函数都没有支持检查容量以及扩容的逻辑,那么意味着你要实现所谓的扩容的话,那么你得手动造轮子了
所以在这样的c语言的字符串这样的大背景下,那么string类就诞生了,首先string类采取的就是动态数组的方式实现,从上文我们便知道静态数组实现肯定是不可取的,其次string类内部为自动维护字符串的合法性,也就是它无论进行什么操作,比如字符串的增以及删或者改,那么它都会自动的在字符串末尾添加\0,并且字符串增删改查的相关成员函数在string内部都有提供,并且它们都支持容量的检查以及扩容的逻辑,意味着其功能实现的十分完善并且安全,所以string的诞生就是解决了c语言的字符串的各种缺陷,并且无需我们自己手动来维护,那么这些工作在底层都有对应的实现,那么我们只需要放心的对字符串做增删改查等操作即可
2.string如何使用
那么这里知道了string的意义之后,那么接下来我就会从两个维度来你认识string,首先就是我会给你展示string的一些常见的成员函数如何使用,那么展示完之后,我们再来模拟实现string,不仅能够帮组我们更清楚string的底层来加深我们对于string的理解和使用,还可以来锻炼一下类和对象的相关知识
那么对于string类来说,那么它内部的成员函数超过了50个,所以这里我们学习string的时候没必要全部掌握所有的成员函数,只需要掌握最常用的那几个就可以了,那么既然是string类,那么第一首先要谈到肯定则是构造函数
构造函数
那么对于string来说,那么它提供了多个重载版本的构造函数来满足用户不同的初始化需求,那么这里我们我们关照使用频率最高的几个重载版本的构造函数,那么第一个则是接收一个C风格的字符串
string(const char* str);
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{
string s1("WangZhuo");cout<<s1<<endl;
}
其次是拷贝构造函数:
string(const string& str);
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1("Pengyuyan");string s2(s1);cout<<s2<<endl;return 0;
}
最后就是一个无参数的构造函数,那么它则是构建一个空字符串
string();
2.size函数
那么由于string是采取动态数组的方式来实现的,那么内部会封装一个指向在堆上开辟的动态数组的首元素地址的字符指针以及一个记录字符串有效长度的size变量还有记录动态数组当前容量的capacity,那么size函数的作用就是返回其中的size成员变量
size_t size();
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1("WangZhuo");cout<<s1.size()<<endl;
}
那么这里其实还有一个length函数,那么它的功能和size函数是一模一样,那么上文我们就说过由于历史原因,string的诞生是早于STL的出现,那么当时的的设计者认为length能够更好的表达字符串的长度的含义所以选择了这个名字,但是随着的STL出现,其统一了容器的命名规范,那么容器中存储的有效数据的大小都用size来表示,而这里string为了适应STL,那么添加了size函数,但是也向下兼容了length,所以这就是上文为什么说在严格意义上string不属于的容器的一部分,那么这里还是建议读者统一使用size函数
3.capacity函数
那么capacity函数的作用就是返回动态数组目前的容量,这里就不在过多赘述了
str.capacity();
4.reserve函数
那么reserve函数的使用频率则很高,那么它的作用就是我们可以调用reserve预先开辟好一定大小的一个动态数组,来减少扩容,那么它则是接收一个size_t的参数,那么该参数就是我们要开辟的空间的大小
void reserve(size_t n);
那么要注意的就是,如果我们申请的空间比原来的capacity要大的话,那么此时就会扩容,那么如果申请的空间比capacity小,比如现在的capacity是100,结果你调用reserve申请50的空间,那么注意这里不会有缩容的行为
5.push_back函数
那么push_back函数的作用就是尾插一个字符,那么这里将字符插入到字符串的末尾
void push_back(char ch);
6.append函数
push_back函数则是追加字符到原字符串的末尾,那么append的函数则是追加一个字符串到原字符串的末尾,那么这里append也提供了多个重载版本,那么其中最为常用的便是追加一个c风格的字符串以及追加一个string
void append(const char* str);
void appned(const string& str);
7.运算符重载函数
下标访问运算符重载函数
那么这里string还提供了多个运算符重载函数,那么我们知道string的底层是采取的数组的方式来实现的,那么我们访问一个string内的字符串的每一个字符,那么通过[]运算符来访问就非常的直观与形象,所以这里string重载了[]运算符,那么我们可以按照数组的方式来访问遍历字符串的每一个字符或者修改字符串的每一个字符
char& operator[](size_t pos);
const char& operator[](size_t pos);
那么这里[]运算符重载函数那么支持了两个重载版本,分别针对普通的string类型以及const修饰的string类型,那么对于第一种则是可读可写,那么对于第二种则是只能读不能写
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1="WangZhuo";
for(int i=0;i<s1.size();i++)
{cout<<s1[i];
}return 0;
}
加等运算符重载函数:
其次string还重载了加等运算符,那么其功能就是拼接字符串,那么对于加等重载运算符来说,string提供了两个重载版本,那么加等运算符重载函数能直观的表示出字符串的拼接,提供代码的可读性
string& operator+=(const char* str);
string& operator+=(const string& str);
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1;s1+="Pengyuyan";cout<<s1<<endl;return 0;
}
加运算符重载函数:
那么这里加运算符重载函数与加等运算符重载函数的区别就是加等运算符是在原字符串的空间中拼接字符串,而对于加运算符来说,则不是在原字符串上拼接,而是会创建一个原字符串拷贝得到的临时对象,用该临时对象来进行字符串的拼接,然后将其返回拷贝左值
string operator+(const char*);
string operator+(const string& str);
赋值运算符重载函数:
那么string也重载了赋值运算符重载函数,那么要注意的就是string内部是采取动态数组的方式实现的,所以赋值运算符重载函数底层的实现原理采取的是深拷贝而不是浅拷贝,那么这里赋值运算符也提供了多个重载版本,那么作为常见的就是用字符串或者string类来赋值
string operator=(const char* str);
string operator(const string& str);
8.迭代器
那么在学习各种容器的过程中,我们一定会接触到迭代器,那么所谓的迭代器就是访问容器的一个接口,也就意味着我们可以通过迭代器来访问容器中存储的各个元素,那么对于string来说,由于string内部将动态数组等成员变量定义为了私有成员变量,那么我们不可以通过实例化的对象来访问私有的成员变量,所以string内部就提供了一个接口来满足我们读取或者修改string内部保存的字符串的各个字符的需求,那么就是通过迭代器的方式来访问,那么string内部的迭代器本质上就是一个字符指针,由于string底层的构造很简单,采取的就是动态数组的方式来实现,所以迭代器的构造也很简单,本质就是一个字符指针,只不过typedef重命名成了iterator,那么对于其他容器比如链表乃至后面会学的set和map等容器,那么他们本身的结果就很复杂,所以这些容器所对应的迭代器的实现甚至封装成了一个类
typedef char* iterator;
typedef const char* const_iterator;
那么这里string内部提供了两种迭代器,第一种是针对访问非const修饰的string对象,那么可以通过该迭代器来访问并且可以修改string对象中字符串的各个字符的内容,而第二个迭代器则是针对的是const修饰的string对象,那么该迭代器就只能访问const修饰string对象的字符串中的各个字符,也就是只能读,不能写
那么我们知道了迭代器的本质是一个字符指针,所以定义的时候就得初始化指向字符串的某个位置,所以string内部提供了两个接口,分别是begin函数以及end,那么从名字就可以看出这两个函数的作用,那么这两个函数分别返回字符串的起始位置以及字符串的结束位置或者说\0所在的位置,那么这样我们就可以通过begin函数以及end函数初始化迭代器,从而来遍历字符串
char* begin();
const char* begin() const;
char* end();
const char* end() const;
那么这里begin以及end也重载两个版本分别满足非const修饰以及const修饰的string对象
要注意的是迭代器的声明是放在string类的类域中,所以我们在使用迭代器的时候,也要指定类域
string::iterator it=s1.begin();
while(it!=s1.end())
{cout<<*it;it++;
}
所以在引入了迭代器之后,那么此时我们有两种方式来遍历一个string对象中保存的字符串,第一种则是通过下标访问运算符[]重载函数来遍历,第二种则是这里的迭代器,但是我更推荐第一种方式来访问,因为更为直观并且可读性更高,虽然迭代器使用上来说确实没有下标访问运算符来的方便,但是也不要忽视迭代器的作用
,比如在下文所说的一个场景:
那么我们知道c++支持一个语法糖,也就是范围for,范围for不需要设置for循环遍历的次数,那么范围for的底层就是利用迭代器来实现,那么编译器会将其处理为调用迭代器来接收一个begin函数的返回值,然后遍历直到匹配end函数的返回值结束,那么这里我们可以通过查看汇编代码,或者在下文我会模拟实现一个string类,也可以来验证,当使用自己实现的一个string类来实现范围for,如果我将begin函数或者end函数给注释掉,那么看编译器此时还能否编译通过,甚至我们都可以不需要注释,也就是稍微改一下函数的名字,比如将end改成End函数,那么你会发现看似编译器很智能很聪明,其实对于范围for的底层实现来说,还是很死板的,那么这里我就先埋下一个伏笔,那么读者可以心里可以先记住,那么我在后文的彩蛋部分,来验证这个情况
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1="WangZhuo is good";//你的视角for(auto ch:s1){cout<<ch;}//编译器的视角/*string::iterator it=s1.begin();while(it!=s1.end()){cout<<*it;it++;}*/return 0;
}
9.insert函数
之前的append函数以及push_back函数都只能是在原始字符串的尾部插入一个字符串或者一个字符,那么这里insert函数本质上也是插入字符,但是它可以在字符串有效长度内的任意位置插入字符或者字符串,那么这里string内部提供了多个insert函数的重载版本,那么这里我就讲解使用频率最高的几个insert函数的重载版本
void insert(size_t pos,const char* str);
void insert(size_t pos,const string& str);
void insert(size_t pos,size_t n,char ch);
那么这里第一个重载版本就是在字符串有效长度内的任意位置插入一个C风格的字符串,第二个则是插入string对象中保存的字符串,第三个则是在字符串有效长度内插入任意n个字符
那么insert的底层实现会涉及到元素的移动以及复制,那么z字符串有效长度内的任意一个位置插入一个长度为k的字符串,而在第一个位置插入的时间复杂度是最坏的,因为之后的所有字符串全部得往后移动,那么每个字符串移动的距离都是k,那么意味着insert函数的时间复杂度是O(n*k),最坏能达到o(N^2)级别,而对于尾插来说,那么不会涉及到元素的移动,那么时间复杂度是o(1),但是我们已经有尾插的相关函数了比如push_back以及append,不会专门用insert来实现尾插,所以这里的建议就是尽量少去使用insert函数去插入元素
10.erase函数
那么既然有插入,那么肯定就会有删除函数,那么erase函数就是用来删除字符串有效长度内的任意长度的子串,那么这里erase函数也提供了多个重载版本,那么我认为使用频率最高的就是以下两个重载的erase函数
void erase(size_t pos,size_t len=npos);
void erase(iterator first,iterator last);
那么第一种方式就是从pos位置开始删除n个字符,那么这里注意这里第二个参数len提供了一个npos的缺省值,那么这里npos是string内部的一个静态的size_t的成员变量,其值为-1,而我们知道size_t是无符号整形,那么-1对应的无符号整形就是整数最大值,所以npos的意义就是用来表示一个无效值,那么这里如果我们没有提供第二个参数,那么第二个参数会使用缺省值也就是npos,那么npos肯定远远大于从pos位置开始的剩余的字符串的长度,所以此时就会被解读为直接从pos位置删除之后所有的内容
其次如果我们给的len大于pos位置之后的字符串的长度的话,那么也会采取删除pos位置之后所有的字符来处理
那么对于第二种方式,传递了是两个迭代器,那么上文我们说过可迭代器本质是一个字符指针,那么这里我们传递两个字符指针一个指向删除的起始位置,另一个指向删除的结束位置,那么这两个指针构成一个左闭右开的区间,那么erase则是删除这个左闭右开的区间中的所有内容
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1="WangZhuo";
//第一个重载版本
s1.erase(0,2);cout<<"s1"<<s1<<endl;
//第二个重载版本
string::iterator first=s1.begin();
string::iterator last=s1.end();
s1.erase(first,last);cout<<"s1:"<<endl;return 0;
}
11.resize函数
那么这里的resize函数的作用就是来修改字符串的有效内容,那么这里假设该string对象中的字符串的长度是20,那么此时我们希望该字符串的有效长度为10的话,那么此时我们就可以调用resize函数来将字符串的长度给截取为10,而我们知道string底层是采取动态数组的方式来实现的,那么此时resize函数不会去缩小动态数组的容量,只是去修改size成员变量的大小,并且截取后的字符换末尾添加\0
而如果我们此时是这种场景,那么字符串的长度是20,而capacity是50,那么我们修改字符串的长度为40,那么此时注意此时resize的行为,那么它不会去扩容,因为容量足够,那么这时候resize会更新size的值,那么之后新增加的20个长度的内容默认以\0来填充,当然我们也可以指定字符来填充这新增加的20个长度的内容
而如果此时capacity是50,你resize的值是大于50的话,比如你resize的长度是80,那么此时就会涉及到扩容
resize(size_t len);
resize(size_t len,char ch);
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1="WangZhuo";cout<<"s1 size is "<<s1.size()<<endl;
//第一个重载版本
s1.resize(3);cout<<"s1 size is "<<s1.size()<<endl;cout<<s1<<endl;
//第二个重载版本
s1.resize(10,'x');cout<<"s1 size is "<<s1.size()<<endl;cout<<s1<<endl;return 0;
}
12.substr函数
那么最后想说的就是substr函数,那么它是用来分割子串的函数,那么它会接收两个参数,第一个参数就是分割子串的起始位置,那么该位置一定得在字符串的有效长度之内,那么第二个参数便是分割子串的长度
string substr(size_t pos,size_t len=npos);
那么这里注意的是第二个参数len提供了缺省值,那么当我们传递第二个参数的时候,那么len会采取npos,那么npos是远大于pos位置之后的字符串的长度的,所以这里substr所做的行为就是将pos位置之后的子串给全部提取出来,同理如果你的len长度大于pos位置之后的子串长度所做的行为也是一样
使用示例:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1="WangZhuo";cout<<s1.substr(2,3)<<endl;return 0;
}
那么上面这12个关于string类的成员函数的介绍以及展示就到此结束,那么上面的这些成员函数肯定没有涵盖完所有的string的成员函数,但是上面的这些成员函数就已经涵盖了关于字符串的操作的基本功能,那么掌握这些就基本上能够可以把string给玩起来了,那么对string感兴趣的读者下来还可以去自己查看更多的成员函数
那么第一个展示的环节就已经结束,而第二个环节那么就是我们来尝试模拟实现一个string类,来帮组我们加深对于string的理解与使用
string的模拟实现
前置知识准备
那么在具体实现string的各个函数模块之前,那么我们得有一个前置知识的储备
1.短字符串优化
那么我们知道string在底层是采取的动态数组的方式来实现字符串的存储,但是现代的编译器对string做了一定的优化,该优化就是sso短字符串优化
我们知道对于动态数组会调用new来申请空间,那么其中new申请空间就会涉及到查找相应的空闲的连续的内存块,然后将其分配给相应的对象,并且一旦string对象被销毁,还涉及到调用析构函数来清理释放在堆上申请的空间,也就是说调用new是有一定的成本的,而string的设计者肯定是希望减少new的调用的,所以这里创建一个string对象的时候,那么可以调用new来预先分配一定大小的空间,从而避免后序的扩容,但是这个方式面临的问题就是不知道string对象具体保存的字符串有多长,那么如果预先开辟的数组大小不合适,那么就会造成空间的浪费
所以这里采取了更为合理的设计就是如果你的字符串是小于某个标准,比如字符串长度小于22或者15,那么其实没必要创建一个动态数组,因为你开辟了大小为22或者15甚至大小为2的动态数组,那么后序如果该string对象进行比如字符串拼接或者插入的行为,那么一定会涉及到扩容,所以我们更希望是那种较长的字符串使用堆数组,这样能够分配较大的空间,可以减少扩容,而对于较小的字符串,那么采取的就是用栈数组来存储即可,那么这就是string的短字符串的优化
那么在VS平台下,采取的是如果你的字符串的长度是小于15的话,那么就会采取短字符串优化,采取栈数组来存储,其他平台可能采取的是字符串长度可能是22
我们可以写一段简答的代码来验证在各个平台下具体设置的短字符串的长度:
#include<iostream>
#include<string>
using namespace std;
int main()
{string s1 ;cout << s1.capacity() << endl;return 0;
}
VS平台:
那么这里我们如果要模拟实现短字符串优化的话,那么我们就得利用匿名联合体,那么可能有些小伙伴可能对于联合体就很陌生,那么这里我来简单回顾一下联合体,那么对于联合体很熟悉的小伙伴可以跳过,那么匿名联合体是string类的实现的一个关键
那么我们知道对于结构体来说,那么它内部的成员变量是按照其在内部声明的顺序,根据内存对齐的规则,按照一定的偏移量在对象中存储的,而对于联合体来说,那么联合体中的所有的成员变量的偏移量都在联合体对象的起始地址,也就意味着,此时联合体中的所有的成员变量是共享联合体的内存空间,所以联合体对象的内存布局和结构体是完全不同的,那么既然联合体中所有的成员变量的偏移量都是从联合体的起始地址开始,那么意味着一个问题,那么就是联合体只能有一个成员变量的数据是有效的,假设存在这样的场景:
union wz{int tp1;double tp2;}; union wz s1;
那么这里对于联合体wz来说,那么它的成员变量tp1以及tp2的起始地址都是在联合体对象的起始地址,那么意味着如果你此时对tp1的成员变量赋值,那么它必然会覆盖tp2的值,同理你对tp2赋值,那么同样会覆盖tp1,所以这就是为什么联合体中的只能有一个成员变量有效,那么我写了一份简单的代码来验证联合体的特性:
#include<iostream>using namespace std;union wz{int tp1;double tp2;};int main(){union wz s1;s1.tp1 = 4;cout <<"before tp1:" <<s1.tp1 << endl;s1.tp2 = 10.95;cout << "after tp1:" << s1.tp1 << endl;cout << "tp2: " << s1.tp2 << endl;return 0;
}
那么会出现这样的运行结果,那么大家可以从联合体的内存布局,就能够清晰的认识到:
那么其次就是对于联合体对象的所占据的空间大小,那么它的大小一般是等于最大的成员变量的大小,但前提是其满足是默认对齐数的整数倍,那么VS的默认对齐数是4,那么在上面的例子中该联合体的大小就是8,而对于下面这个联合体来说:
union wz{char tp1[5];int tp2;};
那么最大的成员变量的大小是5,那么默认对齐数是4,那么这里整体的大小不满足是4的整数倍,所以最终该联合体的大小就是8:
这里我们采取的就是匿名联合体来实现,那么匿名联合体和命名联合体不同的是,匿名联合体中定义的成员变量是直接展开到其所处的作用域,那么直接通过成员变量名来访问,而我们这里我们就可以定义两个成员变量,一个是栈数组buffer,大小为15,还有一个则是匿名结构体,里面封装了一个指向动态数组的指针ptr,以及记录有效字符串长度的大小size和一个数组的容量capacity
其次由于联合体共享内存的原因,那么这里就得定义一个成员变量来标记,来记录当前string对象采取的是栈数组还是堆数组,那么这里我在类中定义了一个bool类型的成员变量
#define sso_size 15
namespace wz{
class string{private:union{struct{char* ptr;size_t _size;size_t _capacity;};char buffer[sso_size];};bool is_short;.......
}
}
2.memcpy函数
那么string类中很多的成员函数都会涉及到拷贝,那么这里选择拷贝的函数就有讲究,这里我选择的都是memcpy函数而不是strcpy函数,因为strcpy函数拷贝到\0结束,但是我们有的string对象保存的可能是这样的字符串:“hello\0wz”
那么如果你采取strcpy,那么最终只能拷贝“hello”,所以这就是为什么string内部要设置一个size来记录有效字符串的长度,那么memcpy函数则是接收一个拷贝的目标地址和拷贝的源地址以及一个拷贝的字节数,那么它会按照该字节数将源地址的内容以字节为单位拷贝到目标地址处,那么memcpy函数的返回值就是目标地址
void* memcpy(void* des,void* src,size_t count);
那么有了这两个前置知识之后,我们就可以动手来实现我们的string类了,那么为了避免与库中的string类冲入,那么这里我都是将其定义在了wz命名空间中
string类实现
1.构造函数
那么这里构造函数有三个,第一个是无参的构造函数,那么上文,我们知道了string会采取短字符串优化,所以无参的构造函数采取的就是栈数组,那么我们要做的就是将标记变量is_short设置为true,并且将栈数组的首元素给设置为\0代表空串
string():is_short(true){buffer[0] = '\0';}
而这里还可以用c风格的字符串来初始化,那么这里得注意判断字符串的长度,如果字符串的长度小于15,那么我们采取的就是栈数组来存储,如果字符串的长度大于15,那么采取的就是对堆数组,然后设置size以及capacity的值
string(const char* str){size_t len = strlen(str);if (len >= sso_size){ptr = new char[len + 1];memcpy(ptr, str, len);_size = len;ptr[_size] = '\0';_capacity = len + 1;is_short = false;}else{memcpy(buffer, str, len);buffer[len] = '\0';}}
第三个则是拷贝构造函数,那么这里由于存在短字符串优化,那么接受的string对象的值可能是栈数组,也可能是堆数组,那么这里我们就得进行判断,然后进行相应的拷贝逻辑
string(const string& str){if (str.is_short){memcpy(buffer, str.buffer, sso_size);is_short = true;}else{ptr = new char[str._capacity];memcpy(ptr, str.ptr, str._size);_size = str._size;_capacity = str._capacity;is_short = false;}}
2.reserve函数
那么reserve函数是来申请空间,那么首先我们就得先判断当前的string对象的状态,也就是采取的是栈数组还是堆数组来存储,那么如果是栈数组,我们在来比较新的申请的空间与栈数组的大小,如果小于则延续当前的栈数组,如果大于15,那么意味着栈数组到堆数组的切换,就要将栈数组的内容拷贝给堆数组,这里实现的时候,注意由于栈数组和堆数组是联合体内的成员变量,那么这里我们不能直接将栈数组buffer拷贝给堆数组,因为内存共享的原因,这里就得准备一个临时变量,将栈数组的内容拷贝给临时变量,然后在通过临时变量拷贝给堆数组,而如果当前string对象已经是堆数组,那么我们就只需比较新申请的空间与capacity的大小,如果小于就不管,大于就扩容然后再拷贝数据
void reserve(size_t n){if (is_short){if (n > sso_size){size_t len = strlen(buffer);char temp[sso_size];memcpy(temp, buffer, sso_size);ptr = new char[n + 1];memcpy(ptr, temp, sso_size);_size = len;_capacity = n + 1;is_short = false;}}else{if (n + 1 > _capacity){_capacity = std::max(_capacity * 2, n + 1);char* temp = new char[_capacity];memcpy(temp, ptr, _size);delete[] ptr;ptr = temp;temp = nullptr;}}}
3.insert函数
那么对于insert函数,那么我们先来认识插入一个c风格的字符串的实现原理,那么首先我们得判断插入的位置的合法性,如果插入的位置合法,那么接下来判断当前string对象的状态,如果当前string对象是栈数组,那么就得判断当前栈数组的容量能否容纳新插入后的字符串的长度,不能就会涉及到栈数组到堆数组的切换,可以的话,那么就在栈数组的基础上插入字符串,涉及到元素的移动与字符串的拷贝,而如果当前是堆数组,那么我们就比较新插入后的字符串的长度是否会超过capacity,会的话则涉及到扩容,那么接下来的内容就是元素的移动与字符串的拷贝,然后更新size值
void insert(size_t pos, const char* str){if (is_short){int len = strlen(buffer);assert(pos <= len);}else {assert(pos <= _size);}int len = strlen(str);if (is_short){if (pos + len >= sso_size){reserve(_size + len + 1);}}else{if (_size + len >= _capacity){reserve(_size + len + 1);}}if (is_short){size_t end = strlen(buffer);while (end >= pos && end != npos){buffer[end + len] = buffer[end];end--;}for (int i = 0;i < len;i++){buffer[pos++] = str[i];}}size_t end = _size;while (end >= pos && end != npos){ptr[end + len] = ptr[end];end--;}for (int i = 0;i < len;i++){ptr[pos++] = str[i];}_size += len;}
那么这里要注意的就是当我们插入的位置是首元素的时候,也就是pos是0,那么我们元素移动的while循环的条件是end>=pos,但是当end减到-1的时候,由于end的数据类型是size_t,那么此时的-1会被转换成整数最大值,那么就会导致数组越界从而程序崩溃,而string类还有一个静态的成员变量npos,其值就是-1,所以我们这里可以在加一个条件来专门对这种情况进行特判
而对于插入一个string对象,那么逻辑就几乎一样,这里我不在赘述
void insert(size_t pos, const string& str){if (is_short){int len = strlen(buffer);assert(pos <= len);}else {assert(pos <= _size);}int len = str.size();if (is_short){if (pos + len >= sso_size){reserve(_size + len);}}else{if (_size + len >= _capacity){reserve(_size + len);}}if (is_short){size_t end = strlen(buffer);while (end >= pos && end != npos){buffer[end + len] = buffer[end];end--;}for (int i = 0;i < str.size();i++){buffer[pos++] = str[i];}}size_t end = _size;while (end >= pos && end != npos){ptr[end + len] = ptr[end];end--;}for (int i = 0;i < str.size();i++){ptr[pos++] = str[i];}_size += len;}void erase(size_t pos, size_t len = npos){if (is_short){int _len = strlen(buffer);assert(pos <= _len);}else {assert(pos <= _size);}int remain = _size - pos;if (len >= remain || len == npos){if (is_short){buffer[pos] = '\0';}else{_size = pos;ptr[_size + 1] = '\0';}return;}if (is_short){size_t end = pos + len;int _len = strlen(buffer);while (end <= _len){buffer[end] = buffer[pos++];end++;}//0 1 2 3 \0 pos:1 }size_t end = pos + len;while (end <= _size){ptr[end] = ptr[pos++];end++;}_size -= len;}
那么这里我就具体讲解了我认为实现最容易出错的三个函数,那么其他成员函数的实现就很简单了,只不过要添加一个判断栈数组以及处理栈数组的逻辑
源码
mystring.h:
#pragma once
#include<iostream>
#include<stdio.h>
#include<string.h>
#include<assert.h>
#include<algorithm>
#define sso_size 15
namespace wz
{class string{private:union{struct{char* ptr;size_t _size;size_t _capacity;};char buffer[sso_size];};bool is_short;static size_t npos;public:typedef char* Iterator;typedef const char* const_Iterator;string():is_short(true){buffer[0] = '\0';}string(const char* str){size_t len = strlen(str);if (len >= sso_size){ptr = new char[len + 1];memcpy(ptr, str, len);_size = len;ptr[_size] = '\0';_capacity = len + 1;is_short = false;}else{memcpy(buffer, str, len);buffer[len] = '\0';}}string(size_t n, char ch){if (n >= sso_size){ptr = new char[n + 1];for (int i = 0;i < n;i++){ptr[i] = ch;}ptr[n] = '\0';_size = n;_capacity = n + 1;is_short = false;}else{for (int i = 0;i < n;i++){buffer[i] = ch;}buffer[n] = '\0';}}string(const string& str){if (str.is_short){memcpy(buffer, str.buffer, sso_size);is_short = true;}else{ptr = new char[str._capacity];memcpy(ptr, str.ptr, str._size);_size = str._size;_capacity = str._capacity;is_short = false;}}~string(){if(!is_short){delete[] ptr;ptr=nullptr;}}size_t size() const{if (is_short){return strlen(buffer);}return _size;}size_t capacity() const{if (is_short){return sso_size;}return _capacity;}char& operator[](size_t num){if (is_short){size_t len = strlen(buffer);assert(num < len);return buffer[num];}assert(num < _size);return ptr[num];}const char& operator[](size_t num) const{if (is_short){size_t len = strlen(buffer);assert(num < len);return buffer[num];}assert(num < _size);return ptr[num];}void reserve(size_t n){if (is_short){if (n > sso_size){size_t len = strlen(buffer);char temp[sso_size];memcpy(temp, buffer, sso_size);ptr = new char[n + 1];memcpy(ptr, temp, sso_size);_size = len;_capacity = n + 1;is_short = false;}}else{if (n + 1 > _capacity){_capacity = std::max(_capacity * 2, n + 1);char* temp = new char[_capacity];memcpy(temp, ptr, _size);delete[] ptr;ptr = temp;temp = nullptr;}}}void clear(){if(iss_short){buffer[0]='\0';}else{_size=0;ptr[_size]='\0';}}void append(const wz::string& str){if(is_short){size_t len=strlen(buffer);size_t remain=sso_size-_len;if(remain<=str._size){reserve(len+str._size+1);for(int i=0;i<str._size;i++){ptr[len++]=str[i];}ptr[len]='\0';}else{for(int i=0;i<str._size;i++){buffer[len++]=str[i];}buffer[len]='\0';}}else{size_t remain=_capacity-_size;if(remain<=str._size){reserve(_size+str.size+1);}for(int i=0;i<str._size;i++){ptr[_size++]=str[i];}ptr[_size]='\0';}}void append(const char* str){size_t len = strlen(str);if(is_short){int _len=strlen(buffer);size_t remain=sso_size-_len;if(remain<=len){reserve(len+_len+1);for(int i=0;i<len;i++){ptr[_len++]=str[i];}ptr[_len]='\0';_size=_len;}else{for(int i=0;i<len;i++){buffer[_len++]=str[i];}}}else{if(_size+len>=_capacity){reserve(_size+len+1);}for(int i=0;i<len;i++){ptr[_size++]=str[i];}ptr[_size]='\0';}}string& operator+=(const string& str){append(str);return *this;}string& operator+=(const char* str){append(str);return *this;}string operator+(const string& str){string temp;temp += str;return temp;}void push_back(char ch){if (is_short){int len = strlen(buffer);if (len == sso_size){reserve(len + 1);ptr[len] = ch;ptr[len + 1] = '\0';_size = len + 1;}else{buffer[len] = ch;buffer[len + 1] = '\0';}}else{if (_size == _capacity){reserve(_size + 1);}ptr[_size] = ch;ptr[_size + 1] = '0';_size++;}}void pop_back(){if (is_short){int len = strlen(buffer);if (len == 0){return;}}else {if (_size == 0){return;}}if (is_short){int len = strlen(buffer);if (len != 0){buffer[len - 1] = '\0';}}_size--;ptr[_size] = '\0';}string operator+(const char* str){string temp;temp += str;return temp;}string& operator=(const string& str){if (str.is_short){if (is_short) {memcpy(buffer, str.buffer, sso_size);}else {delete[] ptr;memcpy(buffer, str.buffer, sso_size);is_short = true;}}else{if (is_short){ptr = new char[str._capacity];memcpy(ptr, str.ptr, str._size);_size = str._size;_capacity = str._capacity;is_short = false;}else {if (_capacity < str._capacity){reserve(str._capacity);memcpy(ptr, str.ptr, str._size);_size = str._size;_capacity = str._capacity;}else{memcpy(ptr, str.ptr, str._size);_size = str._size;}}}}string& operator=(const char* str){size_t len = strlen(str);if (is_short){if (len > sso_size){reserve(len + 1);memcpy(ptr, str, len);ptr[len] = '\0';_size = len;}else{memcpy(buffer, str, len);buffer[len] = '\0';}}else{if (len >= _capacity){reserve(len + 1);}memcpy(ptr, str, len);ptr[len] = '\0';_size = len;}return *this;}const char* c_str() const{if (is_short){return buffer;}return ptr;}char* begin(){if (is_short){return buffer;}return ptr;}const char* begin() const{if (is_short){return buffer;}return ptr;}char* end(){if (is_short){size_t len = strlen(buffer);return buffer + len;}return ptr + _size;}const char* end()const{if (is_short){size_t len = strlen(buffer);return buffer + len;}return ptr + _size;}void insert(size_t pos, const char* str){if (is_short){int len = strlen(buffer);assert(pos <= len);}else {assert(pos <= _size);}int len = strlen(str);if (is_short){if (pos + len >= sso_size){reserve(_size + len + 1);}}else{if (_size + len >= _capacity){reserve(_size + len + 1);}}if (is_short){size_t end = strlen(buffer);while (end >= pos && end != npos){buffer[end + len] = buffer[end];end--;}for (int i = 0;i < len;i++){buffer[pos++] = str[i];}}size_t end = _size;while (end >= pos && end != npos){ptr[end + len] = ptr[end];end--;}for (int i = 0;i < len;i++){ptr[pos++] = str[i];}_size += len;}void insert(size_t pos, const string& str){if (is_short){int len = strlen(buffer);assert(pos <= len);}else {assert(pos <= _size);}int len = str.size();if (is_short){if (pos + len >= sso_size){reserve(_size + len);}}else{if (_size + len >= _capacity){reserve(_size + len);}}if (is_short){size_t end = strlen(buffer);while (end >= pos && end != npos){buffer[end + len] = buffer[end];end--;}for (int i = 0;i < str.size();i++){buffer[pos++] = str[i];}}size_t end = _size;while (end >= pos && end != npos){ptr[end + len] = ptr[end];end--;}for (int i = 0;i < str.size();i++){ptr[pos++] = str[i];}_size += len;}void erase(size_t pos, size_t len = npos){if (is_short){int _len = strlen(buffer);assert(pos <= _len);}else {assert(pos <= _size);}int remain = _size - pos;if (len >= remain || len == npos){if (is_short){buffer[pos] = '\0';}else{_size = pos;ptr[_size + 1] = '\0';}return;}if (is_short){size_t end = pos + len;int _len = strlen(buffer);while (end <= _len){buffer[end] = buffer[pos++];end++;}//0 1 2 3 \0 pos:1 }size_t end = pos + len;while (end <= _size){ptr[end] = ptr[pos++];end++;}_size -= len;}void resize(size_t n){if (is_short){int len = strlen(buffer);if (n < len){buffer[n] = '\0';}if (n > sso_size){reserve(n);}}else{if (n < _size){ptr[n] = '\0';_size = n;}else if (n > _size && n <= _capacity){memset(ptr + _size, '\0', n - _size);_size = n;}else{reserve(n);memset(ptr + _size, '\0', n - _size);_size = n;}}}string substr(size_t pos, size_t len = npos){if (is_short){size_t _len = strlen(buffer);assert(pos < _len);}else{assert(pos < _size);}if (len == 0){return string();}string temp;if (is_short){size_t _len = strlen(buffer);size_t remain = _len - pos;if (len > remain || len == npos){for (size_t i = pos;i <= _len;i++){temp.push_back(buffer[i]);}}else{for (size_t i = pos;i < pos + len;i++){temp.push_back(buffer[i]);}}return temp;}else{size_t remain = _size - pos;if (len > remain || len == pos){for (int i = pos;i <= _size;i++){temp.push_back(ptr[i]);}}else{for (size_t i = pos;i <= pos + len;i++){temp.push_back(ptr[i]);}}return temp;}}};size_t npos = -1;
}
std::ostream& operator<<(std::ostream& out, wz::string& s1)
{out << s1.c_str();return out;
}
main.cpp:
#include"mystring.h"
using namespace std;
int main()
{wz::string s1;cout << "start size is " << s1.capacity() << endl;s1 = "WangZhuo";s1 += " is beautiful";cout << s1.capacity() << endl;wz::string::Iterator it = s1.begin();while (it != s1.end()){cout << (*it);it++;}cout << endl;s1 = "WangZhe";s1.append("is hadsome");cout << s1 << endl;s1.clear();cout << s1 << endl;cout << "s1 size is:" << s1.size() << endl;s1.push_back('w');s1.push_back('a');s1.push_back('n');s1.pop_back();cout << s1 << endl;cout << s1.capacity() << endl;s1.reserve(200);cout << s1.size() << endl;return 0;
}
运行截图:
彩蛋
那么我这里我来验证范围for的底层实现,我当时说过编译器之所以能够支持范围for,底层采取的就是迭代器,并且使用了begin以及end函数来遍历,那么这里我自定义了string类,那么我将我自定义的string类的end函数的函数名给修改为End,那么看看此时编译器还能不能支持我自定义的string的范围for
#include"mystring.h"
using namespace std;
int main()
{wz::string s1 = "WangZhuo";for (auto ch : s1){cout << ch;}return 0;}
那么你会发现编译器底层对于范围for的实现是很死板的,它会去专门找end以及begin函数
结语
那么这就是本文关于string的全部内容了,那么从多个维度带你全面认识string,那么也希望读者下来可以自己去实现一个string类,那么对你的帮组很大,那么这就本文全部的内容,那么我的下一期博客将会介绍vector,那么我会持续更新,希望你能够多多关注,如果本文有帮组到你的话,还请多多三连加关注哦,你的支持就是我创作的最大动力!
相关文章:
【c++深入系列】:万字string详解(附有sso优化版本的string模拟实现源码)
🔥 本文专栏:c 🌸作者主页:努力努力再努力wz 💪 今日博客励志语录: 当你想放弃时,想想为什么当初坚持走到了这里 ★★★ 本文前置知识: 类和对象(上) 类和对…...
黑马点评redis改 part 4
Redis消息队列实现异步秒杀 一些额外话语:过来人提醒下,不要用stream,可以跳过不看,用专业的消息队列中间件,同时准备好消息队列的八股,否则简陋的stream很容易被问死。 异步持久化还存在消息丢失、消息重复…...
【Python Web开发】01-Socket网络编程01
文章目录 1.套接字(Socket)1.1 概念1.2 类型1.3 使用步骤 Python 的网络编程主要用于让不同的计算机或者程序之间进行数据交换和通信,就好像人与人之间打电话、发消息一样。 下面从几个关键方面通俗易懂地介绍一下: 1.套接字(Socket) 在 Python 网络编…...
『不废话』之Python管理工具uv快速入门
在『不废话』之大模型推理超参数解释『不废话』之动手学MCP 中提到了uv,很多朋友都说没用过,咨询有什么优势? 通常Python新手都会使用conda、miniconda来管理Python环境,稍微高阶水平的会使用pyenv、poetry、virtualenv等工具来管…...
2025年中国高端家电品牌市场分析:海尔Haier、美的Midea、格力GREE三大本土品牌合计占据70%市场份额
一、市场现状:需求升级与结构性增长并存 2024年,中国高端家电市场在复杂的经济环境中展现出“逆势增长”的韧性。尽管全球经济增速放缓,国内家电零售额同比微降0.4%至6957亿元,但高端家电却成为拉动市场的重要引擎。这一现象的背…...
【漫话机器学习系列】217.监督式深度学习的核心法则(Supervised Deep Learning Rule Of Thumb)
监督式深度学习的核心法则:你需要多少数据? 原图作者:Chris Albon 在进行深度学习项目时,我们常常面临一个核心问题:我到底需要多少训练数据?这是许多初学者甚至资深工程师都会困惑的问题。图中给出了一个非…...
OpenCV --- 图像预处理(六)
OpenCV — 图像预处理(六) 文章目录 OpenCV --- 图像预处理(六)十四,图像边缘检测14.1 高斯滤波14.2 计算图像的梯度与方向14.3 非极大值抑制14.4 双阈值筛选14.5 API和使用 十五,绘制图像轮廓15.1 什么是轮…...
WebRTC服务器Coturn服务器的管理平台功能
1、概述 开源的webrtc服务器提供管理平台功能,用户可以通过web页面进行访问配置coturn服务器,主要包括管理平台功能和telnet的管理功能,coturn相当于telnet服务器,可能通过配置来开启这两个功能,方便查看coturn服务器…...
华为网路设备学习-19 路由策略
一、 二、 注意: 当该节点匹配模式为permit下时,参考if else 当该节点匹配模式为deny下时: 1、该节点中的apply子语句不会执行。 2、如果满足所有判断(if-match)条件时,拒绝该节点并跳出(即不…...
理解RAG第六部分:有效的检索优化
在RAG系统中,识别相关上下文的检索器组件的性能与语言模型在生成有效响应方面的性能同样重要,甚至更为重要。因此,一些改进RAG系统的努力将重点放在优化检索过程上。 从检索方面提高RAG系统性能的一些常见方法。通过实施高级检索技术&#x…...
DOCA介绍
本文分为两个部分: DOCA及BlueField介绍如何运行DOCA应用,这里以DNS_Filter为例子做大致介绍。 DOCA及BlueField介绍: 现代企业数据中心是软件定义的、完全可编程的基础设施,旨在服务于跨云、核心和边缘环境的高度分布式应用工作…...
Hadoop----高可用搭建
目录标题 **什么是高可用?****⭐搭建的步骤**一.jdk**安装配置**- **要点**: 二.zookeeper**集群配置**- **要点** 三.Hadoop高可用的搭建- **要点**①环境变量的配置②配置文件的修改 ③内容分发④集群任务的初次启动 什么是高可用? 通过冗余设计 自动…...
2023蓝帽杯初赛内存取证-1
获取关于内存镜像文件的基本信息: vol.py -f memdump.mem imageinfo 得知Image local date and time : 2023-06-21 01:02:27 0800 Image local date and time是本地时区(中国——东八区) 答案:2023-06-21 01:02:27...
算法之回溯法
回溯法 回溯法定义与概念核心思想回溯法的一般框架伪代码表示C语言实现框架 回溯法的优化技巧剪枝策略实现剪枝的C语言示例记忆化搜索 案例分析N皇后问题子集和问题全排列问题寻路问题 回溯法的可视化理解决策树状态空间树回溯过程 回溯法与其他算法的比较回溯法与动态规划的区…...
Linux 内核中 cgroup(控制组) 作用是什么?
cgroup(Control Groups) 是 Linux 内核提供的一种机制,用于对 进程(或线程)组 进行资源限制、优先级分配、统计监控和任务控制。通过将进程分组管理,可以实现对 CPU、内存、磁盘 I/O、网络等系统资源的精细…...
Relay IR的核心数据结构
在 Apache TVM 的 Relay IR 中,基础节点(Var、Const、Call、Function 和 Expr)是构建计算图的核心数据结构。以下是对它们的详细解析,包括定义、作用、内部组成及相互关系: 1. Expr(表达式基类)…...
【MCP Node.js SDK 全栈进阶指南】初级篇(4):MCP工具开发基础
在MCP(模型上下文协议)的生态系统中,工具(Tools)是一种强大的扩展机制,允许AI模型执行各种操作并获取结果。本文将深入探讨MCP TypeScript-SDK中的工具开发基础,包括工具定义与参数验证、Zod模式详解与高级用法、异步工具处理与错误管理以及工具调用与结果格式化。通过学…...
3Blue1Brown/videos - 数学视频生成代码库
本文翻译整理自:https://github.com/3b1b/videos 文章目录 一、关于本项目相关链接资源关键功能特性 二、注意事项三、工作流1、核心原理2、Sublime 专用配置 四、快捷键功能说明 一、关于本项目 本项目包含用于生成 3Blue1Brown 数学解说视频的代码。 相关链接资源…...
vue3 + element-plus中el-drawer抽屉滚动条回到顶部
el-drawer抽屉滚动条回到顶部 <script setup lang"ts" name"PerformanceLogQuery"> import { ref, nextTick } from "vue"; ...... // 详情 import { performanceLogQueryByIdService } from "/api/performanceLog"; const onD…...
【inlining failed in call to always_inline ‘_mm_aesenclast_si128’】
gcc编译错误:inlining failed in call to always_inline ‘_mm_aesenclast_si128’: target specific option mismatch 消除方法: 假如是GCC,则CFLAGS添加如下编译选项:-maes 假如是cmake,参加如下脚本: …...
DB-GPT支持mcp协议配置说明
简介 在 DB-GPT 中使用 MCP(Model Context Protocol)协议,主要通过配置 MCP 服务器和智能体协作实现外部工具集成与数据交互。 开启mcp服务,这里以网页抓取为例 npx -y supergateway --stdio "uvx mcp-server-fetch" …...
前端之勇闯DOM关
一、DOM简介 1.1什么是DOM 文档对象类型(Document Object Model,简称DOM),是W3C组织推荐的处理课扩展标记语言(HTML或者XML)的标准编程接口 W3C已经定义了一系列的DOM接口,通过这些DOM接口可…...
实现鼠标拖拽图片效果
我们需要一个图片 可以是你的女朋友 可以是男朋友 ,我就拿窝的偶像 一个大佬——>甘为例吧! 哈哈哈哈哈 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport&q…...
nodejs模块暴露数据的方式,和引入(导入方式)方式
在 Node.js 中,模块之间通过 模块导出(exports) 和 模块导入(require 或 ESModule 的 import) 来进行数据和功能的共享。下面我详细总结一下两种主要的模块系统: 一、CommonJS 模块(Node.js 默认…...
AXOP33552: 400MHz 高速双通道运算放大器
AXOP33552是一款通用型高速双通道运算放大器,产品的工作电压为2V至5.5V,具有400MHz的带宽,f0.1dB的带宽为 120MHz,单通道静态电流为10mA。产品特别对噪声和THD做了优化,其噪声为5nV/√Hz 1MHz,2次谐波为-85…...
Spring Boot日志配置
目录 logback 使用logback 获取日志对象 日志级别 控制日志输出级别 日志输出格式控制 配置方式 日志转存 示例 日志是应用程序不可或缺的一部分,记录着程序运行的信息。主要作用有: 记录日常运营的重要信息记录应用报错信息记录过程数据等…...
不可变数据:基于持久化数据结构的状态管理
不可变数据:基于持久化数据结构的状态管理 一、 什么是不可变数据? 不可变数据是指一旦创建就无法更改的数据。在计算机科学中,不可变数据结构是指其内容或状态不能被修改的数据结构。在不可变数据中,所有修改操作都会生成新的数据副本&#…...
PyTorch卷积层填充(Padding)与步幅(Stride)详解及代码示例
本文通过具体代码示例讲解PyTorch中卷积操作的填充(Padding)和步幅(Stride)对输出形状的影响,帮助读者掌握卷积层的参数配置技巧。 一、填充与步幅基础 填充(Padding):在输入数据边缘…...
C++手撕STL-其叁
Deque 今天我们进入新的容器:deque,一般叫做双端队列。 比起传统的先入先出的队列queue,deque的出场率显然要低得多,事实上deque比起queue来说最大的特点就是多了一个push_front()和pop_front(),其他并没有太多不同。…...
AI大模型-window系统CPU版安装anaconda以及paddle详细步骤-亲测有效
window系统CPU版安装anaconda以及paddle详细步骤-亲测有效 一 安装anaconda 下载地址:anaconda下载 下载成功后,选择非C盘安装,按提示安装即可修改镜像文件 安装成功后,运行anaconda软件,若提示更新则点击更新,更新完后,修改镜像文件 找到用户目录下的.condarc文件,覆…...
UML概览
🥰名片: 🐳作者简介:乐于分享知识的大二在校生 🌳本系列专栏: (点击直达)统一建模语言UML 🫣致读者:欢迎评论与私信,对于博客内容的疑问都会尽量回复哒!!! 本文序: ⛰️本文介绍&…...
影刀填写输入框(web) 时出错: Can not convert Array to String
环境: 影刀5.26.24 Win10专业版 问题描述: [错误来源]行12: 填写输入框(web) 执行 填写输入框(web) 时出错: Can not convert Array to String. 解决方案: 1. 检查变量内容 在填写输入框之前,打印BT和NR变量的值ÿ…...
LLMs可在2位精度下保持高准确率
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...
C语言高频面试题——结构体和联合体区别
在 C 语言中,结构体(struct) 和 联合体(union) 是两种重要的复合数据类型,用于组织和管理多个相关的变量。尽管它们在语法上有些相似,但在内存布局、用途和行为上有显著的区别。以下是详细的对比…...
App爬虫工具篇-mitmproxy
mitmproxy 是一个支持 HTTP 和 HTTPS 的抓包程序,类似 Fiddler、Charles 的功能,它通过控制台的形式和ui界面的方式 此外,mitmproxy 还有两个关联组件,一个是 mitmdump,它是 mitmproxy 的命令行接口,利用它可以对接 Python 脚本,实现监听后的处理;另一个是 mitmweb,它…...
配置openjdk调试环境
先决条件 首先在Ubuntu 18.04上编译SlowDebug版本的openjdk。注意,这里我选择的是x86处理器的电脑。苹果M系列属于ARM芯片,指令集不一样。由于我在苹果上进行垃圾回收调试的时候会报SIGILL错误。为了了解JVM的内部工作原理,不要在这种问题上…...
加油站小程序实战教程13充值规则配置
目录 1 创建数据源2 搭建管理功能最终效果 我们目前已经实现了会员的注册以及登录功能,有了基础的认证之后就进入到了业务部分的开发。会员的话首先是可以进行充值,在充值的时候通常会有一定的赠送,本篇我们来开发一下充值规则的配置功能。 1…...
jQuery — 总结
介绍 jQuery是一款高效、轻量级的JavaScript库,旨在简化网页开发中的常见任务。自2006年发布以来,它凭借直观的语法和强大的功能迅速成为前端开发的标配工具。其核心设计理念是“写更少,做更多”,通过封装复杂的原生JavaScript操作…...
【信息安全工程师备考笔记】第二章 网络信息安全概述
第二章 网络攻击原理与常用方法 2.1 网络攻击概述 概念:损害网络 系统安全属性 的危害行为 危害行为基本类型 信息泄露攻击(机密性)完整性破坏攻击(完整性)拒绝服务攻击(可用性)非法使用攻击…...
国家自然科学基金答辩ppt制作案例模板下载
国家自然科学基金 National Natural Science Foundation of China 支持基础研究,坚持自由探索,发挥导向作用,发现和培养科学技术人才,促进科学技术进步和经济社会协调发展,逐渐形成和发展了由研究项目、人才项目和环境…...
代码随想录第三十七天|华为秋季笔试真题230823
刷题小记: 主要偏向扎实编码基础的考察,但貌似近些年题目难度有所提高,仅供参考。 卡码网136.获取连通的相邻节点列表(卡码网136.获取连通的相邻节点列表) 题目分析: 题目描述: 存在N个转发…...
KUKA机器人KR 3 D1200 HM介绍
KUKA KR 3 D1200 HM是一款小型机器人,型号中HM代表“Hygienic Machine(卫生机械)用于主副食品行业”,也是一款并联机器人。用于执行高速、高精度的抓取任务。这款机器人采用食品级不锈钢设计,额定负载为3公斤ÿ…...
从零开始创建MCP Server实战指南
一、MCP协议核心概念 1.1 什么是MCP? MCP(Model Context Protocol) 是一个标准化的“沟通规则”,由公司Anthropic提出,专门用于让大语言模型(LLM,比如通义千问、ChatGPT等)与外部工…...
C语言教程(十二):C 语言数组详解
一、引言数组的基本概念 数组是一组具有相同数据类型的元素的集合,这些元素在内存中连续存储。通过一个统一的数组名和下标来访问数组中的每个元素。使用数组可以方便地处理大量相同类型的数据,避免为每个数据单独定义变量。 二、一维数组 2.1 数组的…...
Linux[基础指令][2]
Linux[基础指令][2] cp(复制) 格式:cp [-rf] 源文件 {普通文件,目录} 拷贝 cp -r 递归拷贝目录 蓝色为目录,白色为具体文件 拷贝后面加一个不存在的文件会新建文件再拷贝 cp -ir -i是覆盖的时候询问 如果目标文件存在就会覆盖原有文件 mv(重命名/剪切) 格式:mv 源文件…...
MySQL_MCP_Server_pro接入cherry_studio实现大模型操作数据库
大模型直接与数据库交互,实现基本增删改查操作。首先贴下代码地址: https://github.com/wenb1n-dev/mysql_mcp_server_pro 安装环境:win10 1、下载代码 git clone https://github.com/wenb1n-dev/mysql_mcp_server_pro 2、使用conda创建…...
linux命令集
命令 grep -r --includeAndroid.bp libcfs ./ 参数说明 选项/参数作用-r递归搜索子目录。--includeAndroid.bp仅搜索名为 Android.bp 的文件(精确匹配文件名)。libcfs要搜索的关键字(单引号包裹特殊字符如 以避免被 Shell 解析ÿ…...
数据结构:链表
链表的概念及结构: 链表的概念: 链表是一种物理储存结构上非连续的储存结构,数据元素的逻辑顺序是通过引用链接次序实现的 那物理存储结构连续是什么意思? 之前我们讲过顺序表,顺序表的底层是数组,如下…...
【高并发内存池】从零到一的项目之高并发内存池整体框架设计及thread cache设计
个人主页 : zxctscl 专栏 【C】、 【C语言】、 【Linux】、 【数据结构】、 【算法】 如有转载请先通知 文章目录 前言1. 高并发内存池整体框架设计2. 高并发内存池--thread cache2.1 定长内存池的问题2.2 整体框架2.3 自由链表2.4 thread cache哈希桶的对齐规则2.5…...
电气动调节单座V型球阀带阀杆节流套沟槽孔板的作用-耀圣
电气动调节单座V球阀杆节流套是阀门中的一个重要组件,主要用于调节和控制流体介质的流量、压力或流速,同时兼具导向、密封和稳定阀杆运动降低流速减少冲刷的作用。以下是其具体功能和应用场景的详细说明: 1. 节流与流量控制** 作用原理**&am…...