• 【C++】从使用string类到模拟实现string类


    前言 

            hi~大家好呀!(〃'▽'〃) 欢迎大家点击我的文章!

            本篇文章我会初步介绍string类的相关使用(结合文档的使用笔记),以及到后面如何去简单的模拟实现string类

            我所使用的C++帮助文档:cplusplus.com - The C++ Resources Network

            string类虽然是实现在标准库内的,但是符合STL标准模板库(也在标准库内))的规则的,学完string也能方便的为我们打开STL学习的大门。

      准备好了吗?❥(ゝω・✿ฺ) 启程!

    ---------------------------------------------------------------------------------------------------------------------------------

    目录

    前言 

    一、string类引入和使用

    1.string类引入

    2.string类使用

    1构造函数

    2运算符重载

    赋值重载

    []重载

    +=重载 

    3push_back、append尾插

    4isalpha检查是否为字符

    5iterator迭代器

     范围for

    6指定位置插入insert

    7删除字符erase

    ​编辑

    8string的相关增容

    reserve开空间

    resize开空间也可初始化 

    9兼容c的字符串c_str()

    10查找一个字符、字符串、string对象find\rfind 

    substr取子字符串构造一个新的string对象

    11getline输入一行字符串

    12string下的类型转化

    二、string类的模拟实现

    1.明确实现思路

    2.代码实现


    一、string类引入和使用

    1.string类引入

            首先,上面就是库里面实现的类模板名,而从下面的string开始才是真正的类。

            没错,类模板经过指定模板类型才能真正的成为自定义类型,在通过typedef进行了一下重命名,如string的来源:

            所以这些类的本质就是通过类模板进行分配不同的模板类型而来的。 

            那么上面类中不同的区分又是怎么一回事呢?

            strings就是字符串的意思,字符串也就是要代表文字语言呀。文字语言要用计算机里存储的信号表示出来的话,就必须要有一张参考图。由于最早是美国研究出来的计算机,所以最早的一个参考图就是大家耳熟能详的ASCII码表,由于英文表示起来简单(26大小写字母组合起来而已),自然这张表也就很简单了。

            但是计算机流行全球后,全球这么多国家自然需要不同的参考图。针对于此,世界上就有专门针对全球每个不同的语言进行转变为计算机识别的参考图 -UTF就诞生了。(详细可参考百度哦~)

            不同的编码又和英文出现了很多区别,比如就我们中国汉字而言,仅仅一个字节储存的数字信息是概括不了中国基础汉字的,所以就有了用两个字节来储存汉字的信息,为了和ASCII码值配合,就有了UTF - 8。

            那么其他的类也就可以表示更多的语言了,自然就有了其他类的区分。我们这里就重点学习string类,可以发现是使用的char类型进行类模板显示实例化的。

    2.string类使用

    1构造函数

             首先是默认构造函数:string();

    1. #include
    2. using namespace std;
    3. //using std::string; //自然只展开string也是可以的
    4. //string类存放在标准库中 std是标准库的命名空间 展开std就可以使用string类了
    5. int main()
    6. {
    7. string a;
    8. return 0;
    9. }

            默认即没有给任何参数就实例化一个此类的对象,就调用此类的默认构造函数,通过调试,我们可以发现:

             此类对象实际有着三个成员变量,一个是类似于字符数组,用于存储字符串,还有就是size和capacity,一个存储当前字符个数,一个存储当前字符容量。可以看到,默认构造会给字符串"",即一个空的字符串,但是观察其原始视图不难发现,里面为了兼容c最后一个字符存储着'\0'作为字符串结束标志,但是注意size并没有显示,即size和capacity只显示有效字符个数

            然后就是为了能让c字符串也能存入string里面,并且也能更加方便的进行隐式类型转化,这里便就有来自c字符串的构造函数:string(const char* s);

      

             这里也可以说明size只是表示有效字符个数。并且观察原始视图,可以发现最后一个字符储存的就是'\0'。也就是说,实际储存的size是要比有效字符多一个的。

            然后的那个局部拷贝构造函数实际上就是截取字符串上的一部分。npos在类里定义为-1。由于类型是size_t即无符号的整形,所以就变为了整形的最大值。pos表示下标位置,即从字符串的哪个位置开始拷贝。综上,即传从哪里开始拷贝的位置,然后拷贝几个字符,如果len大于本身字符串的长度就全拷贝完,因为给的有缺省值npos,即如果不默认传拷贝几个字符的话,也就默认相当于从pos位置开始拷贝完。

    1. int main()
    2. {
    3. string a;
    4. string b = "hello world";
    5. string c("Tech otakus save the world!", 17, 3);
    6. string d("Tech otakus save the world!", 17, 12);
    7. string e("Tech otakus save the world!", 17);//注意这个会和string(const char* s, size_t n)重合,所以使用string类
    8. a = "Tech otakus save the world!";
    9. string f(a, 17);
    10. cout << c << endl;
    11. cout << d << endl;
    12. cout << e << endl;
    13. cout << f << endl;
    14. return 0;
    15. }

     

             这里的e对象由于传入的是两个参数,刚好和构造函数中的string(const char* s, size_t n)完美匹配,所以e的意思就是将传入的字符串拷贝n个字节。上面的字符串也会隐式转换为string类型的哦,所以没有问题。

            注意这里的string b = "hello world";这个可不是赋值重载,这个实际也是直接的显示构造哦~别忘了隐式类型转换,字符串会转化为对应的一个string类型,然后发生拷贝构造。即原本时构造+拷贝构造。但是经过编译器优化后,也就变成了单纯的构造咯。

            析构函数实现的是深拷贝,实现时在细谈,目前不说。

            那么这里的 = 就可以涉及到运算符重载的相关成员函数了:

    2运算符重载

    赋值重载

            如上,a = "字符串";就是一个赋值重载

             传入的参数有三类:即string对象,常量字符串,字符。也就是常规的这些:

    1. int main()
    2. {
    3. string a;
    4. a = 'a';
    5. cout << a << endl;
    6. a = "abc";
    7. cout << a << endl;
    8. string b("abcd");
    9. a = b;
    10. cout << a << endl;
    11. return 0;
    12. }

    []重载

             除此之外,最重要的也是能够让string好用的成员函数就时[]重载了,让我们能够像数组一样方便的去使用:

            一个就是正常常规用的,另一个就是针对于const类型使用的。由于其实现在类里,所以有默认的string* const this为第一个参数,也就是[]的第一个操作符了:

    1. int main()
    2. {
    3. string a = "abcde";
    4. for (size_t i = 0; i < a.length(); i++)
    5. {
    6. cout << a[i] << endl;
    7. }
    8. return 0;
    9. }

             比如使用此就可以实现最常用的遍历数组,这里的length也是其中的一个成员函数,能输出当前的size个数,当然,通用的是成员函数size,因为其他STL容器里面可就没有length这一说法了:

    	cout << a.size() << endl;

            当然,满足数组也就可以修改里面的字符数据啦。即[]重载就可以实现对string对象存储的字符串某处进行读写操作。

            针对于const类型,重载了一个[]重载函数,便于const对象读:

    1. int main()
    2. {
    3. const string a = "hi~";
    4. for (size_t i = 0; i < a.size(); i++)
    5. cout << a[i] << endl;
    6. return 0;
    7. }

            需要注意的是数组里可不要越界哦,里面是有严格的越界检查的(assert(),断言检查)。

    at()成员函数

            

             此函数和[]的使用类似,会返回pos位置的字符哦,并且非const对象也可以进行修改:

            但是和[]的区别就是失败抛异常(越界)(捕获异常 程序继续运行),[]就直接断言了。2

    +=重载 

            当你想在string对象后面在加点字符或者字符串的时候,+=重载极其好用:

            一个就是添加string对象,一个就是常量字符串,一个就是字符。

    1. int main()
    2. {
    3. string a = "h";
    4. a += 'e';
    5. a += "ll";
    6. string b(1, 'o');
    7. a += b;
    8. cout << a << endl;
    9. return 0;
    10. }

            注意,>、<就表示string是支持比大小的,利用的是ASCII码值进行的比大小。 

    3push_back、append尾插

             上面使用了+=重载,其实这个成员函数底层调用的就是push_back和append这两个成员函数。其中:push_back尾插字符,append尾插字符串

             比如,将上面的+=程序修改为用append和push_back进行实现:

    1. int main()
    2. {
    3. string a = "h";
    4. a.push_back('e');
    5. a.append("ll");
    6. string b(1, 'o');
    7. a.append(b);
    8. cout << a << endl;
    9. return 0;
    10. }

     

            append第二个实际上就是选取要准备追加的字符串的一部分,第一个参数表示起始位置,第二个表示追加几个字符过去,如果第二个参数没传的话会默认从pos位置开始将直到末尾的字符均追加哦~

    1. int main()
    2. {
    3. string a = "hello";
    4. a.append(a, 3);
    5. cout << a << endl;
    6. a.append(a, 0, 2);
    7. cout << a << endl;
    8. a.append(a, 1, 5);
    9. cout << a << endl;
    10. return 0;
    11. }

             

            自然,如果第二个参数过大还是默认追加到末尾哦~

            append第6个是append迭代器,传入两个string迭代器进去就可以实现对该string对象的一部分进行追加哦~迭代器后面会讲,比如如下传入一个迭代器区间(注意此迭代器区间可以理解为是一个指针区间,只不过里面满足[begin, end))

    扩展

    4isalpha检查是否为字符

            检查字符是不是字母有一个库函数可以检查哦~

             是字母就返回非零,不是就返回零:

    5iterator迭代器

            迭代器可以理解是指向储存数据的一个前后指针。只不过可能是指针,也可能不是。但是用法和指针类似。(实际上string的迭代器实现底层就是指针

            迭代器iterator类似指针,那么它就有一个返回string对象存的字符串的最开始的位置:begin

            const自然也就是指的是const对象,方便其进行只读操作:

             既然有一个指向开 始的位置,那么是否有指向结尾的位置呢?有的,指向储存数据真正的结尾(字符串中除开有效字符,实际最后一个字符就是\0):end

            有了头和尾的指向,我们就可以像数组那样进行遍历:

    1. int main()
    2. {
    3. string a = "hello world";
    4. string::iterator it = a.begin();
    5. while (it != a.end())
    6. {
    7. cout << *it << endl;
    8. ++it;
    9. }
    10. return 0;
    11. }

            

            因为我们只需要编译有效字符部分,所以当不等于'\0'指向的位置的时候,就遍历完了。需要注意此时类型需要指定是string类域中的迭代器iterator

            迭代器非const对象的迭代器同样可以修改数据:

    1. int main()
    2. {
    3. string a = "hello world";
    4. string::iterator it = a.begin();
    5. while (it != a.end())
    6. {
    7. cout << (*it)++ << endl;//每打印数据一次此字符就++
    8. ++it;
    9. }
    10. cout << a << endl;
    11. return 0;
    12. }

            当然,当const对象使用迭代器的时候就要注意类型就是对应的const_iterator了:

            当然,迭代器也提供了反向迭代

            和begin类似,只不过此时是指向有效字符的最后一个:

             和end类似,只不过此时是指向第一个字符的前一个。即和正向迭代的区间[begin, end)反了过来:(rbegin, rend]

            注意,此时的类型也就是reverse_iterator了,const对应的在前面加上const。比如如下我们反向遍历一下string:

    1. void test2()
    2. {
    3. string a = "hello world";
    4. string::reverse_iterator rit = a.rbegin();//必须一一匹配哦
    5. while (rit != a.rend())
    6. {
    7. cout << (*rit)++;//同样可以修改数据
    8. ++rit;
    9. }
    10. cout << endl;
    11. cout << a << endl;
    12. }

     

    综上,可以总结一下string类中的迭代器(iterator):

            一共四种类型:

    iterator

    const_iterator

    reverse_iterator

    const_reverse_iterator(当类型长的时候可以使用auto进行自动推导类型,会非常方便)

     类似指针一样的作用,但是在其他容器里的迭代器就不一定底层是指针实现的了。

            测试一下auto的方便:

    1. void test3()
    2. {
    3. const string a = "hello world";
    4. //string::const_reverse_iterator crit = a.rbegin();//正常写法 -- 但是类型太长了,可以简写:
    5. auto crit = a.rbegin();
    6. while (crit != a.rend())
    7. {
    8. cout << *crit;
    9. ++crit;
    10. }
    11. cout << endl;
    12. }

     范围for

            既然提到了auto,那么就不得不提一下范围for,在之前,我们认为范围for挺高级的,因为它可以自动迭代和自动判断结束,非常方便,但是实际上范围for的底层就是调用的迭代器:(后序模拟实现string类后,实现了迭代器就可以直接用范围for来自动遍历我们的string类了)

    1. void test4()
    2. {
    3. string a = "hello world";
    4. for (auto ch: a)
    5. {
    6. cout << ch;
    7. }
    8. cout << endl;
    9. }

            

            实际上每次就是从a那里拷贝给ch,想要改变里面的值的时候就传auto&引用就好了。

    6指定位置插入insert

             指定位置同样有很多的重载,第一个参数pos实际上指的就是从当前string对象的字符串哪个位置开始插入,然后就传入字符串,或者几个字符,又或者是传入string对象的哪一部分:

    1. void test5()
    2. {
    3. string b = "world";
    4. b.insert(0, "hello", 2);//对于常量字符串,后面n表示传几个字符进去
    5. cout << b << endl;
    6. b = "world";
    7. b.insert(0, "hello");//正常头插
    8. cout << b << endl;
    9. b = "world";
    10. b.insert(0, "hello", 0, 3);//局部对应位置插入
    11. cout << b << endl;
    12. }

            insert一直使用的话效率低下,尽量少用。

    7删除字符erase

            pos指定开始位置下标,len表示删几个,没给默认使用缺省值,即删除从pos位置开始到结尾的字符。

    1. void test6()
    2. {
    3. string a = "hello";
    4. a.erase(1, 1);
    5. cout << a << endl;
    6. a.erase(1);
    7. cout << a << endl;
    8. }

     

    8string的相关增容

            首先是max_size:

            查看其字符串长度的最大值--写死的(整形最大值)

            在string对应底层,capacity就是用来储存容量大小的,可以用capacity()查看当前容量大小 

            比如在vs(当前我使用的编译器vs2022)底下扩容机制是这样的(使用如下代码进行测试):

    1. void test8()
    2. {
    3. string a;
    4. size_t capacity = a.capacity();
    5. cout << capacity << endl;
    6. for (int i = 0; i < 120; i++)
    7. {
    8. a += 'a';
    9. if (capacity != a.capacity())
    10. {
    11. capacity = a.capacity();
    12. cout << capacity << endl;
    13. }
    14. }
    15. }

             在Linux下似乎呈现:0 1 2 4 8 16 32 64....增长,这里不在演示。

    reserve开空间

            因为扩容会消耗,为了避免频繁的扩容的话,可以使用成员函数reserve(注意此单词有保留的意思,和逆转reverse区分开):

            这个就可以提前开好对应的空间大小:(在vs底下为了更好的符合内存对齐,有时编译器会对你指定的大小进行优化)

    resize开空间也可初始化 

             当然,如果想要在开空间的同时进行初始化的话,成员函数resize()自然避免不了:

             n就是开的空间大小并且数据个数,后面加上参数char c就表示初始化为对应的字符。

    1. void test10()
    2. {
    3. string a;
    4. a.resize(16, 'a');
    5. cout << a.capacity() << endl;
    6. cout << a << endl;
    7. //注意是开空间或者初始化赋值,即再开比原本长空间时,会对没有值的空间进行赋值
    8. a.resize(32);
    9. cout << a.capacity() << endl;
    10. cout << a << endl;
    11. a.resize(64, 'x');
    12. cout << a.capacity() << endl;
    13. cout << a << endl;
    14. //但是当开的数据比现有小时,不会缩减空间,但是对于开空间的n之后的字符就会被清除 注意,赋值初始化只会对没有值的地方
    15. a.resize(32);
    16. cout << a.capacity() << endl;
    17. cout << a << endl;
    18. a.resize(16, 'e');
    19. cout << a.capacity() << endl;
    20. cout << a << endl;
    21. }

    总结reserve和resize:

            首先会开辟空间,但是不会缩小空间。

            resize初始化只会对没有值的位置进行赋值,有字符的地方不会。

            resize当给的n小于当前字符串的长度的时候,会删掉n之后的字符哦(存在内存对齐现象)

            resize会同时将size提升上去,默认填'\0'。

    9兼容c的字符串c_str()

             此时这个就可以帮助我们返回存储这个字符串的一个字符数组地址,只不过我们不能进行修改(const)。在进行c和c++语法混合使用的时候会非常有用(读取当前源代码文件,然后打印到控制台上):

    1. void test11()
    2. {
    3. string str = "teststring8_13.cpp";
    4. FILE* fout = fopen(str.c_str(), "r");
    5. char ch = fgetc(fout);//提取字符串
    6. while (ch != EOF)//没有返回EOF
    7. {
    8. cout << ch;//打印到控制台上
    9. ch = fgetc(fout);
    10. }
    11. }

            注意,新的vs编译器已经弃用fopen这个函数,仍要使用的话可以在第一行加上宏:

    #define _CRT_SECURE_NO_WARNINGS 1;

            

             既然知晓了c_str字符串,那么我们就可以区分一下其在输出的时候的区别了:(string以size结束,char*以\0结束)

    1. void test12()
    2. {
    3. string a = "hello world";
    4. a[5] = '\0';//将下标5处的空格修改为\0
    5. cout << a << endl;
    6. cout << a.c_str() << endl;
    7. }

            可以发现截然不同,cout在遇到\0会默认没有,所以就对\0什么都没打印:

      

    10查找一个字符、字符串、string对象find\rfind 

     

            同理,find就是顺着找,rfind就是逆着找,找到对应字符串或者字符出现的第一处下标然后返回,没有找到就返回整形最大值npos(注意到这里如果要匹配必须给入的什么就要完全匹配):

    1. void test13()
    2. {
    3. string a = "hello";
    4. cout << a.find('l') << endl;//正向找,默认从a的0开始--找得到 2
    5. cout << a.find('h', 0) << endl;//字符--找得到 0
    6. cout << a.find('h', 1) << endl;//字符--找不到 最大值
    7. cout << a.find("ell", 1) << endl;//常量字符串--找得到 1
    8. cout << a.find(a, 0) << endl;//string对象 0
    9. cout << a.find("ell", 1, 1) << endl;//常量字符串局部,第一参数为常量字符串的初始位置,n为几个字符--找得到 2
    10. cout << a.rfind('l');//反向找,默认从a的最后开始(缺省值为npos)--找得到 3
    11. //...
    12. }

     

    substr取子字符串构造一个新的string对象

             同样的,从哪一处下标开始,拷贝多少个过去。默认缺省值是npos:

    11getline输入一行字符串

            我们知道,scanf在读取键盘输入的字符的时候,遇到空格和换行(\n)就会停止读入,将前面的字符从缓冲区给对应的变量,然后空格和\n就不会,cin类似。但是,字符串中也还是存在空格的啊,那么我们如何在输入的时候也能同时将空格也给string呢?

            此时就有这么一个成员函数就可以用了,getline():注意包含头文件

            第一个getline第三个参数即就是分隔符,在写入时会将其第一个分隔符开始,后面的均不写入str中:

    1. void test15()
    2. {
    3. string a;
    4. getline(cin, a);
    5. cout << a << endl;
    6. getline(cin, a, ' ');//第三个参数为分隔符,即会将从第一个分隔符开始后面的就不会写入a中
    7. cout << a << endl;

     

    12string下的类型转化

            各种类型转string:

             字符串转整形:

            字符串转双精度浮点型:

    (注意浮点数是无法精确存储的,会精度丢失)

            介绍string的使用差不多啦,让我们来看看如何自己简单实现一个string类吧~

    二、string类的模拟实现

            基于上面我们所了解的内容,为了能够更好的理解string类的底层实现,我们这里就对string类进行一个简单的模拟实现:

    1.明确实现思路

            基于对库内string实现的理解,我有如下草图作为参考:

            下面是对上图的一定解释,可以先看看自己实现实现,在参照下面的代码实现哦~

    1.成员

             在构造函数开始之前,我们首先要了解此类中封装的私有成员:1.char* str(用来储存数据的指针)2.size_t size(存储当前数据个数) 3.size_t capacity(存储当前容量大小)4.const size_t npos = -1 用来指定整形最大值的,加上const可以直接在类里进行初始化,可以发现,这不刚好就是一个顺序表的实现,只不过这次是专门针对于char类型的。

    2.构造函数

            构造函数是一个类实例化成对象的基石。构造函数又有默认构造string(),拷贝构造string(const string& s)(注意要实现里面的深拷贝),以及相对的析构~string()(析构和构造函数一起来说)。

    3.迭代器实现

            迭代器是STL中重要的一个部分,是通用的遍历所有容器的方式,在顺序存储中(即非树、链表结构,像数组那样地址按照顺序进行储存数据的)的底层实现就是指针。通常针对于正常对象const对象begin()(指向第一个数据),和end()(指向最后一个数据的下一个空间)。

    4.操作数据

            操作数据的内容就很多了,实际上也就是string类中的需要实现的重要一部分的功能:首先是根据c++的语法实现的非常好用的运算符重载:operator[](能够像数组一样的进行访问),push_back()(对字符能够进行尾插),append()(对字符串能够进行尾插),operator+=(能够更加方便的实现前面两个对字符和字符串进行尾插的功能,重载的同时底层对其复用即可),insert()(对任意位置进行插入字符和字符串),erase()(对任意位置的数据进行清除),find()(寻找字符或者字符串,更具下标来进行寻找第一个符合要求的子串),substr()(能够根据位置和个数进行构造出一个子串)...

    5.容量

            关于容量的相关操作实际就是查看当前数据个数size(),查看当前容量个数capacity(),查看当前数据是否为0empty(),(注意前面三个操作要把const对象考虑进去哦~),然后就是相对重要的reserve(注意和reverse逆转这个单词进行区分),此函数用于扩充容量,进行拷贝数据,别忘了将原来的数据空间释放掉,防止内存泄漏。其余不做处理。但是还有一个多管闲事的resize就来啦,它不仅仅是扩容,还要对给它的个数里没有初始化的数据根据给入的数据进行初始化,如果给入其个数比原本size还低的话就要注意会发生数据的截断了,超过其个数的就会没有哦~

    6.比较

            比较相对好说,字符串和字符串比较就按照c语言那一套来定义就好了,比较的是ASCII码值,起始只要实现operator<operator==其余的重载符号就都可以进行复用了。< <= == > >= != 。

    7.流插入和流提取

            实际上也就是方便打印string和对string类进行输入。<<打印的时候注意是按照size()的量去打印的,而不是直接把字符串打印出来哦~要一个字符一个字符的去打印。>>的话就需要注意输入' '和'\n'即空格和enter键,键盘输入这两个的时候scanf和cin都会自动认为其为分割符,就不会输入具体的变量中去存储,所有就可以使用in.get()进行输入,方便判断字符的停止条件,当然在这个里面由于要不断调用+=即尾插,也可以先插入一段数组,然后一部分一部分的去插入字符串,这样功耗就会相对少一些哦~

    2.代码实现

            综上,string.h的实现如下代码:命名空间只是作为一个区分,可以自己起名字哦~

    1. #pragma once
    2. #define _CRT_SECURE_NO_WARNINGS 1;
    3. #include
    4. #include
    5. using namespace std;
    6. namespace Yushen
    7. {
    8. class string
    9. {
    10. public:
    11. typedef char* iterator;//迭代器
    12. typedef const char* const_iterator;
    13. //string()//这样初始化空对象是不行的 需要一个\0的空间
    14. // :_str(nullptr)
    15. // ,size(0)
    16. // ,capacity(0)
    17. //{
    18. //}
    19. //构造函数
    20. string(const char* str = "")//给一个缺省值,这样就可以默认给一个\0了。
    21. {
    22. _size = strlen(str);
    23. _capacity = _size;
    24. _str = new char[_capacity + 1];//+1是无效字符个数--即最后的\0
    25. strcpy(_str, str);//会同时将\0拷贝过去
    26. }
    27. // 传统写法
    28. //拷贝构造 需要自己实现深拷贝
    29. //string(const string& s)
    30. //{
    31. // _size = s._size;
    32. // _capacity = s._capacity;
    33. // _str = new char[_capacity + 1];
    34. // strcpy(_str, s._str);
    35. //}
    36. //string(const string& s)
    37. // :_str(new char[s._capacity + 1])
    38. // ,_size(s._size)
    39. // ,_capacity(s._capacity)
    40. //{
    41. // strcpy(_str, s._str);
    42. //}
    43. //赋值重载
    44. //string& operator=(const string& s)
    45. //{
    46. // if (this != &s)
    47. // {
    48. // _size = s._size;
    49. // _capacity = s._capacity;
    50. // delete[] _str;//别忘了原来的那份空间 防止发生内存泄漏
    51. // _str = new char[_capacity + 1];
    52. // strcpy(_str, s._str);
    53. // }
    54. // return *this;
    55. //}
    56. //类交换
    57. void swap(string& s)//直接交换成员 就好 -- 如果不实现此类的交换函数外界的交换的话就会发生拷贝构造 -- 代价高
    58. {
    59. ::swap(_str, s._str);
    60. ::swap(_size, s._size);
    61. ::swap(_capacity, s._capacity);
    62. }
    63. //现代写法
    64. //拷贝构造函数
    65. string(const string& s)
    66. :_str(nullptr)
    67. ,_size(0)
    68. ,_capacity(0)
    69. {
    70. //请一个帮工的
    71. string temp(s._str);
    72. swap(temp);
    73. }
    74. //赋值重载函数
    75. string& operator=(string s)
    76. {
    77. //s形参为打工人
    78. swap(s);
    79. return *this;
    80. }
    81. //析构函数
    82. ~string()
    83. {
    84. delete[] _str;
    85. _str = nullptr;
    86. _size = _capacity = 0;
    87. }
    88. //迭代器
    89. iterator begin()//返回储存字符串的数组第一个的位置
    90. {
    91. return _str;
    92. }
    93. iterator end()//返回\0的位置
    94. {
    95. return _str + _size;
    96. }
    97. const_iterator begin() const//重载begin
    98. {
    99. return _str;
    100. }
    101. const_iterator end() const//重载end
    102. {
    103. return _str + _size;
    104. }
    105. //扩容
    106. void reserve(size_t n)
    107. {
    108. if (n > _capacity)//比原来的大才扩容
    109. {
    110. char* temp = new char[n + 1];//始终预留好最后一个\0的位置
    111. _capacity = n;
    112. strcpy(temp, _str);
    113. delete[] _str;
    114. _str = temp;
    115. }
    116. }
    117. void resize(size_t n, char c = '\0')//给一个缺省值 '\0'
    118. {
    119. //注意如果比原来的小就要进行删除:
    120. if (n > _size)
    121. {
    122. reserve(n);//一上来先扩容 不管n,由reverse里面的去管
    123. for (size_t i = _size; i < n; i++)
    124. {
    125. _str[i] = c;
    126. }
    127. _str[n] = '\0';
    128. }
    129. else
    130. {
    131. //不进行扩容,但是大于n位置的元素会被删除
    132. _str[n] = '\0';
    133. }
    134. _size = n;
    135. }
    136. //输出当前数据个数
    137. size_t size() const
    138. {
    139. return _size;
    140. }
    141. //输出当前容量
    142. size_t capacity() const
    143. {
    144. return _capacity;
    145. }
    146. //判断当前数据是否为0
    147. bool empty() const
    148. {
    149. return _size == 0;
    150. }
    151. //重载<<注意不用定义成友元哦~
    152. //重载[]非常方便好用
    153. char& operator[](size_t n)
    154. {
    155. assert(n < _size);
    156. return _str[n];
    157. }
    158. const char& operator[](size_t n) const
    159. {
    160. assert(n < _size);
    161. return _str[n];
    162. }
    163. //常规尾插 -- 插入一个字符
    164. void push_back(char c)
    165. {
    166. /*if (_size == _capacity)
    167. {
    168. reserve(_capacity == 0 ? 4 : _capacity * 2);
    169. }
    170. _str[_size] = c;
    171. _size++;
    172. _str[_size] = '\0';*/
    173. //可以套用任意位置插入
    174. insert(_size, c);
    175. }
    176. //尾插一个字符串
    177. void append(const char* s)
    178. {
    179. //int n = strlen(s);
    180. //if (_size + n > _capacity)
    181. //{
    182. // reserve(_size + n);//大了就进行扩容
    183. //}
    184. //strcpy(_str + _size, s);
    185. //_size += n;
    186. //可以套用任意位置插入
    187. insert(_size, s);
    188. }
    189. //此时就可以重载+=了,非常好用
    190. string& operator+=(char c)
    191. {
    192. push_back(c);
    193. return *this;
    194. }
    195. string& operator+=(const char* s)
    196. {
    197. //复用append即可
    198. append(s);
    199. return *this;
    200. }
    201. //任意位置插入 位置 插入字符、字符串
    202. string& insert(size_t pos, char c)
    203. {
    204. assert(pos <= _size);//必须在限定范围内进行插入
    205. if (_size == _capacity)//满了
    206. {
    207. reserve(_capacity == 0 ? 4 : _capacity * 2);
    208. }
    209. size_t n = _size;
    210. while (n != pos)
    211. {
    212. _str[n] = _str[n - 1];
    213. --n;
    214. }
    215. _str[pos] = c;
    216. _size++;
    217. _str[_size] = '\0';
    218. return *this;
    219. }
    220. string& insert(size_t pos, const char* s)
    221. {
    222. size_t len = strlen(s);
    223. if (_size + len > _capacity)
    224. {
    225. reserve(_size + len);
    226. }
    227. //移动位置
    228. size_t n = _size + 1;
    229. while (n != pos)
    230. {
    231. _str[n + len - 1] = _str[n - 1];
    232. --n;
    233. }
    234. //strncpy(_str + pos, s, len);
    235. memcpy(_str + pos, s, sizeof(char) * len);
    236. _size += len;
    237. return *this;
    238. }
    239. //删除从pos位置开始的n个字符
    240. void erase(size_t pos, size_t len = npos)//缺省值为全部
    241. {
    242. assert(pos < _size);
    243. if (len == npos || pos + len >= _size)//从pos开始往后删完
    244. {
    245. _str[pos] = '\0';
    246. _size = pos;
    247. }
    248. else
    249. {
    250. size_t n = pos + len;
    251. while (n != _size + 1)
    252. {
    253. _str[n - len] = _str[n];
    254. n++;
    255. }
    256. _size = _size - len;
    257. }
    258. }
    259. //寻找子串
    260. size_t find(char c, size_t pos = 0)//默认从第一个开始找
    261. {
    262. assert(pos < _size);
    263. for (size_t i = pos; i < _size; i++)
    264. {
    265. if (_str[i] == c)
    266. return i;
    267. }
    268. return npos;
    269. }
    270. size_t find(const char* s, size_t pos = 0)
    271. {
    272. assert(pos < _size);
    273. char* tmp = strstr(_str + pos, s);
    274. if (tmp == nullptr)
    275. {
    276. return npos;
    277. }
    278. return tmp - _str;
    279. }
    280. //构造子串
    281. string substr(size_t pos, size_t len = npos) const
    282. {
    283. assert(pos < _size);
    284. size_t n = pos + len;
    285. //第一种情况,超出容量或者默认npos
    286. if (len == npos || pos + len >= _size)
    287. {
    288. n = _size;
    289. }
    290. string tmp;
    291. for (size_t i = pos; i < n; i++)
    292. {
    293. tmp += _str[i];
    294. }
    295. return tmp;
    296. }
    297. //大于小于等于 -- 根据ASCII码值进行比较
    298. //重载 < == 其余均可以直接复用
    299. bool operator<(const string& s) const
    300. {
    301. //复用c语言的字符串比较
    302. return strcmp(_str, s._str) < 0;//<0表示第一个比第二个字符串小
    303. }
    304. bool operator==(const string& s) const
    305. {
    306. return strcmp(_str, s._str) == 0;//==0表示两个字符串相等
    307. }
    308. //复用开始
    309. bool operator<=(const string& s) const
    310. {
    311. return (*this < s) || (*this == s);
    312. }
    313. bool operator>(const string& s) const
    314. {
    315. return !(*this <= s);
    316. }
    317. bool operator>=(const string& s) const
    318. {
    319. return !(*this < s);
    320. }
    321. bool operator!=(const string& s) const
    322. {
    323. return !(*this == s);
    324. }
    325. //clear
    326. void clear()
    327. {
    328. _str[0] = '\0';
    329. _size = 0;
    330. }
    331. private:
    332. char* _str;//存放字符串
    333. size_t _size;//当前个数
    334. size_t _capacity;//容量
    335. const static size_t npos = -1;
    336. };
    337. ostream& operator<<(ostream& out, const string& s)
    338. {
    339. for (size_t i = 0; i < s.size(); i++)
    340. {
    341. cout << s[i];
    342. }
    343. return out;
    344. }
    345. istream& operator>>(istream& in, string& s)
    346. {
    347. //防止不断的扩容造成太多的消耗
    348. //首先将原来的数据清零
    349. s.clear();
    350. char arr[16] = { 0 };
    351. char ch = in.get();
    352. int i = 0;
    353. while (ch != ' ' && ch != '\n')
    354. {
    355. arr[i++] = ch;
    356. if (i == 15)
    357. {
    358. arr[15] = '\0';
    359. s += arr;
    360. i = 0;
    361. }
    362. ch = in.get();
    363. }
    364. arr[i] = '\0';
    365. s += arr;
    366. return in;
    367. }
    368. }

    ps:其中注释掉的部分是为了突出问题,可酌情观看~

  • 相关阅读:
    虚幻引擎图文笔记:动画资源中Force Root Lock的作用
    Spring学习(3) Bean的作用域和生命周期
    洛谷P1423 小玉在游泳
    Python中的函数式编程是什么?
    第17集丨如何为成功“保鲜”
    【历史上的今天】9 月 22 日:2017 年图灵奖得主诞生;计算机软件知识产权保护案;施乐公司的自我毁灭
    15-弹性盒模型
    Python连接MySQL、PostgreSQL数据库
    计算机毕业设计Java校园兼职招聘系统(源码+系统+mysql数据库+Lw文档)
    BGP基础讲解
  • 原文地址:https://blog.csdn.net/weixin_61508423/article/details/126296698