目录
一、基础模板
class string
{
public:
// 对应的成员函数
private:
// 成员变量
char* _str; // 字符串
size_t _size; // 字符串实际大小
size_t _capacity; // 字符串容量
// 表示 "直到字符串结尾" 与库中的npos对应 类外赋值为 -1
static const int npos; //静态成员常量,表示size_t的最大值(Maximum value for size_t)
};
1、默认成员函数:
1、构造函数:
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size;
// 多开一个空间存储'\0' 标志字符串结尾
_str = new char[_capacity + 1];
// strcpy 也会将字符串的'\0'拷贝入,所以不用手动设置'\0'
strcpy(_str, str);
}
2、拷贝构造:
拷贝构造就算我们不写编译器也会生成一个默认的拷贝构造函数,但是默认的拷贝构造函数进行的是浅拷贝,某些情况下(例如析构时同一块内存析构多次)会产生危害,所以需要我们手动写一个深拷贝。
// 拷贝构造得传引用,否则会产生无限递归,s2(s1)
string(const string& str)
{
_str = new char[str._capacity + 1];
strcpy(_str, str._str);
_size = str._size;
_capacity = str._capacity;
}
3、赋值运算符重载:
赋值运算符 = 也会涉及到浅拷贝的问题,下面提供两种方法,传统法与现代法:
传统法:先释放旧的内存空间,然后再申请新的内存,再进行复制。
// s1 = s2
string& operator=(const string& str)
{
char* tmp = new char[str._capacity + 1];
strcpy(_str, str._str);
// 释放s1原来的空间
delete[] _str;
_str = tmp;
_size = str._size;
_capacity = str._capacity;
return *this;
}
现代法:利用构造/拷贝构造创建一个临时对象再用库函数(swap)去进行交换其字符串的内容。
string& operator=(const string& str)
{
if (this != &str)
{
// 创建一个临时变量,这里使用的是构造函数,也可以使用拷贝构造
string tmp(str._str);
// 利用库函数直接交换字符串的内容
std::swap(_str, tmp._str);
_size = tmp._size;
_capacity = tmp._capacity;
}
return *this;
}
4、析构函数:
~string()
{
delete[] _str;
_str = nullptr;
_size = 0;
_capacity = 0;
}
二、字符串遍历:
1、[ ]符号重载:
string库中的字符串可以使用下标遍历,那如果想要自己实现下标遍历的话就需要先对[ ]符号进行重载,如下:
注意:对于const对象,是不能进行修改的,如果使用普通对象的[ ]重载的话会面临权限被放大的问题,所以这里可以用函数重载重载一个const对象使用的即可。
char& operator[](size_t pos)
{
assert(pos < _size); // 引用做返回值还可以这样检查越界
return _str[pos]; // _str这个成员是存放堆上的,出了作用域还在,所以可以用引用返回
}
// 对于const对象无法修改就利用重载:
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
2、迭代器iterator:
要写迭代器之前还要解决对应的begin() 和 end() 的问题,这里可以直接使用typedef去改名,这里也需要注意普通对象和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;
}
关于范围for:
如果按照上面代码这样写的那么使用范围for可以不做任何修改也能跑
// 就当前模板不做任何修改,照样跑得过
for (char c : s1)
{
cout << c << " ";
}
cout << endl;
那是因为范围for的本质其实就是自动替换,底层还是一个迭代器,只不过这个替换比较智能,普通对象走普通对象迭代器,const对象走const对象的迭代器,但是只支持对应替换,如果将自己写的begin改成Begin就无法跑了。
三、修改操作:
1、reserve:
reserve(n)的功能为可以自动扩容,如果_capacity小于n,则进行扩容操作,如果小于n则不做修改,所以我们可以写一个reserve函数来完成我们的扩容操作。
具体操作为:开新空间 -> 拷贝数据 -> 释放旧空间 -> 指针指向新空间。
void reserve(size_t n)
{
if (n > _capacity)
{
// 开新空间 +1预留'\0'的位置
char* tmp = new char[n + 1];
// 拷贝数据
strcpy(tmp, _str);
// 释放旧空间
delete[] _str;
// 指针指向新空间
_str = tmp;
_capacity = n;
}
}
2、push_back与append:
push_back:
void push_back(char ch)
{
// 检查扩容
if (_size == _capacity)
{
// 可以直接运用三目操作符,如果字符串为空则开4个空间,如果不为空则扩容成原容量的2倍
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 字符串原'\0'的位置变成传入的字符
_str[_size] = ch;
++_size;
// 补齐字符末尾的'\0'
_str[_size] = '\0';
}
append:
void append(const char* str)
{
// 检查是否需要扩容
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 直接利用strcpy函数达到尾插字符串的目的
strcpy(_str + _size, str);
_size += len;
}
运算符 += 重载:
在原string库中的 += 符号不仅可以跟字符,也可以跟字符串,以及另一个string类的对象,对于字符和字符串可以直接调用push_back和append,尾插string类对象其实也是沿用append函数的思路。
// += 尾插字符
string& operator+=(char c)
{
push_back(c);
return *this;
}
// += 尾插字符串
string& operator+=(const char* str)
{
append(str);
return *this;
}
// += 尾插string类对象
string& operator+=(const string& str)
{
// 检查扩容:
if (_size + str._size > _capacity)
{
reserve(_size + str._size);
}
strcpy(_str + _size, str._str);
_size += str._size;
return *this;
}
3、insert与erase:
insert:
insert函数功能实现的是指定位置插入字符/字符串,这个可以利用重载分开实现。
指定位置插入字符:
// 指定位置插入删除字符
void insert(size_t pos, char c)
{
// 限定pos位置在有效数据+1的范围内,当pos = _size时就是直接进行尾插
assert(pos <= _size);
// 检查扩容
if (_size == _capacity)
{
// 利用写的reserve函数扩容
reserve(_capacity == 0 ? 4 : 2 * _capacity);
}
// 设置结尾,用于挪动数据,+1是因为对应结尾的'\0'也要进行移动
size_t end = _size + 1;
// 挪动数据
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
// 插入数据
_str[pos] = c;
_size++;
}
指定位置插入字符串:
// 指定位置插入字符串
void insert(size_t pos, const char* str)
{
// 限定pos位置
assert(pos <= _size);
size_t len = strlen(str);
// 如果传入的是空字符串则直接返回
if (len == 0)
{
return;
}
// 检查扩容
if (_size + len > _capacity)
{
reserve(_size + len);
}
// 将要插入字符位置之后的数据往后移动,或者用循环一个个拷也可以
size_t end = _size + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
end--;
}
// 利用strncpy函数将对应空缺位置填上
strncpy(_str + pos, str, len);
// 更新_size
_size += len;
}
erase:
erase(pos, n)函数功能实现的是从pos位置开始删除n个字符,如果不指定右参数,则从pos位置开始删除数据删到结尾。
void erase(size_t pos, size_t len = npos)
{
// 限定pos位置处于有效数据下标内
assert(pos < _size);
// 处理未给右参数以及将pos位置往后的数据全部删除的情况
if (len == npos || len >= _size - pos)// 隐式类型转换
{
_str[pos] = '\0';
_size = pos;
}
// 思路: 直接利用strcpy函数将删除后pos位置后面的数据对pos位置开始往后进行覆盖
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
}
4、swap:
对于swap函数,就算我们不写库中也会提供一份以swap函数的模板生成的swap函数,但这个生成的函数并不好用, 因为他是利用三次拷贝(一次拷贝构造,两次赋值) + 一次析构为代价完成两个string类对象的交换。
我们可以通过直接交换string类对象的成员变量以此来达到交换的目的,这样就可以省去三次的拷贝和析构。
void swap(string& str)
{
// 这里得要指定std库,因为编译器查找原则是就近原则,会优先找局部的swap函数,也就是我们写的这个
// 不指定的话就会报参数不匹配的错误
std::swap(_str, str._str);
std::swap(_size, str._size);
std::swap(_capacity, str._capacity);
}
在日常使用时为了防止误用std库中的swap函数通常会在全局再重载一个swap函数,如下:
void swap(string& str1, string& str2)
{
// 这里调用的是我们手写的string库中的swap函数
str1.swap(str2);
}
当模板函数和具体函数同时出现在全局的时候,调用时优先找具体函数。
5、find与substr:
find 函数:
find(c, pos) / find(sub, pos) 函数功能为从pos位置(pos 默认为0)开始查找字符/字符串,到返回字符/字符串初始位置 的下标,找不到返回npos。
查找字符:
size_t find(char c, size_t pos = 0) const
{
for (size_t i = pos; i < _size; i++)
{
if (_str[i] == c)
{
return i;
}
}
return npos;
}
查找字符串:
size_t find(const char* sub, size_t pos = 0)const
{
assert(pos < _size);
// 这里简单些可以直接用库中的strstr函数暴力匹配对应子串
const char* p = strstr(_str + pos, sub); // 找不到返回空指针
if (p)
{
return p - _str;
}
else
{
return npos;
}
}
substr函数:
substr(pos, len) 函数的功能为从pos‘位置开始取len个字符,返回初始位置的下标。len默认为npos。
string substr(size_t pos, size_t len = npos)
{
string sub;
// 如果len 大于 剩余个数的话就表示为剩下字符有多少取多少
if (len == npos || len >= _size - pos)
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
else
{
for (size_t i = pos; i < pos + len; i++)
{
sub += _str[i];
}
}
return sub;
}
四、流插入/流提取重载:
流插入/流提取都必须重载为全局函数,否则会发生参数不匹配的问题等。
流插入 << 重载:
ostream& operator<<(ostream& out, const string& str)
{
// 这种写法就不需要访问内部成员,可不用友元声明
for (char c : str)
{
out << c;
}
return out;
}
流提取 >> 重载:
// clear 不清空间只清数据
void clear()
{
_size = 0;
_str[_size] = '\0';
}
istream& operator>>(istream& in, string& str)
{
// 这里得要调用clear函数将str原有的数据清空,因为in的本质是覆盖
str.clear();
char c;
// 这里不能直接用in >> c 因为cin读取单个字符时是无法读取到空格和换行的
c = in.get();
while (c != ' ' && c != '\n')
{
str += c;
c = in.get();
}
return in;
}
函数优化:
对于流提取函数,其实还可以进行一些小的优化,就是对于开空间扩容这里,因为不知道会输入多少个字符进来,所以一开始也不好直接使用reserve函数进行扩容(空间给大了浪费,少了也一样要多次扩容), 所以我们可以用下面的方式来进行改进:
istream& operator>>(istream& in, string& str)
{
str.clear();
char c;
// buff是在栈上开的空间,且为局部变量
char buff[128];
size_t i = 0;
c = in.get();
// 将提取的字符放入buff数组中
while (c != ' ' && c != '\n')
{
buff[i++] = c;
// 输入字符少于128并不会多次开空间,大于128按 (数量 /128 + 1)次开空间
if (i == 127)
{
buff[127] = '\0';
str += buff;
}
c = in.get();
}
if (i > 0)
{
buff[i] = '\0';
str += buff;
}
return in;
}
getline:
getline的功能为获取一行的数据,包括空格 ,实现的方法就跟上面的流提取重载一样,只是条件去除了遇到空格停止。
istream& getline(istream& in, string& str)
{
str.clear();
char c;
c = in.get();
while (c != '\n')
{
str += c;
c = in.get();
}
return in;
}