前面已经学习了string类的用法,这篇文章将更深入的学习string类,了解string类的底层是怎么实现的。当然,这里只是模拟一些常用的,不常用的可以看文档学习。
我们一共创建两个文件,一个是test.cpp文件,用于测试;另一个是string.h文件,用于声明和定义要模拟的string类。模拟的string类会与C++标准库里的string类冲突,所以这里可以使用命名空间来解决这个问题。
namespace yss
{
class string
{
public:
private:
};
}
我们要模拟的string类在yss这个命名空间里,所以等会使用这个string类就去yss里找。
注:声明和定义都在头文件里写
私有成员变量主要有:
_str——字符数组
_size——字符有效个数
_capacity——数组总空间大小(总空间大小体现在数组开辟的空间大小)
npos——静态成员常量,无符号整型的最大值
private:
size_t _size;
size_t _capacity;
char* _str;
const static size_t npos = -1;
为什么声明顺序不是先字符串,这个等会再讨论,先来看下构造函数
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
这里我们定义_capacity 与_size 相同,方便后续开辟空间。_size 可以用strlen计算字符串个数,为什么_capacity 不用?因为再使用一个strlen又要遍历一遍字符串,加大了时间复杂度。
注意:strlen计算字符串长度不包括斜杠0
前面因为总空间大小和有效字符个数一样,又因为字符串末尾要有斜杠0,所以实际开辟的字符数组的空间大小比_capacity 多一个位置,用于放斜杠0;_str 字符数组的空间开辟好后,就把参数字符串拷贝过来即可。
注意:strcpy会连同斜杠0一起拷贝
如果参数为空字符串该咋办?可以给缺省值,但是不能为斜杠0,因为斜杠0是字符类型,而我们这里的形参是字符串类型,虽然可以发生类型转换,但毕竟不好;nullptr指针呢?也不行,因为如果我们没有传参那么它就是空指针,最后要打印对空指针解引用不就报错了吗。所以这里给个空字符串即可。
回到前面的声明私有成员变量的问题,我们知道声明的顺序就是初始化列表的顺序,但是前面的代码没有用到初始化列表。其实前面的代码只是一种写法,构造函数也可以使用初始化列表,或者初始化列表与函数体混着使用,如果用初始化列表就得注意声明顺序了,所以为了防止出错,声明顺序就与我们在构造函数里成员变量定义的顺序相同。
验证一下前,我们还要实现一个返回C格式的函数,顺便把返回字符有效个数、交换和返回容量也写下,方便后面操作:
//C格式返回
const char* c_str() const
{
return _str;
}
//返回字符个数
const size_t size() const
{
return _size;
}
//返回容量
const size_t capacity() const
{
return _capacity;
}
//交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
总结:
一:缺省值给空字符串
二:多开一个位置放斜杠0
析构函数与我们以前的destroy()函数差不多,作用是清理空间。要注意下这里使用的是与new[] 匹配的delete[] ,不能把中括号漏了。
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
之前的文章提过,我们不写编译器会默认生成它的拷贝构造,但这是浅拷贝,对于非指针变量没有太大关系,但如果是指针成员变量,导致两个指针指向同一块空间,会有一块内存重复释放的风险,所以要用深拷贝,额外开一块空间,自己来实现。
先上代码:
string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
深拷贝意味着要临时多开一块空间,这里多开的一块空间就是_str的,因为_str是刚被初始化的对象的字符串,所以给它开辟空间,方式与前面的构造函数相同,_size 与_capacity 值拷贝。
这个写法更简化:
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
也要额外开辟一块空间,定义一个string类型的变量tmp,构造的内容与要拷贝的内容相同,然后将s2的内容与tmp交换就行了。
因为tmp是局部变量,出函数作用域要被销毁掉,拷贝构造结束调用析构清理tmp。(交换后tmp指向空,释放空指针不进行任何操作)
赋值重载类似拷贝构造,也要有一个额外空间。
代码:
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
返回值是string&可以减少拷贝,效率高;开始的 if 语句是为了判断this指针与要赋值的对象是否相同(函数参数列表里的取地址符是引用,if 判断里的是取地址),如果相同,就没必要赋值了,因为是自己赋值自己。如果不同,才可以进行赋值。
创建一个临时的空间,把要赋值的内容拷贝到临时空间里,然后把this指针指向的原来的空间销毁掉(this指针不显示写,还有this指针的字符串原来是有自己的一块空间的),然后指向这块临时的空间,其他成员变量值拷贝。最后返回this指针。
赋值重载的另一个写法也很简洁:
string& operator=(string s)
{
swap(s);
return *this;
}
形参s没有引用,说明它是实参的一份拷贝,独立占一块空间,正因为独立占一块空间,所以它就可以作为this指针的_str交换的临时对象。
如果传的是引用,形参s与原来的被赋值的对象共用一块空间,交换的话不就把被赋值的对象的内容改变了吗
好了,既然如此,那么不加引用,可是为什么不加const了呢?注意,不是什么情况都能加const。如果这里加上const,那么s就不可以改变了,交换就是改变s,会报错。
注意一下:拷贝构造函数的参数列表里也可以不加const,但是某些情况就必须加const,这“某些情况”后面会提到。此时这里加不加const都没关系,但为了保险起见,一般带上const较好
有两种写法,先来看第一种,没有const的
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
iterator是迭代器,我们以重命名的方式让它成为字符指针的别名。begin返回的是指向首元素地址的指针,end返回的是指向斜杠0的指针。
遍历方式是定义一个指针刚开始指向字符串首元素的位置,然后循环不等于斜杠0就打印,直到遇到斜杠0跳出。
yss::string s1("hello world");
yss::string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it;
++it;
}
cout << endl;
有const的写法:
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
也有const和非const的写法,使用方式就是常见的下标遍历
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
使用assert可以预防pos下标越界的问题
如果要插入字符或字符串,当字符数组的空间大小不满足需求时,就要进行扩容。模拟实现的函数是reserve。
reserve:
1.不改变有效字符个数,不影响字符串内容,只影响空间大小
2.当参数大于空间总大小时,空间总大小增加;小于等于时不变
3.不会缩容
代码:
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
扩容的也需要开辟一块临时空间tmp,tmp的实际空间大小为n+1(多一个放斜杠0),将_str的内容拷贝到tmp临时空间里,然后销毁掉原来的空间,指向新的空间,原来的空间大小_capacity扩到n,完成扩容。
resize可以改变字符串的有效字符个数,当传入的参数n小于原来有效字符个数时,有效字符个数为n,同时字符串的内容也发生改变;如果大于,在原来字符串的末尾填充字符,填充的个数为参数减去原来的有效字符个数。
void resize(size_t n, char ch)
{
if (n <= _size)
{
_str[n] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
}
_size = n;
}
n小于等于_size,就在下标为n的位置改为斜杠0,(等于_size就是原来斜杠0的位置放斜杠0)。n大于有效字符个数的情况:如果n大于空间大小,就要扩容;只要大于不管有没有扩容都要在末尾逐个插入字符,然后在下标为n处放置斜杠0(这里不用担心越界问题,有或者没有扩容,实际字符数组都有多一个位置来处理这个斜杠0)。最后有效字符个数要改为n。
如果有效字符个数为0返回真,否则假(打印结果真为1,假为0)。清理的是有效字符个数,对空间大小没有影响
// 判空
bool empty()
{
return _size == 0;
}
// 清理
void clear()
{
_size = 0;
_str[0] = '\0';
}
注意:清理函数里如果字符串的首元素不改为斜杠0,清理完后再打印它依然可以打印出这个字符串,因为返回C格式字符串是有带斜杠0返回的,也就是说正常情况下打印这个字符串到它的斜杠0位置结束,所以这里的意思是函数里如果字符串的首元素不改为斜杠0,在实际的空间里这个字符串于原来没被清理的状态一样,只是有效字符个数_size改变了而已。在字符串的首元素改为斜杠0,那么打印时遇到斜杠0停下,即什么都没有打印出来,这个字符串就与被清理的效果一样了。
尾插字符要注意空间是否够用,不够要扩容
代码:
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
当_size等于_capacity就要扩容,扩到原来_capacity的两倍大小;然后尾插字符,有效字符个数加1,再最后放斜杠0
运行结果:
先要计算要插入的字符串的字符个数,如果原来字符串的有效个数加上插入的字符串的字符个数大于空间大小,就要扩容。然后进行尾插,有效字符个数为相加的结果。
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
为什么这里是_size + len > _capacity而不是>=
假设原来的字符串的_size=8,_capacity=9,新插入的字符串就一个字符。此时_capacity还有一个位置刚好可以放这一个字符,所以这种情况就没必要扩容。还有一种情况是要插入的字符串为空串,那么最后得到的还是自己原来的字符串,就算原来的字符串_size == _capacity,也不需要扩容,因为此时又没有要插入的东西。(或者说插入空串相当于啥也没插入)
+=运算符可以实现两种尾插方式,既可以尾插字符,也可以尾插字符串
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
复用前面的函数,然后返回this指针
插入操作都要考虑是否要扩容,扩容与前面尾插字符一样。插入的位置可能是头也可能是中间,所以要挪动数据。
void insert(char ch, size_t pos)
{
assert(pos < _size);
//插入要考虑是否要扩容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
//挪动数据
int end = _size;
while (end >= (int)pos)//强转类型
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
}
主要这几点,考虑是否扩容,挪动数据,拷贝字符串
代码:
void insert(const char* str, size_t pos)
{
assert(pos < _size);
size_t len = strlen(str);
//考虑是否扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}
strncpy(_str + pos, str, len);
_size++;
}
这里的拷贝字符串就跟前面的不一样了,strncpy会把要插入的字符串拷贝到中间空出来的位置去,并且会控制个数,即在中间某个位置插入一个字符串,这个位置之后原来的字符串没有被覆盖掉,如果是用strcpy,那么插入的位置之后原来的字符串就都被要插入的字符串覆盖。
如果要删除的个数len是默认的缺省值,从pos位置开始后面全部删除;或者pos的值加上len大于等于字符个数,也是从pos位置开始后面全部删除,pos位置就放斜杠0,有效字符个数变为pos的值。从pos开始后面不全删,就把pos位置加len后的字符串覆盖到pos位置之后,_size减去len。
代码:
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
// 从pos开始后面都删除
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
//从pos开始后面不全删
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
查找字符/字符串
从左往右查找字符,找到了返回该字符的下标,否则返回npos
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = 0; i < _size; i++)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
从左往右查找字符串,使用字符串函数strstr,定义一个变量得到字符串函数的返回结果,这个返回结果如果是空,说明没找到,就返回npos,否则返回该指针的下标,即找到字符串的第一个字符下标。
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
char* ptr = strstr(_str, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
代码:
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t end = pos + len;
if (len == npos || pos + len >= _size)
{
end = _size;
}
string ss;
ss.reserve(end - pos);
for (size_t i = pos; i < end; i++)
{
ss += _str[i];
}
return ss;
}
先定义一个变量end确定截取的字符串在源字符串的最后位置,假设pos+len的值就是end指向的下标处。如果len是缺省值,或者pos加上要len大于_size,end就等于_size;不满足条件,就是假设的值。定义一个对象,使其尾插原字符串从pos位置到end位置的字符,最后返回的就是截取的字符串。
这里就可以解决前面的一个问题了:拷贝构造函数的第二种写法某些情况必须加const
刚刚写的截取字符串是传值返回,返回的是返回值的临时拷贝,这个临时对象具有常属性,用其他对象接收时要带上const
图示:
可能有人会想这里为什么要用引用接收,不用引用行不行?答案是绝对不行,因为没有引用,拷贝构造会无限递归,一直循环调用。
代码:
//大于
bool operator>(string& s) const
{
return strcmp(_str, s._str) > 0;
}
//等于
bool operator==(string& s) const
{
return strcmp(_str, s._str) == 0;
}
//大于等于
bool operator>=(string& s) const
{
return (*this == s && *this > s);
}
//小于
bool operator<(string& s) const
{
return !(*this >= s);
}
//小于等于
bool operator<=(string& s) const
{
return !(*this > s);
}
//不等于
bool operator!=(string& s) const
{
return !(*this == s);
}
之前我们写输出的函数要用友元函数,现在有个方法可以不用友元:
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
范围for直接打印对象里面的字符。
输入的函数也不需要友元:
istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != '\n' && ch != ' ')
{
s += ch;
in >> ch;
}
return in;
}
运行一下:
结果发现好像停不下来了,因为cin和scanf一样,读取时对空格或者换行会进行忽略,所以一直在循环里
解决方法:使用cin的get函数,可以读取到空格或者换行
istream& operator>>(istream& in, string& s)
{
s.clear();//清理之前的字符串
char ch = in.get();
while (ch != '\n' && ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
#include
#include
#include
using namespace std;
namespace yss
{
class string
{
public:
//迭代器遍历
typedef char* iterator;
typedef const char* const_iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
//构造
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//析构
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
//C格式返回
const char* c_str() const
{
return _str;
}
//返回字符个数
const size_t size() const
{
return _size;
}
//返回容量
const size_t capacity() const
{
return _capacity;
}
//交换
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//拷贝构造
/*string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}*/
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
//赋值重载
/*string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}*/
string& operator=(string s)
{
swap(s);
return *this;
}
//下标遍历访问
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
//扩容
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
//修改有效字符
void resize(size_t n, char ch)
{
if (n <= _size)
{
_str[n] = '\0';
}
else
{
if (n > _capacity)
{
reserve(n);
}
for (size_t i = _size; i < n; i++)
{
_str[i] = ch;
}
_str[n] = '\0';
}
_size = n;
}
// 判空
bool empty()
{
return _size == 0;
}
// 清理
void clear()
{
_size = 0;
_str[0] = '\0';
}
//尾插字符
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//尾插字符串
void append(const char* str)
{
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
// +=
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
//插入
void insert(char ch, size_t pos)
{
assert(pos < _size);
//插入要考虑是否要扩容
if (_size == _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : 2 * _capacity;
reserve(newcapacity);
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
_size++;
}
void insert(const char* str, size_t pos)
{
assert(pos < _size);
size_t len = strlen(str);
//考虑是否扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
//挪动数据
int end = _size;
while (end >= (int)pos)
{
_str[end + len] = _str[end];
--end;
}
strncpy(_str + pos, str, len);
_size++;
}
//删除
void erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
// 从pos开始后面都删除
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
//从pos开始后面不全删
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
//查找字符
size_t find(char ch, size_t pos = 0)
{
assert(pos < _size);
for (size_t i = 0; i < _size; i++)
{
if (ch == _str[i])
{
return i;
}
}
return npos;
}
//查找字符串
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
char* ptr = strstr(_str, str);
if (ptr == nullptr)
{
return npos;
}
else
{
return ptr - _str;
}
}
//截取子串
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
size_t end = pos + len;
if (len == npos || pos + len >= _size)
{
end = _size;
}
string ss;
ss.reserve(end - pos);
for (size_t i = pos; i < end; i++)
{
ss += _str[i];
}
return ss;
}
//大于
bool operator>(string& s) const
{
return strcmp(_str, s._str) > 0;
}
//等于
bool operator==(string& s) const
{
return strcmp(_str, s._str) == 0;
}
//大于等于
bool operator>=(string& s) const
{
return (*this == s && *this > s);
}
//小于
bool operator<(string& s) const
{
return !(*this >= s);
}
//小于等于
bool operator<=(string& s) const
{
return !(*this > s);
}
//不等于
bool operator!=(string& s) const
{
return !(*this == s);
}
private:
size_t _size;
size_t _capacity;
char* _str;
const static size_t npos = -1;
};
// <<
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s)
{
out << e;
}
return out;
}
// >>
/*istream& operator>>(istream& in, string& s)
{
char ch;
in >> ch;
while (ch != '\n' && ch != ' ')
{
s += ch;
in >> ch;
}
return in;
}*/
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
while (ch != '\n' && ch != ' ')
{
s += ch;
ch = in.get();
}
return in;
}
}
#include "string.h"
int main()
{
/*yss::string s1("hello yss");
cout << s1.c_str() << endl;*/
/*yss::string s1("hello yss");
yss::string s2(s1);
cout << s2.c_str() << endl;*/
/*yss::string s1("hello yss");
yss::string s2 = s1;
cout << s2.c_str() << endl;*/
//yss::string s1("hello world");
//yss::string::iterator it = s1.begin();
//while (it != s1.end())
//{
// cout << *it;
// ++it;
//}
//cout << endl;
/*yss::string s1("hello world");
yss::string::const_iterator it = s1.begin();
while (it != s1.end())
{
cout << *it;
++it;
}
cout << endl;*/
/*yss::string s1("hello world");
for (size_t i = 0; i < s1.size(); i++)
{
cout << s1[i] << " ";
}
cout << endl;*/
/*yss::string s1("hello yss");
cout << s1.c_str() << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;
s1.resize(12,'q');
cout << s1.c_str() << endl;
cout << s1.size() << endl;
cout << s1.capacity() << endl;*/
/*yss::string s1("hello yss");
cout << s1.empty() << endl;
cout << s1.size() << endl;
cout << s1.c_str() << endl;
cout << s1.capacity() << endl;
s1.clear();
cout << s1.empty() << endl;
cout << s1.size() << endl;
cout << s1.c_str() << endl;
cout << s1.capacity() << endl;*/
/*yss::string s1("hello yss");
s1.push_back('a');
cout << s1.c_str() << endl;*/
/*yss::string s1("hello yss");
s1.append("abcd");
cout << s1.c_str() << endl;*/
/*yss::string s1("hello yss");
s1 += 'a';
cout << s1.c_str() << endl;
yss::string s2("hello yss");
s2 += "abcd";
cout << s2.c_str() << endl;*/
/*yss::string s1("hello yss");
s1.insert('a', 3);
cout << s1.c_str() << endl;*/
/*yss::string s1("hello yss");
s1.insert("abcd", 3);
cout << s1.c_str() << endl;*/
/*yss::string s1("hello yss");
s1.erase(3);
cout << s1.c_str() << endl;
yss::string s2("hello");
s2.erase(2, 2);
cout << s2.c_str() << endl;*/
/*yss::string s1("hello yss");
size_t p = s1.find('o');
cout << p << endl;
p = s1.find("ss");
cout << p << endl;*/
/*yss::string s1("hello yss");
yss::string s2 = s1.substr(1, 7);
cout << s2.c_str() << endl;*/
/*yss::string s1("hello yss");
cout << s1 << endl;*/
/*yss::string s1;
cin >> s1;
cout << s1 << endl;*/
/*yss::string s1("hello");
cout << s1 << endl;
cin >> s1;
cout << s1 << endl;*/
yss::string s1("hello");
yss::string s2("aabbc");
cout << (s1 > s2) << endl;
cout << (s1 == s2) << endl;
cout << (s1 >= s2) << endl;
cout << (s1 < s2) << endl;
cout << (s1 <= s2) << endl;
cout << (s1 != s2) << endl;
return 0;
}