• 【C++】string类的模拟实现


    目录

    一、构造函数

    二、拷贝构造

    三、=操作符重载

    四、迭代器

    五、c_str 、capacity、size

    六、[ ]操作符重载

    七、reserve

    八、push_back

    九、append

    十、+=操作符重载

    十一、insert

    十二、erase

    十三、clear

    十四、<<操作符重载 >>操作符重载

    十五、find

    十六、substr

    十七、> 、>=、<、<=、==、!=

    十八、resize

    总结


    一、构造函数

    首先要明确一点string最基本要有三个成员变量:str, size , capacity

    string类具有很多构造函数,string类的很多构造函数都是没有用的

    我们具体实现一个缺省的构造函数

    1. string(const char* str = "")
    2. {
    3. _size = strlen(str);
    4. _capacity = _size;
    5. _str = new char[_capacity + 1];
    6. strcpy(_str, str);
    7. }

    缺省值是“” 它是一个常量字符串,默认后面带一个'\0'

    然后依次将_size,_capacity赋值

    _size和_capacity就是str的长度,如果我们没有传字符串就用缺省值。

    传入字符串就用,传入的 与库中的string类用法保持一致

    有人可能会是这样写构造函数

    1. string(const char* str = "")
    2. :_size(strlen(str))
    3. ,_capacity(_size)
    4. ,_str(new char[_capacity + 1])
    5. {
    6. strcpy(_str, str);
    7. }

    这是一个极其隐蔽的错误,首先是初始化列表的初始化顺序,是由声明的顺序来决定的

    就算我们修改了声明顺序,也是相当的不推荐这样写,因为在后期维护时,有人可能觉得你的声明顺序不顺眼,而修改声明顺序,从而导致了程序崩溃,因此我们不要使用初始化列表,不要将声明顺序与初始化顺序绑定。

    二、拷贝构造

    拷贝构造会涉及深浅拷贝的问题

    如果在类中没有显式地声明一个拷贝构造函数,那么,编译器将会自动生成一个默认的拷贝构造函数,该构造函数完成对象之间的位拷贝。位拷贝又称浅拷贝,浅拷贝会带来数据安全方面的隐患。在进行赋值之前,为指针类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝 [1]  。这种拷贝称为深拷贝。深拷贝有两种方式实现:层层clone的方法和利用串行化来做深拷贝。层层clone的方法:在浅拷贝的基础上实现,给引用类型的属性添加克隆方法,并且在拷贝的时候也实现引用类型的拷贝。此种方法由于要在多个地方实现拷贝方法,可能会造成混论。利用串行化来做深拷贝:为避免复杂对象中使用clone方法可能带来的换乱,可以使用串化来实现深拷贝。先将对象写到流里,然后再从流里读出来

    默认拷贝构造,会对内置类型进行浅拷贝或者值拷贝

    因此我们要使用深拷贝,就要我们自己实现

    1. string(const string& s)
    2. {
    3. _size = s._size;
    4. _capacity = s._capacity;
    5. _str = new char[_capacity + 1];
    6. strcpy(_str, s._str);
    7. }

    我们使用strcpy会将'\0'拷贝过去

    这里使用的是传统写法

    下面使用的是现代写法

    现代写法是调用构造函数,创建一个局部对象,然后使用swap函数进行深拷贝的交换

    局部对象出了作用域就销毁了,不用担心内存泄漏问题。

    1. void swap(string& s)
    2. {
    3. ::swap(_str, s._str);
    4. ::swap(_size, s._size);
    5. ::swap(_capacity, s._capacity);
    6. }
    7. //拷贝构造现代写法
    8. string(const string& s)
    9. :_str(nullptr)
    10. , _size(0)
    11. , _capacity(0)
    12. {
    13. string tmp(s.c_str());
    14. swap(tmp);
    15. }

    我们要写一个swap函数,该函数我们可以调用全局的swap函数

    我们使用swap函数将类的每个成员的地址与tmp对象的地址交换,就算是拷贝构造了一个新对象。

    但是我们要先对要进行构造的对象,简易初始化,因为delete[]无法处理随机数据。

    三、=操作符重载

    =操作符重载也涉及深拷贝问题,同时也有传统写法和现代写法.

    同时我们要避免自己给自己赋值。或者是看上去是自己给自己赋值。

    首先自己给自己赋值毫无意义,同时由于自定义类型通常较大,自己给自己赋值 是没有意义的消耗。

    其次我们以深拷贝的思路来写,为了避免内存泄漏,会将原空间先释放,释放之后_str成为nullptr

    会将原本的值改变,同时这个类也是无法使用的,会出现野指针问题。

    我们还是先写传统写法

    1. string& operator=(const string& s)
    2. {
    3. if (this != &s)
    4. {
    5. char* tmp = new char[s._capacity + 1];
    6. strcpy(tmp, s._str);
    7. delete[] _str;
    8. _size = s._size;
    9. _capacity = s._capacity;
    10. _str = tmp;
    11. }
    12. return *this;
    13. }

    先定义一个tmp保存原数据,然后处理完成之后,再让_str指向tmp

    这是为了避免new失败,从而导致的数据丢失。

    接下来我们看一下现代写法

    1. string& operator=(const string& s)
    2. {
    3. if (this != &s)
    4. {
    5. string tmp(s);
    6. swap(tmp);
    7. }
    8. return *this;
    9. }

    =操作符重载的现代写法与拷贝构造现代写法类似,都是调用了构造函数

    构造出一个对象然后交换即可,它最大限度的复用了代码。

    四、迭代器

    迭代器是C++ STL容器的统一遍历方式

    iterator是像指针一样的类型,有可能是指针也有可能不是指针,但是它的用法像指针一样。

    string不喜欢用iterator,因为【】更好用

    vector同理

    list/map/set……只能用迭代器。

    我们这里实现迭代器是为了使用范围for,范围for的底层就是迭代器,编译器会进行傻瓜式的替换,将

    1. for (auto& e : str)
    2. {
    3. cout << e << " ";
    4. }

    直接替换成

    1. string::itreator it = str.begin();
    2. while (it != str.end())
    3. {
    4. cout << *it << endl;
    5. it++;
    6. }

    类似这样的形式

    string的空间是连续的,所以它的迭代器是原生指针

    我们目前只能实现iterator和const_iterator这两种迭代器,反向迭代器暂时无法实现。

    1. typedef char* itreator;
    2. typedef const char* const_iterator;

    需要先将iterator typedef一下,原生指针就相对简单了,将头指针和尾指针返回就可以了

    五、c_str 、capacity、size

    这三个函数比较简单,同时方法类似,就是将三个成员变量返回

    注意,要返回const变量,防止在外部修改

    1. const char* c_str() const
    2. {
    3. return _str;
    4. }
    5. const size_t size()const
    6. {
    7. return _size;
    8. }
    9. const size_t capacity()const
    10. {
    11. return _capacity;
    12. }

    六、[ ]操作符重载

    我们要先想一想,[ ] 的功能,它的[ ] 类似数组的,可以直接进行修改,也就是要接收要访问的位置,并且为了支持修改我们要返回引用

    无论是string类还是vector等其它容器的模拟实现,但凡涉及到位置的函数都要对该位置进行合法性检验,防止越界,[ ] 是用的assert 而at(与[ ] 功能类似)使用的是抛异常。

    1. char& operator[](size_t pos)
    2. {
    3. assert(pos < _size);
    4. return _str[pos];
    5. }
    6. const char& operator[](size_t pos)const
    7. {
    8. assert(pos < _size);
    9. return _str[pos];
    10. }

    我们实现了两个[ ] 操作符重载,为了能够使const对象也能够调用[ ] 

    库中也实现了两种

    七、reserve

    reserve就是扩容函数,扩大capacity,size不变

    扩容在后面的很多函数都有调用

    所以先实现出来

    扩容也是深拷贝,既然是扩容,因此只能够扩大空间,而不能缩小空间,如果传入的参数比原空间小就不会进行任何处理

    注意要释放原空间

    1. void rersize(size_t n, char ch = '\0')
    2. {
    3. if (n > _size)
    4. {
    5. reserve(n);
    6. for (size_t i = _size; i < n; i++)
    7. {
    8. _str[i] = ch;
    9. }
    10. }
    11. _str[n] = '\0';
    12. _size = n;
    13. }

    八、push_back

    push_back就是尾插,我们的成员变量_size指向的是有效数据的下一个位置

    我们先扩容,然后在_size位置插入新数据,_size++

    插入完之后,很容易忘记一件事,将_size指向的位置再次置为'\0',因为字符串是以'\0'为结尾。

    1. void push_back(char ch)
    2. {
    3. if (_size == _capacity)
    4. {
    5. reserve(_capacity == 0 ? 0 : _capacity * 2);
    6. }
    7. _str[_size] = ch;
    8. _size++;
    9. //不要忘记\0
    10. _str[_size] = '\0';
    11. }

    九、append

    append与push_back类似,不过append有更多的尾插方式可以选择

    我们只重点实现几个函数

    1. void append(const char* str)
    2. {
    3. size_t len = strlen(str);
    4. if (_size + len > _capacity)
    5. {
    6. reserve(_size + len);
    7. }
    8. strcpy(_str + _size, str);
    9. _size += len;
    10. }

    尾插一个字符串,首先还是计算要插入的字符串的长度

    判断是否需要进行扩容,之后进行插入数据,我们可以借助strcpy来插入


    尾插一个string对象,这个也比较简单

    我们复用前面的append就可以

    1. void append(const string& s)
    2. {
    3. append(s.c_str());
    4. }

    尾插n个字符

    这个也比较简单

    1. void append(size_t n, char ch)
    2. {
    3. if (n + _size > _capacity)
    4. {
    5. reserve(n + _size);
    6. }
    7. for (size_t i = 0; i < n; i++)
    8. {
    9. push_back(ch);
    10. }
    11. }

    十、+=操作符重载

    +=操作符重载也分为两种,一种是尾插一个字符,另一个是尾插字符串

    我们直接调用push_back和append即可

    1. string& operator+=(char ch)
    2. {
    3. push_back(ch);
    4. return *this;
    5. }
    6. string& operator+=(const char* str)
    7. {
    8. append(str);
    9. return *this;
    10. }

    十一、insert

    insert也是老套路,先检查位置合法性

    其次是扩容

    接下来就是挪动数据

    1. string& insert(size_t pos, char ch)
    2. {
    3. assert(pos <= _size);
    4. //扩容
    5. if (_size == _capacity)
    6. {
    7. reserve(_capacity == 0 ? 4 : _capacity * 2);
    8. }
    9. //移动数据
    10. size_t end = _size + 1;
    11. while (end > pos)
    12. {
    13. _str[end] = _str[end - 1];
    14. --end;
    15. }
    16. _str[pos] = ch;
    17. ++_size;
    18. return *this;
    19. }

    挪动数据有一个注意点size_t是无符号整型,它永远大于等于0,所以挪动数据要小心,控制边界。最后插入数据。

    十二、erase

    erase就是删除指定位置的字符或者字符串

    我们会发现len是有缺省值npos的

     

    npos是一个无符号的整型,赋值为-1,也就是说npos是一个42亿左右的字节

    STL认为没有长度为42亿左右的字符串

    如果不传len也就是说从pos位置删除数据一直到字符串结尾。

    如果len+pos大于_size也会删到结尾。

    删除也比较简单还是用strcpy将pos位置后面的数据往前覆盖。

    1. void erase(size_t pos, size_t len = npos)
    2. {
    3. assert(pos < _size);
    4. //判断原字符串是否大于len+pos
    5. if (len == npos || pos + len > _size)
    6. {
    7. _str[pos] = '\0';
    8. _size = pos;
    9. }
    10. else
    11. {
    12. strcpy(_str + pos, _str + pos + len);
    13. _size -= len;
    14. }
    15. }

     

    十三、clear

    clear函数十分的简单,它就是清空字符串,它并不会涉及到缩容。

    1. void clear()
    2. {
    3. _str[0] = '\0';
    4. _size = 0;
    5. }

    十四、<<操作符重载 >>操作符重载

    流提取和流插入重点说流插入操作符重载

    1. istream& operator>>(istream& in, string& s)
    2. {
    3. char ch;
    4. ch = in.get();
    5. const size_t N = 32;
    6. char buff[N];
    7. size_t i = 0;
    8. while (ch != ' ' && ch != '\n')
    9. {
    10. buff[i++] = ch;
    11. if (i == N - 1)
    12. {
    13. buff[i] = '\0';
    14. s += buff;
    15. i = 0;
    16. }
    17. ch = in.get();
    18. }
    19. buff[i] = '\0';
    20. s += buff;
    21. return in;
    22. }
    23. ostream& operator<<(ostream& out, const string& s)
    24. {
    25. for (size_t i = 0; i < s.size(); i++)
    26. {
    27. out << s[i];
    28. }
    29. return out;
    30. }

    流提取操作符重载并没有声明为友元,因为我们有[ ] 可以直接访问字符串

    流插入要注意的问题有很多,为了仿照库中功能

    首先要清空空字符串,其次是cin输入到库中的string时,它并不会读取空格和回车

    还有最后一点是,我们每次尾插数据string会频繁的检查是否需要扩容,开辟空间

    消耗较大,因此我们可以使用一个buff数组先存储输入的数据,然后一组一组的插入到字符串中

    十五、find

    find重点实现两个重载

    一个是在字符串中查找字符,另一个是在字符串中查找子串

    两者都是找到返回下标,找不到返回npos

    在查找子串时可以使用strstr来找,找到返回地址,找不到返回nullptr

    可是我们是要返回下标,可以使用找到的元素的地址减去首元素的地址,也就减去是_size

    1. size_t find(char ch, size_t pos = 0)const
    2. {
    3. assert(pos < _size);
    4. for (size_t i = pos; i < _size; i++)
    5. {
    6. if (ch == _str[i])
    7. {
    8. return i;
    9. }
    10. }
    11. return npos;
    12. }
    13. size_t find(const char* sub, size_t pos = 0)const
    14. {
    15. assert(pos < _size);
    16. assert(sub);
    17. char* ptr = strstr(_str + pos, sub);
    18. if (ptr == nullptr)
    19. {
    20. return npos;
    21. }
    22. else
    23. {
    24. return ptr - _str;
    25. }
    26. }

    十六、substr

    这个函数有点多余但是也实现一下。

    首先还是检查pos位置,计算出字符串真正的长度,然后定义一个临时对象将数据尾插到对象中,然后返回即可。

    1. string substr(size_t pos, size_t len = npos)const
    2. {
    3. assert(pos < _size);
    4. size_t reallyLen = len;
    5. if (len == npos || pos + len > _size)
    6. {
    7. reallyLen = _size - pos;
    8. }
    9. string sub;
    10. for (size_t i = 0; i < reallyLen; i++)
    11. {
    12. sub += _str[pos + i];
    13. }
    14. return sub;
    15. }

    十七、> 、>=、<、<=、==、!=

    这些操作符我们只需要实现==和>剩下的直接服用即可

    >和=可以借助strcmp来解决

     

    通过它的返回值不同就可以判断出是否大于

    1. bool operator==(const string& s)const
    2. {
    3. return strcmp(_str, s._str) == 0;
    4. }
    5. bool operator>(const string& s)const
    6. {
    7. return strcmp(_str, s._str) > 0;
    8. }
    9. bool operator>=(const string& s)const
    10. {
    11. return (*this > s) || (*this == s);
    12. }
    13. bool operator<(const string& s)const
    14. {
    15. return strcmp(_str, s._str) < 0;
    16. }
    17. bool operator<=(const string& s)const
    18. {
    19. return !(*this > s);
    20. }
    21. bool operator!=(const string& s)const
    22. {
    23. return !(*this == s);
    24. }

     

    十八、resize

    resize与reserve很容易混淆,当我们实现它的底层之后就自然而然地理解它们两者的不同了

    resize也会开空间,它不但会将_capacity改变,同时也会改变_size,因为resize会进行初始化

    默认是用'\0'来进行初始化。

    同时resize支持缩容

    1. void rersize(size_t n, char ch = '\0')
    2. {
    3. if (n > _size)
    4. {
    5. reserve(n);
    6. for (size_t i = _size; i < n; i++)
    7. {
    8. _str[i] = ch;
    9. }
    10. }
    11. _str[n] = '\0';
    12. _size = n;
    13. }

    扩容后之后要用传入的参数进行初始化,直接尾插就可以了。

    而缩绒的操作就简单多了,直接在容量的下标位置加上'\0'就可以,别忘了将size变成n


    总结


    例如:以上就是今天要讲的内容,本文仅仅简单介绍了string的模拟实现。string中其它的函数实现可参考STL的源码。

  • 相关阅读:
    【Rust 笔记】17-并发(下)
    什么是美颜SDK?为什么开发者应该考虑集成直播美颜SDK?
    Maven导入程序包jakarta.servlet,但显示不存在
    计算机网络408考研 2014
    【21天python打卡】第5天 基础语法(3)
    GIS杂记(三):MaxEnt模型中的图像地理范围不匹配【全网最好的方法,没有之一】
    强大好用的shell:shell的工作原理是什么
    Linux网络编程9——UDP 和 本地/网络套接字
    猿创征文|一文吃透JAVA初学者的开发工具
    springboot+自行车网上商城 毕业设计-附源码130948
  • 原文地址:https://blog.csdn.net/m0_62179366/article/details/126348594