当前位置:   article > 正文

string类的模拟实现(C++)

string类的模拟实现(C++)

一、前言

想要模拟实现一个库中的类,那就要首先要熟悉如何使用这个类。建议通过下面博客,完成对C++string类的学习。

C++的string类-CSDN博客

二、模拟实现

我们将从string的成员函数即成员变量入手,模拟实现string类。

成员变量

string类的实现并未给出对应的标准,因此实现比较多样。为了防止出现命名冲突,我们将自己实现的string独自封装到单独的命名空间。下面是string的成员变量的实现。

  1. namespace MyString
  2. {
  3. class string
  4. {
  5. private:
  6. char* _a;
  7. size_t _size;
  8. size_t _capacity;
  9. };
  10. }

string我们可以看成是由数组、数组大小、数组空间构成的一个类。

成员函数

由于string的成员函数写的十分冗余,被业内也大量吐槽,因此我们实现几个主要的成员方法,也是最常用的几个成员方法。

构造函数:

构造函数用来实现函数调用时的初始化,需要注意的是,在初始化时,初始化列表阶段走的顺序不是写入初始化列表的顺序,而是成员变量的声明顺序。

需要注意两个个坑点:

坑点一:const char* 不能去初始化char*

当我们开始实现时,发现程序报错了,这是为什么呢?

这是因为:const char*类型的值不能用于初始化char*类型的值。

在C++中,const char*和char*之间的区别与C语言中的相同:const char*是指向常量字符的指针,意味着通过这个指针你不能修改所指向的字符。这种类型通常用于指向字符串字面量,因为这些字面量在程序中是只读的,存储在程序的常量区。

那就意味着,我们无法用_a指向str。因此我们需要给_a额外开辟一块空间,将str的值拷贝到这块空间。

坑点二:初始化列表走的顺序是变量声明的顺序

  1. string::string(const char* str)
  2. :_a(new char[_capacity + 1])
  3. ,_size(strlen(str))
  4. , _capacity(_size + 1)
  5. {}

有了上面的经验,我们想把_a开辟一块新空间,把内容拷贝进去。但是上述的代码也是不正确的!

我们出初始化先走的是_a,给_a开辟空间的时候,_capacity并未初始化,因此_capacity + 1的大小是未定义的。

正确方法:

为了合理的初始化顺序(重点观众声明顺序),我们采用初始化列表和函数体共同使用的方法去初始化构造函数。

  1. string::string(const char* str)
  2. :_size(strlen(str))
  3. , _capacity(_size)
  4. {
  5. _a = new char[_capacity + 1]; //多开一个存储\0
  6. strcpy(_a, str); //strcpy会给dest字符串,自动添加\0
  7. }

我们先控制好大小之后,再去开辟空间,完成对_a的初始化。

优化:

先说明一个知识。“” 这个字符串代表的是常量字符串,长度是0。

  1. int main()
  2. {
  3. cout << strlen("") << endl;
  4. return 0;
  5. }

上述给出的构造函数,并不是默认构造,因为我们没有给出缺省值。下面通过给出一个默认的常量字符串,完成初始化。

  1. string::string(const char* str = "")
  2. :_size(strlen(str)) //默认常量字符串长度: 0
  3. , _capacity(_size)
  4. {
  5. _a = new char[_capacity + 1]; //多开一个存储\0
  6. strcpy(_a, str); //strcpy会给dest字符串,自动添加\0
  7. }

当我们不给出参数时,会使用缺省的长脸字符串,_size == 0 ;  _capacity == 0;  _a的大小是1(给\0预留空间)。

通过初始化列表与函数体的结合使用,这个函数便实现了默认构造与构造函数的结合

析构函数

析构函数的任务是完成数据的销毁与资源的释放。在string中,需要完成如下。

  1. string::~string()
  2. {
  3. _size = _capacity = 0;
  4. delete[] _a;
  5. _a = nullptr; //需要让指针置空
  6. }

c_str

c_str可以返回数组,并且返回的类型兼容C语言数组的属性。

const char* string::c_str() const
{
    return _a;
}

[]重载

由于[]存在读和写两种两种需求,所以需要写出重载函数,来完成读和写的功能。在返回时,应尽量采用引用返回。同时应该注意assert去检查给出的下标是否合法。

  1. char& string::operator[] (size_t pos)
  2. {
  3. assert(pos <= _size - 1); //pos是size_t,所以 >=0 恒成立
  4. return _a[pos];
  5. }
  6. const char& string::operator[] (size_t pos) const
  7. {
  8. assert(pos <= _size - 1);
  9. return _a[pos];
  10. }

capacity、size

  1. size_t string::capacity() const
  2. {
  3. return _capacity;
  4. }

应该用const修饰函数,保证const成员与非const成员都可以访问。

  1. size_t string::size() const
  2. {
  3. return _size;
  4. }

reserve

reserve函数用来完成空间的修正。对_capacity进行修正。一般来说空间大小只增不减,因此只有当作新空间大小大于原来空间大小的时候,才能进行开辟空间操作。

对于C++而言,开辟空间我们一般用new。因此reserve函数的实现必然包含以下步骤:1.开辟新空间 2.内容拷贝 3.释放旧空间(防止泄露)

  1. void string::reserve(size_t n) //开辟空间,对_capacity作出修改
  2. {
  3. //1.重新开空间 2.深拷贝 3.释放旧空间
  4. if (n > _capacity)
  5. {
  6. char* tmp = new char[n + 1];
  7. memcpy(tmp, _a, _size + 1);
  8. delete[] _a;
  9. _a = tmp;
  10. _capacity = n; //将空间修正为n
  11. }
  12. }

push_back

功能是尾插。需要额外注意,空间是否还有剩余。在尾插完成之后,需要人为添加\0,使得C++能够兼容C。

  1. void string::push_back(char ch)
  2. {
  3. if (_size == _capacity) //利用reserve扩容
  4. {
  5. reserve(_capacity == 0 ? 4 : 2 * _capacity);
  6. }
  7. _a[_size] = ch;
  8. ++_size; //_size类比length
  9. _a[_size] = 0; //补充\0,符合C语言的规范
  10. }

append

字符串的追加。字符串的追加也需要扩容,但是不是简单的二倍扩容这么简单,扩容时需要保证扩容后的大小必须可以容纳新的字符串。

在追加时可以用strcpy函数,strcpy的特性在代码中有所体现。

  1. void string::append(const char* str) //追加字符串
  2. {
  3. size_t len = strlen(str);
  4. if (len + _size > _capacity)
  5. reserve(len + _size);
  6. strcpy(_a + _size, str); //从_size处,完成内容的拷贝
  7. /*
  8. strcpy :自动添加\0,可从任意位置开始拷贝
  9. */
  10. _size += len;
  11. }

operator+=

完成字符与字符串的追加。返回的*this就是对象

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

insert

完成任意位置的插入。需要额外注意  1.头插和尾插能不能完成。 2.\0需要完成移动

因此我们直接借助下标end = _size+1;即可完成数据的移动

  1. void string::insert(size_t pos, char ch)
  2. {
  3. assert(pos <= _size); // = 是尾插
  4. if (_size == _capacity) //利用reserve扩容
  5. {
  6. reserve(_capacity == 0 ? 4 : 2 * _capacity);
  7. }
  8. size_t end = _size + 1; //保证可以完成头插
  9. while (end > pos) //后移(包含\0)
  10. {
  11. _a[end] = _a[end - 1];
  12. --end;
  13. }
  14. _a[pos] = ch;
  15. ++_size;
  16. //_a[_size] = 0; //可有可无,\0也发生了后移
  17. }
  1. void string::insert(size_t pos, const char* str)
  2. {
  3. assert(pos <= _size); //不需要检查是否>0
  4. size_t len = strlen(str);
  5. if (len + _size > _capacity)
  6. reserve(len + _size + 1); //对this指针对象进行扩容
  7. int end = _size; //防止出现size_t的死循环
  8. while (end >= (int)pos)
  9. {
  10. _a[end + len] = _a[end];
  11. --end;
  12. }
  13. strncpy(_a + pos, str, len);
  14. _size += len;
  15. }

注意,当需要完成字符串的插入时,最好用int作为end的类型,防止出现死循环,同时强转pos类型(防止出现提升)。


/*
strncpy 函数在C语言中用于拷贝字符串。它的原型是:

char *strncpy(char *dest, const char *src, size_t n);
这个函数从源字符串 src 拷贝至目标字符串 dest,最多拷贝 n 个字符。
如果源字符串的长度小于 n,strncpy 会在目标字符串后面添加额外的空字符 ('\0'),直到总共拷贝了 n 个字符。
如果源字符串的长度大于或等于 n,则不会在目标字符串后面添加空字符。
*/

erase

用来完成删除操作。值得一提的是,缺省参数在声明时可以给出,在定义时不可以给出。

  1. void string::erase(size_t pos, size_t len) //声明给出,定义不给出缺省
  2. {
  3. assert(pos < _size); // == _size是\0的位置,不能删除
  4. if (len == npos || pos + len >= _size) //len过长时
  5. {
  6. _a[pos] = 0;
  7. _size = pos;
  8. }
  9. else //len的长度合适时
  10. {
  11. int cur = pos + len;
  12. while (cur <= (int)_size)
  13. {
  14. _a[pos++] = _a[cur++];
  15. }
  16. _size -= len;
  17. }
  18. }

clear

Erases the contents of the string, which becomes an empty string (with a length of 0 characters).

清楚内容,长度清零。

  1. void string::clear()
  2. {
  3. _size = 0;
  4. _a[0] = 0;
  5. }

函数重载

只要完成 ==  >就可以服用全部重载。

  1. bool string::operator<(const string& s) const
  2. {
  3. return strcmp(_a, s._a) < 0; //内部可使用private成员
  4. }
  5. bool string::operator==(const string& s) const
  6. {
  7. return strcmp(_a, s._a) == 0;
  8. }
  9. bool string::operator<=(const string& s) const
  10. {
  11. return *this == s || *this < s;
  12. }
  13. bool string::operator>(const string& s) const
  14. {
  15. return !(*this <= s);
  16. }
  17. bool string::operator>=(const string& s) const
  18. {
  19. return !(*this < s);
  20. }
  21. bool string::operator!=(const string& s) const
  22. {
  23. return !(*this == s);
  24. }

迭代器函数

对于迭代器的实现有很多种方式,可以采用下标,可以采用指针。实现方式没有明确规定,我们这里采用指针来实现。

public:
    typedef char* iterator;        //typedef受到访问权限的限制
    typedef const char* const_iterator;
 

在类的内部,将迭代器iterator有指针类型实现。typedef收到访问权限的限制。

  1. string::iterator string::begin()
  2. {
  3. return _a;
  4. }
  5. string::iterator string::end()
  6. {
  7. return _a + _size; //end返回的是最后一个元素的下一个位置。
  8. }
  9. string::const_iterator string::begin() const
  10. {
  11. return _a;
  12. }
  13. string::const_iterator string::end() const
  14. {
  15. return _a + _size;
  16. }

需要注意的是end是最后一个元素的下一个位置。

/*
在C++中,迭代器的`end()`函数代表的是最后一个元素的下一位置。
它是用来标记容器的超出末端的位置,所以在进行遍历等操作时,通常会使用这个迭代器来检查是否到达了容器的末尾。
例如,如果你有一个 vector 容器,并且想要遍历它,你可以这样做:

std::vector<int> vec = {1, 2, 3, 4, 5};
for (auto it = vec.begin(); it != vec.end(); ++it) {
    std::cout << *it << std::endl;
}
在这个循环中,`it` 会在每次迭代后递增,直到它等于 `vec.end()`,这时循环结束。
在循环体中,`*it` 是有效的并且指向当前的元素,当 `it` 达到 `vec.end()` 时,它不再指向任何元素,因此不应该被解引用。

*/

赋值重载与拷贝构造

string内部提供了swap函数,我们可以借助swap函数进行标题函数的实现。

  1. void string::swap(string& s)
  2. {
  3. std::swap(_a, s._a);
  4. std::swap(_size, s._size);
  5. std::swap(_capacity, s._capacity);
  6. }

在swap内部,交换三个成员变量。

拷贝构造的实现:1.完成初始化 2.swap 一下tmp与*this

  1. //s1(s2)
  2. string::string(const string& s) //在进入函数体之前,先对this进行初始化
  3. :_a(nullptr)
  4. , _size(0)
  5. , _capacity(_size)
  6. {
  7. string tmp(s._a);
  8. swap(tmp);
  9. }

/*
在C++中,未初始化的对象是不应该调用析构函数的。
析构函数是用来释放对象所拥有的资源的,如果一个对象没有被正确地初始化
它可能没有分配资源,或者分配的资源处于未知的状态
在这种情况下调用析构函数可能会导致未定义行为(UB),包括程序崩溃或者数据损坏。
*/
 

赋值重载的实现:同样借助swap函数。假设S2 = S3,那我们利用S3生成一个形参,swap S2 形参即可。交换完成之后,形参出作用域也可以自动销毁。同时为了保证=的连续性,返回类型应该是string对象类型。

  1. // s2 = s3
  2. string& string::operator=(string tmp) //直接形参接收
  3. {
  4. swap(tmp);
  5. return *this; //保证返回类型也是string,可以连等
  6. }

resize

resize用来修改size的大小。可分为两个情况讨论。void string::resize(size_t n, char ch)    //更新size

当n >= _size 和 n < _size。分别对应扩大size和缩小size

  1. void string::resize(size_t n, char ch) //更新size
  2. {
  3. if (n <= _size)
  4. {
  5. _size = n;
  6. _a[_size] = 0;
  7. }
  8. else
  9. {
  10. reserve(n); //函数内部开辟n + 1个空间
  11. while (_size < n)
  12. {
  13. _a[_size++] = ch;
  14. }
  15. _a[_size] = 0;
  16. }
  17. }

find

用来查找内容,可以查找字符或者字符串。

查找字符

  1. size_t string::find(char ch, size_t pos) //函数的定义不能有缺省值
  2. {
  3. for (size_t i = pos; i < _size; ++i)
  4. {
  5. if (_a[i] == ch)
  6. return i;
  7. }
  8. return npos;
  9. }

查找字符串有两种方法:方法一,使用strstr函数

  1. size_t string::find(const char* sub, size_t pos)
  2. {
  3. const char* ptr = strstr(_a + pos, sub);
  4. if (ptr == nullptr)
  5. return npos;
  6. return ptr - _a;
  7. }

方法二:自己实现功能的查找

需要遍历str1,去找到第一个匹配的字符,匹配成功之后后移

  1. /*
  2. 思路:遍历原数组,匹配往后走。
  3. */
  4. size_t string::find(const char* sub, size_t pos = 0)
  5. {
  6. assert(pos < _size);
  7. size_t i = pos;
  8. for (; i < _size; ++i) //遍历原数组
  9. {
  10. if (_a[i] == sub[0])
  11. {
  12. size_t len = strlen(sub);
  13. size_t m = i;
  14. for (size_t j = 0; j < len; ++j) //循环:判断
  15. {
  16. if (m >= _size || sub[j] != _a[m])
  17. {
  18. break;
  19. }
  20. ++m; //后移
  21. }
  22. if (m - i == len) //如果完全一致(长度替代flag)
  23. return i;
  24. }
  25. }
  26. return npos;
  27. }

m - i == len则表示两个字符串完全一致。不能直接在判断语句写出m++。由于m在每次比较后都会递增,所以m - i的计算可能不会反映实际的匹配长度,因为m可能会超出实际不匹配的位置。即位置匹配成功之后,才可以m++

substr

用来生成子串。其核心逻辑是1.设定好大小开空间 2.拷贝内容

开大小:确定合适的长度        拷贝内容:确定合适的终止位置end

  1. //1.设定好大小开空间 2.拷贝内容
  2. string string::substr(size_t pos, size_t len)
  3. {
  4. assert(pos < _size);
  5. string tmp;
  6. size_t end = pos + len;
  7. //设定大小
  8. if (pos + len >= _size || len == npos) //len太长
  9. {
  10. len = _size - pos;
  11. end = _size;
  12. }
  13. tmp.reserve(len); //开空间
  14. for (size_t i = pos; i < end; i++) //内容拷贝
  15. {
  16. tmp += _a[i];
  17. }
  18. return tmp; //临时对象不用引用返回。
  19. }

全局函数

    ostream& operator<<(ostream& out, const string& s);

string函数支持直接流提取与流插入的操作。

流提取

  1. ostream& MyString::operator<<(ostream& out, const string& s)
  2. {
  3. for (auto ch : s) //写了迭代器之后,就可以用范围for
  4. out << ch;
  5. return out;
  6. }

流插入

我们借助一个插入缓冲区,来防止过多的浪费空间。步骤:1.输入内容 2.流入缓冲区 3.从缓冲区提取内容 4.继续从将内容读取到缓冲区中  最后需要清除缓冲区

  1. istream& MyString::operator>>(istream& in, string& s)
  2. {
  3. s.clear(); //清空对象
  4. char ch = in.get();
  5. size_t i = 0;
  6. char* buff = new char[129]; //建立缓冲区
  7. while (ch != ' ' && ch != 10)
  8. {
  9. buff[i++] = ch; //流入缓冲区
  10. if (i == 128)
  11. {
  12. buff[i] = 0;
  13. s += buff; //从缓冲区读取
  14. i = 0;
  15. }
  16. ch = in.get(); //继续读取缓冲区
  17. }
  18. if (i != 0)
  19. {
  20. buff[i] = 0;
  21. s += buff;
  22. }
  23. return in;
  24. }


 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小舞很执着/article/detail/923888
推荐阅读
相关标签
  

闽ICP备14008679号