前言
本文将模拟实现string的一些常见功能,目的在于加深理解string与回顾类与对象的相关知识。
一、前置知识
- string是表示可变长的字符序列的类
- string的底层是使用动态顺序表存储的
- string对象不以’\0’字符为终止算长度,而是以size有效字符的个数算长度
- 为了兼容C,所以string对象在最后追加的一个’\0’字符,但是这个’\0’字符不属于string对象的有效字符
- 建议在模拟实现之前熟悉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:
- 命名空间
- 作用:使用命名空间的目的就是对标识符的名称进行本地化,以避免命名冲突或命名污染
- 定义:namespace后面跟命名空间名字,然后接一对{}即可,{}中即为命名空间的成员
- 静态成员变量:
- 静态成员属于类为所有类对象共享,不属于某个具体的对象,存放在静态区
- 静态成员也是类的成员,受访问限定符的限制
- 一般静态成员变量不能在声明时给缺省值,因为缺省值是给初始化列表使用的,初始化列表是初始化对象的成员变量,而静态成员变量不属于类的任何一个对象
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。常与静态成员变量配套使用
- 在类外部定义静态成员
- 静态成员只要能突破类域和访问限定符就可以访问
- 了解:const整数类型的静态成员变量可以在类内部初始化,但是不建议。
- string的npos:
- size_t npos = -1,表示该类型的最大值
- 当string成员函数的参数的缺省值为npos时,表示“直到字符串结束”
- 当npos作为返回值时,一般表示没有匹配(例如:find)
2、构造函数
string类常用的构造函数有:
- string():默认构造函数,构造一个空的string对象,即空字符串。
- 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:
- 默认构造函数:
- 三种默认构造函数:无参构造函数、全缺省构造函数、编译器默认生成的构造函数
- 注意:默认构造函数只能存在一个——虽然语法上可以同时存在,但是无参调用时存在歧义,所以默认构造函数只能有一个
- 推荐使用全缺省默认构造函数——优点:即可以不传参使用缺省值初始化对象,也可以传参自己初始化对象
- 不传参就可以调用的就是默认构造函数
- 给成员变量赋初值的方式有:
- 使用初始化列表
- 使用构造函数的函数体
- 建议给成员变量赋初值都使用初始化列表,因为初始化列表是成员变量定义的地方
- 构造函数体与初始化列表结合使用,因为总有一些事情是初始化列表不能完成的。
- 初始化列表:
- 初始化列表是成员变量定义的地方,所以每一个成员变量在初始化列表中最多只能出现一次
- 引用成员变量、const成员变量、没有默认构造函数的自定义成员,必须在初始化列表初始化(因为这三种成员变量有一个共同特征,在定义时就必须初始化)
- 注意: 成员变量在类中的声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
- string的底层存储是使用动态顺序表实现
- 在C++中使用new和delete操作符进行动态内存管理
- 申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和delete[],注意:配合使用
- C字符串后默认以’\0’结束,为了兼容C所以string要多开一个空间,保存’\0’
3、析构函数
//析构函数——有动态申请资源,需要显示实现析构释放资源
~string()
{
delete[] _str;
_str = nullptr;//delete与free一样释放完了,不会改变_str,所以将其置为空
_size = _capacity = 0;
}
tip:
- 一般当对象涉及动态申请资源,就需要显示实现析构函数
- delete释放完空间之后与free一样,不会改变指向动态空间的指针变量,有危险,建议释放完之后将其置为空
4、拷贝构造函数
拷贝构造的浅拷贝
: 按字节序完成拷贝。
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成两个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象。
所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。
拷贝构造深拷贝
: 有自己的空间。
深拷贝是把值拷贝到自己的空间。
深浅拷贝就像我们平时考试时抄别人的作业,浅拷贝——名字、学号都抄别人的,深拷贝——知道修改名字、学号。
编译器默认生成的拷贝构造只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。
拷贝构造的深拷贝的两种实现:
- 传统写法:自己开空间,自己拷贝
- 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们。
传统写法:
//传统写法——自己开空间,自己拷贝
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:
- string不以’\0’字符为终止,所以拷贝构造的现代写法有bug
- 所以string的拷贝构造,我们不使用现代写法,使用传统写法
5、operator=赋值重载
赋值重载的浅拷贝
: 按字节序完成拷贝。
浅拷贝按字节序拷贝,s1和s2的内容是一样的。当类的成员变量涉及动态资源申请时,会造成三个问题:①同一块空间,析构两次,程序崩溃;②修改一个对象会影响另一个对象;③旧空间没释放,造成内存泄漏。
所以当类的成员变量涉及动态资源申请时,需要有自己的空间,即深拷贝。
赋值重载的深拷贝
: 有自己的空间且要释放旧空间。
深拷贝是把值拷贝到自己的空间,并且将旧空间释放。
编译器默认生成的赋值重载只能完成浅拷贝,所以一旦涉及动态资源申请,即深拷贝,就需要我们显式实现拷贝构造。
赋值重载的深拷贝的两种实现:
- 传统写法:自己开空间,自己拷贝,自己释放旧空间。
- 现代写法:先叫别人帮我们开好空间,拷贝好了,再把数据给我们,最后的旧空间也由别人帮我们释放。
传统写法:
//传统写法:自己开空间,自己拷贝,自己释放旧空间。
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:
- 现代写法:就是别人做好了,我们交换拿结果就可以了。
6、c_str成员函数获取C格式字符串
//获取C格式字符串
const char* c_str()const//内部不改变成员变量,建议加上const
{
return _str;
}
tip:
- const成员
- 将const修饰的成员函数称为const成员函数
- 建议只要成员函数内部不修改成员变量,都应该加const,这样普通对象和const对象都可以调用
- 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:
- string的长度,即有效字符个数。
- 注意:string不以’\0’字符为终止算长度,是以有效字符的个数算长度。
8、capacity成员函数获取string当前空间容量
//获取string的当前空间容量
size_t capacity()const//内部不改变成员变量,建议加上const
{
return _capacity;
}
tip:
- 说明: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:
- reserve申请n个字符的空间容量:
- 如果n大于当前string容量,则申请扩容到n或比n大。
- 如果n小于当前string容量,则申请缩容,但是该申请是不具约束力的。
- 我们模拟实现的reserve,只有n大于当前string容量时,按需申请n个字符的空间容量,n小于当前string容量时不做处理。
- 注意: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:
- resize是将string的长度调整为n个字符的长度:
- 如果n大于string长度,则扩容+填值赋值(默认填’\0’)
- 如果n小于string长度,删除n之后的字符,但不会缩容
- 注意:当n大于当前string的长度时,resize既影响size也影响capacity。
11、clear成员函数清理string的有效字符
//清空string的有效字符——注:不会影响容量
void clear()
{
_size = 0;
_str[_size] = '\0';
}
tip:
- clear:清空string的有效字符,使之成为空字符串。
- 注意:clear不会影响容量。
12、empty成员函数判断字符串是否为空
//判断string是否为空
bool empty()const//内部不改变成员变量,建议加上const
{
return _size == 0;
}
tip:
- 有效字符个数为空,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:
- operator[]:
- operator[]越界是断言处理
- operator[]必须是成员函数
- operator[]通常定义两个版本:一个返回普通引用能读能写,一个返回常量引用只能读不能写
- 重载函数调用时,会走最匹配的,普通对象调用普通的,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:
- 迭代器类似于指针类型,提供了对对象的间接访问,可以读写对象。在string中迭代器就是字符指针
- begin成员返回指向第一个字符的迭代器
- end成员返回指向尾字符的下一个位置的迭代器
- begin和end也有普通版本和const版本
- 有了迭代器,就可以使用范围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:
- 首先断言pos位置是否合理
- 判断是否需要扩容
- 把[pos,size]区间的字符都往后挪动len
- len:len是插入字符的有效个数
- size:size位置是’\0’,‘\0’也挪动保证插入之后的string对象最后也有’\0’
- 插入:注意是从pos位置开始插入len个字符
- 注意:当循环变量的类型是size_t时,一定要注意边界0
- 当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;②string对象为了兼容C最后要加’\0’
- 方式2:复用insert
- 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:
- 插入之间判断是否需要扩容
- 插入:使用memcpy拷贝即可,memcpy由我们自己控制拷贝多少字节,不是拷贝到’\0’就结束
- 插入之后记得更新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;
}
- string的operator+=的实现就是复用append和push_back
- 所以operator+=不仅可以追加单个字符,还可以追加字符串
- operator+=的使用相比push_buck和append更加人性化,所以一般我们更加喜欢使用operator+=
- operator+=
- 一般将复合赋值运算符重载定义为类的成员函数
- 为与内置类型的复合赋值一致,类的复合赋值运算符也要返回其左侧运算对象的引用
- 复合运算符都会影响左侧操作数,因为他们都会返回左侧操作数
- 自定义类型做函数返回值时,为了提高程序效率,能引用返回尽量引用返回
- 引用做返回值:
- 优点:①减少拷贝提高效率;②可以读写返回值
- 注意:当返回值出了函数体,不存在了,就不能用引用返回
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:
- 断言pos是否合理
- 如果当len为缺省值npos或len太大时,则把pos后的所有字符删掉
- 反之len+pos<size时,删除len个字符,即从pos+len向前挪动数据
- 删除之后记得更新有效字符个数
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:
- find:从string对象pos位置开始往后找字符或字符串,找到则返回该字符或字符串在string中第一次出现的位置,找不到返回npos
- 注意断言pos位置是否合理
- 指针-指针:
- 前提:两个指针要指向同一块空间
- 作用:得到两个指针之间的元素个数
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:
- 断言pos位置是否合理
- 避免多次扩容,算出子串的大小,一次reserve
- 从pos位置拷贝子串
22、operator<<非成员函数输出string对象
//operator<<
//1、ostream必须引用
//2、必须在类外定义
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
- operator<<必须是非成员函数:
- 因为成员函数的左操作数是隐含的this,所以operator<<必须是非成员函数
- ostream不允许拷贝构造,所以ostream对象必须引用
- 范围for的底层是迭代器,所以只要实现了迭代器就可以直接使用
- <<运算符从左向右结合,可以连续打印,所以要返回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;
}
- operator>>必须是非成员函数:
- 因为成员函数的左操作数是隐含的this,所以operator>>必须是非成员函数
- istream不允许拷贝构造,所以istream对象必须引用
- 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);
}
- string对象的关系运算:
- 比较对应字符的ASCII码值,如果相等,则继续比较,直到出现不同的字符
- 特殊:当其中一个string对象比较完之后都相等,这个时候比较两个string对象的长度
- 关系运算重载的实现:
- 先实现<和==( 或>和==)
- 其余利用他们之间的互斥直接复用