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

news2024/12/23 9:08:51

一、前言

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

C++的string类-CSDN博客

二、模拟实现

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

成员变量

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

namespace MyString
{
	class string
	{



	private:
		char* _a;
		size_t _size;
		size_t _capacity;
	};
	

}

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

成员函数

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

构造函数:

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

需要注意两个个坑点:

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

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

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

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

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

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

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

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

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

正确方法:

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

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

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

优化:

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

int main()
{
	cout << strlen("") << endl;

	return 0;
}

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

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

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

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

析构函数

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

string::~string()	
{
	_size = _capacity = 0;
	delete[] _a;
	_a = nullptr;	//需要让指针置空

}

c_str

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

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

[]重载

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

char& string::operator[] (size_t pos)
{
	assert(pos <= _size - 1);	//pos是size_t,所以 >=0 恒成立
	return _a[pos];
}

const char& string::operator[] (size_t pos) const
{
	assert(pos <= _size - 1);
	return _a[pos];
}

capacity、size

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

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

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

reserve

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

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

void string::reserve(size_t n)	//开辟空间,对_capacity作出修改
{
	//1.重新开空间	2.深拷贝 3.释放旧空间

	if (n > _capacity)
	{
		char* tmp = new char[n + 1];
		memcpy(tmp, _a, _size + 1);
		delete[] _a;
		_a = tmp;
		_capacity = n;	//将空间修正为n
	}

}

push_back

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

void string::push_back(char ch)
{
	if (_size == _capacity)	//利用reserve扩容
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	_a[_size] = ch;
	++_size;	//_size类比length
	_a[_size] = 0;	//补充\0,符合C语言的规范
}

append

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

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

void string::append(const char* str)	//追加字符串
{
	size_t len = strlen(str);

	if (len + _size > _capacity)
		reserve(len + _size);

	strcpy(_a + _size, str);	//从_size处,完成内容的拷贝
	/*
		strcpy :自动添加\0,可从任意位置开始拷贝
	*/

	_size += len;
}

operator+=

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

string& string::operator+=(char ch)
{
	push_back(ch);
	return *this;
}

string& string::operator+=(const char* str)
{
	append(str);
	return *this;
}

insert

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

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


void string::insert(size_t pos, char ch)
{
	assert(pos <= _size);		// = 是尾插

	if (_size == _capacity)	//利用reserve扩容
	{
		reserve(_capacity == 0 ? 4 : 2 * _capacity);
	}

	size_t end = _size + 1;	//保证可以完成头插

	while (end > pos)	//后移(包含\0)
	{
		_a[end] = _a[end - 1];
		--end;
	}

	_a[pos] = ch;
	++_size;
	//_a[_size] = 0;	//可有可无,\0也发生了后移
}

void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);		//不需要检查是否>0
	size_t len = strlen(str);

	if (len + _size > _capacity)
		reserve(len + _size + 1);	//对this指针对象进行扩容

	int end = _size;	//防止出现size_t的死循环
	while (end >= (int)pos)
	{
		_a[end + len] = _a[end];
		--end;
	}
	
	strncpy(_a + pos, str, len);	
	_size += len;

}

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


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

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

erase

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


void string::erase(size_t pos, size_t len)		//声明给出,定义不给出缺省
{
	assert(pos < _size);		// == _size是\0的位置,不能删除

	if (len == npos || pos + len >= _size)	//len过长时
	{
		_a[pos] = 0;
		_size = pos;
	}

	else	//len的长度合适时
	{
		int cur = pos + len;

		while (cur <= (int)_size)
		{
			_a[pos++] = _a[cur++];
		}

		_size -= len;
	}
}

clear

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

清楚内容,长度清零。

void string::clear()
{
	_size = 0;
	_a[0] = 0;
}

函数重载

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


bool string::operator<(const string& s) const
{
	return strcmp(_a, s._a) < 0;		//内部可使用private成员
}

bool string::operator==(const string& s) const
{
	return strcmp(_a, s._a) == 0;
}

bool string::operator<=(const string& s) const
{
	return *this == s || *this < s;
}

bool string::operator>(const string& s) const
{
	return !(*this <= s);
}

bool string::operator>=(const string& s) const
{
	return !(*this < s);
}

bool string::operator!=(const string& s) const
{
	return !(*this == s);
}

迭代器函数

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

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

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


string::iterator string::begin()
{
	return _a;
}

string::iterator string::end()
{
	return _a + _size;	//end返回的是最后一个元素的下一个位置。
}

string::const_iterator string::begin() const
{
	return _a;
}

string::const_iterator string::end() const
{
	return _a + _size;
}

需要注意的是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函数进行标题函数的实现。

void string::swap(string& s)
{
	std::swap(_a, s._a);
	std::swap(_size, s._size);
	std::swap(_capacity, s._capacity);
}

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

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

//s1(s2)
string::string(const string& s)	//在进入函数体之前,先对this进行初始化
	:_a(nullptr)
	, _size(0)
	, _capacity(_size)

{
	string tmp(s._a);
	swap(tmp);
}

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

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

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

resize

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

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

void string::resize(size_t n, char ch)	//更新size
{
	if (n <= _size)
	{
		_size = n;
		_a[_size] = 0;
	}
	else
	{
		reserve(n);		//函数内部开辟n + 1个空间
		
		while (_size < n)
		{
			_a[_size++] = ch;
		}

		_a[_size] = 0;
	}
}

find

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

查找字符

size_t string::find(char ch, size_t pos)	//函数的定义不能有缺省值
{
	for (size_t i = pos; i < _size; ++i)
	{
		if (_a[i] == ch)
			return i;
	}

	return npos;
}

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


size_t string::find(const char* sub, size_t pos)
{
	const char* ptr = strstr(_a + pos, sub);
	if (ptr == nullptr)
		return npos;
	return ptr - _a;
}

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

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

/*
思路:遍历原数组,匹配往后走。
*/

size_t string::find(const char* sub, size_t pos = 0)
{
	assert(pos < _size);
	size_t i = pos;

	for (; i < _size; ++i)		//遍历原数组
	{
		if (_a[i] == sub[0])
		{
			size_t len = strlen(sub);
			size_t m = i;

			for (size_t j = 0; j < len; ++j)	//循环:判断
			{
				if (m >= _size || sub[j] != _a[m])		
				{
					break;
				}
				++m;	//后移
			}

			if (m - i == len)	//如果完全一致(长度替代flag)
				return i;


		}
	}

	return npos;

}

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

substr

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

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


//1.设定好大小开空间 2.拷贝内容
string string::substr(size_t pos, size_t len)
{
	assert(pos < _size);
	string tmp;
	size_t end = pos + len;
	//设定大小
	if (pos + len >= _size || len == npos)	//len太长
	{
		len = _size - pos;
		end = _size;
	}

	tmp.reserve(len);	//开空间

	for (size_t i = pos; i < end; i++)	//内容拷贝
	{
		tmp += _a[i];
	}

	return tmp;		//临时对象不用引用返回。
}

全局函数

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

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

流提取

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

流插入

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


istream& MyString::operator>>(istream& in, string& s)
{
	s.clear();	//清空对象
	char ch = in.get();
	size_t i = 0;
	char* buff = new char[129];		//建立缓冲区

	while (ch != ' ' && ch != 10)
	{
		buff[i++] = ch;		//流入缓冲区
		
		if (i == 128)
		{	
			buff[i] = 0;
			s += buff;		//从缓冲区读取
			i = 0;
		}

		ch = in.get();		//继续读取缓冲区
	}

	if (i != 0)
	{
		buff[i] = 0;
		s += buff;
	}
	return in;

}


 

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

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

相关文章

Spring Boot项目实现调用腾讯云进行人脸识别

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 背景 随着科技的不断发展&#xff0c;人脸识别技术已经成为了一种日益重要的身份验证和安全监控手段。腾讯云人脸识别功能便是其中之一&#xff0c;而Java语言又是现在最为常用且高效的编程语言之一。本篇论…

mongoDB 的安装和使用【docker compose,springboot】

一 介绍 NoSQL数据库 NoSQL&#xff1a;Not Only SQL ,本质也是一种数据库的技术&#xff0c;相对于传统数据库技术&#xff0c;它不会遵循一些约束&#xff0c;比如&#xff1a;sql标准、ACID属性&#xff0c;表结构等。 Nosql优点 l 满足对数据库的高并发读写 l 对海量数…

思维+dfs,CF 269C - Flawed Flow

一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 269C - Flawed Flow 二、解题报告 1、思路分析 考虑源点相连的边的方向是确定的&#xff0c;因为流量是从源点往外流的 我们设cap[u] 为 和u相连边的容量和&#xff0c;显然入边容量要和出边容量相等&…

6.C基础_输入输出函数

putchar 功能&#xff1a;输出一个字符 函数声明&#xff1a; int putchar(int c);返回值&#xff1a;参数c的ASCLL码值 c&#xff1a;要输出的字符&#xff0c;可以为字符常量、字符变量或表达式 注意点&#xff1a;输出的结果不带\n getchar 功能&#xff1a;从键盘读…

Day28 | 56. 合并区间 738.单调递增的数字 968.监控二叉树

语言 Java 56. 合并区间 合并区间 题目 以数组 intervals 表示若干个区间的集合&#xff0c;其中单个区间为 intervals[i] [starti, endi] 。请你合并所有重叠的区间&#xff0c;并返回 一个不重叠的区间数组&#xff0c;该数组需恰好覆盖输入中的所有区间 。 思路 创建…

基于单片机的ELF可执行文件加载以及Bootloader程序实现程序切换

目录 1.ELF可执行文件加载 1.1 ELF文件分类 1.2 ELF文件格式 1.3链接地址 ​编辑 2.Bootloader程序切换 2.1 Bootloader介绍 2.2 代码实现 1.ELF可执行文件加载 ELF&#xff08;Executable and Linkable Format&#xff09;文件是一种标准的文件格式&#xff0c;用于可…

秒懂Linux之编写小程序——进度条

目录 一.前文摘要 二.进度条编写 三全部代码&#xff08;非Linux环境下测试&#xff09; 一.前文摘要 在开始编写之前我们先来学习一些后面会用到的知识点~ 测试结果发现&#xff0c;Sleep无作用&#xff0c;编译完会立刻打印~ 再来看另一个测试~我们同样没有换行&#xff0c…

前后端中的日期格式转换问题

从前端接收到的日期转为想要的格式 JSON日期的反序列化为java对象时 JsonFormat(timezone “GMT8”, pattern “yyyy-MM-dd HH:mm:ss”) 从后端发送的日期转为想要的格式给前端 Java对象中的日期的序列化为JSON时会用到 Date数据序列化为JSON发往前端时&#xff0c;按以下格…

【机器学习西瓜书学习笔记——支持向量机】

机器学习西瓜书学习笔记【第六章】 第六章 支持向量机6.1 间隔与支持向量硬间隔最大化 6.2 对偶问题6.3核函数定义构建核函数应用优劣优势劣势 6.4 软间隔与正则化软间隔正则化 6.5支持向量回归&#xff08;SVR&#xff09;函数间隔和几何间隔SVR的原理SVR数学模型线性硬间隔SV…

The dependencies of some of the beans in the application context form a cycle

你们好&#xff0c;我是金金金。 场景 启动服务时&#xff0c;报错&#xff1a;应用程序上下文中的某些bean的依赖关系形成了一个循环 循环依赖 依赖循环指的是两个或多个类之间相互依赖的情况&#xff0c;即类A依赖类B&#xff0c;同时类B也依赖类A。 这种情况会导致编译器无…

java接口只能定义抽象方法吗?

写在前面 在Java中接口时作为规范来存在的&#xff0c;那么除了抽象方法&#xff0c;接口中还能定义其他方法吗?比如静态方法&#xff1f;本文一起来看下。 1&#xff1a;正文 这并不是一个绝对的是和否的问题&#xff0c;不同的jdk版本表现不同&#xff0c;在<1.7的版本…

用例管理框架

用例管理框架之pytest单元测试框架&#xff08;上&#xff09; 一、pytest用例管理框架&#xff08;单元测试框架&#xff09; 1.分类&#xff1a; python&#xff1a;unittest&#xff0c;pytest 必须非常熟练 2.主要作用&#xff1a; 发现测试用例&#xff1a;从多个py文…

深入理解 C 语言中的联合体

目录 引言 一、 联合体的定义与基本用法 1.联合体的定义 2.基本用法 二、 联合体与结构体的区别 1.结构体 2.联合体 3.对比 ​编辑三、联合体的优势 1. 节省内存 2. 提高效率 3. 代码简洁性 四、联合体的存储细节 1.内存对齐 2.大小计算 五、联合体的高级用法…

Windows内核态开发笔记

文章目录 r3/r0通信x64 HOOK回调监控进程强杀minifilterObRegisterCallbacksWFP后记 r3/r0通信 用户态 #include <Windows.h> #include <stdio.h>#define SENDSTR CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) void main() {HANDLE …

Reader

FileInputStream和FileOutputStream其实还叫字节输入流和字节输出流。关于输入和输出这2个总是有点模糊。 以内存为基准&#xff0c;InputStream是文件流向内存&#xff0c;就是从文件中读取数据&#xff0c;又称为输入流。 OutputStream是从内存中流向文件&#xff0c;就是向…

使用s3cmd 2.x 与 Cyberduck 管理在 DigitalOcean Spaces 对象存储中的数据

有很多用户在使用 DigitalOcean Spaces 对象存储的过程中&#xff0c;还没有找到一个合适的数据管理工具。其实目前有很多开源工具都可以使用&#xff0c;比如 s3cmd、Cyberduck、rclone、transmit5。Cyberduck 提供了直观的图形用户界面&#xff0c;而 s3cmd 2.x 则擅长于批处…

怎么搭建AI带货直播间生成虚拟主播?

随着电商直播带货的热潮不断升温&#xff0c;虚拟主播逐渐崭露头角&#xff0c;成为电商直播领域的新宠&#xff0c;相较于真人主播&#xff0c;虚拟主播具备无档期风险、人设稳定可控、24小时不间断直播等显著优势。 本文将深入探讨如何搭建一个AI带货直播间&#xff0c;并详…

最新小猫咪PHP加密系统源码V1.4_本地API接口_带后台

简介&#xff1a; 最新小猫咪PHP加密系统源码V1.4_完全本地化加密API接口_带后台 小猫咪PHP加密系统历时半年&#xff0c;它再一次迎来更新&#xff0c;更新加密算法&#xff08;这应该是最后一次更新加密算法了&#xff0c;以后主要更新都在框架功能上面了&#xff09;&…

在WordPress上启用reCAPTCHA的指南

随着网络安全问题的日益严重&#xff0c;网站管理员必须采取措施保护自己的网站免受恶意攻击。对于WordPress用户来说&#xff0c;可以通过启用谷歌的reCAPTCHA功能来增强网站的安全性。本文将介绍两种在WordPress上启用reCAPTCHA的方法&#xff1a;使用插件和手动添加代码。 一…

Git基础:使用指南

Git是一个开源的分布式版本控制系统&#xff0c;可以有效、高速地处理从很小到非常大的项目版本管理。分布式相比于集中式的最大区别在于开发者可以提交到本地&#xff0c;每个开发者通过克隆&#xff0c;在本地机器上拷贝一个完整的Git仓库。 一、版本管理 1.1 创建版本库 版…