【C++】—— string模拟实现
- 0 前言
- 1 string的底层结构
- 2 默认成员函数的实现
- 2.1 构造函数
- 2.1.1 无参构造
- 2.1.2 带参构造
- 2.1.2 合并
- 2.2 析构函数
- 2.3 拷贝构造函数
- 2.3.1 传统写法
- 2.3.2 现代写法
- 2.3 赋值重载
- 2.3.1 传统写法
- 2.3.2 现代写法
- 2.3.3 传统写法与现代写法的优劣
- 3 size、capacity、operator[] 的实现
- 4 正向迭代器的模拟实现
- 4.1 问题引出
- 4.2 模拟实现
- 4.3 const 迭代器
- 4.4 进一步理解封装
- 5 string 增删查改的模拟实现
- 5.1 reserve 的模拟实现
- 5.2 push_back 的模拟实现
- 5.3 append 的模拟实现
- 5.4 operator+= 的模拟实现
- 5.5 insert 的模拟实现
- 5.5.1 插入一个字符
- 5.5.2 插入字符串
- 5.6 erase 的模拟实现
- 5.6.1 npos 的定义和声明
- 5.6.2 实现 erase
- 6 string 查找的模拟实现
- 6.1 find 的模拟实现
- 6.1.1 查找字符
- 6.1.2 查找字符串
- 6.2 substr 的模拟实现
- 7 非成员函数的模拟实现
- 7.1 operator 比较系列
- 7.2 operator<<
- 7.3 operator>>
- 10 完整代码
- 10.1 string.h
- 10.2 string.cpp
0 前言
为了让我们更加深入理解 string
,接下来我们将模拟实现一个简易版的
s
t
r
i
n
g
string
string。而为了和STL库
中的
s
t
r
i
n
g
string
string 以示区分,我们将使用命名空间namespace
对其封装。
1 string的底层结构
s
t
r
i
n
g
string
string 简单来说就是一个被封装的可动态增长的数组,其的底层结构我们可以类比顺序表
来实现
namespace my_string
{
class string
{
public:
//···
private:
char* _str;//指向字符串的指针
size_t _size;//有效字符个数(不包括'\0')
size_t _capacity;//有效字符个数(不包括'\0')
};
}
2 默认成员函数的实现
2.1 构造函数
构造函数,我们可以写一个无参构造的和一个带参构造
2.1.1 无参构造
在学习类与对象时(【C++】—— 类与对象(二)),我们曾讲过成员变量尽量走初始化列表
:
string()
:_str(nullptr)
,_size(0)
,_capacity(0)
{}
但是,库中的
s
t
r
i
n
g
string
string,无参时并不是空指针,而是放着一个空字符
,我们进行以下修改:
string()
:_str(new char(''))
, _size(0)
, _capacity(0)
{}
这里,我们开辟一个字节空间放空字符串,也就是 ‘\0’
因为 _capacity
显示的空间大小是 不包括 ‘\0’ 的,因此 _
c
a
p
a
c
i
t
y
capacity
capacity 显示的空间大小永远比实际开辟的空间小一个。这里开辟了一个空间放 '/0'
,而 _
c
a
p
a
c
i
t
y
capacity
capacity为 0
2.1.2 带参构造
再写个带参的:
string(const char* str)
:_str(new char[strlen(str) + 1])
, _size(strlen(str))
, _capacity(_size)
{
strcpy(_str, str);//将str中的内容拷贝
}
上述代码中,要多开辟一个空间,因为 s t r l e n strlen strlen 是不计算 ‘\0’ 的。
不难发现带参的构造函数用初始化列表初始化并不太好,因为要调用 s t r l e n strlen strlen函数 2 次。即使初始化列表中的 _ s t r str str 和 _ s i z e size size 位置互换也没用,因为初始化列表的初始化顺序是根据成员变量在类中声明的顺序进行初始化的。
这时,我们可以使用函数体初始化
,虽然默认也会走初始化列表,但都是内置类型,并不影响。
初始化列表只是推荐使用,但并不是所有情况都适合
string(const char* str)
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];//实际开辟空间比_capacity多一
strcpy(_str, str);//将str中的内容拷贝
}
这里再多提一句,_
c
a
p
a
c
i
t
y
capacity
capacity 不计算 '\0'
,因此虽然实际开的空间比 _
s
i
z
e
size
size 多一个,但 _capacity 等于_size
2.1.2 合并
其实我们可以将上述无参和带参的构造函数合二为一
。
怎么做呢?用一个缺省参数就好啦
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
2.2 析构函数
析构函数的实现很简单,只需要把资源释放就行
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
2.3 拷贝构造函数
在学习了前面类与对象中(【C++】—— 类与对象(三)),我们知道:对于有指向资源的类,我们必须要自己实现拷贝构造,实现深拷贝。
2.3.1 传统写法
拷贝构造的传统写法很简单,直接调用
s
t
r
c
p
y
strcpy
strcpy 函数拷贝
就行了
string(const string& s)
{
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
2.3.2 现代写法
拷贝构造除了传统的写法
,还有一种现代写法
。
现代写法的通俗来说就是窃取别人的劳动成果,什么意思呢?我们先来看代码
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
原理如下:
- 先用传入的 s s s 对象的字符串构造 t m p tmp tmp 对象(此时调用的是
构造函数
)- 再将 * t h i s this this 与 t m p tmp tmp 交换
- 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦
- 最后
出了函数作用域
,交换后的 t m p tmp tmp 调用析构后销毁
注:
s
w
a
p
swap
swap 交换函数在上一章已讲解,有疑惑的小伙伴可移步至:【C++】—— string 类的了解与使用
可以说,现代写法的活全是构造函数干的,拷贝构造其实是用 s w a p swap swap 窃取构造函数的劳动成果,从而达到拷贝构造的目的。
注:上述代码还是有缺陷的。因为在交换前 (*this)._str
仅仅是在建立栈帧时开了空间,其本身随机值。交换给
t
m
p
tmp
tmp,
t
m
p
tmp
tmp 出了函数作用域会调用析构函数,此时
t
m
p
tmp
tmp 释放的是一个随机的地址,是个野指针。因此,最好在声明处给 _
s
t
r
str
str 一个默认值(缺省值):
n
u
l
l
p
t
r
nullptr
nullptr
namespace my_string
{
class string
{
public:
//···
private:
char* _str = nullptr;
size_t _size = 0;
size_t _capacity = 0;
};
}
2.3 赋值重载
赋值重载与拷贝构造相似,我们一起来看看
2.3.1 传统写法
string& operator=(const string& s)
{
if (this != &s)
{
delete[] _str;
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
我们知道,赋值会将原来的数据覆盖掉,
s
t
r
i
n
g
string
string 也应如此
赋值重载要先将旧空间释放掉,再开辟新空间进行拷贝
注:存在自己给自己赋值的情况要单独处理
2.3.2 现代写法
与拷贝构造一样,赋值重载也有现代写法
,且原理类似
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s._str);
swap(tmp);
}
return *this;
}
- 先用传入的
s 对象
的字符串构造 tmp 对象
(此时调用的是构造函数
)- 再将 * t h i s this this 与 t m p tmp tmp 交换
- 然后 * t h i s this this 的内容就是传入的 s s s 对象的内容啦
这种写法不仅仅是交换那么简单,还将 t h i s this this 原来的资源交给了 t m p tmp tmp,这样 t m p tmp tmp 出函数作用域还能顺便将 t h i s this this 旧的资源释放掉。
赋值的现代写法还有一个简化版本:
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
这里用了传值传参
,程序调用拷贝构造产生形参 tmp
,
t
h
i
s
this
this 再与
t
m
p
tmp
tmp 交换,出了作用域
t
m
p
tmp
tmp 销毁。
这里不再判断是否自己给自己赋值是因为:当走入函数体时,拷贝已经完成
了,此时再判断已经没意义
了。而且即使是自己给自己赋值也不会出问题,只是效率低了点。
2.3.3 传统写法与现代写法的优劣
其实,传统写法与现代写法在效率上并没有区别,他们都要去完成拷贝,只是是谁来完成这份工作的问题而已
3 size、capacity、operator[] 的实现
s i z e size size 与 c a p a c i t y capacity capacity 的实现很简单,我们直接上代码
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
operator[]
的实现也同样很简单,但需要注意的是
o
p
e
r
a
t
o
r
operator
operator[ ] 的实现要加断言以判断所给的下标是否合法
//普通版本
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
//const 版本
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
4 正向迭代器的模拟实现
4.1 问题引出
在【C++】—— string 类的了解与使用中,我们知道
s
t
r
i
n
g
string
string 的遍历有三种方式:下标访问
、迭代器访问
、范围 for
那范围for
那么好用,我们可不可以直接使用呢?
void test1()
{
string s1 = "hello world";
for (auto i : s1)
{
cout << i << " ";
}
}
可以看到,报错了。开始报错的内容很奇怪,说没有
b
e
g
i
n
begin
begin 函数,为什么呢
我们曾说过,范围for
并没有什么神奇的,其底层其实就迭代器
范围
f
o
r
for
for 与迭代器是一种傻瓜式的替换,并不看你实现了什么,只要名字相同就行
。
下面我们就一起来模拟实现
s
t
r
i
n
g
string
string 的迭代器
4.2 模拟实现
我们知道,迭代器模拟的是指针
的行为,而
s
t
r
i
n
g
string
string 类的指针
c
h
a
r
char
char* 完美满足迭代器的各种需求,因此我们直接用指针作为迭代器
我们直接用原生指针
t
y
p
e
d
e
f
typedef
typedef 一个
i
t
e
r
a
t
o
r
iterator
iterator 出来
class string
{
public:
//···
typedef char* iterator;
private:
//···
};
其实,所有的迭代器
i
t
e
r
a
t
o
r
iterator
iterator 都是
t
y
p
e
d
e
f
typedef
typedef 出来的,只是其他容器不像
s
t
r
i
n
g
string
string 这么简单除暴直接用指针
,他们可能是自定义类型
,经过封装后再
t
y
p
e
d
e
f
typedef
typedef 成
i
t
e
r
a
t
o
r
iterator
iterator
我们再把
b
e
g
i
n
begin
begin 和
e
n
d
end
end 实现出来
class string
{
public:
//···
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
private:
//···
};
我们来测试一下:
void test2()
{
string s1 = "hello world";
for (auto i : s1)
{
cout << i << " ";
}
cout << endl;
string s2 = "ni hao";
string::iterator it = s2.begin();
while (it != s2.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
可以看到,不仅迭代器访问
好了,范围for访问
也好了,可见范围
f
o
r
for
for 的底层其实就是迭代器。
4.3 const 迭代器
当然,还有 const迭代器
:
class string
{
public:
//···
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
private:
//···
};
4.4 进一步理解封装
迭代器的设计本质是一种 封装的体现
我们之前理解的封装
是这样的:
我把数据和方法都放到类里面,都封装起来,不能让你随便修改。想让你访问的设置成公有,不想让你访问的设置成私有
而迭代器的设计也是一种封装
有各种各样的容器,它们底层可能是顺序表、链表甚至可能是树。现在我不管你底层结构是什么,差别有多大,我都用一个叫
i
t
e
r
a
t
o
r
iterator
iterator 的东西来进行访问。
iterator
属于类域,它是个什么类型我不知道。现在你封装以后给我提供一个
b
e
g
i
n
begin
begin,那我就知道无论你是什么容器,这就是指向第一个有效数据位置
,
e
n
d
end
end 就是最后一个数据的下一个位置。我要访问就解引用,我要访问下一个位置就 ++
。
这样,我们就不需要再关心这个数据结构到底是顺序表还是链表还是树,iterator 屏蔽了底层的结构和实现细节,它提供了统一的接口,我们可以对不同的数据结果用类似的方式进行访问
5 string 增删查改的模拟实现
上面所实现的函数,大多都是一些比较短小的函数
,又或是需要频繁调用的函数
,我们将他们放在类中,以成内联函数,提高程序的效率
。下面的大部分函数,我们可以对其进行进行声明和定义的分离
5.1 reserve 的模拟实现
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
我们这里实现的 reserve
只扩容,不缩容,因此要先判断 n 是否 > _capacity
r
e
s
e
r
v
e
reserve
reserve 的整体实现思路如下:开辟更大的新空间 -> 将就空间数据拷贝至新空间 -> 释放就空间
char* tmp = new char[n + 1];
这里要记住,实际开空间要多开一个空间,为 ‘\0’ 留位置strcpy(tmp, _str);
拷贝数据delete[] _str;
释放旧空间_str = tmp;
指向新空间_capacity = n
;更新 _ c a p a c i t y capacity capacity
5.2 push_back 的模拟实现
void string::push_back(char ch)
{
if (_size == _capacity)
{
size_t n = _capacity == 0 ? 4 : 2 * _capacity;
reserve(n);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
既然是插入数据,那么一定要判断是否需要扩容
这里,我们调用
r
e
s
e
r
v
e
reserve
reserve 来完成扩容:如果 _
c
a
p
a
c
i
t
y
capacity
capacity 为 0,则开 4 个空间,否则扩容两倍
括完容就简单啦,在最后的位置插入
c
h
ch
ch,并将 _
s
i
z
e
size
size++,最后,千万别忘了要在最后补上 ‘\0’
5.3 append 的模拟实现
void string::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
reserve(n);
}
strcpy(_str + _size, str);
_size += len;
}
a
p
p
e
n
d
append
append 同样需要判断是否需要扩容:当 _size + 新字符串长度 > _capacity
则表示需要扩容
这里有一个问题:括多大呢?如果_size + len
比扩容 2 倍后的空间还大怎么办呢
因此,这里要进行一下判断,如果比 2 倍扩容后的空间小,则正常 2 倍扩容,反之扩容_capacity + len
的大小
5.4 operator+= 的模拟实现
上面实现了
p
u
s
h
push
push_
b
a
c
k
back
back 和
a
p
p
e
n
d
append
append,那么
o
p
e
r
a
t
o
r
operator
operator+= 的实现就很简单啦,只需要直接复用
他们就行了
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
5.5 insert 的模拟实现
5.5.1 插入一个字符
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t n = _capacity == 0 ? 4 : 2 * _capacity;
reserve(n);
}
size_t i = _size;
while (i >= pos)
{
_str[i + 1] = _str[i];
--i;
}
_str[pos] = ch;
++_size;
}
assert(pos <= _size);
首先要判断下标合不合法if (_size == _capacity)
既然要插入数据,那么一定要判断扩不扩容- 再接着便是
挪动数据
,这里我们选择从后往前挪动。如果是从前往后,会造先挪动的数据会覆盖后挪动的数据
那么上述代码有问题吗?
有!头插的时候会造成死循环!
为什么呢?因为
i
i
i 是
s
i
z
e
size
size_
t
t
t 类型,当头插时,
p
o
s
pos
pos 为 0 。挪动最后一个数据后:
i
i
i 从 0 减1;可是
i
i
i 是
s
i
z
e
size
size_
t
t
t 类型,再 -1 得到的是一个很大的数
,后继续不断减,但永远大于等于0
,程序死循环。
那把
i
i
i 改为
i
n
t
int
int 类型可不可以呢?
int i = _size;
while (i >= pos)
{
_str[i + 1] = _str[i];
--i;
}
还是不可以,因为while (i >= pos)
语句发生了类型转换
C语言规定,当操作符两边操作数类型不同时,类型小的向范围大的去提升。(详情请看:【C语言】——详解操作符(下))
上述情况就是
i
n
t
int
int 类型的
i
i
i 提升成了
s
i
z
e
size
size_
t
t
t 类型,因此还是无法解决问题。
解决方法有多种,这里简单列举几个
//法一:将pos强转成int
int i = _size;
while (i >= (int)pos)
{
_str[i + 1] = _str[i];
--i;
}
//法三:单独判断 i == npos(-1) 的情况
size_t i = _size;
while (i >= pos && i != npos)
{
_str[i + 1] = _str[i];
--i;
}
//法三:修改 i 的起始位置
size_t i = _size + 1;
while(i > pos)
{
_str[i] = _str[i - 1];
--i;
}
5.5.2 插入字符串
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
reserve(n);
}
size_t i = _size;
while (i >= pos && i != npos)
{
_str[i + len] = _str[i];
--i;
}
memcpy(_str + pos, str, sizeof(char) * len);
_size += len;
}
插入字符串与前面的情况很类似,前面步骤都是断言->扩容->挪数据
。
不同的是,挪动数据是向后挪动 len 个位置
最后是拷贝数据,拷贝数据可以直接用
m
e
m
c
p
y
memcpy
memcpy 拷贝。注意,这里不能用
s
t
r
c
p
y
strcpy
strcpy,因为
s
t
r
c
p
y
strcpy
strcpy 会额外多拷贝一个 '\0'
,这样不仅会覆盖掉一个数据
,并且插入字符串之后的数据也不可见
。
当然,我们也可以自己实现拷贝
for (size_t i = 0; i < len; i++)
{
_str[pos + i] = s[i];
}
5.6 erase 的模拟实现
5.6.1 npos 的定义和声明
不了解 n o p s nops nops 的小伙伴可以移步:【C++】—— string 类的了解与使用 来学习
我们知道,
n
o
p
s
nops
nops 是
s
t
r
i
n
g
string
string 中的一个静态常量成员
它在
s
t
r
i
n
g
string
string 类中的声明如下
class string
{
public:
//···
private:
//···
static const size_t npos;
};
但需要注意的是,它不能在string.h文件
中进行定义,像这样:
string.h
class string
{
//···
};
const size_t string::npos = -1;
因为 .
h
h
h 文件会被多个 .
c
p
p
cpp
cpp 文件包含 ,被几个 .cpp文件
包含就意味着 npos
被定义了几次,而一个变量规定只能定义一次。
因此,
n
p
o
s
npos
npos 应在 string.cpp文件
中定义
string.cpp
namespace my_string
{
const size_t string::npos = -1;
void erase(size_t pos, size_t n)
{
//···
}
//···
};
但是,有一个特殊情况:对于
n
p
o
s
npos
npos,可以在声明时直接给缺省值
class string
{
public:
//···
private:
//···
static const size_t npos = -1;
};
虽然但是,还是不建议这种写法。
因为静态变量是不走初始化列表
的,因此理论上是不能在声明时给缺省值的
只有 static + const + 整型
才可以,
d
o
u
b
l
e
double
double 都不行,我们可以认为编译器特殊处理了。
5.6.2 实现 erase
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (npos == len || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; ++i)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
e
r
a
s
e
erase
erase 的数据删除分为两种情况
:一种是
p
o
s
pos
pos 之后的数据全部删除
;一种是删除部分数据
当len == npos
或 pos + len > _size
,则表明之后的数据全部删除。此时只需在
p
o
s
pos
pos 位置改为 ‘\0’ 并调整 _
s
i
z
e
size
size 即可
另一种情况就是挪动数据
,这次是从前往后挪,并调整 _
s
i
z
e
size
size
6 string 查找的模拟实现
6.1 find 的模拟实现
6.1.1 查找字符
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
这个比较简单,大家直接看代码即可,需要注意的是如果没找到记得返回
n
p
o
s
npos
npos
6.1.2 查找字符串
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
char* p = strstr(_str + pos, str);
if (nullptr == p)
{
return npos;
}
return p - _str;
}
找子串的问题可以用暴力匹配的算法
,也可以用 kmp算法
,更好的有 BM算法
,大家可以自行去学习。这里就不过多介绍,我们这里直接复用 C语言 中的
s
t
r
s
t
r
strstr
strstr 函数。(
s
t
r
s
t
r
strstr
strstr 的模拟实现可以移步:【C语言】——字符串函数的使用与模拟实现(下))
这里需要注意,当找不到目标字符串时,
s
t
r
s
t
r
strstr
strstr 返回的是
n
u
l
l
p
t
r
nullptr
nullptr,这种情况我们要单独处理
6.2 substr 的模拟实现
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (pos + len > _size || len == npos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = pos; i < pos + len; ++i)
{
sub += _str[i];
}
return sub;
}
首先,我们先把
l
e
n
len
len 更新一下,如果 pos + len > _size
,那么 len
就是 _size - pos
。
剩下的就非常简单啦,创建一个新的对象,并提前预留空间,最后再一个一个字符 += (
p
u
s
h
push
push_
b
a
c
k
back
back)
注:这里一定要先写好拷贝构造,当我们没有自己实现拷贝构造时,编译器默认生成的是浅拷贝,会出大问题。
string s1 = "hello world";
string s2 = s1.substr(6);
cout << s2.c_str() << endl;
上述
s
u
b
s
t
r
substr
substr 用的是传值返回,传值返回会调用拷贝构造来创建一个临时对象
,临时对象再调用一次拷贝构造来拷贝给上
s
2
s2
s2.
如果我们没实现拷贝构造,那么编译器实现的是浅拷贝。
s
2
s2
s2 与
s
u
b
sub
sub 指向的是同一块空间
。
s
u
b
sub
sub 出了作用域后会销毁,它所开辟的空间也跟着释放。
s
2
s2
s2 指向一块已经被释放的空间,要是再调用析构函数,自然会出问题。
不过在新一点的编译器可能不会出现问题,如:VS2022。
编译器一般都会进行优化,在进行传值拷贝构造时,编译器可能不会进行
s
u
b
sub
sub 对象的创建,直接用临时对象拷贝构造
s
2
s2
s2。
而在 VS2022 等较新的编译器,优化更为激进,直接合三为一
,
s
u
b
sub
sub 和临时对象都不创建了,直接拷贝构造
s
2
s2
s2!所以也就不存在同一块空间被多次析构的问题了!关于编译器的优化
,感兴趣的小伙伴可以移步:【C++】—— 类与对象(五)
7 非成员函数的模拟实现
7.1 operator 比较系列
为什么 s t r i n g string string类中, o p e r a t o r operator operator 比较系列要实现成全局函数呢?
因为在类中实现,那第一个参数必须是 this指针
,那么
s
t
r
i
n
g
string
string 永远只能放在左边。库中想 string与字符串比较
,字符串与string都实现
,因此将 operator比较系列
放在类外
o p e r a t o r operator operator 比较系列比较简单,我们直接看代码就好
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);
}
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);
}
6 个比较函数,只需任意实现其中两个,另外4个函数都可以通过服用得到
7.2 operator<<
流插入的实现和简单,遍历一遍就好了
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
注:这里ostream& out
不能加
c
o
n
s
t
const
const,因为这是流插入,要往
o
u
t
out
out 里面去写,out是要进行修改的,加上
c
o
n
s
t
const
const 就不能修改了。
7.3 operator>>
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
in >> ch;
while (ch != ' ' && ch != '\n')
{
s += ch;
in >> ch;
}
return in;
}
注:这里string& s
前不能加
c
o
n
s
t
const
const,因为是流提取提取出来的字符是放到
s
s
s 中,是要对 s 进行改变的
上述代码先将string s
清空,后不断提取缓冲区的字符到变量
c
h
ch
ch中,直到出现 ' '
或'\n'
。
但我们检验一下就知道,这个代码是有问题的:因为流提取读到' '
和'\n'
时默认将他们跳过(流提取将他们当做是字符串与字符串之间的分隔符,因此跳过他们)
s
c
a
n
f
scanf
scanf 也是如此,因此程序会一直让你输入,无法停止。
我们可以用
g
e
t
get
get 函数,
g
e
t
get
get 函数不管你是什么字符,都会给你读进来
,与 C语言 中的
g
e
t
c
getc
getc函数 类似
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get(;
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = in.get();
}
return in;
}
但上述代码还有个缺点,因为是一个字符一个字符地插入,因此需要频繁的扩容
怎么解决呢?
我们可以给一个大小合适的
b
u
f
f
buff
buff 数组,现在每次来的字符先不往
s
t
r
i
n
g
string
string 进行 +=,先放到 buff 数组
里面,当
b
u
f
f
buff
buff 满了,再一次性放入
s
t
r
i
n
g
string
string 里面。当遇到' '
或'\0'
,再将buff数组
中剩下的字符放入
s
t
r
i
n
g
string
string 中。
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
const int N = 256;
char buff[N];
int i = 0;
while (ch != ' ' && ch != '\0')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
10 完整代码
10.1 string.h
#pragma once
#include<iostream>
#include<assert.h>
#include<string>
using namespace std;
namespace my_string
{
class string
{
public:
friend ostream& operator<<(ostream& out, const string& s);
friend istream& operator>>(const istream& in, string& s);
string(const char* str = "")
{
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);//将str中的内容拷贝
}
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
string(const string& s)
{
string tmp(s._str);
swap(tmp);
}
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
const char* c_str() const
{
return _str;
}
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
size_t size() const
{
return _size;
}
size_t capacity() const
{
return _capacity;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
typedef const char* const_iterator;
const_iterator begin() const
{
return _str;
}
const_iterator end() const
{
return _str + _size;
}
void clear()
{
_str[0] = '\0';
_size = 0;
}
void reserve(size_t n);
void push_back(char ch);
void append(const char* str);
string& operator+=(char ch);
string& operator+=(const char* str);
void insert(size_t pos, char ch);
void insert(size_t pos, const char* str);
void erase(size_t pos, size_t len = npos);
size_t find(char ch, size_t pos = 0);
size_t find(const char* str, size_t pos = 0);
string substr(size_t pos = 0, size_t len = npos);
static const size_t npos;
private:
char* _str = nullptr;//指向字符串的指针
size_t _size = 0;//有效字符个数(不包括'\0')
size_t _capacity = 0;//有效字符个数(不包括'\0')
};
bool operator<(const string& s1, const string& s2);
bool operator<=(const string& s1, const string& s2);
bool operator>(const string& s1, const string& s2);
bool operator>=(const string& s1, const string& s2);
bool operator==(const string& s1, const string& s2);
bool operator!=(const string& s1, const string& s2);
ostream& operator<<(ostream& out, const string& s);
istream& operator>>(istream& in, string& s);
}
10.2 string.cpp
#include"string.h"
namespace my_string
{
const size_t string::npos = -1;
void string::reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void string::push_back(char ch)
{
if (_size == _capacity)
{
size_t n = _capacity == 0 ? 4 : 2 * _capacity;
reserve(n);
}
_str[_size++] = ch;
_str[_size] = '\0';
}
void string::append(const char* str)
{
size_t len = strlen(str);
if (len + _size > _capacity)
{
size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
reserve(n);
}
strcpy(_str + _size, str);
_size += len;
}
string& string::operator+=(char ch)
{
push_back(ch);
return *this;
}
string& string::operator+=(const char* str)
{
append(str);
return *this;
}
void string::insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size == _capacity)
{
size_t n = _capacity == 0 ? 4 : 2 * _capacity;
reserve(n);
}
size_t i = _size;
while (i >= pos && i != npos)
{
_str[i + 1] = _str[i];
--i;
}
_str[pos] = ch;
++_size;
}
void string::insert(size_t pos, const char* str)
{
assert(pos <= _size);
size_t len = strlen(str);
if (_size + len > _capacity)
{
size_t n = len > _capacity ? _capacity + len : 2 * _capacity;
reserve(n);
}
size_t i = _size;
while (i >= pos && i != npos)
{
_str[i + len] = _str[i];
--i;
}
memcpy(_str + pos, str, sizeof(char) * len);
_size += len;
}
void string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (npos == len || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
for (size_t i = pos + len; i <= _size; ++i)
{
_str[i - len] = _str[i];
}
_size -= len;
}
}
size_t string::find(char ch, size_t pos)
{
assert(pos < _size);
for (size_t i = pos; i < _size; ++i)
{
if (_str[i] == ch)
{
return i;
}
}
return npos;
}
size_t string::find(const char* str, size_t pos)
{
assert(pos < _size);
char* p = strstr(_str + pos, str);
if (nullptr == p)
{
return npos;
}
return p - _str;
}
string string::substr(size_t pos, size_t len)
{
assert(pos < _size);
if (pos + len > _size || len == npos)
{
len = _size - pos;
}
string sub;
sub.reserve(len);
for (size_t i = pos; i < pos + len; ++i)
{
sub += _str[i];
}
return sub;
}
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);
}
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);
}
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch;
ch = in.get();
const int N = 256;
char buff[N];
int i = 0;
while (ch != ' ' && ch != '\0')
{
buff[i++] = ch;
if (i == N - 1)
{
buff[i] = '\0';
s += buff;
i = 0;
}
ch = in.get();
}
if (i > 0)
{
buff[i] = '\0';
s += buff;
}
return in;
}
}