string类的完整实现放这里啦!快来看看吧
1. string类的成员
string类的作用就是将字符串类型实现更多功能,运算符重载,增删改查等等操作,所以其成员就包含char*的字符串
private:
char* _str;
size_t _capacity;
size_t _size;
2. 构造函数
2.1 带参构造函数
在之前的学习过程中,我们了解到类中存在的六个默认函数,其中就包含默认构造函数,那么对于string类是否需要用户自己实现构造函数呢?
答案是需要的,我们需要根据字符串的长度开辟空间,也需要将字符串拷贝到开辟的空间当中
// 带参的构造函数
string(const char* str)
{
_capacity = strlen(str);
_size = _capacity;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
我们的代码实现中_capacity是计算到’\0’停止,所以在堆上new的时候要加1,给’\0’预留空间
2.2 不带参构造函数
string()
{
_str = new char[1];
_str[0] = '\0';
_capacity = _size = 0;
}
不带参数就开辟1个字节的空间存放\0
2.3 默认缺省构造函数
string(const char* str = "")
{
_capacity = strlen(str);
_size = _capacity;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
对于不传参的string类初始化,则是采用默认值的方式"" 该字符串自带斜杠0
这里需要区分清楚:‘\0’ / “\0” / “” 这几种情况的区别:
- '\0’是字符\0 ASCII码值为0 相当于nullptr 拿nullptr作为默认值,后面strlen直接报错
- 而"\0" 则是用两个\0初始化字符串
- ""就是单纯的\0
3. 拷贝构造函数
那么拷贝构造是否也需要用户实现呢?
答案是需要的,因为string类的构造会开辟新空间,那么要实现对空间的深拷贝就需要自己实现
// 深拷贝
string(const string& s)
{
_capacity = s._size;
_size = _capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
4. 赋值重载函数
// s1 = s3;
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;
}
首先要判断f赋值符号左右两边是否是同一对象,是则直接返回*this
,否则再进一步操作
拓展:现代写法的拷贝构造和赋值重载
5. 析构函数
~string()
{
delete[] _str;
_str = nullptr;
_size = _capacity = 0;
}
6. c_str()
const char* c_str() const
{
return _str;
}
c_str 返回的是string类中的字符串数组的地址,对于流插入(<<) 会自动识别类型,const char* 类型会以字符串的形式打印输出%s
所以,string类通常用该接口打印字符串
7. size()
size_t size() const
{
return _size;
}
返回当前字符串长度(不包含\0) 最好设计成size_t 类型:因为size不会出现负数的情况
8. capacity()
size_t capacity() const
{
return _capacity;
}
返回当前字符串容量
9. 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];
}
对[]的重载,[]访问分为两种情况:普通对象(可修改数据)const对象(不可修改)
传引用返回更加高效,也可修改数据
记得assert断言,防止出现越界访问的情况
10. 迭代器的实现
迭代器的底层可能是指针,也可能不是(list等其他结构),在string类中迭代器的底层就是char* 类型的指针
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
范围for循环的本质就是迭代器 范围for自动替换成迭代器中的begin和end,如果把begin替换成Begin也无法配对(报错)
11. reserve() – 调整字符串容量(扩容)
当我们要进行增删改查等操作时,如果空间过小则无法继续进行,需要扩容
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
实现接口的方式就是在堆上重新找一块n大小的空间,将原空间拷贝过来并释放原空间
将_str指向新空间,将_capacity置为n
12. 尾插单个字符
void push_back(char ch)
{
if (_size == _capacity)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
在尾插单个字符的时候要注意,之前_size位置的元素是\0,所以在_size++之后要在将_size放入\0
13. 尾插字符串
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+=(const char* str)
{
append(str);
return *this;
}
而尾插字符串时因为str自带\0,strcpy会拷贝\0,所以_size+_len 位置就是\0,不需要单独处理
14. 在任意位置插入单个字符
string& insert(size_t pos,char ch)
{
assert(pos <= _size);
if (_capacity == _size)
{
size_t newCapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newCapacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
15. 在任意位置插入字符串
16. 删除len个字符
17. 查找字符/子串
18. 调整大小
19. 流插入、流提取
在之前的学习过程中,我们了解到流提取和流插入一般都不会实现成成员函数,因为istream / ostream 会抢占this指针的位置
提问:是不是流提取和流插入一定要实现成友元全局函数呢?
答:不是,全局函数是正确的(不能定义在类体内) 但是不一定要实现成友元,友元的作用是帮助我们去访问类内的成员。
流提取和流插入是为自定义类型而生的(C++),printf和scanf对内置类型非常友好但是无法识别自定义类型(C语言)
所以在打印string类时还是推荐使用流插入
经过get函数的优化已经可以基本实现流提取功能,但是针对众多字符的插入采用+=操作会导致频繁扩容的情况,那么如何优化呢?
优化1:临时空间减少扩容
优化2:覆盖数据
到这里string类就完整实现啦!撒花✿✿ヽ(°▽°)ノ✿