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

news2024/7/4 6:33:39

前言

本文将模拟实现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. 关系运算重载的实现:
    • 先实现<和==( 或>和==)
    • 其余利用他们之间的互斥直接复用

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

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

相关文章

嵌入式开发过程中应该养成的习惯!

最近有一些读者来咨询:有什么需要注意的编程好习惯?给大家分享一下。 第一:多看官方文档 不要被这几个字吓到,官方文档其实都是宝藏。 一个成熟的技术诞生,可以没有博客没有书籍,但一定会有一个官方文档,毋庸置疑,它一定是最准确、最实时的资料。编写官方文档的人,…

【SAP2000】碰撞分析 Impact Analysis

碰撞分析 Impact Analysis CSI程序的动力分析功能非常广泛。一个例子是分析两个质量或结构之间碰撞效应的能力。 The possibilities of dynamic analysis with CSI programs are very extensive. An example of this is the ability to analyze the effects of collision bet…

Leetcode算法题笔记(2)

目录 图论51. 岛屿数量解法一 52. 腐烂的橘子解法一 53. 课程表解法一 54. 实现 Trie (前缀树)解法一 回溯55. 全排列解法一 56. 子集解法一解法二 57. 电话号码的字母组合解法一 58. 组合总和解法一解法二 59. 括号生成解法一解法二 60. 单词搜索解法一 61. 分割回文串解法一 …

一文看懂什么是OpenHarmony流转架构

随着全场景多设备的生活方式不断深入&#xff0c;用户拥有的设备越来越多&#xff0c;不同设备都能在适合的场景下提供良好的体验&#xff0c;例如手表可以提供及时的信息查看能力&#xff0c;电视可以带来沉浸的观影体验。但是&#xff0c;每个设备也有使用场景的局限&#xf…

【Python】Data Science with Python 数据科学(1)环境搭建

一、操作系统 使用运行在Windows11主机上的Ubuntu 22.04虚拟机&#xff0c;虚拟化平台为Oracle VM VirtualBox。 二、PyCharm安装 有关PyCharm的安装和快捷方式创建&#xff0c;可分别参考我的博客 Ubuntu安装PyCharm、Ubuntu创建桌面快捷方式 &#xff0c;以及Ubuntu创建桌…

systemd-journal(一)之journalctl命令详解

文章目录 写在前面概述描述不传递参数传递一个或多个匹配参数示例 源选项用法--system, --user-M, --machine-m, --merge-D DIR, --directoryDIR--fileGLOB--rootROOT--imageIMAGE--image-policypolicy--namespaceNAMESPACE 过滤选项用法-S, --since, -U, --until举例&#xff…

孙中茂:摸清自己的性格很重要,只要你的本事够了,在哪个地方都是都会发光的。

《程客有话说》是我们最新推出的一个访谈栏目&#xff0c;邀请了一些国内外有趣的程序员来分享他们的经验、观点与成长故事&#xff0c;我们尝试建立一个程序员交流与学习的平台&#xff0c;也欢迎大家推荐朋友或自己来参加我们的节目&#xff0c;一起加油。 本期我们邀请的程…

第四百二十六回

文章目录 1. 概念介绍2. 实现方法2.1 原生方式2.1 插件方式 3. 示例代码4. 内容总结 我们在上一章回中介绍了"如何修改程序的桌面图标"相关的内容&#xff0c;本章回中将介绍如何处理ListView中的事件冲突.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介…

YOLOv9有效改进专栏汇总|未来更新卷积、主干、检测头注意力机制、特征融合方式等创新![2024/3/23]

​ 专栏介绍&#xff1a;YOLOv9改进系列 | 包含深度学习最新创新&#xff0c;助力高效涨点&#xff01;&#xff01;&#xff01; 专栏介绍 YOLOv9作为最新的YOLO系列模型&#xff0c;对于做目标检测的同学是必不可少的。本专栏将针对2024年最新推出的YOLOv9检测模型&#xff0…

NSCaching: Simple and Efficient NegativeSampling for Knowledge Graph Embedding

摘要 知识图嵌入是数据挖掘研究中的一个基本问题&#xff0c;在现实世界中有着广泛的应用。它的目的是将图中的实体和关系编码到低维向量空间中&#xff0c;以便后续算法使用。负抽样&#xff0c;即从训练数据中未观察到的负三元组中抽取负三元组&#xff0c;是KG嵌入的重要步…

Open CASCADE学习|显示文本

目录 1、修改代码 Viewer.h&#xff1a; Viewer.cpp&#xff1a; 2、显示文本 OpenCasCade 你好啊 霜吹花落 1、修改代码 在文章《Open CASCADE学习|显示模型》基础上&#xff0c;增加部分代码&#xff0c;实现对文本显示的支持&#xff0c;具体如下&#xff1a; Viewer…

随机链表的深拷贝

目录 一、何为深拷贝&#xff1f; 二、题目 三、思路 1.拷贝节点插入到原节点后面 2.控制拷贝节点的random 3.脱离原链表 : 尾插的思想 四、代码 五、附加 一、何为深拷贝&#xff1f; 一个引用对象一般来说由两个部分组成&#xff1a;一个具名的Handle&#xff0c;也就…

cinder学习小结

1 官方文档 翻译官方文档学习 链接Cinder Administration — cinder 22.1.0.dev97 documentation (openstack.org) 1.1 镜像压缩加速 在cinder.conf配allow_compression_on_image_upload True可打开开关 compression_format xxx可设置镜像压缩格式&#xff0c;可为gzip 1.2 …

SPP和SPPF的比较

SPP的结构是将输入并行通过多个不同大小的MaxPool层&#xff0c;然后做进一步融合&#xff0c;能在一定程度上解决多尺度问题。 而SPPF结构则是讲输入串行通过多个5*5的MaxPool层&#xff0c;这里需要注意两个5*5的MaxPool层和一个9*9的MaxPool的计算结果是一样的&#xff0c;而…

[蓝桥杯 2022 省 A] 求和

[蓝桥杯 2022 省 A] 求和 题目描述 给定 n n n 个整数 a 1 , a 2 , ⋯ , a n a_{1}, a_{2}, \cdots, a_{n} a1​,a2​,⋯,an​, 求它们两两相乘再相加的和&#xff0c;即 S a 1 ⋅ a 2 a 1 ⋅ a 3 ⋯ a 1 ⋅ a n a 2 ⋅ a 3 ⋯ a n − 2 ⋅ a n − 1 a n − 2 ⋅ a…

3、创建项目,什么是路由

一、创建项目 第一次全局安装脚手架 npm install -g vue/clivue create 项目名 二、什么是路由&#xff1f; 路由就是一组 key-value 的对应关系多个路由&#xff0c;需要经过路由器的管理 1、后端路由&#xff1a; 每个url地址都对应着不同的静态资源对于普通的网站。所有…

记录整合ssm项目时的报错java: Compilation failed: internal java compiler error

启动的时候报错java: Compilation failed: internal java compiler error&#xff0c;这说明是内部编译器错误。如下图所示&#xff1a; 大概率是jdk版本不兼容的问题&#xff0c;也有IDEA初始划分的堆内存不够的原因。 查阅了很多博客的解决方法也都是上述两种&#xff0c;但…

C++引用学习day2

思维导图 定义一个矩形类&#xff08;Rectangle&#xff09;&#xff0c;包含私有成员&#xff1a;长(length)、宽&#xff08;width&#xff09;, 定义成员函数&#xff1a; 设置长度&#xff1a;void set_l(int l) 设置宽度&#xff1a;void set_w(int w) 获取长度&#…

vscode 配置c++环境——3个文件搞定!!!

前提&#xff1a; 在vscode中安装了c扩展 创建文件settings.json {"files.associations": {"string": "cpp","vector": "cpp","array": "cpp","atomic": "cpp","*.tcc"…

Springboot快速整合bootstrap-table使用,接口对接

这个表格加持还是不错了&#xff0c;自带了全局搜索&#xff0c;分页&#xff0c;数据导出&#xff0c;卡片视图&#xff0c;等&#xff0c;本次整合添加了数据添加弹窗和编辑数据回显弹窗&#xff0c;附完整页面代码&#xff0c;只需要拿过来替换自己实际的接口即可。 效果图 …