【STL学习】(2)string的模拟实现

前言

本文将模拟实现string的一些常见功能,目的在于加深理解string与回顾类与对象的相关知识。

一、前置知识

  1. string是表示可变长的字符序列的类
  2. string的底层是使用动态顺序表存储的
  3. string对象不以’\0’字符为终止算长度,而是以size有效字符的个数算长度
  4. 为了兼容C,所以string对象在最后追加的一个’\0’字符,但是这个’\0’字符不属于string对象的有效字符
  5. 建议在模拟实现之前熟悉string的常用接口,并且查看文档。

二、string常用接口的模拟实现

1、string的成员变量

//我们模拟实现的string,将其封装在wjs的命名空间中,与库中的string区别开
namespace wjs
{
	class string
	{
		//成员变量
	private:
		size_t _size;//有效字符个数
		size_t _capacity;//存储有效字符的空间容量,注不包含'\0'
		char* _str;//指向堆申请的连续空间

		//静态成员变量
	public:
		const static size_t npos;
		//了解:const整数类型的静态成员可以在类内部初始化
		//static const size_t npos = -1;
	};
	//静态成员变量在类外部定义
	const size_t string::npos = -1;
}

tip:

  1. 命名空间
    • 作用:使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突或命名污染
    • 定义:namespace后面跟命名空间名字,然后接一对{}即可,{}中即为命名空间的成员
  2. 静态成员变量:
    • 静态成员属于类为所有类对象共享,不属于某个具体的对象,存放在静态区
    • 静态成员也是类的成员,受访问限定符的限制
    • 一般静态成员变量不能在声明时给缺省值,因为缺省值是给初始化列表使用的,初始化列表是初始化对象的成员变量,而静态成员变量不属于类的任何一个对象
    • 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。常与静态成员变量配套使用
    • 在类外部定义静态成员
    • 静态成员只要能突破类域和访问限定符就可以访问
  3. 了解:const整数类型的静态成员变量可以在类内部初始化,但是不建议。
  4. string的npos:
    • size_t npos = -1,表示该类型的最大值
    • 当string成员函数的参数的缺省值为npos时,表示“直到字符串结束”
    • 当npos作为返回值时,一般表示没有匹配(例如:find)

2、构造函数

string类常用的构造函数有:

  1. string():默认构造函数,构造一个空的string对象,即空字符串。
  2. string(const char* str):用C格式字符串来构造一个string对象。
//默认构造函数——即可以传参构造,也可以使用缺省值构造
string(const char* str = "")
	//注意:初始化列表按照类中声明次序初始化,建议不要修改顺序,易错点!
	:_size(strlen(str)),
	_capacity(_size),
	_str(new char[_capacity + 1])//C字符串后默认以'\0'结束,为了兼容C所以要多开一个空间,保存'\0'
{
	//上面的new只是开了空间,所以需要把str拷贝到_str中
	//注意:strcpy拷贝到'\0'才结束
	strcpy(_str, str);
}

tip:

  1. 默认构造函数:
    • 三种默认构造函数:无参构造函数、全缺省构造函数、编译器默认生成的构造函数
    • 注意:默认构造函数只能存在一个——虽然语法上可以同时存在,但是无参调用时存在歧义,所以默认构造函数只能有一个
    • 推荐使用全缺省默认构造函数——优点:即可以不传参使用缺省值初始化对象,也可以传参自己初始化对象
    • 不传参就可以调用的就是默认构造函数
  2. 给成员变量赋初值的方式有:
    • 使用初始化列表
    • 使用构造函数的函数体
    • 建议给成员变量赋初值都使用初始化列表,因为初始化列表是成员变量定义的地方
    • 构造函数体与初始化列表结合使用,因为总有一些事情是初始化列表不能完成的。
  3. 初始化列表:
    • 初始化列表是成员变量定义的地方,所以每一个成员变量在初始化列表中最多只能出现一次
    • 引用成员变量、const成员变量、没有默认构造函数的自定义成员,必须在初始化列表初始化(因为这三种成员变量有一个共同特征,在定义时就必须初始化)
    • 注意: 成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
  4. string的底层存储是使用动态顺序表实现
    • 在C++中使用new和delete操作符进行动态内存管理
    • 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:配合使用
  5. C字符串后默认以’\0’结束,为了兼容C所以string要多开一个空间,保存’\0’

3、析构函数

//析构函数——有动态申请资源,需要显示实现析构释放资源
~string()
{
	delete[] _str;
	_str = nullptr;//delete与free一样释放完了,不会改变_str,所以将其置为空
	_size = _capacity = 0;
}

tip:

  1. 一般当对象涉及动态申请资源,就需要显示实现析构函数
  2. delete释放完空间之后与free一样,不会改变指向动态空间的指针变量,有危险,建议释放完之后将其置为空

4、拷贝构造函数

  • 拷贝构造的浅拷贝 按字节序完成拷贝。

在这里插入图片描述
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成两个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象。

所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。

  • 拷贝构造深拷贝 有自己的空间。

在这里插入图片描述
深拷贝是把值拷贝到自己的空间。

深浅拷贝就像我们平时考试时抄别人的作业,浅拷贝——名字、学号都抄别人的,深拷贝——知道修改名字、学号。

编译器默认生成的拷贝构造只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。

拷贝构造的深拷贝的两种实现:

  1. 传统写法:自己开空间,自己拷贝
  2. 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们。

传统写法:

//传统写法——自己开空间,自己拷贝
string(const string& s)//参数只有一个且必须是类类型对象的引用
{
	//自己开空间
	_str = new char[s._capacity + 1];
	//自己拷贝
	_size = s._size;
	_capacity = s._capacity;
	//不能使用strcmp拷贝,因为strcmp到'\0'结束,但是string不是以'\0'结束的
	//例如:hello\0xxx
	memcpy(_str, s._str, s._size + 1);
}

现代写法:

//方式2:现代写法——先叫别人帮我们开好空间,拷贝好了,再把数据给我们。
//拷贝构造的现代写法对于hello\0xxxx的string有bug,所以我们使用传统写法
string(const string& s)//参数只有一个且必须是类类型对象的引用
	//注意:C++并没有规定对内置类型进行初始化,所以我们需要将其初始化
	:_size(0),
	_capacity(0),
	_str(nullptr)
{
	string tmp(s._str);//缺点:string不以'\0'字符结束
	swap(tmp);
}

tip:

  1. string不以’\0’字符为终止,所以拷贝构造的现代写法有bug
  2. 所以string的拷贝构造,我们不使用现代写法,使用传统写法

5、operator=赋值重载

  • 赋值重载的浅拷贝 按字节序完成拷贝。

在这里插入图片描述
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成三个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象;③旧空间没释放,造成内存泄漏。

所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。

  • 赋值重载的深拷贝 有自己的空间且要释放旧空间。

在这里插入图片描述
深拷贝是把值拷贝到自己的空间,并且将旧空间释放。

编译器默认生成的赋值重载只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。

赋值重载的深拷贝的两种实现:

  1. 传统写法:自己开空间,自己拷贝,自己释放旧空间。
  2. 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们,最后的旧空间也由别人帮我们释放。

传统写法:

//传统写法:自己开空间,自己拷贝,自己释放旧空间。
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;
		_capacity = s._capacity;
		_size = s._size;
	}
	return *this;
}

现代写法:

//现代写法:全部叫别人做,然后交给自己
//string& operator=(const string& s)
//{
//	if (this != &s)
//	{
//		string tmp(s);
//		swap(tmp);
//		//std::swap(tmp, *this);//swap的内部实现调用了operator=,所以会造成无限递归
//	}
//	return *this;
//}
//现代写法:进一步叫别人做,直接从参数开始
string& operator=(string tmp)
{
	swap(tmp);
	return *this;
}

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

tip:

  1. 现代写法:就是别人做好了,我们交换拿结果就可以了。

6、c_str成员函数获取C格式字符串

//获取C格式字符串
const char* c_str()const//内部不改变成员变量,建议加上const
{
	return _str;
}

tip:

  1. const成员
    • 将const修饰的成员函数称为const成员函数
    • 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用
  2. C格式字符串与string:
    • C格式字符串不是一种类型,string是表示字符序列的一个类
    • C字符串以’\0’字符为终止算长度,string不以’\0’字符为终止,以size为终止算长度(说明:C++为了兼容C,所以string对象的最后都追加了一个’\0’字符,但是这个’\0’不属于string对象的元素)

7、size成员函数获取string的长度

//获取string的长度
size_t size()const//内部不改变成员变量,建议加上const
{
	return _size;
}

tip:

  1. string的长度,即有效字符个数。
  2. 注意:string不以’\0’字符为终止算长度,是以有效字符的个数算长度。

8、capacity成员函数获取string当前空间容量

//获取string的当前空间容量
size_t capacity()const//内部不改变成员变量,建议加上const
{
	return _capacity;
}

tip:

  1. 说明:capacity不一定等于string的长度。他可以大于或等于。

9、reserve成员函数申请n个字符的空间容量

//申请n个字符的空间容量——只会改变容量,不会改变长度
void reserve(size_t n = 0)
{
	//如果n大于当前string容量,则按需申请n个字符的空间容量
	if (n > _capacity)
	{
		//避免申请失败,先使用一个临时变量保存
		char* tmp = new char[n + 1];
		//不能使用strcmp拷贝,因为strcmp到'\0'结束,但是string不是以'\0'结束的
		//例如:hello\0xxx
		memcpy(tmp, _str, _size + 1);
		//成功之后,将申请的新空间给string对象,旧空间释放
		delete[] _str;
		_str = tmp;
		tmp = nullptr;
		//注:reserve只改变容量,不改变长度
		_capacity = n;
	}
}

tip:

  1. reserve申请n个字符的空间容量:
    • 如果n大于当前string容量,则申请扩容到n或比n大。
    • 如果n小于当前string容量,则申请缩容,但是该申请是不具约束力的。
  2. 我们模拟实现的reserve,只有n大于当前string容量时,按需申请n个字符的空间容量,n小于当前string容量时不做处理。
  3. 注意:reserve只是单纯的开空间,所以reserve只会改变容量,不改变长度

10、resize成员函数调整string的长度

//调整string的长度——不仅会改变长度,还会改变容量
void resize(size_t n, char ch = '\0')//ch用于填值赋值,不传参使用缺省值
{
	//1、如果n小于string长度,删除n之后的字符,但不会缩容
	if (n < _size)
	{
		//即只改变长度
		_size = n;
		//为了兼容C,string对象的最后都追加了一个'\0'
		_str[_size] = '\0';
	}
	else//如果n大于string长度,则扩容+填值赋值
	{
		//1、扩容——改变容量
		reserve(n);
		//2、填值赋值——改变长度
		for (size_t i = _size; i < n; i++)
		{
			_str[i] = ch;
		}
		_size = n;
		//为了兼容C,string对象的最后都追加了一个'\0'
		_str[_size] = '\0';
	}
}

tip:

  1. resize是将string的长度调整为n个字符的长度:
    • 如果n大于string长度,则扩容+填值赋值(默认填’\0’)
    • 如果n小于string长度,删除n之后的字符,但不会缩容
  2. 注意:当n大于当前string的长度时,resize既影响size也影响capacity。

11、clear成员函数清理string的有效字符

//清空string的有效字符——注:不会影响容量
void clear()
{
	_size = 0;
	_str[_size] = '\0';
}

tip:

  1. clear:清空string的有效字符,使之成为空字符串。
  2. 注意:clear不会影响容量。

12、empty成员函数判断字符串是否为空

//判断string是否为空
bool empty()const//内部不改变成员变量,建议加上const
{
	return _size == 0;
}

tip:

  1. 有效字符个数为空,string即为空。

13、operator[]成员函数返回pos位置字符的引用

//operator[]
//版本1:能读能写
char& operator[](size_t pos)
{
	//注意:operator[]越界断言
	assert(pos < _size);
	return _str[pos];
}
//版本2:只能读不能写
const char& operator[](size_t pos)const
{
	//注意:operator[]越界断言
	assert(pos < _size);
	return _str[pos];
}

tip:

  1. operator[]:
    • operator[]越界是断言处理
    • operator[]必须是成员函数
    • operator[]通常定义两个版本:一个返回普通引用能读能写,一个返回常量引用只能读不能写
  2. 重载函数调用时,会走最匹配的,普通对象调用普通的,const对象调用const的

14、迭代器

//迭代器
// 在string中迭代器就是字符指针
//版本1:能读能写
typedef char* iterator;
iterator begin()
{
	//返回指向string第一个字符的指针
	return _str;
}
iterator end()
{
	//返回指向string尾字符的下一个位置的指针
	return _str + _size;
}
//版本2:只能读不能写
typedef const char* const_iterator;
const_iterator begin()const
{
	//返回指向string第一个字符的指针
	return _str;
}
const_iterator end()const
{
	//返回指向string尾字符的下一个位置的指针
	return _str + _size;
}

tip:

  1. 迭代器类似于指针类型,提供了对对象的间接访问,可以读写对象。在string中迭代器就是字符指针
  2. begin成员返回指向第一个字符的迭代器
  3. end成员返回指向尾字符的下一个位置的迭代器
  4. begin和end也有普通版本和const版本
  5. 有了迭代器,就可以使用范围for,因为范围for底层就是end和begin实现的(C++11)

15、insert成员函数在stringpos位置字符之前插入字符或字符串

//insert
//1、在string中pos指向的字符前插入n个字符ch
string& insert(size_t pos, size_t n, char ch)
{
	assert(pos <= _size);
	//1、插入之前判断是否扩容
	if (_size + n > _capacity)
	{
		//至少扩容到_size + n
		reserve(_size + n);
	}
	//2、pos指向的字符前插入n个字符ch
	//①往后挪动
	size_t end = _size;//从'\0'开始
	while (end >= pos && end != npos)
	{
		//向后挪动n个字符
		_str[end + n] = _str[end];
		//迭代
		end--;//特殊:当pos为0时,end=npos时,结束循环
	}
	//②插入
	for (size_t i = 0; i < n; i++)
	{
		_str[i + pos] = ch;
	}
	_size += n;
	return *this;
}
//2、在string中pos指向的字符前插入C字符串
string& insert(size_t pos, const char* str)
{
	assert(pos <= _size);
	size_t len = strlen(str);
	//1、插入之前判断是否扩容
	if (_size + len > _capacity)
	{
		//至少扩容到_size + len
		reserve(_size + len);
	}
	//2、pos指向的字符前插入C字符串
	//①往后挪动
	size_t end = _size;
	while (end >= pos && end != npos)
	{
		//向后挪动C字符串的长度
		_str[end + len] = _str[end];
		//迭代
		end--;//特殊:当pos为0时,end=npos时,结束循环
	}
	//②插入
	for (size_t i = 0; i < len; i++)
	{
		_str[i + pos] = str[i];
	}
	_size += len;
	return *this;
}

tip:

  1. 首先断言pos位置是否合理
  2. 判断是否需要扩容
  3. 把[pos,size]区间的字符都往后挪动len
    • len:len是插入字符的有效个数
    • size:size位置是’\0’,‘\0’也挪动保证插入之后的string对象最后也有’\0’
  4. 插入:注意是从pos位置开始插入len个字符在这里插入图片描述
  5. 注意:当循环变量的类型是size_t时,一定要注意边界0
  6. 当insert不是尾插时,需要挪动数据,效率低,所以一般很少使用

16、push_back成员函数在string后尾插一个字符

//push_back
//在string对象后尾插一个字符
void push_back(char ch)
{
	方式1:自己实现
	1、插入之前判断是否扩容
	//if (_size == _capacity)
	//{
	//	//按2倍扩容
	//	reserve(_capacity == 0 ? 4 : 2 * _capacity);//注意为空串的情况
	//}
	2、尾插ch
	//_str[_size] = ch;
	//_size++;
	//_str[_size] = '\0';

	// 方式2:复用insert
	insert(_size, 1, ch);
}

tip:

  1. 方式1:自己实现
    • 插入之前判断是否需要扩容
    • 插入:直接尾插,插入之后需要注意:①有效字符个数+1;②string对象为了兼容C最后要加’\0’
  2. 方式2:复用insert
  3. string对象的尾插时间复杂度为O(1),效率高

17、append成员函数在string后追加C字符串或string对象

//append
//1、在string对象后追加C字符串
void append(const char* str)
{
	//方式1:自己实现
	//size_t len = strlen(str);
	1、插入之前判断是否扩容
	//if (_size + len > _capacity)
	//{
	//	//至少扩容到_size + len
	//	reserve(_size + len);
	//}
	2、尾插str
	//memcpy(_str + _size, str, len + 1);
	//_size += len;

	//方式2:复用insert
	insert(_size, str);
}
//2、在string对象后追加string对象
void append(const string& str)
{
	size_t len = str._size;
	//1、插入之前判断是否扩容
	if (_size + len > _capacity)
	{
		//至少扩容到_size + len
		reserve(_size + len);
	}
	//2、尾插str
	memcpy(_str + _size, str._str, len + 1);
	_size += len;
}

tip:

  1. 插入之间判断是否需要扩容
  2. 插入:使用memcpy拷贝即可,memcpy由我们自己控制拷贝多少字节,不是拷贝到’\0’就结束
  3. 插入之后记得更新string的有效字符个数

18、operator+=成员函数追加字符或字符串

//operator+=
string& operator+=(const string& str)
{
	append(str);
	return *this;
}
string& operator+=(const char* str)
{
	append(str);
	return *this;
}
string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}
  1. string的operator+=的实现就是复用append和push_back
  2. 所以operator+=不仅可以追加单个字符,还可以追加字符串
  3. operator+=的使用相比push_buck和append更加人性化,所以一般我们更加喜欢使用operator+=
  4. operator+=
    • 一般将复合赋值运算符重载定义为类的成员函数
    • 为与内置类型的复合赋值一致,类的复合赋值运算符也要返回其左侧运算对象的引用
    • 复合运算符都会影响左侧操作数,因为他们都会返回左侧操作数
  5. 自定义类型做函数返回值时,为了提高程序效率,能引用返回尽量引用返回
  6. 引用做返回值:
    • 优点:①减少拷贝提高效率;②可以读写返回值
    • 注意:当返回值出了函数体,不存在了,就不能用引用返回

19、erase成员函数从pos位置删除len个字符

//erase
//从pos位置开始删除len个字符
string& erase(size_t pos = 0, size_t len = npos)
{
	assert(pos < _size);
	//如果len=npos或pos+len>=size时,则从pos删除到string末尾
	if (len == npos || pos + len >= _size)
	{
		_size = pos;
		_str[_size] = '\0';
	}
	else
	{
		//向前挪动数据
		size_t begin = pos + len;
		while (begin <= _size)//=size是为了把'\0'也挪动了
		{
			_str[pos++] = _str[begin++];
		}
		_size -= len;
	}
	return *this;
}

tip:

  1. 断言pos是否合理
  2. 如果当len为缺省值npos或len太大时,则把pos后的所有字符删掉
  3. 反之len+pos<size时,删除len个字符,即从pos+len向前挪动数据
  4. 删除之后记得更新有效字符个数在这里插入图片描述

20、find成员函数从stringpos位置开始查找字符或字符串

//find
//1、从pos位置开始往后找字符ch
size_t find(char ch, size_t pos = 0)const//内部不改变成员变量,建议加上const
{
	assert(pos < _size);
	for (size_t i = pos; i < _size; i++)
	{
		//找到返回该字符的位置
		if (_str[i] == ch)
		{
			return i;
		}
	}
	//找不到,返回npos
	return npos;
}
//2、从pos位置开始往后找字符串str
size_t find(const char* str, size_t pos = 0)const//内部不改变成员变量,建议加上const
{
	assert(pos < _size);
	//strstr找到返回子串位置,找不到返回null
	const char* ret = strstr(_str, str);
	if (ret)
	{
		return ret - _str;//后一个元素的下标等于前面的元素个数
	}
	else
	{
		return npos;
	}
}

tip:

  1. find:从string对象pos位置开始往后找字符或字符串,找到则返回该字符或字符串在string中第一次出现的位置,找不到返回npos
  2. 注意断言pos位置是否合理
  3. 指针-指针:
    • 前提:两个指针要指向同一块空间
    • 作用:得到两个指针之间的元素个数

21、substr成员函数获取string的子串

//substr
//获取string对象的子串,子串从pos开始,截取len个字符
string substr(size_t pos = 0, size_t len = npos)const//内部不改变成员变量,建议加上const
{
	assert(pos < _size);
	//避免多次扩容,算出子串大小,一次reserve
	size_t n = len;
	if (len == npos || len >= _size)
	{
		n = _size - len;
	}
	string tmp;
	tmp.reserve(n);

	//拷贝子串
	for (size_t i = pos; i < pos + n; i++)//注意:结束条件为pos+n
	{
		tmp += _str[i];
	}

	return tmp;
}

tip:

  1. 断言pos位置是否合理
  2. 避免多次扩容,算出子串的大小,一次reserve
  3. 从pos位置拷贝子串

22、operator<<非成员函数输出string对象

//operator<<
//1、ostream必须引用
//2、必须在类外定义
ostream& operator<<(ostream& out, const string& s)
{
	for (auto ch : s)
	{
		out << ch;
	}
	return out;
}
  1. operator<<必须是非成员函数:
    • 因为成员函数的左操作数是隐含的this,所以operator<<必须是非成员函数
  2. ostream不允许拷贝构造,所以ostream对象必须引用
  3. 范围for的底层是迭代器,所以只要实现了迭代器就可以直接使用
  4. <<运算符从左向右结合,可以连续打印,所以要返回ostream

23、operator>>非成员函数输入string对象

//operator>>
istream& operator>>(istream& in, string& s)
{
	//每次输入前清空字符串,避免追加
	s.clear();
	//多个数值用换行或空格分割,所以cin不会读取换行和空格
	//所以istream提供了一个成员函数get,读取每一个字符
	char ch = in.get();
	//处理前缓冲区前面的空格或者换行
	while (ch == ' ' || ch == '\n')
	{
		ch = in.get();
	}
	//读取数据
	char buff[128];//避免多次扩容,先把数据读到buff数组,数组满了和读取结束了,将数组中的数据给string对象
	size_t i = 0;
	while (ch != ' ' && ch != '\n')
	{
		buff[i++] = ch;
		//数组满了
		if (i == 127)
		{
			buff[i] = '\0';
			s += buff;//追加字符串
			i = 0;
		}
		ch = in.get();
	}
	//读取结束了
	if (i != 0)
	{
		buff[i] = '\0';
		s += buff;
	}
	return in;
}
  1. operator>>必须是非成员函数:
    • 因为成员函数的左操作数是隐含的this,所以operator>>必须是非成员函数
  2. istream不允许拷贝构造,所以istream对象必须引用
  3. string的operator<<实现:
    • 读取之前,需要清空string对象
    • 处理前缓存区的空格和换行
    • 避免多次扩容,创建一个局部数组,先保存读取的数据,再将数组的数据追加到string对象
    • <<运算符也是可以连续读取的,所以需要返回istream

24、关系运算重载非成员函数比较string对象

//关系比较
//①先实现operator==和operator<
//②其余利用他们之间的互斥关系复用
//operator<的方式1:自己实现
//bool operator<(const string& s1, const string& s2)
//{
//	//对应位置的字符比较
//	size_t i = 0;
//	size_t j = 0;
//	size_t len1 = s1.size();
//	size_t len2 = s2.size();
//	while (i < len1 && j < len2)
//	{
//		if (s1[i] > s2[j])
//		{
//			return false;
//		}
//		else if (s1[i] < s2[j])
//		{
//			return true;
//		}
//		else
//		{
//			//迭代
//			i++;
//			j++;
//		}
//	}
//	// "hello" "hello"   false
//	// "helloxx" "hello" false
//	// "hello" "helloxx" true
//	//即只有s1的长度小于s2时,才为真
//	return len1 < len2;
//}
//operator<的方式2:复用memcmp
bool operator<(const string& s1, const string& s2)
{
	//对应位置的字符比较
	size_t len1 = s1.size();
	size_t len2 = s2.size();
	int ret = memcmp(s1.c_str(), s2.c_str(), len1 < len2 ? len1 : len2);//ret<0为真
	// "hello" "hello"   false
	// "helloxx" "hello" false
	// "hello" "helloxx" true
	//当ret = 0时,s1的长度小于s2时也为真
	return ret == 0 ? len1 < len2 : ret < 0;
}

bool operator==(const string& s1, const string& s2)
{
	size_t len1 = s1.size();
	size_t len2 = s2.size();
	return len1 == len2
		&& memcmp(s1.c_str(), s2.c_str(), len1) == 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 !(s1 == s2);
}
  1. string对象的关系运算:
    • 比较对应字符的ASCII码值,如果相等,则继续比较,直到出现不同的字符
    • 特殊:当其中一个string对象比较完之后都相等,这个时候比较两个string对象的长度
  2. 关系运算重载的实现:
    • 先实现<和==( 或>和==)
    • 其余利用他们之间的互斥直接复用

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

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

相关文章

7.2024

小明发现了一个奇妙的数字。它的平方和立方正好把 0 ~ 9 的 10 个数字每个用且只用了一次。你能猜出这个数字是多少吗&#xff1f; 代码&#xff1a; import java.util.HashSet; import java.util.Set;public class 第七题 {public static void main(String[] args) {int i1;…

Docker数据卷与网络模式

华子目录 数据卷注意数据卷操作查看镜像&#xff0c;容器&#xff0c;数据卷所占空间 Docker的网络模式查看指定容器的网络模式bridge模式none模式host模式container模式 数据卷 数据卷是一个可供一个或多个容器使用的特殊目录&#xff0c;它绕过UFS&#xff0c;可以提供很多有…

LangChain-Chatchat

文章目录 关于 LangChain-Chatchat特性说明实现原理文档处理流程技术路线图&#xff08;截止0.2.10&#xff09; 使用 关于 LangChain-Chatchat Langchain-Chatchat&#xff08;原Langchain-ChatGLM&#xff09;基于 Langchain 与 ChatGLM 等语言模型的本地知识库问答。 gith…

阿赵UE学习笔记——21、武器插槽

阿赵UE学习笔记目录 大家好&#xff0c;我是阿赵。   继续学习虚幻引擎的使用&#xff0c;这次来看看骨骼插槽的用法。 一、准备资源 这次的目的很简单&#xff0c;就是给之前做了角色蓝图的钢铁侠手上加一把枪。   所以先要找到枪的资源。在虚幻商城里面搜索weapon&#…

Transformer 模型中增加一个 Token 对计算量的影响

Transformer 模型中增加一个 Token 对计算量的影响 Transformer 模型中增加一个 Token 对计算量的影响1. Transformer 模型简介2. Token 对计算量的影响3. 增加一个 Token 的计算量估算4. 应对策略5. 结论 Transformer 模型中增加一个 Token 对计算量的影响 Transformer 模型作…

【二】TensorFlow神经网络模型构建之卷积函数

卷积函数是构建神经网络的重要支架&#xff0c;是在一批图像上扫描的二维过滤器。 tf.nn.convolution(input,filter,padding,stridesNone,dilation_rateNone,nameNone,data_formatNone)该函数计算N维卷积的和。tf.nn.conv2d(input,filter,padding,strides,use_cudnn_on_gpuNon…

前端学习<二>CSS基础——02-CSS属性:背景属性

background 的常见背景属性 css2.1 中&#xff0c;常见的背景属性有以下几种&#xff1a;&#xff08;经常用到&#xff0c;要记住&#xff09; background-color:#ff99ff; 设置元素的背景颜色。 background-image:url(images/2.gif); 将图像设置为背景。 background-repeat…

201812 CSP认证 | CIDR合并

CIDR合并 难是真的不难但是也写了我几个小时服了 这道题在有计网的基础上就很好理解了&#xff0c;没有在格式上有任何刁难你的。这里不讲背景了 官网提交结果以及满分代码如下&#xff1a; #include<bits/stdc.h> using namespace std; typedef long long ll; typedef…

鸿蒙开发实例:【demo-搜索历史记录】

图片演示效果&#xff1a; 鸿蒙OS开发更多内容↓点击HarmonyOS与OpenHarmony技术鸿蒙技术文档开发知识更新库gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md在这。或mau123789学习&#xff0c;是v喔 代码演示&#xff1a; // 注&#xff1a;当前代码基于宽度为…

【Leetcode】top 100 二叉树

基础知识补充 完全二叉树&#xff1a;顺序存储&#xff08;数组&#xff09; 非根节点的父节点序号floor((i-1)/2) 序号i的左孩子节点序号2*i1 右孩子节点序号2*i2 一般二叉树&#xff1a;链式存储 结构&#xff1a;left指针指向左子节点&#xff0c;right指针指向右子节点&am…

vue3+threejs新手从零开发卡牌游戏(十五):创建对方场地和对方卡组

首先创建对方场地&#xff0c;game/site/p2.vue和p1.vue代码一样&#xff0c;注意把里面的命名“己方”修改成“对方”&#xff0c;game/site/index.vue代码如下&#xff0c;用rotateZ翻转一下即可得到镜像的对方场地&#xff1a; // 添加战域plane const addSitePlane () >…

Leetcode 76 最小覆盖子串 java版

官网链接&#xff1a; . - 力扣&#xff08;LeetCode&#xff09; 1. 问题&#xff1a; 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串&#xff0c;则返回空字符串 "" 。 注意&#xff1a; 对于 …

【项目管理——时间管理】【自用笔记】

1 项目时间管理&#xff08;进度管理&#xff09;概述 过程&#xff1a;&#xff08;2—6&#xff09;为规划过程组&#xff0c;7为监控过程组 题目定义&#xff1a;项目时间管理又称为进度管理&#xff0c;是指确保项目按时完成所需的过程。目标&#xff1a;时间管理的主要目标…

FlyControls 是 THREE.js 中用于实现飞行控制的类,它用于控制摄像机在三维空间中的飞行。

demo演示地址 FlyControls 是 THREE.js 中用于实现飞行控制的类&#xff0c;它用于控制摄像机在三维空间中的飞行。 入参&#xff1a; object&#xff1a;摄像机对象&#xff0c;即要控制的摄像机。domElement&#xff1a;用于接收用户输入事件的 HTML 元素&#xff0c;通常…

蓝桥杯刷题8

1. 世纪末的星期 import java.util.Calendar; public class Main {public static void main(String[] args) {Calendar calendar Calendar.getInstance();for(int year 1999;year<100000;year100){calendar.set(Calendar.YEAR,year);calendar.set(Calendar.MONTH,11);cale…

力扣hot100:207. 课程表

这是一道拓扑排序问题&#xff0c;也可以使用DFS判断图中是否存在环。详情请见&#xff1a;官方的BFS算法请忽略&#xff0c;BFS将问题的实际意义给模糊了&#xff0c;不如用普通拓扑排序思想。 数据结构&#xff1a;图的拓扑排序与关键路径 拓扑排序&#xff1a; class Sol…

手撕算法-三数之和

描述 分析 排序双指针直接看代码。 代码 public static List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);List<List<Integer>> res new ArrayList<>();for(int k 0; k < nums.length - 2; k){if(nums[k] > 0) break; …

通讯录管理系统实现(C++版本)

1.菜单栏的设置 &#xff08;1&#xff09;我么自定义了一个showmenu函数&#xff0c;用来打印输出我们的菜单栏&#xff1b; &#xff08;2&#xff09;菜单栏里面设置一些我们的通讯录里面需要用到的功能&#xff0c;例如增加联系人&#xff0c;删除联系人等等 2.退出功能…

【Python系列】Python 中 YAML 文件与字典合并的实用技巧

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

MySQL数据库------------探索高级SQL查询语句

目录 一、常用查询 1.1按关键字排序 1.2简单的select条件查询(where) 二、排序 2.1升序排列 2.2降序排序 三、order by 查询结果排序 ①order by还可以结合where进行条件过滤&#xff0c;筛选地址是哪里的学生按分数降序排列 ②查询学生信息先按hobbyid降序排列&#…