目录
1.构造函数
模拟实现
2.析构函数
模拟实现
3.string遍历
3.1 c_str、size、lenth、capacity等
模拟实现
3.2 字符串元素访问
3.2.1 []操作符重载、at
模拟实现
3.2.2 front、back等
3.3 迭代器
模拟实现
4.赋值操作
4.1 赋值重载函数
模拟实现
4.2 assign函数
编辑
5.size与capacity修改
5.1 shrink_to_fit缩容、reserve扩容
模拟实现
5.2 resize函数
模拟实现
5.3 clear函数
模拟实现
6.增删查改
6.1 string插入
6.1.1 push_back尾插字符
模拟实现
6.1.2 append尾插字符串
模拟实现
6.1.3 +=操作符重载
模拟实现
6.1.4 insert随机插入
模拟实现
6.2 string删除
模拟实现
6.3 string替换
6.4 string查找
模拟实现
6.5 字符串截取
模拟实现
7.交换string对象的值
模拟实现
8.比较操作符重载
模拟实现
9.输入和输出
9.1 流插入
9.1.1 << 流插入运算符重载
模拟实现
9.2 流提取
9.2.1 >> 流提取操作符重载
模拟实现
9.2.2 getline函数
模拟实现
10.拷贝构造函数、赋值重载运算符的改进
在C语言中我们在处理字符串时采取的方案是使用一个字符数组,这样的方式对于我们管理和使用字符串会有一些麻烦。所以在C++中,一方面兼容了C语言中的字符串形式,另一方面则是通过内置的string类来进行管理。C++内置的string类配备了许多方便的成员函数,这对我们使用C++处理字符串提供了极大的便利,这篇文章主要是总结string类的使用方法以及一些特点,同时对于一些常用的函数我们会自主模拟一份,以帮助我们更细致地掌握细节。
1.构造函数
对于一个类而言,首要考虑的就是构造函数。C++为string的构造重载了许多函数,在使用string类的时候需要注意包含头文件 #include<string> 。
void Test1()
{
//default (1): string();
//copy(2): string(const string& str);
//substring(3): string(const string& str, size_t pos, size_t len = npos);
//from c - string(4): string(const char* s);
//from sequence(5): string(const char* s, size_t n);
//fill(6): string(size_t n, char c);
string s1;
string s2("hello world!");
string s3(s2);
string s4(s2,3,5);
string s5(s2,3,10);
string s6(s2,3);
string s7("good luck", 3);
string s8(7,'#');
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
cout << s5 << endl;
cout << s6 << endl;
cout << s7 << endl;
cout << s8 << endl;
}
对于以上的构造函数进行解释:
(1)默认构造;
(2)拷贝构造;
(3)部分拷贝:str字符串的pos下标开始len个长度。如果超出str范围或未传参则会一直拷贝到结尾。npos值为-1,对于size_t则是最大值;
(4)使用C-string构造;
(5)拷贝s字符串的前n个字符;
(6)n个c字符构造;
模拟实现
在我们模拟实现一个string类的时候首先需要注意命名空间的问题。标准库中的标识符存在std中,所以我们需要重新定义一个命名空间,并在该命名空间中完成和string类有关的函数的实现,这样因为命名空间隔绝,所以可以放心使用函数名。在类外使用自主实现的string时需要记得指定命名空间域。
string类中有三个成员变量:存储字符串的指针char*,记录字符串长度的整型和记录存储长度上限的整型。
构造函数初始化使用初始化列表,要注意成员变量的初始化。需要注意的是实际开辟的空间大小需要比capacity大一个,因为需要存储一个'\0'。
using namespace std; //展开std命名空间,对标识符的搜索先局部在全局,所以在以下m_string的命名空间中,所有标识符会优先搜索局部,即m_string中的标识符
namespace m_string //定义一个新的命名空间,将我们实现的string放在其中,防止和std中的string产生冲突
{
class string
{
public:
//构造函数
string(const char* str = "") //构造函数,全缺省输入c字符串,当无输入的时候""也有一个\0,不会产生错误
:_size(strlen(str)),
_capacity(_size),
_str(new char[_capacity + 1]) //初始化列表,初始化顺序按照成员变量声明顺序,_str需要多开一个空间存储\0
{
strcpy(_str, str); //使用strcpy进行赋值操作
}
//拷贝构造函数
string(const string& s) //string对象中有_str,需要深拷贝
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
private:
size_t _size;
size_t _capacity;
char* _str;
}
}
2.析构函数
析构函数的用法没什么特殊的,销毁对象的时候自动调用。
模拟实现
因为是释放char*的空间,析构函数需要自己写一份。
//析构函数
~string() //由于_str,所以需要主动实现析构函数
{
delete[] _str;
_str = NULL;
_capacity = _size = 0;
}
3.string遍历
3.1 c_str、size、lenth、capacity等
完成string遍历的操作首先需要有手段获取string对象的信息,string类中有许多函数来提供访问对象的基本信息的渠道。
void Test1()
{
//length和size没有区别,都是返回字符串长度
string s1("Say hello to the world!");
cout << s1.size() << endl;
cout << s1.length() << endl;
//最大字符串长度(很少使用)
cout << s1.max_size() << endl;
//返回容量
cout << s1.capacity() << endl;
}
模拟实现
//c_str,作用是返回一个C字符串
const char* c_str() const //函数内和返回值都不会修改,所以const修饰this指针,const修饰返回值
{
return _str;
}
//size,返回string的长度
size_t size() const
{
return _size;
}
//capacity,返回string容量
size_t capacity() const
{
return _capacity;
}
3.2 字符串元素访问
除此之外,我们还需要能够像C语言一样通过下标等方式访问字符串的元素。因此,C++中也有不同的成员函数来提供诸多访问方式。
3.2.1 []操作符重载、at
在C++中,我们可以看到[]操作符被重载了两份,分别对应const和非const对象。可以通过下标访问对应位置的字符,at和[]的功能一致。
string s1("hello world");
for (int i = 0; i < s1.size(); i++)
//size()函数返回字符串长度,不包含'\0'
{
cout << s1[i] << ' ';
s1[i]++;
cout << s1[i] << ' ';
//[]重载可读可写
//char& operator[] (size_t pos);
//const字符串使用const修饰的[]重载,返回const char&,不可以写操作
//const char& operator[] (size_t pos) const;
}
cout << endl;
//[]、at()功能一样,返回对应位置字符
string s1("hello world");
cout << s1[3] << endl;
cout << s1.at(3) << endl;
模拟实现
//[]重载,需要重载const和非const版本,保证非const对象可以被修改,而const对象不可修改
char& operator[](size_t pos) //针对非const对象,返回对应下标字符的引用
{
return _str[pos];
}
const char& operator[](size_t pos) const //针对const对象,返回对应下标字符的const引用
{
return _str[pos];
}
3.2.2 front、back等
front和back函数用于返回头元素和尾元素,使用场景很少,其实现更多的是为了和其他STL容器保持一致。
//front()、back()分别返回头尾字符
cout << s1.front() << endl;
cout << s1.back() << endl;
3.3 迭代器
迭代器的主要作用就是遍历容器内的元素,使用迭代器有利于面向对象的封装特性,迭代器在提供遍历方案的同时隐藏了容器的具体实现方法,更加安全。除此之外,使用迭代器可以让我们不用在乎容器中的数据类型,迭代器会自动帮我们处理,这就让其使用起来更加方便。
迭代器在使用时需要定义一个iterator类型的变量,使用string类中的begin函数与end函数,获得容器中第一个元素和最后一个元素的标识,然后进行遍历访问即可。
//迭代器,it类似于一个指针,通过it访问元素时需要解引用,begin和end分别返回第一个和'\0'的位置
string::iterator it = s2.begin();
while (it != s2.end())
{
*it = '*';
it++;
}
cout << s2 << endl;
//范围for循环:底层也是迭代器
for (auto c : s2)
{
cout << c << endl;
}
迭代器会根据迭代方向和迭代对象是否为const类型进行一些变化。反向迭代器开始位置为rbegin,终止位置为rend,在迭代过程中使用reverse_iterator变量,并且以++来迭代。const迭代器则是需要const_iterator变量。
void Test1()
{
//迭代器:正向、反向、const、非const
string s1("hello world");
//正向非const迭代器
string::iterator it = s1.begin();
while (it != s1.end())
{
cout << *it << ' ';
it++;
}
cout << endl;
//反向非const迭代器
string::reverse_iterator rit = s1.rbegin();
while (rit != s1.rend())
{
cout << *rit << ' ';
rit++; //注意是++
}
cout << endl;
const string s2("hello world");
//正向const迭代器
string::const_iterator c_it = s2.begin();
while (c_it != s2.end())
{
cout << *c_it << ' ';
c_it++;
}
cout << endl;
//反向const迭代器
string::const_reverse_iterator c_rit = s2.rbegin();
while (c_rit != s2.rend())
{
cout << *c_rit << ' ';
c_rit++;
}
cout << endl;
}
模拟实现
对于迭代器,不同的容器使用的方法是不同的。在string类中,采取了类似于指针的方式完成迭代器,其实际实现可能不是指针,但是使用指针也可以达到相似的迭代效果,所以我们使用指针模拟一个迭代器。
//非const迭代器
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
//const迭代器
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
4.赋值操作
4.1 赋值重载函数
赋值重载函数也有多个重载形式,支持使用string对象、c字符串和字符进行赋值。注意的一点是赋值是会覆盖原字符串的。
void Test2()
{
//string(1): string& operator= (const string & str);
//c - string(2): string & operator= (const char* s);
//character(3): string& operator= (char c);
string s1("hello");
string s2;
s2 = s1;
cout << s2 << endl;
s2 = "world";
cout << s2 << endl;
s2 = '&';
cout << s2 << endl;
}
对以上重载形式解释:
(1)用string对象赋值;
(2)用C-string赋值;
(3)用字符赋值。
模拟实现
由于涉及到深拷贝,所以需要自己实现赋值重载函数,开辟空间并将信息拷贝过来即可。
//赋值重载函数
string& operator=(string& s)
{
char* tmp = new char[s._capacity + 1]; //使用一个临时字符数组开辟空间,保存待拷贝的字符串
strcpy(tmp, s._str);
delete[] _str; //原string对象可能已有值,所以需要先释放空间,然后再把新开辟的带有已有字符串的空间赋值给对象
_str = tmp;
_size = s._size;
_capacity = s._capacity;
return *this;
}
4.2 assign函数
除了赋值重载操作符外,string类还提供了另一个名为assign的函数同样具有赋值操作,但是一般很少使用,当有赋值需求时一般会用 = 重载。
string s1("hello world");
cout << s1 << endl;
//assign()
//赋值(覆盖)
s1.assign("hello");
cout << s1 << endl;
5.size与capacity修改
5.1 shrink_to_fit缩容、reserve扩容
shrink_to_fit实现的是缩容操作,该函数会将对象的capacity缩减至合适位置,即会将capacity缩小至与size相等,即所有空间都被使用的合适位置。
reserve实现的是对capacity的控制,对于reserve的参数n,当n>capacity的时候,会进行扩容操作,将容量扩展至n;当n<=capacity时,函数不做任何动作,直接返回。
string s1("Say hello to the world!");
//缩容
s1.shrink_to_fit();
cout << s1.capacity() << endl;
//扩容(自动扩容)
s1.reserve(40); //扩容到参数值大小,当reverse参数大于capacity才会执行
cout << s1.capacity() << endl;
模拟实现
//reserve(size_t n):重设capacity
// n>capacity:扩容
// n<=capacity:不做处理
void reserve(size_t n)
{
if (n > _capacity) //当n<=_capacity直接return
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
5.2 resize函数
resize函数用于修改对象的size。当参数n大于对象的size时,则会将size扩大至n,扩大的部分则由字符参数c来填充,如果没有输入c则使用'\0'来填充。当参数n小于等于size时,则会将字符串截断,使其的size为n。
//void resize(int n,char c = '\0'),重设字符串size大小
//n < size 删除
//size < n < capacity 插入
//capacity < n 扩容并插入
string s2("hello world");
s2.resize(9);
cout << s2 << endl;
cout << s2.capacity() << endl;
s2.resize(15,'&');
cout << s2 << endl;
cout << s2.capacity() << endl;
s2.resize(20,'#');
cout << s2 << endl;
cout << s2.capacity() << endl;
模拟实现
需要注意,修改size需要时刻关注capacity的大小,可以使用reserve函数,来让capacity设置成为我们想要的大小。
//resize(int n,char c):
// n>size:将size扩大至n,扩展出的空间使用参数c进行填充
// n<=size:将size缩小至n,直接截断原字符串
void resize(size_t n, char c = '\0')
{
if (n > _size)
{
reserve(n); //扩容,如果不需要扩容reserve就不会发生作用
for (size_t i = _size; i < n; i++)
{
_str[i] = c;
}
}
_str[n] = '\0'; //n<size,就截断;n>size,就在末尾补'\0'
_size = n;
}
5.3 clear函数
clear函数可以将字符串清空,也就是设置size为0。
string s1("Say hello to the world!");
//清空数据,不对容量做修改
s1.clear();
cout << s1 << endl;
cout << s1.capacity() << endl;
模拟实现
//clear:将字符串置为空
void clear()
{
_size = 0; //置空只需要将size置为0,然后注意字符串以'\0'结尾
_str[_size] = '\0';
}
6.增删查改
6.1 string插入
6.1.1 push_back尾插字符
push_back函数支持在string对象的字符串后尾插元素,注意push_back函数仅仅是尾插一个字符,不支持尾插字符串。
string s1("hello world");
cout << s1 << endl;
//push_back(char c)
//在字符串尾插字符
s1.push_back('!');
cout << s1 << endl;
模拟实现
//push_back:尾插字符
void push_back(char c)
{
if (_size == _capacity) //判断容量是否足够
{
reserve(_capacity == 0 ? 4 : 2 * _capacity); //扩容交给reserve,需要考虑capacity==0的情况
}
_str[_size] = c;
++_size;
_str[_size] = c;
}
6.1.2 append尾插字符串
append函数支持尾插一个字符串,而根据字符串来源的不同,C++中提供了很多重载形式来支持,比如string对象的全部或部分尾插,c字符串等。
string s1("hello world");
cout << s1 << endl;
//append()--append有许多接口
//在字符串尾插字符串
s1.append("have a good time");
cout << s1 << endl;
模拟实现
//append:尾插字符串
void append(const char* str) //使用c_str做参数,类型是const char*,所以如果参数写成char*,权限放大会报错
{
size_t len = strlen(str);
reserve(_size + len);
strcpy(_str + _size, str); //strcpy会自动补一个'\0'
_size += len;
}
6.1.3 +=操作符重载
当有尾插需求时,一般更习惯使用+=操作符,因为+=操作符重载多个函数后,字符和字符串的尾插都可以实现,而且逻辑清晰易懂,代码简洁。
string s1("hello world");
cout << s1 << endl;
//+=
//尾插字符或字符串
s1 += '!';
cout << s1 << endl;
s1 += "Perfect";
cout << s1 << endl;
模拟实现
//+=运算符重载:1.尾插字符;2.尾插字符串
string& operator+=(char c)
{
push_back(c);
return *this;
}
string& operator+=(const char* str) //使用c_str做参数,类型是const char*,所以如果参数写成char*,权限放大会报错
{
append(str);
return *this;
}
6.1.4 insert随机插入
除了以上所说的尾插外,我们势必会面临在任意位置插入字符或字符串的需求,这个时候就需要使用insert函数了。insert函数支持任意指定下标位置的字符,字符串插入。
//insert()
//在pos位置插入
string s1("hello");
s1.insert(3, " *** ");
cout << s1 << endl;
模拟实现
insert模拟实现我们需要重载两个版本,分别支持字符和字符串的插入。需要注意insert函数实现时下标的关系,防止越界情况发生,具体细节可以参考代码注释。
//insert:1.在pos下标位置插入字符;2.在pos下标位置插入字符串
void insert(size_t pos, char c)
{
assert(pos <= _size);
if (_size == _capacity) //判断容量是否足够
{
reserve(_capacity == 0 ? 4 : 2 * _capacity); //扩容交给reserve,需要考虑capacity==0的情况
}
size_t end = _size + 1; //注意此处end是交换后的下标,所以极端情况(头插)下最小为0,可以用size_t
//如果end=_size则是交换前的下标,极端情况下最小为-1,所以只可以使用int类型
while (end > pos) //最后一次交换:pos位置换到pos+1位置,end是交换后的下标,所以大于pos即可
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = c;
++_size;
}
void insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
reserve(_capacity + len);
size_t end = _size + len; //注意此处end是交换后的下标,所以极端情况(头插)下最小为0,可以用size_t
//如果end=_size则是交换前的下标,极端情况下最小为-1,所以只可以使用int类型
while (end > pos + len - 1) //最后一次交换:pos位置换到pos+len的位置,end是交换后的下标,所以需要大于pos+len-1
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
}
6.2 string删除
string字符串删除需要函数erase来完成,erase会删除从pos位置开始的len个长度的字符。
我们在这里发现了参数len的缺省值为npos,那么这个npos是什么呢,通过查询我们发现npos实际上是string类内定义的一个静态成员变量,是一个无符号整型的-1值,换言之就是整型的最大值。所以我们可以理解,当没有传入长度参数时,使用缺省值删除2^32-1个长度的字符,也就是将pos位置后的字符全部删除的意思。
string s1("hello");
//在pos位置删除len长度字符串,缺省pos从开头删,缺省len删到结尾为止
s1.erase(3, 2);
cout << s1 << endl;
模拟实现
在实现erase之前,我们也需要定义一个静态成员变量npos,静态成员变量在类内声明,类外定义。
//string类内
public:
static const size_t npos; //声明静态成员变量npos,属于整个类共享
//string类外
const size_t m_string::string::npos = -1; //静态成员变量在类内声明,在类外定义,需要注意npos的所在域:命名空间m_string内的类string的静态成员npos
在完成erase时,需要注意len==npos的情况,因为这种情况很有可能造成我们判断的时候溢出,从而产生bug。
//erase:删除pos位置开始的len个字符
void erase(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
if (len >= _size - pos) //截断的情况
//删除的末尾位置超过或就是字符串的最后一个字符len+pos>=_size,这个表达式len+pos可能会溢出;或者剩余字符长度小于等于待删除字符长度len>=_size-pos,不存在溢出风险
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len); //直接将后续字符覆盖到pos位置
_size -= len;
}
}
6.3 string替换
replace函数完成string中部分字符替换的功能,使用场景不是很多,只需要知道有这么一个函数支持这个功能就好。replace会将pos位置开始的len个字符替换为指定字符串。
//replace(pos, len, str)
//把pos位置开始的len长度替换为str字符串
s1.replace(3, 2, "&& & &&");
cout << s1 << endl;
6.4 string查找
string类对于字符串查找给出了许多函数。分别支持顺序查找、倒序查找、找第一个匹配的、找最后一个匹配的、找第一个不匹配的、找最后一个不匹配的等。在这些函数中,我们最应该了解的就是最纯真的find函数了。
//从前往后、从后往前找与字符串中包含的字符相匹配、不匹配的字符
//size_t find_first_of (const string& str, size_t pos = 0) const;
//size_t find_last_of (const string& str, size_t pos = npos) const;
//size_t find_first_not_of(const string & str, size_t pos = 0) const;
//size_t find_last_not_of (const string& str, size_t pos = npos) const;
string s1("hello world");
cout << s1.find_first_of("aeiou") << endl;
cout << s1.find_last_of("aeiou") << endl;
cout << s1.find_first_not_of("aeiou") << endl;
cout << s1.find_last_not_of("aeiou") << endl;
find函数也给出了很多查找的重载形式,但是使用方式一致,查找pos位置开始的字符、字符串。如果找到了就会返回对应位置的下标,否则返回npos。
//size_t find(str/char, pos)
//从pos位置找对应的字符串/字符,找到返回下标,没找到返回npos
size_t pos = 0;
while ((pos = s1.find(' ', pos)) != string::npos)
{
s1.replace(pos, 1, "##");
}
cout << s1 << endl;
模拟实现
//find:寻找字符串中pos开始首个匹配的字符、子串的下标,找不到就返回npos
size_t find(char c, size_t pos = 0) const
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0) const
{
assert(pos < _size);
const char* tmp = strstr(_str, str);
if (tmp == nullptr)
{
return npos;
}
else
{
return tmp - _str;
}
}
6.5 字符串截取
substr函数的作用是截取字符串,substr会将从pos位置开始的len长度的字符截取出来创建一个新的string对象并返回。
//size_t find (const string& str / char c, size_t pos = 0) const; find从pos位置开始查找指定字符串、字符,返回下标
//size_t rfind (const string& str, size_t pos = npos) const; rfind,从后往前找
//string substr(size_t pos = 0, size_t len = npos) const; 取出pos开始len长的子串
string s2("abc#def#ghi");
cout << s2.substr(s2.find('#')) << endl;
cout << s2.substr(s2.rfind('#')) << endl;
模拟实现
//substr:将pos位置开始len长度的字符串作为string对象
string substr(size_t pos = 0, size_t len = npos)
{
assert(pos < _size);
string ret;
if (len < _size - pos) //拷贝不到原字符串结尾,避免溢出
{
for (size_t i = pos; i < pos + len; i++) //拷贝到pos+len
{
ret += _str[i];
}
}
else //拷贝剩余所有字符
{
for (size_t i = pos; i < _size; i++) //拷贝到_size
{
ret += _str[i];
}
}
return ret;
}
7.交换string对象的值
在c++中,string类中实现了swap函数,可以通过swap函数来交换两个string对象的值。在这里不得不提的是,C++中有swap函数模板,对于基本数据类型而言,编译器会自动推导数据类型并使用模板生成对应的函数完成交换操作。而对于自定义类型则需要自己实现,针对swap函数string类就自主实现了交换的操作。
但是我们知道类的成员函数的参数默认第一个是this指针,而且在调用时需要使用s1.swap(s2);的方式进行调用,这和我们常规的swap函数的使用方式有一些不同。所以出于方便考虑,string在类外定义了一个swap函数,以包裹类内的swap函数以完成swap(s1,s2);的调用方式。
模拟实现
//string类内
//swap:交换两个string对象
void swap(string& s) //swap在C++中以函数模板存在,所以对于自定义类型需要自主实现
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
//string类外
//在使用swap时一般会用swap(s1,s2)的形式,所以需要再写一个swap来满足该种形式的调用
void swap(string& s1, string& s2)
{
s1.swap(s2);
}
8.比较操作符重载
string中实现了compare成员函数进行比较,但是在习惯上我们更喜欢使用比较操作符重载来完成比较操作。
比较操作符被string类重载为了类外函数,这么做的目的主要是满足特殊的比较情况。因为字符串比较有可能将一个const char*类型的对象放在操作符的左边,但是成员函数的第一个参数this指针必然是操作符第一个操作数,因此就无法支持这种情况了,所以在类外实现重载。
模拟实现
在实现比较操作符重载时,我们只需要重载两个参数都是string对象的即可,因为我们使用的构造函数是单参数构造函数,如果使用C字符串则会存在隐式类型转换,将字符串类型转化变为string对象。
//比较操作符重载,因为需要支持char*和string比较,所以全局重载,若在类中重载,那么this指针必然是第一个参数,就无法支持char*在==的左侧了
//由于单参数构造支持隐式类型转换,所以char*会变为string类型进行比较,因而支持了string和char*之间的任意次序的比较
bool operator==(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) == 0;
}
bool operator!=(const string& s1, const string& s2)
{
return !(strcmp(s1.c_str(), s2.c_str()) == 0);
}
bool operator<(const string& s1, const string& s2)
{
return strcmp(s1.c_str(), s2.c_str()) < 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);
}
9.输入和输出
之前在日期类的实现中,我们提到过流插入、流提取为了满足参数顺序关系所以定义在类外。
9.1 流插入
9.1.1 << 流插入运算符重载
模拟实现
可以使用迭代器来进行流插入。
//流插入
ostream& operator<<(ostream& out, const string& s)
{
for (auto e : s) //使用迭代器遍历,因为编译器支持内置类型的流插入,所以直接使用就好
{
out << e;
}
return out;
}
9.2 流提取
9.2.1 >> 流提取操作符重载
对于流提取操作符而言,c++规定遇到空格字符或换行符则标志着一次提取完成,换言之>>遇到空格或换行就结束。
模拟实现
在实现中我们需要注意:
①流提取会覆盖原来的string对象内的数据,所以需要清空再写入;
②流提取碰到空格或换行就结束,同时c++内的>>操作符读取不到空格和换行字符,所以需要istream的成员函数get来读取字符;
③为了更好的提高效率,我们可以构造类似于缓冲区的结构,对读取的字符进行缓冲并整体写入。
//流提取:遇到空格字符或换行字符就结束
istream& operator>>(istream& in, string& s)
{
s.clear(); //流提取是覆盖原数据,所以需要先清空
char buff[128];
size_t cou = 0;
char c;
//in >> c; //C++规定:流提取自动忽略空格字符和换行字符
c = in.get(); //使用istream类的成员函数get来读取字符
while (c != ' ' && c != '\n') //当没有读到空格字符和换行字符时就继续
{
//s += c; //一个字符一个字符的尾插效率不高
//使用一个buff数组当做缓冲区,当缓冲区满了之后一并尾插,提高效率
buff[cou++] = c;
if (cou == 127)
{
buff[cou] = '\0';
s += buff;
cou = 0;
}
c = in.get();
}
if (cou > 0) //最后缓冲数组还有数据,则全部尾插
{
buff[cou] = '\0';
s += buff;
}
return in;
}
9.2.2 getline函数
与流提取操作符不同,getline会读取一行数据,即会读取空格,遇到换行才会停止。
模拟实现
getline和>>操作符实现只在判断读取终点的时候有所不同,其余部分思路代码一致。
//getline:只在遇到换行字符结束
// 和流插入只在while判断处不同
istream& getline(istream& in, string& s)
{
s.clear();
char buff[128];
size_t cou = 0;
char c;
c = in.get();
while (c != '\n') //没有读到换行字符时就继续
{
buff[cou++] = c;
if (cou == 127)
{
buff[cou] = '\0';
s += buff;
cou = 0;
}
c = in.get();
}
if (cou > 0)
{
buff[cou] = '\0';
s += buff;
}
return in;
}
10.拷贝构造函数、赋值重载运算符的改进
对于拷贝构造和赋值重载运算符,我们的策略是对string对象中的每个成员变量进行拷贝复制。但是如果面对成员变量很多的对象,这种方法就会造成代码的增加,写起来会很麻烦。所以我们可以考虑函数复用的方式来减少我们所需要写的代码。
在实现复用前,我们需要强调:①局部对象在出函数后会自动销毁;②传引用传参时不会进行拷贝,而传对象传参则会调用拷贝函数,创建一个函数内的局部变量。
拷贝构造函数
//string(const string& s) //string对象中有_str,需要深拷贝
//{
// _str = new char[s._capacity + 1];
// strcpy(_str, s._str);
// _capacity = s._capacity;
// _size = s._size;
//}
//拷贝构造函数——函数复用
string(const string& s)
{
string tmp(s._str);
swap(tmp);
//创建临时变量tmp,交换tmp和*this,这样*this就完成了拷贝,同时出函数后指向空的对象tmp会被销毁
}
拷贝构造函数可以通过调用构造函数创建一个和参数一样的局部变量,然后将这个局部变量交给this指针,而把this指针的值交给局部变量,这样就可以将创建的对象带回了。
赋值重载函数
//string& operator=(string& s)
//{
// char* tmp = new char[s._capacity + 1]; //使用一个临时字符数组开辟空间,保存待拷贝的字符串
// strcpy(tmp, s._str);
// delete[] _str; //原string对象可能已有值,所以需要先释放空间,然后再把新开辟的带有已有字符串的空间赋值给对象
// _str = tmp;
// _size = s._size;
// _capacity = s._capacity;
//
// return *this;
//}
赋值重载函数——函数复用1
//string& operator=(string& s)
//{
// string tmp(s);
// swap(tmp);
// return *this;
// //拷贝构造创建临时对象tmp,然后和this交换,this就是和s相同的对象,而tmp在出作用域就销毁
//}
//赋值重载函数——函数复用2
string& operator=(string tmp)
{
swap(tmp);
return *this;
//函数传参传了对象,所以会默认调用一次拷贝构造构造tmp,然后交换tmp和this,出了函数就会销毁tmp
}
赋值重载运算符也可以创造一个相同的局部变量,然后和this指针交换带回,而原来this指针的对象因为换给了tmp,所以离开了函数就会被销毁。也可以使用传值传参的形式完成拷贝函数的调用,创建局部对象。
如上的改进并不会对效率产生影响,但是由于函数复用,将我们所要完成的代码交给了已经实现的函数去处理可以提高我们的工作效率。