【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. 关系运算重载的实现:
    • 先实现<和==( 或>和==)
    • 其余利用他们之间的互斥直接复用
  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值