string(四)————底层实现

news2024/11/15 8:25:33

目录

引言

外层包装

成员变量设计

接口实现


引言

在之前的博客中我简单介绍了string的相关使用方法和接口,现在我们自己来模拟实现一下它的底层(注:不同编译器底层实现不同,这里只是其中一种的实现)

外层包装

本来应该是在外层套个basic_string<char>的模板的,但因为模板不支持声明定义分离所以这里不用模板了,直接将string类放到命名空间sak中,测试函数放在头文件中,在命名空间里面,分两个文件链接。

namespace sak
{
    class string
    {
        public:
           //...
        private:
           //...
    };
    Test();
};

成员变量设计

我的 string类中成员变量设置三个:char* _str , size_t size(有效数据大小) , size_t capacity(容量)

因为字符串涉及到 '\0' 问题,要多给一个空间储存 '\0',所以在开辟空间时给_str 多开一个字节空间,capacity不包含 '\0' 的大小,表示有效数据的容量。


    class string
    {
        public:
           //...
        private:
           size_t capacity;
           size_t size;
           char*_str;
    };

接口实现

1、构造函数接口

构造可以用字符串构造,也可以用string类型对象构造。注意成员变量声明的顺序,为了尽可能减少strlen函数的调用,先对_capacity初始化,再对_size和_str初始化,因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后 次序无关,所以要先声明_capacity,再声明其他的。

string(const char* str)
	:_capacity(strlen(str))
	,_size(_capacity)
	,_str(new char[_capacity+1])
{
	strcpy(_str, str);
}

string(const string& s)
	:_capacity(s._capacity)
	,_size(s._size)
	,_str(new char[s._capacity+1])
{
	strcpy(_str, s._str);
}

这里不能直接将str 指针赋值给_str,虽然确实将str指向的内容赋给了_str,但是如果参数str是常量字符串,那么后面调用push_back接口需要扩容的话,无法对常量字符串操作,所以这里先给_str开辟空间(上面说了因为 '\0'的原因多开一个空间)。

还有一种情况:无参类型的构造

		string()
		{
			_str = new char[1];
			_str[0] = '\0';
			_size = _capacity = 0;
		}

或者这里可以通过字符串类型参数的缺省来实现无参类型的构造:

		string(const char* str = "")
			:_capacity(strlen(str))
			,_size(_capacity)
			,_str(new char[_capacity+1])
		{
			strcpy(_str, str);
		}
		string(const char* str = "")

这里为什么是"",需要先辨析清楚 '\0' , " \0" , "" 三者之间的不同。

'\0' :字符\0,ASCLL码值是0,即就代表了0

" \0" :字符串都以 '\0' 结尾,所以是\0\0,有2个 

"" : 空字符串,里面有一个 \0

size_t strlen ( const char * str );

strlen函数的参数部分是const char* 类型的,会计算 '\0'之前有几个字符。因此这里不能是

string(const char* str = '\0'或者nullptr)  而是  string(const char* str = "")

2、析构函数接口

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

3、c_str 接口

		const char* c_str() const
		{
			return _str;
		}

在尚未实现  流输出<< 和   流提取>> 操作符时,可以用c_str打印string类型对象。

4、size 接口

		size_t size() const
		{
			return _size;
		}

5、capacity 接口

		size_t capacity() const
		{
			return _capacity;
		}

 这里capacity实现的和VS编译器下不同,所以表现结果也不同

6、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];
		}

7、迭代器实现(char*指针方式)

迭代器底层实现有很多方法,内部类、指针.......  这里我用指针实现。

    	typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}
	string::iterator it = s2.begin();
	while (it != s2.end())
	{
		(*it)++;
		it++;
	}
	cout << s0.c_str() << endl;

范围for底层其实就是迭代器,是直接用迭代器替换的,所以范围for并没有想象的那么神奇。

底层是将对象赋给 it变量,再将*it 赋给e,其实很简单。

8、reserve接口

当要改变的空间大小n < capacity,对容量没有变化,当n < capacity ,reserve实质上就是扩容的实现。在C语言中扩容有realloc,C++这里我用的new,需要自己实现一下扩容。

		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n + 1];
				strcpy(tmp, _str);
				delete[]_str;
				_str = tmp;

				_capacity = n;
			}
		}

9、push_back、append、+= 接口

		void push_back(char c)
		{
			if (_size == _capacity) {
				size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newcapacity);
			}
			_str[_size] = c;
			_size++;
			_str[_size] = '\0';
		}
		void append(const char* s)
		{
			size_t len = strlen(s);
			if (len + _size > _capacity) {
				reserve(_size + len);
			}
			strcpy(_str + _size, s);
			_size += len;
		}
		string& operator+=(char c)
		{
			push_back(c);
			return *this;
		}
		string& operator+=(const char* s)
		{
			append(s);
			return *this;
		}

10、insert、erase接口

insert函数方便我们在任何位置插入数据。和上面一样,分为插入1个字符或字符串。

在插入之前要先挪动数据(尾插除外),挪动的过程中要注意边界问题。

		string& insert(char ch, size_t pos)
		{
			assert(pos <= _size);
			if (_capacity == _size)
			{
				size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
				reserve(newcapacity);
			}
			size_t end = _size+1;
			while (end >= pos)
			{
				_str[end] = _str[end-1];
				--end;
			}
			_str[pos] = ch;
			_size++;
			_str[_size] = '\0';
			return *this;
		}

		string& insert(const char* str, size_t pos)
		{
			size_t len = strlen(str);
			if (len + _size > _capacity)
			{
				reserve(len + _size);
			}
			size_t end = _size+len;
			while (end >= pos + len)
			{
				_str[end] = _str[end-len];
				--end;
			}
			strncpy(_str + pos, str, len);
			_size += len;
			return *this;
		}

erase删除同样需要挪动数据,给删除个数一个缺省值npos,定义为常量-1,对于size_t来说是2^32,当没有给定删除个数或要删除的个数超出剩余的字符数就意味着删除pos位置之后所有数据;否则删除pos位置后面len个字符,可以用strcpy直接覆盖。

		string& erase(size_t pos,size_t len = npos)
		{
			if (len == npos || pos + len >= _size)
			{
				_str[pos] = '\0';
			}
			else
			{
				strcpy(_str + pos,_str+pos+len);
				_size -= len;
			}
			return *this;
		}
		const static int npos = -1;

 

11、find接口

find接口不仅要支持查找,还要能支持从指定位置开始查找。同样的,有查找单个字符的功能,还有查找字符串的功能。找到返回下标,找不到返回npos。

查找单个字符比较简单,直接遍历即可。

查找字符串可以用C函数库的strstr函数暴力查找,注意该函数返回的是指针,所以最后返回下标值时要用返回的指针 - 头指针。

		const static int npos = -1;

		size_t find(char ch, size_t pos = 0)
		{
			assert(pos < _size);
			while (pos < _size)
			{
				if (_str[pos] == ch)
					return pos;
				++pos;
			}
			return npos;
		}

		size_t find(const char* str, size_t pos = 0)
		{
			assert(pos < _size);
			const char* ptr = strstr(_str + pos, str);
			if (ptr == nullptr)
				return npos;
			else
				return ptr - _str;
		}

12、resize接口

resize是改变容量的函数,可以变大也可以变小。

		void resize(size_t n,char ch = '\0')
		{
			if (n > _size)
			{
				reserve(n);
				_size = n;
				for (size_t i = _size; i < n; ++i)
				{
					_str[i] = ch;
				}
				_str[n] = '\0';
			}
			else
			{
				_size = n;
				_str[n] = '\0';
			}
		}

13、流插入cout

流插入操作符<<重载,需要注意[ ]的重载,上面只重载了[ ]的非const版本,对于只读对象调用需要const版本的。我们再重载一个只读的[ ]方法。

   			const char& operator[](size_t pos)const
		{
			assert(pos < _size);
			return _str[pos];
		}

    ostream& operator<<(ostream& out,const string& s)
	{
		for (size_t i = 0; i < s.size(); ++i)
		{
			out << s[i];
		}
		return out;
	}

 有了c_str还不够,还需要 <<流插入。这两者打印的方式是不一样的。

c_str是按char*方式打印,字符串中间出现 '\0' 不会打印后面的内容;<<碰到 '\0' 依然会继续打印,'\0' 打印显示的是不可见字符。

14、流提取 >>

与流插入对应。流提取碰到空格或换行就停下,后面部分进入缓冲区。

想要拿到空格之后的内容需要用到get函数。

	istream& operator>>(istream& in,string& s)
	{
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			s += ch;
			ch = in.get();
		}
		return in;
	}

这种方式有一个缺陷,字符串很长时会扩容很多次。想解决这个问题也不好先reserve给定capacity大小,因为capacity给大了浪费,小了没有效果。

因此可以借鉴C++标准库的实现方式,先设定一个buff数组,分段将字符串放入。

	istream& operator>>(istream& in, string& s)
	{
		char buff[128] = { '\0' };
		size_t i = 0;
		char ch = in.get();
		while (ch != ' ' && ch != '\n')
		{
			if (i == 127)//满了
			{
				s += buff;
				i = 0;
			}
			buff[i++] = ch;
			ch = in.get();
		}
		if (i > 0)
		{
			buff[i] = '\0';
			s += buff;
		}
		return in;
	}

15.拷贝构造

这里我们必须手动写一个拷贝构造函数,否则系统默认的是浅拷贝,对于自定义类型来说会出现指针指向同一空间以及内存泄漏的问题。

 拷贝构造有两种写法:传统写法和现代写法。

1、传统写法

		//传统写法
		string(const string& s)
		{
			_str = new char[s._capacity + 1];
			_capacity = s._capacity;
			_size = s._size;
			strcpy(_str, s._str);
		}

 传统写法就是创建一个和拷贝对象一样大小的新空间,再将数据拷贝到新空间中。

2、现代写法

		void swap(string& s)
		{
			std::swap(_str, s._str);
			std::swap(_capacity, s._capacity);
			std::swap(_size, s._size);
		}
		//现代写法
		string(const string& s)
			:_str(nullptr)
			,_capacity(0)
			,_size(0)
		{
			string tmp(s._str); //构造函数
			swap(tmp);
		}

现代写法就是先用构造函数构造一个tmp,此时tmp拥有和拷贝对象s0一样的空间和数据,交换tmp和s1,使两者的空间和数据完全交换。因为tmp最后要调用析构函数清理空间,和s1交换后tmp指向的是随机值,所以提前让s1指向空,避免释放野指针的问题。

 注意:现代写法并不能提高效率,只是简化了代码(结合赋值函数来看)

16、赋值重载

赋值也有传统和现代写法,与拷贝构造类似。

1、传统写法

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

2、现代写法

		//现代写法1(不推荐)
		string& operator=(const string& s)
		{
			if (this != &s)
			{
				string tmp(s);
				swap(tmp);
			}
			return *this;
		}

		//现代写法2(推荐√)
		string& operator=(string s)
		{
			swap(s);
			return *this;
		}

现代写法1还是先拷贝构造tmp,再将tmp与s1交换,这种写法tmp有点多余了。

其实可以直接传值传参,在传参的过程中拷贝构造s(写了拷贝构造这里是深拷贝),直接交换s和s1即可。

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

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

相关文章

Allegro如何在PCB上查看焊盘信息操作指导

Allegro如何在PCB上查看焊盘信息操作指导 在做PCB设计的时候需要查看焊盘的信息,Allegro上支持直接在PCB上查看焊盘的信息,如下图 具体操作如下 选择Tools-Pad stack选择Modify Design Padstack

【漏洞复现】Django SQL注入漏洞 (CVE-2022-28346)

文章目录一、简介二、漏洞概述三、漏洞影响版本四、漏洞分析五、漏洞复现六、修复方法一、简介 Django是用Python开发的一个免费开源的Web结构&#xff0c;几乎包括了Web使用方方面面&#xff0c;能够用于快速建立高性能、文雅的网站&#xff0c;Diango提供了许多网站后台开发…

pcl 姿态变换 之 旋转平移

一、简介 最近在做一个点云的项目&#xff0c;姿态的变换是一个很重要的环节&#xff0c;从数学上需要彻底理解这些东西之前一直在使用&#xff0c;但是没有系统的总结过&#xff0c;接着2023年元旦的三天时间好好学习一下&#xff0c;然后在同事面前说自己是数学系的很丢人啊…

【MySQL进阶】从计算机层面看索引凭什么让查询效率提高这么多?

【MySQL进阶】从计算机层面看索引凭什么让查询效率提高这么多&#xff1f; 文章目录【MySQL进阶】从计算机层面看索引凭什么让查询效率提高这么多&#xff1f;磁盘IO和预读&#xff1a;索引是什么&#xff1f;BTree索引BTree索引让我们先来了解一下计算机的数据加载。磁盘IO和预…

中国为印尼建设的高铁顺利推进,印度网友与日本网友就高铁互怼

日前中国为印尼建设的雅万高铁已开始进行试运行测试&#xff0c;预计将在明年6月正式运行&#xff0c;与雅万高铁差不多时间开始的日本为印度孟买建设的高铁项目才建设了15公里&#xff0c;为此印度网友和日本网友对中日高铁技术的差距展开了争论。2011年日本相关机构开始对印尼…

羊的第四天,开始这篇年终总结

比较尴尬&#xff0c;从今年“羊”到明年&#xff0c;所以这篇文章也是每天抽出一点时间写写&#xff0c;可能会比较乱&#xff0c;先大致分下核心内容吧&#xff1a;今年总结新年展望今年总结先是完成了《数字硬件建模系列的Verilog篇》&#xff0c;效果不好不坏&#xff0c;主…

算法设计与分析复习03:动态规划算法

算法设计与分析复习03&#xff1a;动态规划算法 文章目录算法设计与分析复习03&#xff1a;动态规划算法复习重点动态规划算法斐波那契数列及其应用矩阵链乘法凸多边形剖分矩阵链乘法凸多边形剖分最长公共子序列最大子段和&#xff08;字数组&#xff09;0-1背包编辑距离钢条切…

pycharm-qt5-designer1

pycharm-qt5-designer1一: designer界面介绍1. 新建模板二: 控件箱简介1. Layouts 布局2. Spacers 间隔(透明)3. Button4. Item views5. Item Widgets 条目控件6. Containers 容器7. input Widgets 输入控件8. Display Widgets 显示控件三: 控件属性简介1. sizePolicy: 控件大小…

gitlab-ci.yml关键字(四)allow_failure 、artifacts 、cache

allow_failure 我们知道&#xff0c;流水线作业在运行时如果失败了&#xff0c;就会停止运行&#xff0c;但allow_failure可以让我们自由的控制当前作业失败时&#xff0c;是否还需要继续运行。 要让管道继续运行后续作业&#xff0c;请使用allow_failure: true要停止管道运行…

OASIS协议标准文档的解读_第一部分

译者注&#xff1a; 利用2022年圣诞假期&#xff0c;终于解读完OASIS标准协议的文档。本翻译文档基于SEMI 草案标准 3626 (2003/04/23). 因为SEMI的原版标准草案涉及到版权的一些问题&#xff0c;并不是公开的。因此我并不是原文原样翻译&#xff0c;会加入很多我自己的理解和…

cnpm : 无法将“cnpm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。

从报错来看明显是没有装 cnpm 检查本地是否安装了cnpm包管理工具 命令&#xff1a;npm list --depth0 -global 查看一下电脑是否安装了cnpm 如果已经安装了&#xff0c;那么会有如下图所示的内容&#xff1a; 从以上来看确实是没有装 则需要安装镜像&#xff0c;执行命令为…

Vue3详细讲解

Vue 3 介绍 文章目录Vue 3 介绍为什么要学习 vue 3Vue3 动机 和 新特性Vite 的使用vite介绍为什么选 Vite &#xff1f;Vite 的基本使用Vue3.0项目介绍vscode插件说明组合式APIcomposition API vs options API体验 composition APIsetup 函数reactive 函数ref 函数script setup…

【云原生 | Kubernetes 实战】19、K8s Ingress-Controller 高可用方案

目录 一、Ingress 和 Ingress Controller 概述 1.1 回顾下 service 四层代理 1.2 Ingress 介绍 1.3 Ingress Controller 介绍 1.4 Ingress 和 Ingress Controller 总结 1.5 使用 Ingress Controller 代理 k8s 内部 pod 的流程 二、创建两个 ingress-controller 高可用…

凌云驭势 亚马逊云科技开启re:Invent中国行

‍‍数据智能产业创新服务媒体——聚焦数智 改变商业近日&#xff0c;亚马逊云科技召开了2022 re:Invent全球大会。作为云计算的开创者&#xff0c;每年亚马逊云科技举办的re:Invent全球大会都会成为产业的风向标&#xff0c;备受业内人士关注。2022年&#xff0c;面对全球数字…

【STL学习之路】vector的模拟实现

文章目录一、接口总览二、vector成员变量三、默认成员函数构造函数① -- 默认无参构造构造函数② -- 迭代器区间构造构造函数③ -- n个val构造拷贝构造函数赋值运算符重载析构函数四、迭代器六、容量以及元素访问的相关接口emptysize和capacityreserveresize七、增删查改等接口…

async await 的基础使用和实现原理

async await 使用基础原理 async/await用法 其实你要实现一个东西之前&#xff0c;最好是先搞清楚这两样东西 这个东西有什么用&#xff1f; 这个东西是怎么用的&#xff1f; 有什么用&#xff1f; async/await的用处就是&#xff1a;用同步方式&#xff0c;执行异步操作&…

商会机构源码模板系统包含了信息管理、新闻管理、广告管理、系统管理等功能 v3.9

内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 XYCMS商会机构源码模板系统是以aspaccess进行开发的商会网站源码&#xff0c;包含了信息管理、新闻管理、广告管理、系统管理等功能。 XYCMS商会机构源码模板系统功能简述&#xff1a; 商…

大数据面试题Spark篇(1)

1.spark数据倾斜 数据倾斜俩大直接致命后果&#xff1a;Out Of Memory&#xff0c;运行速度慢。这主要是发生在Shuffle阶段。同样Key的数据条数太多了。导致了某个key所在的Task数据量太大了&#xff0c;远远超过其他Task所处理的数据量。 数据倾斜一般会发生在shuffle过程中…

使用Eclipse开发第一个Java程序

虽然在《使用记事本编写运行Java程序》一节中已经开发过一个 Java 程序&#xff0c;但是那毕竟是通过记事本创建的。在上一节《Java Eclipse下载安装教程》中&#xff0c;我们已经安装了 Eclipse 工具&#xff0c;因此本节将介绍如何通过 Eclipse 开发 Java 程序。 在 Eclipse …

SD存储卡介绍

SD存储卡简介 SD存储卡是一种基于半导体快闪记忆器的新一代记忆设备&#xff0c;由于它体积小、数据传输速度快、可热插拔等优良的特性&#xff0c;被广泛地于便携式装置上使用&#xff0c;例如数码相机、平板电脑、多媒体播放器等。 SD存储卡实物图 SD存储卡特点 1、高存储容…