【C++之STL】摸清 string 的模拟实现(上)

文章目录

  • 1. 为什么要模拟实现?
  • 2. 基本框架搭建
  • 3. 构造函数
    • 3. 1 默认构造/from c_str
    • 3. 2 拷贝构造
      • 3. 2. 1 深浅拷贝
    • 3. 3 fill
    • 3. 4 迭代器区间构造
  • 4. 容量操作
    • 4. 1 `size()`和`capacity()`和`empty()`
    • 4. 2 `clear()`
    • 4. 3 `resize()`
    • 4. 4 `reserve()`


1. 为什么要模拟实现?

可能有人觉得STL既然已经提供了功能如此丰富的string类,就没有必要再学习它的底层,但这显然是错误的看法。
原因有2:

  1. 找工作时面试官很喜欢让求职者回答STL一些容器的模拟实现的思路。
  2. 学习底层有助于巩固知识,还能帮助我们更好地理解这个容器,使用时能更加得心应手。

2. 基本框架搭建

首先,我们要实现一个string类,就不可避免地会和库中string冲突,这时就需要用到命名空间,将自己实现的这个类放进一个和std不同的命名空间中就能避免冲突。

string是对字符数组的封装,尽管一些编译器在具体实现时会加上一些其它的东西来进行优化,但我们先不考虑这些。这里我们实现的string类的成员变量就只有一个字符指针 char* _str;

string的实现可以模仿数据结构初阶中的顺序表,还要有_size_capacity两个成员变量。

另外,虽然字符串的长度可以用strlen这个库函数获取,但这个函数的时间复杂度是O(N),会导致代码效率降低,不如创建一个变量存储起来。

这样我们就可以搭建出最基本的框架:

// 我们实现的 string 不是模板类,可以声明与定义分离。
namespace test
{class string{private:char* _str;size_t _capacity;size_t _size;};
}

当然这个基础和实际上的库中的string类是有差别的。

这是cplusplus上给出的string的定义:

typedef basic_string<char> string;

可以看到库中的 string 是对一个类模版basic_string指定类型char得到的,实际上这个类可以得到四个类:

stringString class (class )
wstringWide string (class )
u16string String of 16-bit characters (class )
u32string String of 32-bit characters (class )

也就是其他不同的字符类型,不是很常用,而且使用方式和string完全相同,就不介绍了。

除此之外,如果你使用的是VS2022编译器,在监视窗口也可以发现VS2022中除了这个字符指针之外还添加了一个16个字节大小Buf数组:

_Buf

当这个Buf数组存满之后,才会将数据转移到_Ptr中,然后进行可能的扩容操作,这是VS编译器独特(应该)的优化方式。

当然gcc下string的定义也不是我们像我们写的这样简单,它用到了写时拷贝,这一点在本文最后面会补充。

3. 构造函数

注:只会实现最常用的构造函数,其它的构造函数原理也差不多。

3. 1 默认构造/from c_str

在类和对象(中)中说过,我们应该显示地实现一个默认构造,最好是全缺省的,所以我们就实现一个全缺省的默认构造。

// string.h
string(const char* str="");// string.c
// 声明与实现分离时,实现中不能写出缺省值
string::string(const char* str):_capacity(strlen(str))
{// 初始化列表是按照类模版中变量声明的顺序走的,所以需要注意顺序。不如把其他放在函数体内初始化,防止出错// 当然,把 _size 和 _str 都在初始化列表初始化也是可以的,但是一定要注意顺序_size = _capacity;	_str = new char[_capacity + 1];// 这里一定要注意要把 str 的内容拷贝到 _str 中,千万不能 _str = str 完事strcpy(_str, str);
}

构造string对象的时候,不要让_strnullptr,不然可能会导致报错,不如直接初始化为""来得方便。

3. 2 拷贝构造

这样写可以吗?

//string::string(const string s);	// 会导致循环构造
string::string(string& s):_size(s.size), _capacity(s._capacity)
{_str = s._str;
}

这里就不得不提关于深浅拷贝的问题了。

3. 2. 1 深浅拷贝

  1. 浅拷贝:也称位拷贝,编译器只是将对象中的值(包括指针)直接拷贝过来。如果对象中有动态资源就会导致多个对象同时操作一个空间的内容。并且当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,就会继续对资源进行操作,发生野指针访问。

  2. 如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。并且一般情况都要用深拷贝方式实现。

像上面那样直接_str = s._str就是典型的浅拷贝。

如果要实现string的深拷贝,可以使用strcpy函数,将s._str中的数据挨个复制到_str中,这样_str就和s._str完全无关了。

因此拷贝构造要这么写:

string::string(const string& s):_capacity(s._capacity),_size(s._size)
{// 记得申请空间_str = new char[_capacity + 1];strcpy(_str, s._str);
}

3. 3 fill

string::string(size_t n, char c):_capacity(0), _size(0)
{for (int i = 0; i < n; i++)push_back(c);
}

这里用到了push_back(),可以节省不必要的代码,体现了复用的思想。

3. 4 迭代器区间构造

在此之前,我们先讲一下怎么实现迭代器:

typedef char* iterator;	//切记迭代器必须是这个名字!!!

现代编译器中迭代器的实现远比这个复杂,但功能上并没有差异,并且早期的STL版本中随机迭代器string的迭代器就是随机迭代器,迭代器的其他种类以后介绍)就是这样的实现。

既然迭代器就是一个指针,那么使用它进行构造也就算不上难事了。

// 注意,迭代器的类型各不相同,要使用模板(尽管string的构造一般也只会使用string的迭代器)
template<class InputIterator>
string(InputIterator first, InputIterator end):_str(nullptr), _size(0),_capacity(0)	// 对成员变量进行初始化
{reserve(end - first);	// 先预留足够的空间,减少损耗while (first != end){push_back(*first);	// 直接使用push_back(),减少重复的代码量,当然也可以不使用push_back()而是直接插入first++;}
}

这里要特别说明一下为什么用while (first != end)而不是while (first < end),因为迭代器不只有这一种类型,比如说链表也有迭代器,尽管说通过操作符重载也能实现和string的迭代器差不多的功能,但是链表的每个节点都是动态开辟的,而动态开辟的地址的大小是不确定的,所以first不一定小于end,为了兼容所有类型的迭代器,在需要对迭代器进行遍历操作时,我们统一都使用first != end作为循环的结束条件。

4. 容量操作

4. 1 size()capacity()empty()

前两个直接返回_size()_capacity就可以了。

size_t string::size()const
{return _size;
}size_t string::capacity()const
{return _capacity;
}

empty()就是返回_size是否为0.

bool string::empty()const
{return _size == 0;
}

别忘了在函数名后面加上const来兼容不可修改的实例化对象。

4. 2 clear()

将有效字符全部删除,大小置为0。

其实直接修改_size的大小,并在第一位加上'\0'就可以了。

void string::clear()
{_size = 0;_str[0] = '\0';
}

4. 3 resize()

void resize(size_t n, char c = '\0');

resize()需要处理两种情况:

  1. 新的大小>原来的大小:用字符c填补,还要判断容量是否足够。
  2. 新的大小<=原来的大小:缩减字符串的大小到新的大小。
void string::resize(size_t n, char c)
{// 第一种情况if (n > _size){// 判断是否需要扩容if (n + _size > _capacity){// 扩容,因为我们的构造函数不会吧_capacity置为0,而是2或其他值,所以不用判空size_t newcapacity = n + _size > 2 * _capacity ? n + _size : 2 * _capacity;reserve(newcapacity);_capacity = newcapacity;}// 插入字符 c,使 _size == nfor (size_t i = _size; i < n + _size; i++)_str[i] = c;// 添加'\0'_str[n + _size] = '\0';// 更新数组大小_size += n;}else{// 这个和clear()很像,只是不是把大小置为0而是n_str[n] = '\0';_size = n;}
}

但是第一种情况的书写未免太过复杂,有没有办法简化?

后面我们写push_back()的时候就会发现,其实第一种情况和push_back()的代码是高度重合的,我们可以直接对push_back()进行复用,这是string现代写法,本文会有一章来介绍所谓的现代写法。

4. 4 reserve()

void reserve (size_t n = 0);

用于预留空间。

需要注意的是,reserve()在扩容时一般不是n给多少就扩容到多少,我们先来看这个代码:

#include<string>
#include<iostream>
using namespace std;
int main()
{string s("");int last = 0;int i = 0;while (last < 1000){s.reserve(i++);if (s.capacity() != last){last = s.capacity();cout << last << endl;}}return 0;
}

这个程序可以输出当string对象的容量变化时的新容量。

VS2022上的输出结果为:VS2022

devc++(5.11,gcc4.9.2)的输出结果为:devc++

可以发现reserve()在VS2022上是以1.5倍的大小进行扩容(第一扩容是二倍,因为第二章中提到的_Buf数组),只有当新的容量大于现在的容量的1.5倍,才会进行扩容操作。

而devc++是2倍扩容,但是会在一些情况下缩小容量。

为了简化,我们采用简化版的VS2022的实现。

  1. n<=_capacity

这个函数不会做任何举动。

  1. n>_capacity

如果 n <= 1.5 * _capacity,就直接扩容至1.5倍,如果n <= 1.5 * _capacity,就扩容至n

void string::reserve(size_t n)
{// 如果 n < _capacity 就不做任何操作if (n > _capacity){int newcapacity = 0;// 如果 n <= 1.5 * _capacity,就直接扩容至1.5倍,如果 n <= 1.5 * _capacity ,就扩容至 nif (n < _capacity * 1.5)newcapacity = _capacity * 1.5;elsenewcapacity = n;// 创建新的字符指针,注意要动态开辟char* newstr = new char[newcapacity + 1]; // +1是为了给'\0'留位置// 拷贝数据strcpy(newstr, _str);// 释放原空间delete[] _str;// 修改成员变量_str = newstr;_capacity = newcapacity;}
}

剩余内容请看:(本文发布后3天内更新)
string模拟实现下

谢谢你的阅读,喜欢的话来个点赞收藏评论关注吧!
我会持续更新更多优质文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/59961.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker: ubuntu系统下Docker的安装

安装依赖 操作系统版本 Ubuntu Kinetic 22.10Ubuntu Jammy 24.04 (LTS)Ubuntu Jammy 22.04 (LTS)Ubuntu Focal 20.04 (LTS)Ubuntu Bionic 18.04 (LTS) CPU架构支持 ARMx86_64 查看我们的系统版本信息 uname -a通过该命令查得cpu架构是x86_64的&#xff1b; cat /etc/*re…

vue2+3 —— Day5/6

自定义指令 自定义指令 需求&#xff1a;当页面加载时&#xff0c;让元素获取焦点&#xff08;一进页面&#xff0c;输入框就获取焦点&#xff09; 常规操作&#xff1a;操作dom “dom元素.focus()” 获取dom元素还要用ref 和 $refs <input ref"inp" type&quo…

如何确保爬取的数据准确性和完整性?

在数据驱动的业务环境中&#xff0c;爬虫程序的准确性和完整性至关重要。本文将探讨如何使用Java编写爬虫程序&#xff0c;并确保其在爬取数据时的准确性和完整性。 1. 精确的HTML解析 确保数据准确性的第一步是精确地解析HTML。Jsoup是Java中常用的HTML解析库&#xff0c;它提…

关于Web Component

2024年8月14日 引言 Web Component 是一种用于构建可复用用户界面组件的技术&#xff0c;开发者可以创建自定义的 HTML 标签&#xff0c;并将其封装为包含逻辑和样式的独立组件&#xff0c;从而在任何 Web 应用中重复使用&#xff0c;并且可以做到无框架跨框架。 不同于 Vue…

【MySql】实验十六 综合练习:图书管理系统数据库结构

文章目录 创建图书管理系统数据库结构一、创建数据表1.1 book表1.2 reader表1.3 borrow表 二、插入示例数据2.1 向book表插入数据2.2 向reader表插入数据2.3 向borrow表插入数据 三、查询操作3.1 根据语义为借书表borrow的bno列和 rno列建立外键3.2 查询张小海编写的“数据库原…

AutoDL部署视觉大模型llama3.2-vision,从视频中寻找特定目标

注&#xff1a; windows11系统。示例为此项目&#xff1a;https://github.com/win4r/VideoFinder-Llama3.2-vision-Ollama 在当今的人工智能领域&#xff0c;深度学习模型的计算需求日益增长&#xff0c;特别是在处理复杂的视觉任务时&#xff0c;强大的算力往往是实现高效应用…

SHELL笔记(条件测试)

基本概念&#xff1a; 条件测试用于在 Shell 脚本中对各种条件进行判断&#xff0c;根据判断结果来决定是否执行特定的命令或代码块。条件测试可以用于比较数值、字符串&#xff0c;检查文件或目录的属性&#xff0c;以及判断命令的执行结果等。 格式&#xff1a; 格式1&…

JDK、MAVEN与IDEA的安装与配置

1.认识JDK、MAVEN与IDEA JDK 提供了编译和运行Java程序的基本环境。Maven 帮助管理项目的构建和依赖。IDEA 提供了一个强大的开发环境&#xff0c;使得编写、调试和运行Java程序更加高效。 2. 安装与环境配置 2.1 官网地址 选择你需要的版本下载&#xff1a; MAVEN下载传送…

微信小程序-prettier 格式化

一.安装prettier插件 二.配置开发者工具的设置 配置如下代码在setting.json里&#xff1a; "editor.formatOnSave": true,"editor.defaultFormatter": "esbenp.prettier-vscode","prettier.documentSelectors": ["**/*.wxml"…

【Mac】未能完成该操作 Unable to locate a Java Runtime

重生之我做完产品经理之后回来学习Data Mining Mac打开weka.jar报错"未能完成该操作 Unable to locate a Java Runtime" 1. 打开终端执行 java -version 指令&#xff0c;原来是没安装 JDK 环境 yyzccnn-mac ~ % java -version The operation couldn’t be comple…

【ArcGIS微课1000例】0127:计算城市之间的距离

本文讲述,在ArcGIS中,计算城市(以地级城市为例)之间的距离,效果如下图所示: 一、数据准备 加载配套实验数据包中的地级市和行政区划矢量数据(订阅专栏后,从私信查收数据),如下图所示: 二、计算距离 1. 计算邻近表 ArcGIS提供了计算点和另外点之间距离的工具:分析…

【WPF】Prism学习(五)

Prism Commands 1.错误处理&#xff08;Error Handling&#xff09; Prism 9 为所有的命令&#xff08;包含AsyncDelegateCommand&#xff09;提供了更好的错误处理。 避免用try/catch包装每一个方法根据不同遇到的异常类型来提供特定的逻辑处理可以在多个命令之间共享错误处…

【element-tiptap】Tiptap编辑器核心概念----结构篇

core-concepts 前言&#xff1a;这篇文章来介绍一下 Tiptap 编辑器的一些核心概念 &#xff08;一&#xff09;结构 1、 Schemas 定义文档组成方式。一个文档就是标题、段落以及其他的节点组成的一棵树。 每一个 ProseMirror 的文档都有一个与之相关联的 schema&#xff0c;…

2024.6使用 UMLS 集成的基于 CNN 的文本索引增强医学图像检索

Enhancing Medical Image Retrieval with UMLS-Integrated CNN-Based Text Indexing 问题 医疗图像检索中&#xff0c;图像与相关文本的一致性问题&#xff0c;如患者有病症但影像可能无明显异常&#xff0c;影响图像检索系统准确性。传统的基于文本的医学图像检索&#xff0…

初识Linux · 信号处理 · 续

目录 前言&#xff1a; 可重入函数 重谈进程等待和优化 前言&#xff1a; 在前文&#xff0c;我们已经介绍了信号产生&#xff0c;信号保存&#xff0c;信号处理的主题内容&#xff0c;本文作为信号处理的续篇&#xff0c;主要是介绍一些不那么重要的内容&#xff0c;第一个…

微信小程序 最新获取用户头像以及用户名

一.在小程序改版为了安全起见 使用用户填写来获取头像以及用户名 二.代码实现 <view class"login_box"><!-- 头像 --><view class"avator_box"><button wx:if"{{ !userInfo.avatarUrl }}" class"avatorbtn" op…

WPF MVVM框架

一、MVVM简介 MVC Model View Control MVP MVVM即Model-View-ViewModel&#xff0c;MVVM模式与MVP&#xff08;Model-View-Presenter&#xff09;模式相似&#xff0c;主要目的是分离视图&#xff08;View&#xff09;和模型&#xff08;Model&#xff09;&#xff0c;具有低…

【算法】【优选算法】前缀和(下)

目录 一、560.和为K的⼦数组1.1 前缀和1.2 暴力枚举 二、974.和可被K整除的⼦数组2.1 前缀和2.2 暴力枚举 三、525.连续数组3.1 前缀和3.2 暴力枚举 四、1314.矩阵区域和4.1 前缀和4.2 暴力枚举 一、560.和为K的⼦数组 题目链接&#xff1a;560.和为K的⼦数组 题目描述&#x…

两大新兴开发语言大比拼:Move PK Rust

了解 Move 和 Rust 的差异有助于开发者根据项目的具体需求选择最合适的语言。选择不恰当的语言可能会导致项目后期出现技术债务。不同语言有其独特的优势。了解 Move 和 Rust 的差异可以帮助开发者拓展技术视野&#xff0c;发现不同语言在不同领域的应用潜力。 咱们直奔主题&a…

Scaling Law的“终结“还是新起点?——开源实践者的深度思考

作者&#xff1a;宋大宝&#xff0c;与大宝同学因那篇《回顾总结展望「融合RL与LLM思想&#xff0c;探寻世界模型以迈向AGI」》结识于今年春天&#xff0c;虽我们当时某些思想观念有些出入&#xff0c;也碰撞出了很多火花与共鸣&#xff0c;并持续地相互启发的走到了现在。他是…