👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
- 一、准备工作
- 二、string的结构
- 三、模拟实现常见初始化操作
-
-
- [3.1 用C字符串构造](#3.1 用C字符串构造)
- [3.2 无参构造(默认构造)](#3.2 无参构造(默认构造))
- [3.3 拷贝构造](#3.3 拷贝构造)
-
- 四、析构函数
- 五、模拟实现常见遍历操作
-
-
- [5.1 下标访问[]](#5.1 下标访问[])
- [5.2 迭代器](#5.2 迭代器)
-
- 六、尾插
-
-
- [6.1 push_back - 尾插一个字符 + reserve - 扩容](#6.1 push_back - 尾插一个字符 + reserve - 扩容)
- [6.2 append - 尾插字符串](#6.2 append - 尾插字符串)
- [6.3 运算符重载+= - push_back和append升级版](#6.3 运算符重载+= - push_back和append升级版)
-
- 七、插入操作insert
- 八、删除操作erase
- 九、查找操作find
- 十、截取操作substr
- 十一、改变字符串的有效个数resize
- 十二、流插入<<
- 十三、流提取>>
- 十四、清空操作clear
- 十五、比较操作
-
-
- [15.1 <](#15.1 <)
- [15.2 ==](#15.2 ==)
- [15.3 <=](#15.3 <=)
- [15.4 >](#15.4 >)
- [15.5 >=](#15.5 >=)
- [15.6 !=](#15.6 !=)
-
- 十六、交换操作swap
- 十七、赋值运算符重载=
- 十八、源码string.h
一、准备工作
为了方便管理代码,分两个文件来写:
Test.cpp
- 测试代码逻辑string.h
- 模拟实现string
二、string的结构
我们知道,string
是一个管理字符数组的类,底层其实就是一个支持动态增长的字符数组,就像数据结构学的动态顺序表。
cpp
namespace wj
{
class string
{
public:
private:
char* _str; // 动态字符数组
size_t _size; // 字符个数
size_t _capacity; // 容量(不包含'\0')
};
}
string
类的成员变量有三个,一个字符指针_str
指向开辟的动态数组,_size
标识有效数据个数(不包含'\0'
),_capacity
记录容量的大小(不包含'\0'
)
还需要注意的是:我们新命名了命名空间域wj
,就是避免和库中的string
产生冲突。
三、模拟实现常见初始化操作
3.1 用C字符串构造
cpp
namespace wj
{
class string
{
public:
// 用C字符串构造
string(const char* s)
:_str(new char[strlen(s) + 1]) // +1是为'\0'
, _size(strlen(s)) // _size不包含'\0'
, _capacity(_size)
{
// 拷贝数据
memcpy(_str, s, _size + 1);
// +1是为了拷贝'\0'
}
private:
char* _str; // 动态字符数组
size_t _size; // 字符个数
size_t _capacity; // 容量
};
}
以上代码有个易错点:要注意初始化列表的顺序,是按照成员变量的顺序来赋值的!
为了验证代码的正确性,需要打印出结果。由于自己模拟实现的string
,还没有实现重载流插入<<
,所以不能直接打印string
对象,而流插入<<
是可以打印内置类型的。因此string
是有提供转为内置类型的接口s_str
:
cpp
const char* c_str() const
{
return _str;
}
为什么会在函数后加个const
呢 ?在往期博客我们讲过:只要成员函数内部不修改成员变量,都应该加上const
接下来来测试代码:
【Test.cpp】
3.2 无参构造(默认构造)
无参构造的结果就是空字符,默认是有
'\0'
的
cpp
string()
:_str(new char[1])
, _size(0)
,_capacity(_size)
{
_str[0] = '\0';
}
【Test.cpp】
3.3 拷贝构造
自定义类型的拷贝必须先调用拷贝构造函数。但如果不手动编写,编译器会默认生成一个浅拷贝/值拷贝的拷贝构造函数,即将所有成员变量逐一拷贝到新对象中,这种拷贝方式对于基本数据类型或者是自定义类型的成员变量来说是没有问题的。但是,如果类中有动态分配内存的指针变量,则需要手动编写深拷贝的拷贝构造函数。
cpp
// string s1("hello world");
// string s2(s1); // 拷贝构造
// 这里的str是s1的别名,其实就是s1
string(const string& str)
{
// 1. 开一个和s1一样大的空间
_str = new char[str._capacity + 1];
// 2. 将s1的数据拷贝给s2
memcpy(_str, str._str, str._size + 1);
_size = str._size;
_capacity = str._capacity;
}
注意:以上代码其实隐藏了一个this
指针,这个this
指针其实就是s2
,因此以上代码本质就是:
cpp
string(const string& str)
{
// 1. 开一个和s1一样大的空间
this->_str = new char[str._capacity + 1];
// 2. 将s1的数据拷贝给s2
memcpy(this->_str, str._str, str._size + 1);
this->_size = str._size;
this->_capacity = str._capacity;
}
但是高手是不会把this
写出来的hh
【Test.cpp】
四、析构函数
由于成员变量含有动态内存开辟的空间,因此要手动写出析构函数
cpp
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
五、模拟实现常见遍历操作
5.1 下标访问[]
cpp
size_t size() const
{
return _size;
}
// 可读可写
// 可以引用返回:因为返回的对象是在堆上的,出了此作用域不会被销毁
char& operator[](size_t pos)
{
// 断言,防止越界
assert(pos >= 0 && pos < _size);
return _str[pos];
}
// 可读不可写
const char& operator[](size_t pos) const
{
assert(pos >= 0 && pos < _size);
return _str[pos];
}
【Test.cpp】
5.2 迭代器
string
的迭代器iterator
本质就是一个char*
的指针。
cpp
// 可读可写
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;
}
【Test.cpp】
在往期博客我们将过,范围for
的底层就是迭代器 ,因此也可以使用范围for
:
六、尾插
6.1 push_back - 尾插一个字符 + reserve - 扩容
要尾插字符之前,需要考虑当前_size
是否大于_capacity
,如果小于,则不用扩容;如果大于,则需要扩容。因此,string
库里同样也提供了扩容操作:
注意:reserve
一般不会缩容
cpp
void reserve(size_t n)
{
if (n > _capacity)
{
// 开一块新的空间
char* tmp = new char[n + 1];
// 拷贝数据到新的空间
memcpy(tmp, _str, _size + 1);
// 释放旧空间然后指向新空间
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
// 可能存在扩容
if (_size == _capacity)
{
// 默认2倍扩容
// 如果是空串,扩了2倍容量还是0,因此要考虑容量为0的情况
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
【Test.cpp】
6.2 append - 尾插字符串
cpp
string& append(const char* s)
{
// 可能存在扩容
size_t n = strlen(s);
if (_size + n > _capacity)
{
// 至少扩容_size+n
reserve(_size + n);
}
memcpy(_str + _size, s, n + 1);
_size += n;
return *this;
}
【Test.cpp】
6.3 运算符重载+= - push_back和append升级版
它既可以尾插一个字符,还可以尾插字符串 。因此,直接复用push_back
和append
即可
cpp
// +=字符串
string& operator+=(const char* s)
{
append(s);
return *this;
}
// +=字符
string& operator+=(char c)
{
push_back(c);
return *this;
}
【Test.cpp】
七、插入操作insert
- . 在
pos
位置插入n
个字符
【思路】
- 首先要判断下标的合法性。
- 其次还要判断插入的字符加上原有的字符是否超过当前容量,超过就扩容。
- 然后就是挪动数据和插入数据。注意挪动数据一定要从最后一个字符
'\0'
开始挪动;不能从pos
位置开始挪,否则后面的内容就被覆盖了。以下是动图展示
cpp
// pos - 下标
// n - 插入的字符个数
// x - 插入的字符
void insert(size_t pos, size_t n, char x)
{
// 判断下标pos的合法性
assert(pos >= 0 && pos <= _size);
// 可能存在扩容
if (_size + n > _capacity)
{
reserve(_size + n);
}
// 挪动数据
size_t end = _size;
while (end >= pos)
{
_str[end + n] = _str[end];
end--;
}
// 插入数据
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = x;
}
_size += n;
}
通过思路分析,不难可以写出以上代码。但是以上代码有一个bug
,当头插时,程序就崩溃了(如下图)
通过走读代码我们发现,pos
和end
的类型是都是size_t
。因此end
最后自减到-1
,由于类型是size_t
,而无符号的-1
是一个相当大的数,循环条件成立就会一直死循环下去。
因此这里有两种方法:
第一种:将pos
和end
的类型全部改为int
cpp
void insert(int pos, size_t n, char x)
{
// 判断下标pos的合法性
assert(pos >= 0 && pos <= _size);
// 可能存在扩容
if (_size + n > _capacity)
{
reserve(_size + n);
}
// 挪动数据
int end = _size;
while (end >= pos)
{
_str[end + n] = _str[end];
end--;
}
// 插入数据
for (int i = 0; i < n; i++)
{
_str[pos + i] = x;
}
_size += n;
}
以上这种方法虽然可以,但是和库里提供的类型是有所差别的,因此还是有些不好。
第二种:既然size_t
类型的end
自减到-1
就会死循环,那么加个end != -1
不就完事了。恰好,string
库里提供了公共静态成员常量npos
,这个常量使用值-1
定义。
所以,最终代码如下:
cpp
namespace wj
{
class string
{
public:
void insert(size_t pos, size_t n, char x)
{
// 判断下标pos的合法性
assert(pos >= 0 && pos <= _size);
// 可能存在扩容
if (_size + n > _capacity)
{
reserve(_size + n);
}
// 挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
// 插入数据
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = x;
}
_size += n;
}
private:
char* _str; // 动态字符数组
size_t _size; // 字符个数
size_t _capacity; // 容量
static const size_t npos;
};
const size_t string::npos = -1;
}
需要注意的是:静态成员需要在类外定义
【Test.cpp】
- 在
pos
位置插入字符串
思路类似,这里就不过多赘述了
cpp
void insert(size_t pos, const char* str)
{
// 判断下标pos的合法性
assert(pos <= _size);
// 可能存在扩容
size_t length = strlen(str);
if (_size + length > _capacity)
{
reserve(_size + length);
}
// 挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + length] = _str[end];
end--;
}
// 插入数据
for (size_t i = 0; i < length; i++)
{
_str[pos + i] = str[i];
}
_size += length;
}
【Test.cpp】
八、删除操作erase
【思路】
- 首先要检查下标的合法性
- 删除要分情况讨论:
第一种:当前下标往后的字符全都需要删除
第二种:删除的字符是符合范围内的
cpp
string& erase(size_t pos = 0, size_t len = npos)
{
// 检查下标合法性
assert(pos >= 0 && pos < _size);
if (len == npos || len + pos >= _size)
{
// 说明pos(包括pos)后面的字符要全部删完
_str[pos] = '\0';
_size = pos;
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos] = _str[end];
pos++;
end++;
}
_size -= len;
}
return *this;
}
【Test.cpp】
九、查找操作find
- 查找字符
cpp
// 从下标pos开始查找字符,如果实参不传第二个参数,默认从下标为0开始查找
size_t find(char x, size_t pos = 0) const
{
// 检查查找下标的合法性
assert(pos >= 0 && pos < _size);
// 遍历查找即可
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == x)
{
// 找到返回下标
return i;
}
}
// 没找到默认返回npos,其实就是-1
return npos;
}
【Test.cpp】
- 查找字符串
查找子串可以有很多方法,最简便就是使用C语言中的strstr
函数
cpp
size_t find(const char* str, size_t pos = 0) const
{
// 检查查找下标的合法性
assert(pos >= 0 && pos < _size);
//strstr(const char * str1, const char * str2) - 从str1中查找str2
const char* ptr = strstr(_str + pos, str);
// 如果ptr为空说明没找到
if (ptr == nullptr)
{
// 找不到返回npos
return npos;
}
// 否则找到了
else
{
// 指针-指针 --- 返回的是元素个数
return ptr - _str;
}
}
【Test.cpp】
十、截取操作substr
cpp
// pos - 截取的下标
// len - 截取的长度
// npos - 截取的最大长度
string substr(size_t pos = 0, size_t len = npos) const
{
// 判断下标的合法性
assert(pos >= 0 && pos < _size);
// 分两种情况,
// 1. 可能需要截到尾
// 2. 可能需要截取一部分
size_t n = len;
// 截取到尾的情况
if (len == npos || pos + len >= _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
// 将截取的字符全部放去tmp
tmp += _str[i];
}
return tmp;
}
注意:对象tmp
并不是直接返回,返回的过程会中间会生成一个临时变量。因此,tmp
在返回时会调用拷贝构造(深拷贝)。如果没有写深拷贝,程序会崩溃,因为tmp
出了作用域就会调用析构函数销毁空间,这也就导致临时变量指向的空间被销毁。
十一、改变字符串的有效个数resize
三种情况:
- 当
n
小于size
,相当于删除数据,保留n
个字符- 当
n
等于size
,则保留原数据- 当
n
大于size
,则会扩容,同时后面会补充n - size
个字符(不指定默认是'\0'
)
cpp
void resize(size_t n, char ch = '\0')
{
// 相当于删除数据,只保留前n个字符
if (n < _size)
{
_size = n;
_str[_size] = '\0';
}
else // 否则相当于插入数据
{
// 可能有扩容情况,n比_capacity大才会扩容,n=_capacity什么也不发生
reserve(n);
for (size_t i = _size; i < n; i++) // 填数据
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
十二、流插入<<
成员函数默认第一个形参都是对象的地址,也就是隐藏的this
指针,由于cout
抢占了对象的第一个位置,因此不能当做成员函数,就只能写在类外了
cpp
// 注意:必须引用返回,不然会报错。因为ostream这个类做了一个防拷贝
ostream& operator<<(ostream& out, const string& s)
{
// !注意:
// C语言的字符串的打印是只打印到'\0'停止
// 打印string时与'\0'无关与size有关
for (auto ch : s)
{
out << ch;
}
return out;
}
十三、流提取>>
由于输入的字符个数不确定,导致扩容多少也不确定,因此只能一个一个从键盘上拿,istream
中有个get
可以解决(每次读取一个字符)
cpp
istream& operator>>(istream& in, string& s)
{
char ch = in.get();
// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
// 只有getline才可以
while (ch != ' ' && ch != '\n')
{
s += ch;// += 自己就会扩容
ch = in.get();
}
return in;
}
以上代码还是不够完善,当多次对一个对象输入的情况,要对之前形成一次覆盖,可以对比库里的string
自己实现的上一次输入的数据没有清理干净。因此要清理上次的内容
cpp
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
// 只有getline才可以
while (ch != ' ' && ch != '\n')
{
s += ch;// += 自己就会扩容
ch = in.get();
}
return in;
}
【Test.cpp】
还有一个问题,当一开始输入连续空格或者换行时,可以对比自己实现的和库里的:
因此要过滤前面的空格或者换行
cpp
istream& operator>>(istream& in, string& s)
{
s.clear();
char ch = in.get();
// 处理前缓冲区前面的空格或者换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
// [cin >> 对象] 默认读取到空格或者换行就不会往下读了
// 只有getline才可以
while (ch != ' ' && ch != '\n')
{
s += ch;// += 自己就会扩容
ch = in.get();
}
return in;
}
【Test.cpp】
十四、清空操作clear
直接将第一个字符改成'\0'
即可
cpp
void clear()
{
_str[0] = '\0';
_size = 0;
}
十五、比较操作
15.1 <
cpp
bool operator<(const string& s) const
{
// 不能用strcpy,因为它只会比到'\0'
// 而string类是有多少字符比多少字符
// 以短的字符串为基础来比较
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
// "hello" < "hello" false ①
// "helloxx" < "hello" false ②
// "hello" < "helloxx" true ③
// ret为0,类似第②中情况,则长的那个字符串长
// 否则,则ret<0说明s1<s2为真,ret>0,则s1>s2为假
return ret == 0 ? _size < s._size : ret < 0;
}
15.2 ==
cpp
bool operator==(const string& s) const
{
return _size == s._size
&& memcmp(_str, s._str, _size) == 0;
}
当写完<
和==
,剩下的代码就可以复用了。
15.3 <=
cpp
bool operator<=(const string& s) const
{
return *this < s || *this == s;
}
15.4 >
cpp
bool operator>(const string& s) const
{
return !(*this <= s);
}
15.5 >=
cpp
bool operator>=(const string& s) const
{
return !(*this < s);
}
15.6 !=
cpp
bool operator!=(const string& s) const
{
return !(*this == s);
}
十六、交换操作swap
cpp
// 16. swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
十七、赋值运算符重载=
默认不写是以值的方式逐字节拷贝(浅拷贝/值拷贝),因此内置类型成员变量是直接赋值的,而自定义类型成员会去调用它的默认函数,但要注意动态开辟的成员变量。如果不写深拷贝,两个对象会同时指向动态开辟的空间。
法一:传统写法:
cpp
// s1 = s2
// s1是隐藏的this指针
// 方法:
// 首先开一个和s2同样大的空间并且把s2的数据拷贝
// 然后再释放掉s1指向的空间,最后再让s1指向新拷贝的那个空间
string& operator=(const string& s)
{
if (this != &s)
{
// 首先开一个和s2同样大的空间并且把s2的数据拷贝
char* tmp = new char[s._capacity + 1];
memcpy(tmp, s._str, s._size + 1);
// 然后再释放掉s1指向的空间
delete[] _str;
// 最后再让s1指向新拷贝的那个空间
_str = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
法二:现代写法
cpp
// 方法:
// 用s2拷贝构造tmp的对象,然后再让tmp和s1交换
string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}
法二延伸:
cpp
//s1 = s2
// 传参时,s2调用拷贝构造给tmp(深拷贝),然后再和s1交换
// s1是隐藏的this指针
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
十八、源码string.h
cpp
#pragma once
#include <assert.h>
namespace wj
{
class string
{
public:
// 1. 用C语言字符串的方式初始化
// 以下不是默认构造
string(const char* str)
:_str(new char[strlen(str) + 1]) // 开空间。+1算上'\0'
, _size(strlen(str)) // 字符个数
, _capacity(_size) // 容量只存有效字符
// 注意初始化列表的顺序(按照声明的顺序)
{
memcpy(_str, str, _size + 1);
// +1 是为了拷贝'\0'
}
// 2. 由于成员变量含有动态内存开辟的空间
// 因此要写析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
// 3. 观察结果
// 无论是cout还是printf都不能直接打印string类型
// 因此就需要将string类转换为char类型,可以用c_str
const char* c_str() const
{
return _str;
}
// 4. 无参构造(默认构造)
// 默认构造:无参、全缺省、编译器自动生成的
// 注意:默认构造不能重载
string()
:_str(new char[1]) // 空字符串至少有一个'\0'
, _size(0)
, _capacity(0)
{
_str[0] = '\0';
}
// 5. string遍历
// 元素个数
size_t size() const // 加const的原因:只要成员函数内部不修改成员变量,都应该加上const
{
return _size;
}
// ① operator[]
// 可读可写
// 可以引用返回(因为返回的对象是在堆上的,出了此作用域不会被销毁)
char& operator[](size_t pos)
{
assert(pos < _size); // 断言,防止越界
return _str[pos];
}
// 只能读,不能写
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
// 迭代器的遍历
// string的迭代器本质就是一个char*的指针
// 可读可写
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;
}
// 6. string增
// Request a change in capacity - reserve
// 一般不会缩容
void reserve(size_t n)
{
if (n > _capacity)
{
// +1是给'\0'
char* tmp = new char[n + 1];
memcpy(tmp, _str, _size + 1);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
// 只能插入一个字符
void push_back(char ch)
{
// 可能存在扩容
if (_size == _capacity)
{
// 2倍扩容
reserve(_capacity == 0 ? 4 : _capacity * 2); // 如果是空串,扩了2倍容量还是0,因此要考虑容量为0的情况
_str[_size] = ch;
_size++;
_str[_size] = '\0';
}
}
// 尾插字符串
void append(const char* s)
{
size_t length = strlen(s);
// 可能存在扩容
if (_size + length > _capacity)
{
// 至少扩容到_size+length
reserve(_size + length);
}
memcpy(_str + _size, s, length + 1);
_size += length;
}
// 运算符重载+=,push_back和append升级
// 即可以尾插一个字符
string& operator+=(char x)//+=完还是要返回该对象(string类型)且对象不会被销毁(引用)
{
push_back(x);
return *this;
}
// 也可以尾插一个字符串
string& operator+=(const char* s)
{
append(s);
return *this;
}
// 7. string插入
// pos - 下标
// n - 插入的字符个数
// x - 插入的字符
string& insert(size_t pos, int n, char x) // 函数重载
{
// 判断下标pos的合法性
assert(pos <= _size);
// 可能存在扩容
if (_size + n > _capacity)
{
reserve(_size + n);
}
// 挪动数据
size_t end = _size;
// while(end >= pos)
// 如果不强制类型转化为int会死循环,因为pos类型是size_t,end的类型是int,
// 他们在比较的时候会发生算术转换(低的类型向高的类型转换)
// 当end减减为-1时,由于end的类型转化为size_t,是一个非常大的数
// 所以可以将pos强制转化成int,避免算术转化
// 如果不想强制类型转化,可以用npos。
//while (end >= (int)pos && end != npos)
while (end >= pos && end != npos)
{
_str[end + n] = _str[end];
end--;
}
// 插入数据
for (size_t i = 0; i < n; i++)
{
_str[pos + i] = x;
}
_size += n;
return *this;
}
string& insert(size_t pos, const char* str)
{
// 判断下标pos的合法性
assert(pos <= _size);
// 可能存在扩容
size_t length = strlen(str);
if (_size + length > _capacity)
{
reserve(_size + length);
}
// 挪动数据
size_t end = _size;
while (end >= pos && end != npos)
{
_str[end + length] = _str[end];
end--;
}
// 插入数据
for (size_t i = 0; i < length; i++)
{
_str[pos + i] = str[i];
}
_size += length;
return *this;
}
// 7. string的删除erase
string& erase(size_t pos, size_t len = npos) //size_t len = npos - 从坐标pos开始往后删完
{
// 检查pos的合法性
assert(pos <= _size);
if (len == npos || len + pos >= _size)
{
// 说明pos(包括pos)后面的字符要全部删完
_str[pos] = '\0';
_size = pos;
}
else
{
size_t end = pos + len;
while (end <= _size)
{
_str[pos] = _str[end];
pos++;
end++;
}
_size -= len;
}
return *this;
}
// 8. string的查找find
// 查找字符
size_t find(char x, size_t pos = 0) // 从下标pos开始查找字符,如果实参不传第二个参数,默认从下标为0开始查找
{
assert(pos < _size);
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == x)
{
// 找到返回下标
return i;
}
}
// 没找到默认返回npos
return npos;
}
// 查找字符串
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
// BM算法orkmp算法?
//strstr(const char * str1, const char * str2) - 从str1中查找str2
const char* ptr = strstr(_str + pos, str);// strstr - 查找子串
// 如果ptr为空说明没找到
if (ptr == nullptr)
{
// 找不到返回npos
return npos;
}
// 否则找到了
else
{
// 指针-指针 --- 返回的是元素个数
return ptr - _str;
}
}
// 9. 截取子串
// pos - 截取的下标
// len - 截取的长度
// npos - 截取的最大长度
string substr(size_t pos = 0, size_t len = npos) const
{
// 判断下标的合法性
assert(pos < _size);
size_t n = len;
if (len == npos || pos + len > _size)
{
n = _size - pos;
}
string tmp;
tmp.reserve(n);
for (size_t i = pos; i < pos + n; i++)
{
tmp += _str[i];
}
// 由于对象tmp是自定义类型,返回时会生成一个临时变量(可能是寄存器),然后tmp会拷贝给临时变量
// 自定义类型的拷贝必须先调用拷贝构造函数。但如果不手动编写,编译器会默认生成一个浅拷贝/值拷贝的拷贝构造函数
// 即将所有成员变量逐一拷贝到新对象中,这种拷贝方式对于基本数据类型或者是自定义类型的成员变量来说是没有问题的
// 但是 如果类中有动态分配内存的指针变量,则需要手动编写深拷贝的拷贝构造函数
// 因为如果不写,tmp和_str会指向同一块内存开辟的空间
return tmp;
}
// 10. 拷贝构造函数
string(const string& s)
{
// 深拷贝
_str = new char[s._capacity + 1];
memcpy(_str, s._str, s.size() + 1);
_size = s._size;
_capacity = s._capacity;
}
// 11. resize
// 改变字符串的有效个数,如果改变的个数大于容量,则会扩容,并且resize还能初始化
void resize(size_t n, char ch = '\0')
{
if (n < _size) // 相当于删除数据,只保留前n个字符
{
_size = n;
_str[_size] = '\0';
}
else // 否则相当于插入数据
{
reserve(n);// 可能有扩容情况,n比_capacity大才会扩容,比_capacity小什么也不发生
for (size_t i = _size; i < n; i++) // 填数据
{
_str[i] = ch;
}
_size = n;
_str[_size] = '\0';
}
}
// 14. clear
void clear()
{
_str[0] = '\0';
_size = 0;
}
// 15. 比较大小(字符/字符串比较都是按ascII)
bool operator<(const string& s) const
{
// 不能用strcpy,因为它只会比到'\0'
// 而string类是有多少字符比多少字符
int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);
// "hello" < "hello" false ①
// "helloxx" < "hello" false ②
// "hello" < "helloxx" true ③
// 如果等于0,
return ret == 0 ? _size < s._size : ret < 0;
}
bool operator==(const string& s) const
{
return _size == s._size
&& memcmp(_str, s._str, _size) == 0;
}
bool operator<=(const string& s) const
{
return *this < s || *this == s;
}
bool operator>(const string& s) const
{
return !(*this <= s);
}
bool operator>=(const string& s) const
{
return !(*this < s);
}
bool operator!=(const string& s) const
{
return !(*this == s);
}
// 16. swap
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
// 17. 赋值运算符重载
// 默认是以值的方式逐字节拷贝(浅拷贝/值拷贝)
// 因此内置类型成员变量是直接赋值的,而自定义类型成员会去调用它的默认函数,
// 但是要注意动态开辟的成员变量。如果不写(深拷贝)两个对象会同时指向动态开辟的空间
// s1 = s2
// s1是隐藏的this指针
// 法一(传统写法):
// 首先开一个和s2同样大的空间并且把s2的数据拷贝,然后再释放掉s1指向的空间,最后再让s1指向新拷贝的那个空间
/*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;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}*/
// 法二(现代写法):
// 用s2拷贝构造tmp的对象,然后再让tmp和s1交换
/*string& operator=(const string& s)
{
if (this != &s)
{
string tmp(s);
swap(tmp);
}
return *this;
}*/
// 法二的延伸
//s1 = s2
// 传参时,s2调用拷贝构造给tmp(深拷贝),然后再和s1交换
string& operator=(string tmp)
{
swap(tmp);
return *this;
}
private:
char* _str;
size_t _size;
size_t _capacity;
static size_t npos;
// 如果静态成员变量想在类里定义,加个const即可(但是只能整型这样做,其他类型会报错)
//const static size_t npos; // 这种方式不建议!!!
};
// 静态成员变量在类里是声明,并且定义只能在类外
size_t string::npos = -1;
// 12. 流插入<< (插入到屏幕)
// 成员函数默认(隐藏)第一个形参都是对象的地址,也就是this指针
// cout抢占了对象的第一个位置,因此不能当做成员函数,就只能写在类外了
// 注意:必须引用返回,不然会报错。因为ostream这个类做了一个防拷贝
ostream& operator<<(ostream& out, const string& s)
{
// !注意:
// C语言的字符串的打印是只打印到'\0'停止
// 打印string时与'\0'无关与size有关
for (auto ch : s)
{
out << ch;
}
return out;
}
// 13. 流提取
istream& operator>>(istream& in, string& s)
{
// 输入的字符不确定,导致扩容多少也不确定
// 因此只能一个一个的拿,istream中有个get可以解决(每次读取一个字符)
// ① 多次对一个对象输入的情况,对之前形成一次覆盖
// 因此要清理上次的内容
s.clear();
char ch = in.get();
// ② 一开始输入连续空格或者换行
// 处理前缓冲区前面的空格或者换行
while (ch == ' ' || ch == '\n')
{
ch = in.get();
}
while (ch != ' ' && ch != '\n')
{
s += ch;// += 自己就会扩容
ch = in.get();
}
return in;
}
// 优化版
//s.clear();
//char ch = in.get();
处理前缓冲区前面的空格或者换行
//while (ch == ' ' || ch == '\n')
//{
// ch = in.get();
//}
in >> ch;
//char buff[128];
//int i = 0;
//while (ch != ' ' && ch != '\n')
//{
// buff[i++] = ch;
// if (i == 127)
// {
// buff[i] = '\0';
// s += buff;
// i = 0;
// }
// //in >> ch;
// ch = in.get();
//}
如果输入的字符不满127,就要在后补上'\0'
//if (i != 0)
//{
// buff[i] = '\0';
// s += buff;
//}
//return in;
}