【C++】STL之string类模拟-1

简介: 【C++】STL之string类模拟

string的模拟实现

对STL中的string类有了一个基本的认识后,本模块,我会带着你从0 ~ 1去模拟一下s库中string的这些接口,当然是比较常用的一些,代码量大概600行左右

1、前情提要

  • 首先第一点,为了不和库中的string类发生冲突,我们可以在外层包上一个名称为bit的命名空间,此时因为作用域的不同,就不会产生冲突了,如果这一块有点忘记的同学可以再去看看 namespace命名空间
namespace bit
{
  class string {
  public:
    //...
  private:
    size_t _size;
    size_t _capacity;
    char* _str;
  };
}

接下去呢,就在测试的test3.cpp中包含一下这个头文件,此时我们才可以在自己实现的类中去调用一些库函数

#include <iostream>
#include <assert.h>
using namespace std;
#include "string.h"

2、Member functions —— 成员函数

构造函数

好,首先第一个我们要来讲的就是【构造函数】

  • 首先我们从无参的构造函数开始讲起,看到下面的代码,你是否有想起了 C++初始化列表,我们默认给到 _size_capacity 的大小为,然后给字符数组开了一个大小的空间,并且将其初始化为\0
// 无参构造函数
string()
  :_size(0)
  , _capacity(0)
  ,_str(new char[1])
{ 
  _str[0] = '\0';
}
  • 然后我们立即来测试一下,因为我们自己实现的 string类 是包含在了命名空间bit中的,那么我们在使用这个类的时候就要使用到 域作用限定符::
bit::string s1;

然后打印一下这个string对象发现是一个空串

image.png

  • 有无参,那一定要有带参的,可以看到这里我们在初始化_size的时候先去计算了字符串str的长度,因为_size取的就是到 \0 为止的有效数据个数(不包含\0),那么【strlen】刚好可以起到这个功能
  • 然后在_str这一块,我们为其开出的空间就是 ==容量的大小 + 1==,最后的话还要在把有效的数据拷贝到这块空间中,使用到的是【strcpy】
// 有参构造函数
string(const char* str)
  : _size(strlen(str))
  , _capacity(_size)
  ,_str(new char[_capacity + 1])
{
  // 最后再将数据拷贝过来
  strcpy(_str, str);  
}
  • 同样地来进行一个测试

image.png💬 不过呢,我这里再给出一个改进的版本

  • 此处没有使用到初始化列表,而是在直接写在函数体内,注意观察这里的形参部分,这里运用到的知识点为 C++缺省参数,如果忘记了的同学记得去回顾一下
  • 如果外界在构造对象的时候不进行传参,此时使用的便是这个默认的参数,“”代表的是一个空的字符串,但是无论怎样,对于一个字符串来说末尾是一定有\0,此刻你可以将它带入下面的表达式,发现算出后的结果与前面无参是一样的
// 构造函数
string(const char* str = "")
{
  _size = strlen(str);
  _capacity = _size;
  _str = new char[_capacity + 1];
  memcpy(_str, str, _size + 1);
}
  • 可能有的读者注意到了这个 memcpy(),如果有度过 字符串函数与内存函数 一文的话就可以清楚它们的区别在哪里了,对于strcpy()来说拷贝到\0就会发生终止而不会拷贝了,这是我在测试一些极端场景的时候考虑到的

可以看到换回【strcpy】的时候\0后面的内容就不会去进行一个拷贝了,不过这里其实体现得不是很明显,我们在下面的 拷贝构造、赋值运算符重载 中会继续提到这个

image.png💬 有同学觉得上面的缺省参数很是奇妙,于是提出能不能写成下面这样

  • 这肯定是不可以的,从运行结果我们可以看出虽然运行出来也是空串的结果,但是这么写的话总归不太好
string(const char* str = "\0")

image.png

  • 但是呢对于下面这种就更不可以了,因为这在调用【strlen】的时候就会触发 ==空指针异常== 的问题
string(const char* str = nullptr)

image.png

拷贝构造函数

马上,我们就来聊聊有关【拷贝构造函数】的内容

  • 深度探索类的六大天选之子 中我们有提到过若是一个类在没有显示定义拷贝构造对于内置类型不做处理,而对于自定义类型会去调用 类中默认提供的拷贝构造函数 此时就会造成浅拷贝的问题

image.png

  • 我们可以通过调试来浅浅地看一下,便可以看出浅拷贝所带来的危害,光是在调用析构这一块就出现了 ==二次析构== 的问题

  • 所以我们要自己去做一个实现,可以看到我们这里在进数据的拷贝时也是使用到了memcpy()
string(const string& s)
{
  _str = new char[s._capacity + 1];
  memcpy(_str, s._str, s._size);
  _size = s._size;
  _capacity = s._capacity;
}
  • 通过调试再去观察的话,我们可以发现,此时 对象s1 和 对象s2 中的数据存放在不同的空间中,此时去修改或者是析构的话都不会受到影响

image.png

下面呢还有一个新的版本,这一块我放到【赋值重载】去进行讲解

// 拷贝构造函数(新版本)
string(const string& s)
  : _str(nullptr)
  , _size(0)
  , _capacity(0)
{
  string tmp(s._str);
  // tmp出了当前函数作用域就销毁了,和this做一个交换
  this->swap(tmp);
}

赋值运算符重载

对于赋值运算符重载这一块我们知道它也是属于类的默认成员函数,如果我们自己不去写的话类中也会默认地生成一个

  • 但是呢默认生成的这个也会造成一个 ==浅拷贝== 的问题。看到下面图示,我们要执行s1 = s3,此时若不去开出一块新空间的话,那么s1s3就会指向一块同一块空间,此时便造成了下面这些问题
  • 在修改其中任何一者时另一者都会发生变化;
  • 在析构的时候就也会造成二次析构的;
  • 原先s1所指向的那块空间没人维护了,就造成了内存泄漏的问题

image.png

  • 那么此时我们应该自己去开出一块新的空间,将s3里的内容先拷贝到这块空间中来,然后释放掉s1所指向这块空间中的内容,然后再让s1指向这块新的空间。那么这个时候,也就达成了我们所要的【深拷贝】,不会让二者去共同维护同一块空间
  • 最后的话不要忘记去修改一下s1的【_size】和【_capacity】,因为大小和容量都发生了改变

image.png下面是具体的代码,学习过 类的六大天选之子 的同学应该不陌生

string& operator=(const string& s)
{
  if (this != &s)
  {
    char* tmp = new char[s._capacity + 1];
    memcpy(tmp, s._str, s._size + 1);
    delete[] _str;
    _str = tmp;
    _size = s._size;
    _capacity = s._capacity;
  }
  return *this;
}

但是呢,就上面这一种写法并不是最优的,我们来看看下面的这种写法

  • 很多同学非常地震惊,为何这样子就可以做到【深拷贝】呢?
// 赋值重载(pua版本)
string& operator=(const string& s)
{
  if (this != &s)
  {
    string tmp(s);
    this->swap(tmp);
  }
  return *this;
}
  • 有关这个swap()函数,本来是应该下面讲的,既然这里使用到了,那就在这里讲吧,这个接口我在上面并没有介绍到,但是在讲 C++模版 的时候有提到过库中的这个 swap() 函数,它是一个函数模版,可以 根据模版参数的自动类型推导去交换不同类型的数据
  • 可以看到在我们自己实现的这个swap(string& s) 函数中就去调用了std标准库中的函数然后交换一个string对象的所有成员变量
void swap(string& s)
{
  std::swap(_str, s._str);
  std::swap(_size, s._size);
  std::swap(_capacity, s._capacity);
}
  • 接下去来解释一下这里的原理,我们在这个赋值重载的函数内部调用了拷贝构造去获取到一个临时对象tmp,然后再通过swap()函数去交换当前对象和tmp的指向,此时s1就刚好获取到了赋值之后的内容,而tmp呢则是一个临时对象,出了当前函数的作用域后自动销毁,那么原本s1所维护的这块空间刚好就会销毁了,也不会造成内存泄漏的问题

image.png💬 那有同学就说:这个妙啊!太妙了!

  • 哈哈,不知读者有没有听过最近很火的一个词叫做【PUA】
“PUA”的原理,就是打击你的自尊,摧毁你的独立思考能力,让你觉得自己一无所事,
然后对方趁虚而入,让你产生依赖,让你觉得只有对方才能帮助自己,从而被对方操控。
  • 泡面🍜的话相信大家都有吃过,假设说呢有这么一个场景:你呢是家里的哥哥,你还有一个弟弟,这一天的中午你很想吃冰箱里的那桶泡面,但是呢妈妈又不让吃,于是你就和你弟弟说:“冰箱里有一桶很好吃的泡面,你快去泡一下试试看”。那此时你傻傻的弟弟就立马去做了,当他泡完的时候呢你再去找你的妈妈告状,于是这个时候弟弟就被狠狠地骂了一顿(╯▔皿▔)╯
  • 此时这碗泡面就没人吃了,于是这个时候你就乘虚而入把这碗泡面给吃了,但是呢又不想洗碗,于是又把你弟弟给叫了过来,说:“要不你把这个碗去洗了,晚上我给你买雪糕吃🍦”。听到雪糕后你的弟弟又精神起来了,马上就把碗去给洗了

image.png

  • 透过上面这个小例子读者应该对新的这种拷贝构造有了一定的理解:反正你这个tmp对象出了作用域也要销毁的,你手上呢刚好有我想要的东西,那我们换一下吧,此时我得到了我想要的东西,你呢拿到了我的东西,这块地址中的内容刚好就是要销毁的,那tmp在出了作用域后顺带就销毁了,这也就起到了【一石二鸟】的效果

好,我们通过这个调试来观察一下,可以看到就是这个“PUA技术”,很好地达成了我们的目标

💬 但是呢,我觉得上面的这种PUA还不够,还可以再 “精妙” 一些,我们一起来看一下下面这个版本

  •  可以看到,真的是非常简洁,两行代码就足够了,那为什么可以起到这样的效果呢?原因其实就在于这个形参部分,可以看到我并没有使用像上面那样的【引用传参】,而是直接使用的传值传参
  • 那仔细学习过【类和对象】的同学一定可以知道对于【传值传参】的话会先去调用拷贝构造拷贝出一个临时的对象,那么这不就是我们在写上面一个版本的时候在函数内部去调用拷贝构造所做的事吗?那么当外界在给这个函数传递参数对象的时候,此时这个tmp便是外面这个对象的一个临时拷贝,我们直接去操作这个对象的时候也可以到达同样的效果
// 赋值重载(究极pua版本)
string& operator=(string tmp)
{
  this->swap(tmp);
  return *this;
}

一样,我们通过调试来看就可以看得很清晰,一开始按F11的时候我们可以看到进入到了拷贝构造函数内部,这个时候其实就是因为传值传参去调用拷贝构造的缘故


💬 此时我们就可以去谈谈在一模块所讲到的这个【新版本的拷贝构造函数】

  • 这里我把代码再放一遍,读者在看到赋值重载之后再来看这个应该就没有那么陌生了
// 拷贝构造函数(新版本)
string(const string& s)
  : _str(nullptr)
  , _size(0)
  , _capacity(0)
{
  string tmp(s._str);
  // tmp出了当前函数作用域就销毁了,和this做一个交换
  this->swap(tmp);
}
  • 我在这边主要想讲的还是这个初始化列表的问题,读者一定知道如果我们没有手动地去初始化成员变量的话,对于内置类型编译器是不做处理的,对于自定义类型则会去调用默认生成的拷贝构造,那交给编译器去做安全吗?当然是极度地不安全
string(const string& s)
{
  string tmp(s._str);
  // tmp出了当前函数作用域就销毁了,和this做一个交换
  this->swap(tmp);
}
  • 通过调试我们可以观察到,当直接去调用拷贝构造的时候,编译器对当前的对象做了一个初始化的工作,于是在析构的时候就没有出现问题,但是继续执下去到达我们上面的赋值=的时候,因为传值传参的缘故首先会去调用这个拷贝构造拷贝一份临时对象,但是呢在调试的时候可以发现编译器并没有去对当前对象中的成员变量做一个初始化的工作,在执行swap()函数后这个没被初始化的对象就交给tmp来进行维护了,但是呢tmp在出了作用域之后又要销毁,那么此时在执行析构函数的时候便会出问题了,去释放了一块并没有初始化的空间,一定会出现问题的!

💬 所以我们还是不能去相信编译器所做的一些工作,而是要自己经手去做一些事,避免不必要的麻烦

析构函数

最后的话就是析构函数这一块,前面在调试的过程中我们已经看到很多遍了,此处不再细述

~string()
{
  delete[] _str;
  _str = nullptr;
  _size = _capacity = 0;
}

2、Element access —— 元素访问

基本的成员函数我们已经讲完了,string对象也构造出来了,接下去我们来访问一下对象里面的内容吧

operator[ ]

  • 首先最常用的就是这【下标 + [ ]】的形式去进行一个访问,那很简单,我们通过当前所传入的下标值去访问对应的数据即可
  • 下面的话有两种实现形式,一个是可读可写的,一个则是可读不可写
// 可读可写
char& operator[](size_t pos)
{
  assert(pos < _size);
  return _str[pos];
}
// 可读不可写
const char& operator[](size_t pos) const
{
  assert(pos < _size);
  return _str[pos];
}
  • 里面我们就通过循环来访问一下,这里的size()函数和流插入我们会在下面讲到

image.png

  • 此时我们去调用的时候可读可写的版本,是可以在边访问的时候去做一个修改的,效果如下

image.png

  • 但是呢,如果我在定义这个对象的时候在前面加上一个const的话此时这个对象就具有常性了,在调用operator[]的时候调用的便是 可读不可写 的那一个,所以此刻我们去做一个修改操作的话就会出问题了
const bit::string s2("world");

image.png

  • 通过调试我们可以观察到编译器在调用这一块会默认去匹配最相近的重载函数非常得智能

3、Iterator —— 迭代器

那经过上面的学习我们可以知道,要去遍历访问一个string对象的时候,除了【下标 + []】的形式,我们还可以使用迭代器的形式去做一个遍历

  • 而对于迭代器而言我们也是要去实现两种,一个是非const的,一个则是const的

cpp

复制代码

typedefchar* iterator;
typedefconstchar* const_iterator;
  • 这里的话我就实现一下最常用的【begin】和【end】,首位的话就是_str所指向的这个位置,而末位的话则是_str + _size所指向的这个位置
typedef char* iterator;
typedef const char* const_iterator;
  • 实现了普通版本的迭代器之后,我们再来看看常量迭代器。很简单,只需要修改一下返回值,然后在后面加上一个【const成员】,此时就可以构成函数重载了
const_iterator begin() const
{
  return _str;
}
const_iterator end() const
{
  return _str + _size;
}
  • 首先我们来看一下这个普通的迭代器,成功地遍历了这个string对象

image.png

  • 那么对于常对象来说的话,就要使用常量迭代器来进行遍历,但你是否觉得这个迭代器的长度过于长了呢?

image.png

  • 这一点我们在上面也讲到过了,使用C++11中的auto关键字进行自动类型推导即可
auto cit = s2.begin();

之前我们有讲过,一个类只要支持迭代器的话那一定支持范围for,马上我们来试试看吧

  • 分别去遍历一下这两个 string对象 ,可以看到都不成问题
for (auto ch : s1)
{
  cout << ch << " ";
}
cout << endl;
for (auto ch : s2)
{
  cout << ch << " ";
}

这个方式去遍历的话还是很方便的,必须安利一波✌

image.png

相关文章
|
15天前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
47 21
|
30天前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
67 19
|
30天前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
46 13
|
30天前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
48 5
|
30天前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
38 5
|
30天前
|
Serverless 编译器 C++
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
46 4
|
30天前
|
设计模式 IDE 编译器
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
32 3
|
3月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
87 2
|
3月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
150 5
|
3月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
154 4