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


    目录

    1. string 构造函数

    2. 析构函数

    3. string 的基础函数

    4. 迭代器

    5. 拷贝构造函数

    6. 赋值运算符重载

    7. 简便写法(调用swap函数实现)

    8. reserve 与 resize 

    9. string 之插入数据

    10. 流提取、流插入操作符重载

    11. find 和 substr 

    12. 比较函数重载


    接下来我们就来模拟实现一下 string 类的基本操作。

    1. 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”这样的写法实际上是两个字符,为\0 \0,因为常量字符串会默认加一个\0。

    ②推荐使用函数体内初始化,在函数体内初始化只用调用一次 strlen 函数,如果使用初始化列表,其中_size、和_capacity 的计算都应经过 strlen 计算,因为初始化列表的顺序是通过成员变量定义的顺序来决定的。

    ③即使字符串为空,那也至少开辟一个空间,留给\0的空间。


    2. 析构函数

    1. ~string()
    2. {
    3. delete[] _str;
    4. _str = nullptr;
    5. _size = _capacity = 0;
    6. }

    析构函数比较简单,就不多解释了。


    3. string 的基础函数

    以下是一些经常使用的string中的基础函数,特别注意,const对象只能调用const成员函数

    所以,如果我们有 const 成员变量想获取某个位置的值时,我们要提供一个const型的 []操作符函数重载。

    1. const char* c_str() const
    2. {
    3. return _str;
    4. }
    5. size_t size() const
    6. {
    7. return _size;
    8. }
    9. size_t capacity() const
    10. {
    11. return _capacity;
    12. }
    13. const char& operator[](size_t pos) const
    14. {
    15. assert(pos < _size);
    16. return _str[pos];
    17. }
    18. char& operator[](size_t pos)
    19. {
    20. assert(pos < _size);
    21. return _str[pos];
    22. }

    4. 迭代器

    在 string 中,迭代器不过就是一个类似指针的存在,所以我们直接typedef 一下char型指针即可。

    1. typedef char* iterator;
    2. typedef const char* const_iterator;
    3. iterator begin()
    4. {
    5. return _str;
    6. }
    7. iterator end()
    8. {
    9. return _str + _size;
    10. }
    11. const_iterator begin() const
    12. {
    13. return _str;
    14. }
    15. const_iterator end() const
    16. {
    17. return _str + _size;
    18. }

    5. 拷贝构造函数

    即根据所传参数来构建一个相同的对象。

    1. //拷贝构造函数
    2. string(const string& s)
    3. :_str(new char [s._capacity+1])
    4. ,_size(s._size)
    5. ,_capacity(s._capacity)
    6. {
    7. strcpy(_str, s._str);
    8. }

    6. 赋值运算符重载

    赋值运算符重载的实现,有以下几个需要注意的点:

    1. 先检查左右是否为相同的值,避免自己给自己赋值情况。
    2. 先开辟空间,然后将数据拷贝过去,防止开辟空间失败又丢失了数据。
    3. 再释放原先的空间,进行参数的替换。
    1. string& operator=(const string& s)
    2. {
    3. if (this!= &s)
    4. {
    5. char* temp= new char[s._capacity + 1];
    6. strcpy(temp,s._str);
    7. delete[] _str;
    8. _str = temp;
    9. _size = s._size;
    10. _capacity = s._capacity;
    11. }
    12. return *this;
    13. }

    7. 简便写法(调用swap函数实现)

    拷贝构造函数和赋值运算符重载这两个功能的实现,我们可以采用更为简便的方式来实现。

    思路:根据函数参数生成一个临时对象,将该临时对象的数据与this指向的数据进行交换。

    1. //拷贝构造函数
    2. string(const string& s)
    3. :_str(nullptr)
    4. , _size(0)
    5. , _capacity(0)
    6. {
    7. //构造出一个函数
    8. string temp(s._str);
    9. swap(temp);
    10. }
    1. //赋值运算符重载
    2. string& operator=(const string& s)
    3. {
    4. if (this != &s)
    5. {
    6. string temp(s._str);
    7. swap(temp);
    8. }
    9. return *this;
    10. }

    这其中所调用的 swap 函数为全局swap函数,但是全局的 swap 函数会生成一个临时变量,这样便会产生很大的代价。对于string类来说,其交换数据无非是将其中的成员变量_str的指向改变一下而已,完全不需要这么大的代价,所以我们来自己实现一个string类中的 swap 函数。

    1. //编译器会在当前命名空间中寻找,然后再去全局域中找
    2. void swap(string& temp)
    3. {
    4. ::swap(_str, temp._str);
    5. ::swap(_size, temp._size);
    6. ::swap(_capacity, temp._capacity);
    7. }

    加上了域作用限定符则表示调用了全局的 swap 函数,而这里的调用,仅仅是交换一些内置类型,并没有将我们编写的string类拷贝生成一份,这便大大提高了运行效率。

    赋值运算符重载的简化写法还有优化的地方。

    我们知道,函数传参,传的是实参的临时拷贝,那既然函数传参已经生成了临时拷贝,我们便可以利用其自动生成的临时拷贝对象。

    1. //注意,这里是传值传参
    2. string& operator=(string s)
    3. {
    4. swap(s);
    5. return *this;
    6. }

    注意了,这种简洁方式采用的传值传参,然后调用我们 string 类中的 swap 函数。


    8. reserve 与 resize 

    reserve 的实现:

    分清楚resize和reserve的区别:

    reserve是设置了capacity的值,比如reserve(20),表示该容器最大容量为20,但此时容器内还没有任何对象,也不能通过下标访问。

    resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小

    reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。

    1. void reserve(size_t n)
    2. {
    3. //大于_capacity 才扩容
    4. if (n > _capacity)
    5. {
    6. char* temp = new char[n + 1];
    7. strcpy(temp, _str);
    8. delete[] _str;
    9. _str = temp;
    10. _capacity = n;
    11. }
    12. }

    注意:

    需要n个空间,则要开辟n+1开空间,留一个给\0

    注意插入数据后要在末尾放入\0

    resize 的实现:

    1. void resize(size_t n, char ch ='\0')
    2. {
    3. //1.比当前_size大 2.小于等于_size
    4. if (n > _size)
    5. {
    6. //插入数据
    7. reserve(n);
    8. for (size_t i = _size;i
    9. {
    10. _str[i] = ch;
    11. }
    12. _str[n] = '\0';
    13. }
    14. else
    15. {
    16. //删除数据
    17. _str[n] = '\0';
    18. _size = n;
    19. }
    20. }

    9. string 之插入数据

    ① push_back

    push_back的实现其实与实现顺序表的尾插相似。

    1. 先检查大小,然后将数据进行尾插。
    2. 注意 _capacity==0 的情况出现,要额外检查capacity==0的情况。
    3. 将 _size 进行++,然后在_size处放入'\0'即可。
    1. void push_back(char ch)
    2. {
    3. //满了就扩容
    4. if (_size == _capacity)
    5. {
    6. //判断一下,因为我们开始给的缺省值为\0,其中capacity为0
    7. reserve(_capacity == 0 ? 4 : _capacity * 2);
    8. }
    9. _str[_size] = ch;
    10. //插完数据后要再插入一个\0
    11. ++_size;
    12. _str[_size] = '\0';
    13. }

    ② append 的实现

    关于 append 的实现:

    1. 计算传入的字符串大小,根据字符串的大小检查是否开辟空间以及开辟多大空间(防止盲目开辟空间).
    2. 如果是 _size+len > _capacity ,不理解 > 还是 >= 可以代值进行计算一下。
    3. 使用 strcpy 拷贝数据,不过是从_str+_size处开始拷贝,将 str 的数据拷贝到 string 类中。
    4. 将_size += len,表示数据插入完成。
    1. void append(const char* str)
    2. {
    3. //计算需要多大的空间
    4. size_t len = strlen(str);
    5. //满了就扩容
    6. if (_size + len > _capacity)
    7. {
    8. //至少要开辟这么大的空间
    9. reserve(_size + len);
    10. }
    11. strcpy(_str + _size, str);
    12. _size += len;
    13. }

    ③ +=运算符重载(字符)

    实现了 push_back 和 append 后,我们来实现+=运算符重载,关于+=运算符重载,它比push_back和 append 的优势在于:1. 支持连续+=;2.增强代码可读性。

    1. //字符型调用 push_back
    2. string& operator+=(char ch)
    3. {
    4. push_back(ch);
    5. return *this;
    6. }
    7. //字符串类型调用 append
    8. string& operator+=(char* str)
    9. {
    10. append(str);
    11. return *this;
    12. }

    ④ insert

    1. 检查pos位置是否大于 _size,大于则直接报错。
    2. 检查_size 是否与_capacity相等,相等则扩容。
    3. 将数据向后挪动,然后插入数据。
    1. void insert(size_t pos, char ch)
    2. {
    3. assert(pos <= _size);
    4. if (_size ==_capacity)
    5. {
    6. reserve(_capacity==0?4:_capacity*2);
    7. }
    8. size_t end = _size;
    9. while (end >= pos)
    10. {
    11. _str[end + 1] = _str[end];
    12. --end;
    13. }
    14. _str[pos] = ch;
    15. ++_size;
    16. }

    10. 流提取、流插入操作符重载

    接下来我们就来实现流插入和流提取操作符的重载,实现这两个操作符重载,可以方便我们打印的插入数据。

    ①流提取操作符

    1. 注意要重载为全局函数,这样才能不用使用.(成员访问操作符)既可以打印数据了。
    2. 不必声明此函数为string类的友元函数,因为此操作符并未访问私有成员变量
    3. 因为提取操作符不涉及更改数据,所以使用const型函数形式参数,使用[ ]访问数据时,[ ]操作符重载函数必须为 const 型,因为 const 型对象只能调用 const 型成员函数。
    4. 因为支持连续提取,所以要做 ostream& 作为返回值。
    1. ostream& operator<<(ostream& out, const string& s)
    2. {
    3. for (size_t i = 0; i < s.size(); ++i)
    4. {
    5. out << s[i];
    6. }
    7. return out;
    8. }

    ② 流插入操作符

    1. 每次插入数据都要清空当前对象中_str存放的数据。
    2. 因为cin 会将' ' (空格)或 '\n' 视为分隔符,不会读入其中,所以我们无法通过检测' ' (空格)或 '\n' 来停止数据。这时我们便使用istream中的一个成员函数get()来帮我们获取分隔符。
    3. 使用一个字符 ch来存储数据,并将其不断插入到类中。
    4. 返回 in,因为流插入操作符要支持连续插入。
    1. void clear()
    2. {
    3. _str[0] = '\0';
    4. _size = 0;
    5. }
    6. istream& operator>>(istream& in, string& s)
    7. {
    8. s.clear();
    9. char ch;
    10. ch = in.get();
    11. while (ch != ' ' && ch != '\n')
    12. {
    13. s += ch;
    14. ch = in.get();
    15. }
    16. return in;
    17. }

    当输入的字符串很长的时候,不断+=,频繁的扩容,会使效率变得底下,所以我们可以使用一个类似内存池的机制来优化以上这种场景。

    开辟一个大小合适的数组用来临时存储数据,临时数组存放满了之后再将其插入到 string 对象中。

    1. istream& operator >> (istream& in, string& s)
    2. {
    3. s.clear();
    4. char ch;
    5. ch = in.get();
    6. const size_t N = 32;
    7. char buff[N];
    8. size_t i = 0;
    9. while (ch != ' ' && ch != '\n')
    10. {
    11. buff[i++] = ch;
    12. //将数据插入到对象中
    13. if (i == N - 1)
    14. {
    15. buff[i] = '\0';
    16. s += buff;
    17. i = 0;
    18. }
    19. ch = in.get();
    20. }
    21. buff[i] = '\0';
    22. s += buff;
    23. return in;
    24. }

    11. find 和 substr 

    在上一篇博客中我们使用了find和substr实现了网页链接切割,接下来我们就来模拟实现一下这两个功能。

    find函数的实现:

    1. 如果是字符,遍历_str即可。
    2. 字符串可以使用 strstr 函数,其作用是返回字串在字符串中第一次出现的位置。
    3. 然后用 strstr 返回的值减去_str,即可得该子串得起始位置。
    1. //使用find 和 substr 进行域名字符串的切割
    2. size_t find(char ch, size_t pos = 0) const
    3. {
    4. assert(pos < _size);
    5. for (size_t i = pos; i < _size; i++)
    6. {
    7. if (ch == _str[i])
    8. return i;
    9. }
    10. return npos;
    11. }
    12. //子串查找
    13. size_t find(const char* sub, size_t pos = 0) const
    14. {
    15. assert(sub);
    16. assert(pos < _size);
    17. //查找
    18. const char *ptr=strstr(_str + pos, sub);
    19. if (ptr == nullptr)
    20. return npos;
    21. return ptr - _str;
    22. }

    substr 函数的实现:

    1. 检查是否切割到字符串结尾,如果是直接将_pos位置后的数据全部放入临时变量中
    2. 如果不是切割到字符串结尾,则一个一个放入到临时变量中。
    1. string substr(size_t pos, size_t len = npos) const
    2. {
    3. assert(pos < _size);
    4. size_t realLen = len;
    5. //如果切割的部分大于字符串的大小
    6. if (len == npos || pos + len > _size)
    7. {
    8. realLen = _size - pos;
    9. }
    10. //创建一个临时变量,返回切割后的数据。
    11. string sub;
    12. for (size_t i = 0; i < realLen; ++i)
    13. {
    14. sub += _str[pos + i];
    15. }
    16. return sub;
    17. }

    12. 比较函数重载

    这就是实现一些大于、等于、小于此类的操作符重载了。

    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 !(*this > s);
    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. }
  • 相关阅读:
    CocosCreator3.8研究笔记(十五)CocosCreator 资源管理Asset Bundle
    【面试刷题】——堆栈窗口
    系统学习Java语言的15个网站
    【计算机网络:自顶向下方法】(二)应用层
    《双重冲击》读书笔记
    Elasticsearch系列教程之核心概念及用法
    2022联想创新科技大会--数字底座筑基行业智能
    【目标检测】——PE-YOLO精读
    【无标题】
    Android 源码 <Activity> 桌面启动一 [5]
  • 原文地址:https://blog.csdn.net/Brant_zero/article/details/126808022