【C++】—— string 模拟实现

【C++】—— string模拟实现

  • 0 前言
  • 1 string的底层结构
  • 2 默认成员函数的实现
    • 2.1 构造函数
      • 2.1.1 无参构造
      • 2.1.2 带参构造
      • 2.1.2 合并
    • 2.2 析构函数
    • 2.3 拷贝构造函数
      • 2.3.1 传统写法
      • 2.3.2 现代写法
    • 2.3 赋值重载
      • 2.3.1 传统写法
      • 2.3.2 现代写法
      • 2.3.3 传统写法与现代写法的优劣
  • 3 size、capacity、operator[] 的实现
  • 4 正向迭代器的模拟实现
    • 4.1 问题引出
    • 4.2 模拟实现
    • 4.3 const 迭代器
    • 4.4 进一步理解封装
  • 5 string 增删查改的模拟实现
    • 5.1 reserve 的模拟实现
    • 5.2 push_back 的模拟实现
    • 5.3 append 的模拟实现
    • 5.4 operator+= 的模拟实现
    • 5.5 insert 的模拟实现
      • 5.5.1 插入一个字符
      • 5.5.2 插入字符串
    • 5.6 erase 的模拟实现
      • 5.6.1 npos 的定义和声明
      • 5.6.2 实现 erase
  • 6 string 查找的模拟实现
    • 6.1 find 的模拟实现
      • 6.1.1 查找字符
      • 6.1.2 查找字符串
    • 6.2 substr 的模拟实现
  • 7 非成员函数的模拟实现
    • 7.1 operator 比较系列
    • 7.2 operator<<
    • 7.3 operator>>
  • 10 完整代码
    • 10.1 string.h
    • 10.2 string.cpp

0 前言

  为了让我们更加深入理解 string,接下来我们将模拟实现一个简易版的 s t r i n g string string。而为了和STL库中的 s t r i n g string string 以示区分,我们将使用命名空间namespace对其封装。
  
  

1 string的底层结构

   s t r i n g string string 简单来说就是一个被封装的可动态增长的数组,其的底层结构我们可以类比顺序表来实现

namespace my_string
{
	class string
	{
	public:
		//···

	private:
		char* _str;//指向字符串的指针
		size_t _size;//有效字符个数(不包括'\0')
		size_t _capacity;//有效字符个数(不包括'\0')
	};
}

  
  

2 默认成员函数的实现

2.1 构造函数

  构造函数,我们可以写一个无参构造的和一个带参构造
  

2.1.1 无参构造

  在学习类与对象时(【C++】—— 类与对象(二)),我们曾讲过成员变量尽量走初始化列表

	string()
		:_str(nullptr)
		,_size(0)
		,_capacity(0)
	{}

  
  但是,库中的 s t r i n g string string,无参时并不是空指针,而是放着一个空字符,我们进行以下修改:

string()
	:_str(new char(''))
	, _size(0)
	, _capacity(0)
{}

  这里,我们开辟一个字节空间放空字符串,也就是 ‘\0’
  因为 _capacity 显示的空间大小是 不包括 ‘\0’ 的,因此 _ c a p a c i t y capacity capacity 显示的空间大小永远比实际开辟的空间小一个。这里开辟了一个空间放 '/0',而 _ c a p a c i t y capacity capacity0
  
  

2.1.2 带参构造

再写个带参的:

string(const char* str)
	:_str(new char[strlen(str) + 1])
	, _size(strlen(str))
	, _capacity(_size)
{
	strcpy(_str, str);//将str中的内容拷贝
}

  上述代码中,要多开辟一个空间,因为 s t r l e n strlen strlen 是不计算 ‘\0’ 的。

  不难发现带参的构造函数用初始化列表初始化并不太好,因为要调用 s t r l e n strlen strlen函数 2 次。即使初始化列表中的 _ s t r str str 和 _ s i z e size size 位置互换也没用,因为初始化列表的初始化顺序是根据成员变量在类中声明的顺序进行初始化的

  这时,我们可以使用函数体初始化,虽然默认也会走初始化列表,但都是内置类型,并不影响。

  初始化列表只是推荐使用,但并不是所有情况都适合

string(const char* str)
{
	_size = strlen(str);
	_capacity = _size;
	_str = new char[_capacity + 1];//实际开辟空间比_capacity多一
	strcpy(_str, str);//将str中的内容拷贝
}

  这里再多提一句,_ c a p a c i t y capacity capacity 不计算 '\0',因此虽然实际开的空间比 _ s i z e size size 多一个,但 _capacity 等于_size
  
  

2.1.2 合并

  其实我们可以将上述无参和带参的构造函数合二为一
  怎么做呢?用一个缺省参数就好啦

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

  
  

2.2 析构函数

  析构函数的实现很简单,只需要把资源释放就行

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

  

2.3 拷贝构造函数

  在学习了前面类与对象中(【C++】—— 类与对象(三)),我们知道:对于有指向资源的类,我们必须要自己实现拷贝构造,实现深拷贝
  

2.3.1 传统写法

  拷贝构造的传统写法很简单,直接调用 s t r c p y strcpy strcpy 函数拷贝就行了

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

  

2.3.2 现代写法

  拷贝构造除了传统的写法,还有一种现代写法
  现代写法的通俗来说就是窃取别人的劳动成果,什么意思呢?我们先来看代码

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

	string(const string& s)
	{
		string tmp(s._str);
		swap(tmp);
	}

  

原理如下:

  • 先用传入的 s s s 对象的字符串构造 t m p tmp tmp 对象(此时调用的是构造函数
  • 再将 * t h i s this this t m p tmp tmp 交换
  • 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦
  • 最后出了函数作用域,交换后的 t m p tmp tmp 调用析构后销毁

s w a p swap swap 交换函数在上一章已讲解,有疑惑的小伙伴可移步至:【C++】—— string 类的了解与使用
  

  可以说,现代写法的活全是构造函数干的,拷贝构造其实是 s w a p swap swap 窃取构造函数的劳动成果,从而达到拷贝构造的目的。

:上述代码还是有缺陷的。因为在交换前 (*this)._str 仅仅是在建立栈帧时开了空间,其本身随机值。交换给 t m p tmp tmp t m p tmp tmp 出了函数作用域会调用析构函数此时 t m p tmp tmp 释放的是一个随机的地址,是个野指针。因此,最好在声明处给 _ s t r str str 一个默认值(缺省值): n u l l p t r nullptr nullptr

namespace my_string
{
	class string
	{
	public:
		//···

	private:
		char* _str = nullptr;
		size_t _size = 0;
		size_t _capacity = 0;
	};
}

  
  

2.3 赋值重载

  赋值重载与拷贝构造相似,我们一起来看看
  

2.3.1 传统写法

string& operator=(const string& s)
{
	if (this != &s)
	{
		delete[] _str;

		_str = new char[s._capacity + 1];
		strcpy(_str, s._str);
		_size = s._size;
		_capacity = s._capacity;
	}
	return *this;
}

  我们知道,赋值会将原来的数据覆盖掉 s t r i n g string string 也应如此
  赋值重载要先将旧空间释放掉,再开辟新空间进行拷贝

:存在自己给自己赋值的情况要单独处理
  
  

2.3.2 现代写法

  与拷贝构造一样,赋值重载也有现代写法,且原理类似

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

string& operator=(const string& s)
{
	if (this != &s)
	{
		string tmp(s._str);
		swap(tmp);
	}
	return *this}

  

  • 先用传入的 s 对象的字符串构造 tmp 对象(此时调用的是构造函数
  • 再将 * t h i s this this t m p tmp tmp 交换
  • 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦

  这种写法不仅仅是交换那么简单,还 t h i s this this 原来的资源交给了 t m p tmp tmp,这样 t m p tmp tmp 出函数作用域还能顺便将 t h i s this this 旧的资源释放掉

  赋值的现代写法还有一个简化版本

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

string& operator=(string tmp)
{		
	swap(tmp);		
	return *this;
}

  这里用了传值传参,程序调用拷贝构造产生形参 tmp t h i s this this 再与 t m p tmp tmp 交换,出了作用域 t m p tmp tmp 销毁
  这里不再判断是否自己给自己赋值是因为:当走入函数体时,拷贝已经完成了,此时再判断已经没意义了。而且即使是自己给自己赋值也不会出问题,只是效率低了点。
  
  

2.3.3 传统写法与现代写法的优劣

  其实,传统写法与现代写法在效率上并没有区别,他们都要去完成拷贝,只是是谁来完成这份工作的问题而已
  
  

3 size、capacity、operator[] 的实现

   s i z e size size c a p a c i t y capacity capacity 的实现很简单,我们直接上代码

size_t size() const
{
	return _size;
}

size_t capacity() const
{
	return _capacity;
}

  
  operator[] 的实现也同样很简单,但需要注意的是 o p e r a t o r operator operator[ ] 的实现要加断言以判断所给的下标是否合法

//普通版本
char& operator[](size_t pos)
{
	assert(pos < _size);
	return _str[pos];
}

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

  
  

4 正向迭代器的模拟实现

4.1 问题引出

  在【C++】—— string 类的了解与使用中,我们知道 s t r i n g string string 的遍历有三种方式:下标访问迭代器访问范围 for

  那范围for那么好用,我们可不可以直接使用呢?

	void test1()
	{
		string s1 = "hello world";
	
		for (auto i : s1)
		{
			cout << i << " ";
		}
	}

在这里插入图片描述

  可以看到,报错了。开始报错的内容很奇怪,说没有 b e g i n begin begin 函数,为什么呢
  我们曾说过,范围for并没有什么神奇的,其底层其实就迭代器
  范围 f o r for for 与迭代器是一种傻瓜式的替换,并不看你实现了什么,只要名字相同就行
  
  下面我们就一起来模拟实现 s t r i n g string string迭代器
  

4.2 模拟实现

  我们知道,迭代器模拟的是指针的行为,而 s t r i n g string string 类的指针 c h a r char char* 完美满足迭代器的各种需求,因此我们直接用指针作为迭代器
  我们直接用原生指针 t y p e d e f typedef typedef 一个 i t e r a t o r iterator iterator 出来

class string
{
public:
	//···
	typedef char* iterator;
	
private:
	//···
};

  
  其实,所有的迭代器 i t e r a t o r iterator iterator 都是 t y p e d e f typedef typedef 出来的,只是其他容器不像 s t r i n g string string 这么简单除暴直接用指针,他们可能是自定义类型,经过封装后再 t y p e d e f typedef typedef i t e r a t o r iterator iterator
  
  我们再把 b e g i n begin begin e n d end end 实现出来

class string
{
public:
	//···
	iterator begin()
	{
		return _str;
	}

	iterator end()
	{
		return _str + _size;
	}
	
private:
	//···
};

  
我们来测试一下:

void test2()
{
	string s1 = "hello world";

	for (auto i : s1)
	{
		cout << i << " ";
	}
	cout << endl;

	string s2 = "ni hao";
	string::iterator it = s2.begin();
	while (it != s2.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

运行结果:

在这里插入图片描述

  可以看到,不仅迭代器访问好了,范围for访问也好了,可见范围 f o r for for 的底层其实就是迭代器
  
  

4.3 const 迭代器

  当然,还有 const迭代器

class string
{
public:
	//···
	typedef const char* const_iterator;

	const_iterator begin() const
	{
		return _str;
	}
	
	const_iterator end() const
	{
		return _str + _size;
	}
	
private:
	//···
};

  
  

4.4 进一步理解封装

  迭代器的设计本质是一种 封装的体现

  我们之前理解的封装是这样的:
  我把数据方法都放到类里面,都封装起来,不能让你随便修改。想让你访问的设置成公有,不想让你访问的设置成私有

  而迭代器的设计也是一种封装
  有各种各样的容器,它们底层可能是顺序表、链表甚至可能是树。现在我不管你底层结构是什么,差别有多大,我都用一个叫 i t e r a t o r iterator iterator 的东西来进行访问
  iterator 属于类域,它是个什么类型我不知道。现在你封装以后给我提供一个 b e g i n begin begin,那我就知道无论你是什么容器,这就是指向第一个有效数据位置 e n d end end 就是最后一个数据的下一个位置。我要访问就解引用,我要访问下一个位置就 ++
  这样,我们就不需要再关心这个数据结构到底是顺序表还是链表还是树,iterator 屏蔽了底层的结构和实现细节,它提供了统一的接口,我们可以对不同的数据结果用类似的方式进行访问
  
  

5 string 增删查改的模拟实现

  上面所实现的函数,大多都是一些比较短小的函数,又或是需要频繁调用的函数,我们将他们放在类中,以成内联函数提高程序的效率。下面的大部分函数,我们可以对其进行进行声明和定义的分离
  

5.1 reserve 的模拟实现

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

  我们这里实现的 reserve 只扩容,不缩容,因此要先判断 n 是否 > _capacity

   r e s e r v e reserve reserve 的整体实现思路如下:开辟更大的新空间 -> 将就空间数据拷贝至新空间 -> 释放就空间

  • char* tmp = new char[n + 1];这里要记住,实际开空间要多开一个空间,为 ‘\0’ 留位置
  • strcpy(tmp, _str);拷贝数据
  • delete[] _str;释放旧空间
  • _str = tmp;指向新空间
  • _capacity = n;更新 _ c a p a c i t y capacity capacity

  
  

5.2 push_back 的模拟实现

void string::push_back(char ch)
{
	if (_size == _capacity)
	{
		size_t n = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(n);
	}

	_str[_size++] = ch;
	_str[_size] = '\0';
}

  既然是插入数据,那么一定要判断是否需要扩容
  这里,我们调用 r e s e r v e reserve reserve 来完成扩容:如果 _ c a p a c i t y capacity capacity 为 0,则开 4 个空间,否则扩容两倍
  括完容就简单啦,在最后的位置插入 c h ch ch,并将 _ s i z e size size++,最后,千万别忘了要在最后补上 ‘\0’

  
  

5.3 append 的模拟实现

void string::append(const char* str)
{
	size_t len = strlen(str);
	if (len + _size > _capacity)
	{
		size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
		reserve(n);
	}

	strcpy(_str + _size, str);
	_size += len;
}

   a p p e n d append append 同样需要判断是否需要扩容:当 _size + 新字符串长度 > _capacity 则表示需要扩容

  这里有一个问题:括多大呢?如果_size + len 比扩容 2 倍后的空间还大怎么办呢
  因此,这里要进行一下判断,如果比 2 倍扩容后的空间小,则正常 2 倍扩容,反之扩容_capacity + len 的大小

  
  

5.4 operator+= 的模拟实现

  上面实现了 p u s h push push_ b a c k back back a p p e n d append append,那么 o p e r a t o r operator operator+= 的实现就很简单啦,只需要直接复用他们就行了

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

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

  
  

5.5 insert 的模拟实现

5.5.1 插入一个字符

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

	if (_size == _capacity)
	{
		size_t n = _capacity == 0 ? 4 : 2 * _capacity;
		reserve(n);
	}

	size_t i = _size;
	while (i >= pos)
	{
		_str[i + 1] = _str[i];
		--i;
	}

	_str[pos] = ch;
	++_size;
}
  • assert(pos <= _size);首先要判断下标合不合法
  • if (_size == _capacity)既然要插入数据,那么一定要判断扩不扩容
  • 再接着便是挪动数据,这里我们选择从后往前挪动。如果是从前往后,会造先挪动的数据会覆盖后挪动的数据

  那么上述代码有问题吗?
  有!头插的时候会造成死循环!

  为什么呢?因为 i i i s i z e size size_ t t t 类型,当头插时, p o s pos pos 为 0 。挪动最后一个数据后: i i i 从 0 减1;可是 i i i s i z e size size_ t t t 类型,再 -1 得到的是一个很大的数,后继续不断减,但永远大于等于0,程序死循环。
  
  那把 i i i 改为 i n t int int 类型可不可以呢?

int i = _size;
while (i >= pos)
{
	_str[i + 1] = _str[i];
	--i;
}

  还是不可以,因为while (i >= pos)语句发生了类型转换
  C语言规定,当操作符两边操作数类型不同时,类型小的向范围大的去提升。(详情请看:【C语言】——详解操作符(下))
  上述情况就是 i n t int int 类型的 i i i 提升成了 s i z e size size_ t t t 类型,因此还是无法解决问题。
  
解决方法有多种,这里简单列举几个

//法一:将pos强转成int
int i = _size;
while (i >= (int)pos)
{
	_str[i + 1] = _str[i];
	--i;
}

//法三:单独判断 i == npos(-1) 的情况
size_t i = _size;
while (i >= pos && i != npos)
{
	_str[i + 1] = _str[i];
	--i;
}

//法三:修改 i 的起始位置
size_t i = _size + 1;
while(i > pos)
{
	_str[i] = _str[i - 1];
	--i;
}

  
  

5.5.2 插入字符串

void string::insert(size_t pos, const char* str)
{
	assert(pos <= _size);

	size_t len = strlen(str);
	if (_size + len > _capacity)
	{
		size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
		reserve(n);
	}

	size_t i = _size;
	while (i >= pos && i != npos)
	{
		_str[i + len] = _str[i];
		--i;
	}

	memcpy(_str + pos, str, sizeof(char) * len);
	_size += len;
}

  插入字符串与前面的情况很类似,前面步骤都是断言->扩容->挪数据

  不同的是,挪动数据是向后挪动 len 个位置
  最后是拷贝数据,拷贝数据可以直接 m e m c p y memcpy memcpy 拷贝。注意,这里不能用 s t r c p y strcpy strcpy,因为 s t r c p y strcpy strcpy 会额外多拷贝一个 '\0',这样不仅会覆盖掉一个数据,并且插入字符串之后的数据也不可见
  
  当然,我们也可以自己实现拷贝

for (size_t i = 0; i < len; i++)
{
	_str[pos + i] = s[i];
}

  
  

5.6 erase 的模拟实现

5.6.1 npos 的定义和声明

  不了解 n o p s nops nops 的小伙伴可以移步:【C++】—— string 类的了解与使用 来学习

  我们知道, n o p s nops nops s t r i n g string string 中的一个静态常量成员
  它在 s t r i n g string string 类中的声明如下

class string
{
public:
	//···
	
private:
	//···
	static const size_t npos;
};

  
  但需要注意的是,它不能string.h文件中进行定义,像这样:

string.h

class string
{
	//···
};

const size_t string::npos = -1;

  因为 . h h h 文件会被多个 . c p p cpp cpp 文件包含 ,被几个 .cpp文件 包含就意味着 npos 被定义了几次,而一个变量规定只能定义一次

  因此, n p o s npos npos 应在 string.cpp文件 中定义

string.cpp

namespace my_string
{
	const size_t string::npos = -1;

	void erase(size_t pos, size_t n)
	{
		//···
	}

	//···
};

  
  但是,有一个特殊情况:对于 n p o s npos npos,可以在声明时直接给缺省值

class string
{
public:
	//···
	
private:
	//···
	static const size_t npos = -1;
};

  虽然但是,还是不建议这种写法。
  因为静态变量是不走初始化列表的,因此理论上是不能在声明时给缺省值的
  只有 static + const + 整型才可以, d o u b l e double double 都不行,我们可以认为编译器特殊处理了。

  
  

5.6.2 实现 erase

void string::erase(size_t pos, size_t len)
{
	assert(pos < _size);

	if (npos == len || pos + len >= _size)
	{
		_str[pos] = '\0';
		_size = pos;
	}

	else
	{
		for (size_t i = pos + len; i <= _size; ++i)
		{
			_str[i - len] = _str[i];
		}
		_size -= len;
	}
}

   e r a s e erase erase 的数据删除分为两种情况:一种是 p o s pos pos 之后的数据全部删除;一种是删除部分数据
  当len == npospos + len > _size,则表明之后的数据全部删除。此时只需在 p o s pos pos 位置改为 ‘\0’ 并调整 _ s i z e size size 即可
  另一种情况就是挪动数据,这次是从前往后挪,并调整 _ s i z e size size
  
  

6 string 查找的模拟实现

6.1 find 的模拟实现

6.1.1 查找字符

size_t string::find(char ch, size_t pos)
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; ++i)
	{
		if (_str[i] == ch)
		{
			return i;
		}
	}
	return npos;
}

  这个比较简单,大家直接看代码即可,需要注意的是如果没找到记得返回 n p o s npos npos
  
  

6.1.2 查找字符串

size_t string::find(const char* str, size_t pos)
{
	assert(pos < _size);

	char* p = strstr(_str + pos, str);
	if (nullptr == p)
	{
		return npos;
	}
	return p - _str;
}

  找子串的问题可以用暴力匹配的算法,也可以用 kmp算法,更好的有 BM算法,大家可以自行去学习。这里就不过多介绍,我们这里直接复用 C语言 中的 s t r s t r strstr strstr 函数。( s t r s t r strstr strstr 的模拟实现可以移步:【C语言】——字符串函数的使用与模拟实现(下))

  这里需要注意,当找不到目标字符串时, s t r s t r strstr strstr 返回的是 n u l l p t r nullptr nullptr,这种情况我们要单独处理
  
  

6.2 substr 的模拟实现

string string::substr(size_t pos, size_t len)
{
	assert(pos < _size);

	if (pos + len > _size || len == npos)
	{
		len = _size - pos;
	}

	string sub;
	sub.reserve(len);

	for (size_t i = pos; i < pos + len; ++i)
	{
		sub += _str[i];
	}

	return sub;
}

  首先,我们先把 l e n len len 更新一下,如果 pos + len > _size,那么 len 就是 _size - pos
  剩下的就非常简单啦,创建一个新的对象,并提前预留空间,最后再一个一个字符 += ( p u s h push push_ b a c k back back)
  

:这里一定要先写好拷贝构造,当我们没有自己实现拷贝构造时,编译器默认生成的是浅拷贝,会出大问题。

string s1 = "hello world";
string s2 = s1.substr(6);
cout << s2.c_str() << endl;

  上述 s u b s t r substr substr 用的是传值返回,传值返回会调用拷贝构造来创建一个临时对象,临时对象再调用一次拷贝构造来拷贝给上 s 2 s2 s2.

  如果我们没实现拷贝构造,那么编译器实现的是浅拷贝。 s 2 s2 s2 s u b sub sub 指向的是同一块空间 s u b sub sub 出了作用域后会销毁,它所开辟的空间也跟着释放 s 2 s2 s2 指向一块已经被释放的空间,要是再调用析构函数,自然会出问题。

在这里插入图片描述

  不过在新一点的编译器可能不会出现问题,如:VS2022。
  编译器一般都会进行优化,在进行传值拷贝构造时,编译器可能不会进行 s u b sub sub 对象的创建,直接用临时对象拷贝构造 s 2 s2 s2
  而在 VS2022 等较新的编译器,优化更为激进,直接合三为一 s u b sub sub 和临时对象都不创建了,直接拷贝构造 s 2 s2 s2!所以也就不存在同一块空间被多次析构的问题了!关于编译器的优化,感兴趣的小伙伴可以移步:【C++】—— 类与对象(五)
  
  

7 非成员函数的模拟实现

7.1 operator 比较系列

  为什么 s t r i n g string string类中, o p e r a t o r operator operator 比较系列要实现成全局函数呢?

  因为在类中实现,那第一个参数必须是 this指针,那么 s t r i n g string string 永远只能放在左边。库中想 string与字符串比较字符串与string都实现,因此将 operator比较系列放在类外

   o p e r a t o r operator operator 比较系列比较简单,我们直接看代码就好

bool operator<(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) < 0;
}

bool operator<=(const string& s1, const string& s2)
{
	return s1 < s2 || s1 == s2;
}

bool operator>(const string& s1, const string& s2)
{
	return !(s1 <= s2);
}

bool operator>=(const string& s1, const string& s2)
{
	return !(s1 < s2);
}

bool operator==(const string& s1, const string& s2)
{
	return strcmp(s1.c_str(), s2.c_str()) == 0;
}

bool operator==(const string& s1, const string& s2)
{
	return !(s1 == s2);
}

  6 个比较函数,只需任意实现其中两个,另外4个函数都可以通过服用得到
  
  

7.2 operator<<

  流插入的实现和简单,遍历一遍就好了

ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}

:这里ostream& out不能加 c o n s t const const,因为这是流插入,要往 o u t out out 里面去写,out是要进行修改的,加上 c o n s t const const 就不能修改了。
  
  

7.3 operator>>

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch;
	in >> ch;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		in >> ch;
	}

	return in;
}

:这里string& s前不能加 c o n s t const const,因为是流提取提取出来的字符是放到 s s s 中,是要对 s 进行改变的

  上述代码先将string s清空,后不断提取缓冲区的字符到变量 c h ch ch中,直到出现 ' ''\n'
  但我们检验一下就知道,这个代码是有问题的:因为流提取读到' ''\n'默认将他们跳过(流提取将他们当做是字符串与字符串之间的分隔符,因此跳过他们) s c a n f scanf scanf 也是如此,因此程序会一直让你输入,无法停止
  
  我们可以用 g e t get get 函数 g e t get get 函数不管你是什么字符,都会给你读进来,与 C语言 中的 g e t c getc getc函数 类似

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch;
	ch = in.get(;
	while (ch != ' ' && ch != '\n')
	{
		s += ch;
		ch = in.get();
	}

	return in;
}

  但上述代码还有个缺点,因为是一个字符一个字符地插入,因此需要频繁的扩容

  怎么解决呢?
  我们可以给一个大小合适的 b u f f buff buff 数组,现在每次来的字符先不往 s t r i n g string string 进行 +=,先放到 buff 数组里面, b u f f buff buff 满了,再一次性放入 s t r i n g string string 里面。当遇到' ''\0',再将buff数组中剩下的字符放入 s t r i n g string string 中。

istream& operator>>(istream& in, string& s)
{
	s.clear();

	char ch;
	ch = in.get();

	const int N = 256;
	char buff[N];
	int i = 0;
	
	while (ch != ' ' && ch != '\0')
	{
		
		buff[i++] = ch;
		
		if (i == N - 1)
		{
			buff[i] = '\0';
			s += buff;
			i = 0;
		}

		ch = in.get();
	}

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

  
  

10 完整代码

10.1 string.h

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

namespace my_string
{
	class string
	{
	public:
		friend ostream& operator<<(ostream& out, const string& s);
		friend istream& operator>>(const istream& in, string& s);

		string(const char* str = "")
		{
			_size = strlen(str);
			_capacity = _size;
			_str = new char[_capacity + 1];
			strcpy(_str, str);//将str中的内容拷贝
		}

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

		string(const string& s)
		{
			string tmp(s._str);
			swap(tmp);
		}

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

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

		string& operator=(string tmp)
		{
			swap(tmp);
			return *this;
		}

		size_t size() const
		{
			return _size;
		}

		size_t capacity() const
		{
			return _capacity;
		}

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

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

		typedef char* iterator;

		iterator begin()
		{
			return _str;
		}

		iterator end()
		{
			return _str + _size;
		}

		typedef const char* const_iterator;

		const_iterator begin() const
		{
			return _str;
		}

		const_iterator end() const
		{
			return _str + _size;
		}

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

		void reserve(size_t n);
		void push_back(char ch);
		void append(const char* str);
		string& operator+=(char ch);
		string& operator+=(const char* str);

		void insert(size_t pos, char ch);
		void insert(size_t pos, const char* str);
		void erase(size_t pos, size_t len = npos);

		size_t find(char ch, size_t pos = 0);
		size_t find(const char* str, size_t pos = 0);
		string substr(size_t pos = 0, size_t len = npos);


		static const size_t npos;

	private:
		char* _str = nullptr;//指向字符串的指针
		size_t _size = 0;//有效字符个数(不包括'\0')
		size_t _capacity = 0;//有效字符个数(不包括'\0')
	};

	bool operator<(const string& s1, const string& s2);
	bool operator<=(const string& s1, const string& s2);
	bool operator>(const string& s1, const string& s2);
	bool operator>=(const string& s1, const string& s2);
	bool operator==(const string& s1, const string& s2);
	bool operator!=(const string& s1, const string& s2);

	ostream& operator<<(ostream& out, const string& s);
	istream& operator>>(istream& in, string& s);
	
}

10.2 string.cpp

#include"string.h"

namespace my_string
{
	const size_t string::npos = -1;

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

	void string::push_back(char ch)
	{
		if (_size == _capacity)
		{
			size_t n = _capacity == 0 ? 4 : 2 * _capacity;
			reserve(n);
		}

		_str[_size++] = ch;
		_str[_size] = '\0';
	}

	void string::append(const char* str)
	{
		size_t len = strlen(str);
		if (len + _size > _capacity)
		{
			size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
			reserve(n);
		}

		strcpy(_str + _size, str);
		_size += len;
	}

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

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

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

		if (_size == _capacity)
		{
			size_t n = _capacity == 0 ? 4 : 2 * _capacity;
			reserve(n);
		}

		size_t i = _size;
		while (i >= pos && i != npos)
		{
			_str[i + 1] = _str[i];
			--i;
		}

		_str[pos] = ch;
		++_size;
	}

	void string::insert(size_t pos, const char* str)
	{
		assert(pos <= _size);

		size_t len = strlen(str);
		if (_size + len > _capacity)
		{
			size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
			reserve(n);
		}

		size_t i = _size;
		while (i >= pos && i != npos)
		{
			_str[i + len] = _str[i];
			--i;
		}

		memcpy(_str + pos, str, sizeof(char) * len);
		_size += len;
	}

	void string::erase(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (npos == len || pos + len >= _size)
		{
			_str[pos] = '\0';
			_size = pos;
		}

		else
		{
			for (size_t i = pos + len; i <= _size; ++i)
			{
				_str[i - len] = _str[i];
			}
			_size -= len;
		}
	}


	size_t string::find(char ch, size_t pos)
	{
		assert(pos < _size);
		for (size_t i = pos; i < _size; ++i)
		{
			if (_str[i] == ch)
			{
				return i;
			}
		}
		return npos;
	}

	size_t string::find(const char* str, size_t pos)
	{
		assert(pos < _size);

		char* p = strstr(_str + pos, str);
		
		if (nullptr == p)
		{
			return npos;
		}
		return p - _str;
	}

	string string::substr(size_t pos, size_t len)
	{
		assert(pos < _size);

		if (pos + len > _size || len == npos)
		{
			len = _size - pos;
		}

		string sub;
		sub.reserve(len);

		for (size_t i = pos; i < pos + len; ++i)
		{
			sub += _str[i];
		}

		return sub;
	}

	bool operator<(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) < 0;
	}

	bool operator<=(const string& s1, const string& s2)
	{
		return s1 < s2 || s1 == s2;
	}

	bool operator>(const string& s1, const string& s2)
	{
		return !(s1 <= s2);
	}

	bool operator>=(const string& s1, const string& s2)
	{
		return !(s1 < s2);
	}

	bool operator==(const string& s1, const string& s2)
	{
		return strcmp(s1.c_str(), s2.c_str()) == 0;
	}

	bool operator!=(const string& s1, const string& s2)
	{
		return !(s1 == s2);
	}

	ostream& operator<<(ostream& out, const string& s)
	{
		for (auto ch : s)
		{
			out << ch;
		}
		return out;
	}

	istream& operator>>(istream& in, string& s)
	{
		s.clear();

		char ch;
		ch = in.get();

		const int N = 256;
		char buff[N];
		int i = 0;
		
		while (ch != ' ' && ch != '\0')
		{
			
			buff[i++] = ch;
			
			if (i == N - 1)
			{
				buff[i] = '\0';
				s += buff;
				i = 0;
			}

			ch = in.get();
		}

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Bootstrap 字体图标无法显示问题,<i>标签字体图标无法显示问题

bootstrap fileInput 以及 Bootstrap 字体图标无法显示问题。 今天在用 bootstrap fileInput 插件的时候发现图标无法显示&#xff0c;如下&#xff1a; 查看DOM&#xff0c;发现那些图标是<i>标签做的&#xff1a; 网上的方案 方案1 网上很多人说是我们打乱了boots…

每日OJ_牛客_走迷宫(简单bfs)

目录 牛客_走迷宫&#xff08;简单bfs&#xff09; 解析代码&#xff1a; 牛客_走迷宫&#xff08;简单bfs&#xff09; 走迷宫__牛客网 解析代码&#xff1a; 采用一个二维数组&#xff0c;不断的接受迷宫地图(因为有多个地图)&#xff0c;获取到迷宫地图后&#xff0c;采…

信号有效带宽

根据傅里叶变换可以知道信号带宽是无穷大的&#xff0c;这对实际应用是帮助不大的&#xff0c;所以有了有效带宽的概念&#xff0c;可能大家知道常用的经验公式&#xff1a;O.35/Tr或者0.5/Tr等&#xff0c;那这个公式是怎么来的呢&#xff1f;有效带宽又是什么含义呢&#xff…

Go Web 编程 PDF

&#x1f4da; Go Web开发必读:《Building Web Applications with Go》PDF资源分享 &#x1f50d; 找寻良久,终于寻得这本珍贵资源!现在我免费分享给大家 你是否正在学习Go语言开发Web应用?是否想要提升Go并发编程能力?这本书绝对不容错过! &#x1f4d6; 关于这本书 《B…

短信PHP接口平台可以为企业带来哪些优势

短信验证码在我们的日常生活中可以说是无处不在&#xff0c;并且短信验证码目前在市场中已经得到了广泛的使用&#xff0c;这种验证方法可以保证注册人事实名认证&#xff0c;并且可以防止恶意注册&#xff0c;不过也有人觉得短信验证码有一些累赘&#xff0c;那么短信验证码真…

OpenCV中的颜色映射函数applyColorMap的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 OpenCV 中应用类似于 GNU Octave 或 MATLAB 中的颜色映射&#xff0c;虽然 OpenCV 中的颜色映射类型与 GNU Octave 或 MATLAB 中的颜色映射类型名…

深度学习——基于MTCNN算法实现人脸侦测

这里写目录标题 先看效果 MTCNN主体思想级联网络图像金字塔IOU算法iou 公式 nms 算法数据生成celeba 数据代码训练代码侦测代码总结 先看效果 MTCNN 从2016年&#xff0c;MTCNN算法出来之后&#xff0c;属实在工业上火了一把&#xff0c;最近尝试着把论文代码复现了一下。 主…

mybatis特殊符号处理,mybatis一级二级缓存,java反射机制

mybatis特殊符号处理 在 mybatis 中的 xml 文件中&#xff0c;存在一些特殊的符号&#xff0c;比如&#xff1a;<、>、"、&、<>等&#xff0c;正常书写 mybatis 会报错&#xff0c;需要对这些符号进行转义。具体转义如下所示&#xff1a; 特殊字符 转义字…

Python爬虫使用实例-漫kzhan

环境配置 pip install shutil parsel pillow pypdf1/ 单个章节 singleChapter 需要获取参数&#xff1a;chapter_id与comic_id&#xff0c;可能要sign和uid 获取请求地址 urlhttps://comic.mkzhan.com/chapter/content/v1/ # 请求地址获取请求参数 data{chapter_id:499…

Linux下安装MySQL8.0

一、安装 1.下载安装包 先创建一个mysql目录&#xff0c;在将压缩包下载到此 # 下载tar包 wget https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.20-linux-glibc2.12-x86_64.tar.xz等待下载成功 2.解压mysql8.0安装包 tar xvJf mysql-8.0.20-linux-glibc2.12-x86…

java编辑器——IntelliJ IDEA

java编辑器有两种选择——IntelliJ IDEA和VsCode。其中IntelliJ IDEA现在是企业用的比较多的&#xff0c;是专门为java设计的&#xff0c;而VsCode则是通过插件来实现Java编辑的。 1.IntelliJ IDEA 官网下载链接&#xff1a;https://www.jetbrains.com/idea/ 注意选择社区版…

STM32F103调试DMA+PWM 实现占空比逐渐增加的软启效果

实现效果&#xff1a;DMAPWM 实现PWM输出时&#xff0c;从低电平到输出占空比逐渐增加再到保持高电平的效果&#xff0c;达到控制 MOS 功率开关软启的效果。 1.配置时钟 2.TIM 的 PWM 功能配置 选择、配置 TIM 注意&#xff1a;选择 TIM 支持 DMA 控制输出 PWM 功能的通道&a…

【Python报错已解决】`AttributeError: move_to requires a WebElement`

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏: 《C干货基地》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 引言&#xff1a;一、问题描述&#xff1a;1.1 报错示例&#xff1a;1.2 报错分析&#xff1a;1.3 解决思路&#xff…

当网工,华为认证哪种适合我?四个维度来解惑

随着网络技术的不断进步&#xff0c;对网工的专业技能要求也越来越高。 在这种背景下&#xff0c;获得权威认证成为了提升个人技能、证明专业能力的重要途径。 华为&#xff0c;作为全球领先的ICT解决方案提供商&#xff0c;其认证项目在业界享有极高的声誉。 华为认证不仅涵盖…

Day22_K8S

文章目录 3.资源管理方式通过命令管理通过配置文件管理4. 基本概念入门4.1 Namespace4.2 Pod4.3 Label4.4 Deployment4.5 Service5. Pod详解5.1 Pod介绍5.2 Pod配置5.3 Pod生命周期5.3.1 初始化容器5.3.2 钩子函数5.3.3 容器探测5.3.4 重启策略5.4 Pod调度5.4.1 定向调度5.4.2 …

dubbo 服务消费原理分析之引用服务配置

文章目录 前言一、服务监听ContextRefreshedEvent1、AbstractApplicationContext.refresh2、AbstractApplicationContext.finishRefresh3、DubboDeployApplicationListener.onApplicationEvent4、DefaultModuleDeployer .referServices5、SimpleReferenceCache.get 二、引用服务…

WPF性能优化之UI虚拟化

文章目录 前言一、VirtualizingStackPanel1.1 虚拟化功能介绍1、在Window中添加一个ListBox控件。2、在设计视图中用鼠标选中ListBox控件并右健依次单击“编辑其他模板”&#xff0d;“编辑项的布局模板”&#xff0d;“编辑副本”。3、查看生成的模板代码。 1.2 虚拟化参数介绍…

基于free5gc模拟5G核心网和UERANSIM模拟5G用户设备的模拟5G网络环境的部署搭建方法总结和解析。

简介&#xff1a;下文介绍了基于free5gc模拟5G核心网和UERANSIM模拟5G用户设备的模拟5G网络环境的部署搭建方法&#xff0c;并且通过完成相关配置&#xff0c; 简单实现测试5G用户设备接入核心网的注册流程、去注册流程&#xff0c;模拟了一个用户设备通过模拟核心网进行实际的…

Python输出多位数

作者制作不易&#xff0c;关注、点赞、收藏一下吧&#xff01; 1.第一种:正常直接用循环 以三位数为例: for i in range(100, 1000):print(i) 运行结果( 展示一部分 ): 图1-1 2.第二种:特定位数 以三位数为例: for i in range(1, 5): # 括号内指定那个位的范围for j in r…

STM32F4按键状态机--单击、双击、长按

STM32F4按键状态机--单击、双击、长按 一、状态机的三要素二、使用状态机原因2.1资源占用方面2.2 执行效率方面&#xff1a;2.3 按键抖动方面&#xff1a; 三、状态机实现3.1 状态机分析3.1 程序实现 百度解析的状态机概念如下 状态机由状态寄存器和组合逻辑电路构成&#xff0…