简介
因为世界上有很多种语言,所以在STL库里有一个管理字符的模板类:basic_string模板类,而本篇要讲的string类是basic_string模板类的一个实例化,类型为char。
string类的接口设计是比较冗余的,是因为string类有一些历史包袱。在STL出现之前,就已经有string类了。在STL被惠普实验室开源之后,string类既要符合STL的标准又要向前兼容,所以string类的接口比较冗余。
string类的底层是顺序表,它在堆上开辟连续的空间,并将代码段中的常量字符串进行拷贝。通过顺序表实现对字符串的增删查改。可参考C++的动态内存管理一文:
小编会介绍一些常见的接口,解释它们的功能,参数。然后模拟实现一下。为了方便,小编会调用C语言库中的一些字符串操作函数。头文件string.h
模拟实现不是造一个更好的轮子,而是为了帮助我们更好的理解string类。模拟实现的代码是禁不住在各种场景下测试的,一定会出现各种bug。小编会指出一些能想的到的bug。庖丁解牛,恢恢乎游刃有余
C++是一门面向对象的语言,大家不必对每一个接口都有细致的了解。小编写本篇的目的是为了让大家能对string类的主体框架有一定的了解。让大家在查文档时游刃有余。
框架
为了让类名和接口的写法和库里的保持一致,需要封一层命名空间,名字为MyString。
数据设计:
顺序表的有效数据:size_t _size
顺序表的空间大小:size_t _capacity
指向顺序表的指针:char* _str
static const size_t npos = -1;
npos
是string
类中的一个静态成员变量,它表示一个无效的位置或者未找到的位置。在string
类中,当查找某个子字符串或字符时,如果未找到,则返回npos
。
namespace MyString //命名空间 { class string //string类 { public: //接口(方法) private: size_t _size; //数据有效个数 size_t _capacity; //空间大小 char* _str; //指向的空间 static const size_t npos = -1; }; }
构造
功能:构造一个string类对象,并将其初始化
在C++11的标准中,有非常多的构造函数,如下图
模拟实现:
小编先问大家一个问题,当我们用"x\0xxx"字符串构造对象时,有效数据_size应该给成多少呢,1还是5?
可以参考一下标准库里的实现
标准库的逻辑是:在初始化对象时,以第一个"\0'为准。我们就可以调用库中的strlen(),帮我们计算一下要初始化_size的大小。strlen()计算长度时也是以第一个"\0"为准。
后面会有"x\0xxx",但_size是5的情况。因为标准库中的string类的有效数据是以_size为准,而不是以"\0"为准。这是C++的string类和C语言中字符串函数不一样的地方。
全缺省构造函数
代码如下
//全缺省构造 string(const char* str = "") :_size(strlen(str)) ,_capacity(_size + 1) ,_str(new char[_capacity]) { strcpy(_str, str); }
传对象构造函数
代码如下
string(const string& str) :_size(str._size) , _capacity(str._capacity) , _str(new char[str._capacity]) { memcpy(_str, str._str, str._capacity); //这里用strcpy()拷贝会有很坑的bug }
为什么小编会说用strcpy()拷贝数据会有一个很坑的bug呢?
因为string()是以"\0"为基准作为结束条件的。而string类的顺序表可能会存放"x\0xxx"这样的字符串,数据可能会拷贝不全。
拷贝构造
代码如下
//拷贝构造 string(const string& str) { _size = str._size; //有效数据个数 _capacity = str._capacity; //空间 _str = new char[str._capacity]; //开空间,深拷贝 memcpy(_str, str._str, str._capacity); //这里建议用memcpy()防止中间有"\0"的问题 }
现在式写法,代码如下
void swap(string& s) //交换数据函数 { std::swap(_str, s._str); std::swap(_size, s._size); std::swap(_capacity, s._capacity); }
string(const string& s) : _str(nullptr) //初始化数据 ,_size(0) ,_capacity(0) { string Tmp(s._str); //调用构造函数构造一个与s对象一样的临时对象 swap(Tmp); //调用交换函数交换两个对象 }
传统写法:需要自己开空间,自己拷贝数据
现在写法:把事情交给别的函数或方法去做,具有面向对象思维
析构函数
//析构函数 ~string() { delete[] _str; _size = _capacity = 0; }
容量
size()
功能:
返回字符串有效长度 |
模拟实现:
size_t size() const { return _size; }
capacity()
功能:
返回string类的顺序表的空间 |
模拟实现:
size_t capacity() const { return _capacity; }
empty()
功能:
判断字符串是否为空 |
模拟实现:
bool empty() const { if (_size == 0) return true; else return false; }
clear()
功能:
清空有效字符 |
模拟实现:
void clear() { _str[0] = '\0'; _size = 0; }
_str[0] = '\0';这句代码是为了兼容C语言的一些接口
reserve()
功能:
将容量调整至n |
此函数不会影响到字符串 |
n大于_capacity,编译器会将容量调整到n(可能更大,不同版本的STL具体实现不同) |
n小于或等于_capacity,编译器可能会缩容,也可能不会,不具有强制性 |
参数:
无符号整型n,代表要扩容的大小 |
模拟实现:
void reserve(size_t n = 0) { if (n <= _capacity) return; char* ptr = new char[n + 1]; //开辟新空间, 多开一个是为了存放‘\0’ memcpy(ptr, _str, _size + 1); //把旧空间的值拷贝给新空间 delete[] _str; //释放旧空间 _str = ptr; //_str指向新空间 _capacity = n + 1; ptr = nullptr; }
resize()
功能:
将字符串长度调整为n个字符的长度 |
n < _size 时,删除多余字符(删掉_size - n个字符) |
n > _size 时,会先扩容,如果指定了c,会将其初始化为字符c,没有指定,会将其初始化为"\0" |
参数:
无符号整型n,代表要调整字符的长度 |
字符型c,代表要初始化的字符 |
模拟实现:库中给了我们两个接口,但我们只需要实现一个全缺省即可
思路也很简单,只需要处理n的不同情况即可。代码如下,配有详细的注释
void resize(size_t n, char c = '\0')//c为全缺省参数,不指定c时会将新开辟的空间初始化为"\0" { if ( n < 0) //n小于为非法值,直接返回 return; if (n > _size) //n大于有效字符时,会扩容 { reserve(n); //扩容,复用reserve,后面凡是扩容都会复用reserve for (size_t i = _size; i < _capacity - 1; i++) //对开辟的空间初始化 { _str[i] = c; } _str[_capacity - 1] = '\0'; //给最后一块空间赋值为\0 _size = n; //不要忘了调整有效数据 return; } _str[n] = '\0'; //当n小于或等于0时删数据 _size = n; }
遍历
检引用符号"[ ]"的重载
功能:
检索第pos位置的下标 |
参数:
无符号整型pos,代表下标 |
返回值:
char类型,返回string类中顺序表的的第pos位置的字符 |
模拟实现:库中给了两个,一个非成员函数,一个成员函数。代码如下
char& operator[] (size_t pos) { assert(pos >= 0); assert(pos <= _size); //检查下标的合法性 return _str[pos]; //返回顺序表中该下标的值 }
const char& operator[] (size_t pos) const { assert(pos >= 0); assert(pos <= _size); return _str[pos]; }
迭代器
需要把返回值的类型取别名为迭代器的通用类型,代码如下
typedef char* iterator; //正向迭代器,可修改 typedef char* reverse_iterator; //反向迭代器,可修改 typedef const char* const_iterator; //正向迭代器,不可修改 typedef const char* const_reverse_iterator; //反向迭代器,不可修改
因为string类型的底层是基于顺序表实现的,所以string类型的迭代器是指针
begin()
获取有效字符的第一个位置 |
iterator begin() //获取第一个字符的位置 { return _str; } const_iterator begin() const { return _str; }
end()
获取最后一个有效字符的下一个位置 |
iterator end() //最后一个字符的下一个位置 { return _str + _size; } const_iterator end() const { return _str + _size; }
rbegin()
获取最后一个有效字符的位置 |
reverse_iterator rbegin() //获取最后一个字符的位置 { return _str + _size - 1; } const_reverse_iterator rbegin() const { return _str + _size - 1; }
rend()
获取第一个有效字符的前一个位置 |
reverse_iterator rend()//获取第一个字符位置的前一个位置 { return _str - 1; } const_reverse_iterator rend() const { return _str - 1; }
修改
push_back()
功能:
将字符c尾插至string类的字符串后面 |
参数:
字符型c,代表要尾插的字符 |
模拟实现:
void push_back(char c) { if ((_size + 1) == _capacity || _size == _capacity) //判断是否要扩容 { reserve(2 * _capacity); } _str[_size] = c; //尾插 _str[_size + 1] = '\0'; //插入\0 ++_size; //更改有效数组的值 }
"+="运算符重载
功能:
尾插一个类的字符串 |
尾插一个字符串 |
尾插一个字符 |
返回值:
string类的引用 |
这三类运算符重载互相构成函数重载
实现代码如下
string& operator+= (const string& str) { size_t len = str._size + 1; if ((_size + len) >= _capacity) //扩容 { reserve(2 * (_capacity + _size + len)); } strcpy(_str + _size, str._str); //拷贝数据 return *this; }
上述代码中拷贝数据用了strcpy(),在一般场景下是没问题的。但禁不住测试,大家能调试出来吗?这个bug前文已经提过很多次了。
string& operator+= (const char* s) { while (*s) { push_back(*s); s++; } return *this; }
string& operator+= (char c) { push_back(c); return *this; }
小编这里复用了push_back(),小编懒得再造轮子了(害羞)
c_str
功能:
返回C形式的字符串 |
模拟实现:
const char* c_str() const { return _str; }
find()
功能:
查找字符串序列在string类的顺序表中第一次出现的位置 |
参数:
string类类型的引用,表示要查找的字符串序列为string类型的顺序表 |
无符号整型pos,表示从哪一个下标开始找 |
字符型c,表示要查找的字符 |
字符型指针s,表示要查找的字符串 |
返回值:
找到:返回该位置的下标 |
没找到:返回npos |
模拟实现:小编只实现其中的一种
size_t find(const string& str, size_t pos = 0) const { if (pos < 0 || pos > _size) //检查坐标合法性 return npos; char* ptr = strstr(_str + pos, str._str); //strstr()是一种暴力匹配, if (ptr == NULL) { return npos; } else { return ptr - _str; } }
我们不是为了造更好的轮子,用strtstr()即可。
其他
赋值运算符重载
现在式写法
string& operator=(string tmp) { swap(tmp); return *this; }
比较运算符重载
只需实现"<"和"="即可,其他可以复用
"<"运算符重载
bool operator<(const string& s) { size_t i = 0; size_t j = 0; while (i < _size && j < s._size)//按长度小的字符串比较 { if (_str[i] < s._str[j]) { return true; } else if (_str[i] > s._str[j]) { return false; } else { ++i; ++j; } } return _size < s._size;//如果相等长度下相等,长度小的字符串就小 }
"="运算符重载
bool operator==(const string& s) { size_t i = 0; size_t j = 0; while (i < _size && j < s._size) { if (_str[i] < s._str[j] || _str[i] > s._str[j]) { return false; } else { ++i; ++j; } } return _size == s._size ? true : false; }
复用
bool operator<=(const string& s) { return *this <= s; } bool operator>(const string& s) { return !(*this <= s); } bool operator>=(const string& s) { return *this >= s; } bool operator!=(const string& s) { return !(*this == s); }
本文到这里就结束啦