目录
接下来我们就来模拟实现一下 string 类的基本操作。
- string(const char* str = "")
- {
- _size = strlen(str);
- _capacity = _size;
- _str = new char[_capacity + 1];
- strcpy(_str, str);
- }
① 缺省值为“”,“\0”这样的写法实际上是两个字符,为\0 \0,因为常量字符串会默认加一个\0。
②推荐使用函数体内初始化,在函数体内初始化只用调用一次 strlen 函数,如果使用初始化列表,其中_size、和_capacity 的计算都应经过 strlen 计算,因为初始化列表的顺序是通过成员变量定义的顺序来决定的。
③即使字符串为空,那也至少开辟一个空间,留给\0的空间。
- ~string()
- {
- delete[] _str;
- _str = nullptr;
- _size = _capacity = 0;
- }
析构函数比较简单,就不多解释了。
以下是一些经常使用的string中的基础函数,特别注意,const对象只能调用const成员函数。
所以,如果我们有 const 成员变量想获取某个位置的值时,我们要提供一个const型的 []操作符函数重载。
- const char* c_str() const
- {
- return _str;
- }
-
- size_t size() const
- {
- return _size;
- }
-
- size_t capacity() const
- {
- return _capacity;
- }
-
- const char& operator[](size_t pos) const
- {
- assert(pos < _size);
- return _str[pos];
- }
-
- char& operator[](size_t pos)
- {
- assert(pos < _size);
- return _str[pos];
- }
在 string 中,迭代器不过就是一个类似指针的存在,所以我们直接typedef 一下char型指针即可。
- typedef char* iterator;
- typedef const char* const_iterator;
- iterator begin()
- {
- return _str;
- }
- iterator end()
- {
- return _str + _size;
- }
- const_iterator begin() const
- {
- return _str;
- }
- const_iterator end() const
- {
- return _str + _size;
- }
即根据所传参数来构建一个相同的对象。
- //拷贝构造函数
- string(const string& s)
- :_str(new char [s._capacity+1])
- ,_size(s._size)
- ,_capacity(s._capacity)
- {
- strcpy(_str, s._str);
- }
赋值运算符重载的实现,有以下几个需要注意的点:
- string& operator=(const string& s)
- {
- if (this!= &s)
- {
- char* temp= new char[s._capacity + 1];
- strcpy(temp,s._str);
- delete[] _str;
- _str = temp;
- _size = s._size;
- _capacity = s._capacity;
- }
- return *this;
- }
拷贝构造函数和赋值运算符重载这两个功能的实现,我们可以采用更为简便的方式来实现。
思路:根据函数参数生成一个临时对象,将该临时对象的数据与this指向的数据进行交换。
- //拷贝构造函数
- string(const string& s)
- :_str(nullptr)
- , _size(0)
- , _capacity(0)
- {
- //构造出一个函数
- string temp(s._str);
- swap(temp);
- }
- //赋值运算符重载
- string& operator=(const string& s)
- {
- if (this != &s)
- {
- string temp(s._str);
- swap(temp);
- }
- return *this;
- }
这其中所调用的 swap 函数为全局swap函数,但是全局的 swap 函数会生成一个临时变量,这样便会产生很大的代价。对于string类来说,其交换数据无非是将其中的成员变量_str的指向改变一下而已,完全不需要这么大的代价,所以我们来自己实现一个string类中的 swap 函数。
- //编译器会在当前命名空间中寻找,然后再去全局域中找
- void swap(string& temp)
- {
- ::swap(_str, temp._str);
- ::swap(_size, temp._size);
- ::swap(_capacity, temp._capacity);
- }
加上了域作用限定符则表示调用了全局的 swap 函数,而这里的调用,仅仅是交换一些内置类型,并没有将我们编写的string类拷贝生成一份,这便大大提高了运行效率。
赋值运算符重载的简化写法还有优化的地方。
我们知道,函数传参,传的是实参的临时拷贝,那既然函数传参已经生成了临时拷贝,我们便可以利用其自动生成的临时拷贝对象。
- //注意,这里是传值传参
- string& operator=(string s)
- {
- swap(s);
- return *this;
- }
注意了,这种简洁方式采用的传值传参,然后调用我们 string 类中的 swap 函数。
reserve 的实现:
分清楚resize和reserve的区别:
reserve是设置了capacity的值,比如reserve(20),表示该容器最大容量为20,但此时容器内还没有任何对象,也不能通过下标访问。
resize既分配了空间,也创建了对象,可以通过下标访问。当resize的大小
reserve只修改capacity大小,不修改size大小,resize既修改capacity大小,也修改size大小。
- void reserve(size_t n)
- {
- //大于_capacity 才扩容
- if (n > _capacity)
- {
- char* temp = new char[n + 1];
- strcpy(temp, _str);
- delete[] _str;
- _str = temp;
- _capacity = n;
- }
- }
注意:
需要n个空间,则要开辟n+1开空间,留一个给\0
注意插入数据后要在末尾放入\0
resize 的实现:
- void resize(size_t n, char ch ='\0')
- {
- //1.比当前_size大 2.小于等于_size
- if (n > _size)
- {
- //插入数据
- reserve(n);
- for (size_t i = _size;i
- {
- _str[i] = ch;
- }
- _str[n] = '\0';
- }
- else
- {
- //删除数据
- _str[n] = '\0';
- _size = n;
- }
- }
9. string 之插入数据
① push_back
push_back的实现其实与实现顺序表的尾插相似。
- 先检查大小,然后将数据进行尾插。
- 注意 _capacity==0 的情况出现,要额外检查capacity==0的情况。
- 将 _size 进行++,然后在_size处放入'\0'即可。
- void push_back(char ch)
- {
- //满了就扩容
- if (_size == _capacity)
- {
- //判断一下,因为我们开始给的缺省值为\0,其中capacity为0
- reserve(_capacity == 0 ? 4 : _capacity * 2);
- }
- _str[_size] = ch;
- //插完数据后要再插入一个\0
- ++_size;
- _str[_size] = '\0';
- }
② append 的实现
关于 append 的实现:
- 计算传入的字符串大小,根据字符串的大小检查是否开辟空间以及开辟多大空间(防止盲目开辟空间).
- 如果是 _size+len > _capacity ,不理解 > 还是 >= 可以代值进行计算一下。
- 使用 strcpy 拷贝数据,不过是从_str+_size处开始拷贝,将 str 的数据拷贝到 string 类中。
- 将_size += len,表示数据插入完成。
- void append(const char* str)
- {
- //计算需要多大的空间
- size_t len = strlen(str);
- //满了就扩容
- if (_size + len > _capacity)
- {
- //至少要开辟这么大的空间
- reserve(_size + len);
- }
- strcpy(_str + _size, str);
- _size += len;
- }
③ +=运算符重载(字符)
实现了 push_back 和 append 后,我们来实现+=运算符重载,关于+=运算符重载,它比push_back和 append 的优势在于:1. 支持连续+=;2.增强代码可读性。
- //字符型调用 push_back
- string& operator+=(char ch)
- {
- push_back(ch);
- return *this;
- }
-
- //字符串类型调用 append
- string& operator+=(char* str)
- {
- append(str);
- return *this;
- }
④ insert
- 检查pos位置是否大于 _size,大于则直接报错。
- 检查_size 是否与_capacity相等,相等则扩容。
- 将数据向后挪动,然后插入数据。
- void insert(size_t pos, char ch)
- {
- assert(pos <= _size);
- if (_size ==_capacity)
- {
- reserve(_capacity==0?4:_capacity*2);
- }
- size_t end = _size;
- while (end >= pos)
- {
- _str[end + 1] = _str[end];
- --end;
- }
- _str[pos] = ch;
- ++_size;
- }
10. 流提取、流插入操作符重载
接下来我们就来实现流插入和流提取操作符的重载,实现这两个操作符重载,可以方便我们打印的插入数据。
①流提取操作符
- 注意要重载为全局函数,这样才能不用使用.(成员访问操作符)既可以打印数据了。
- 不必声明此函数为string类的友元函数,因为此操作符并未访问私有成员变量
- 因为提取操作符不涉及更改数据,所以使用const型函数形式参数,使用[ ]访问数据时,[ ]操作符重载函数必须为 const 型,因为 const 型对象只能调用 const 型成员函数。
- 因为支持连续提取,所以要做 ostream& 作为返回值。
- ostream& operator<<(ostream& out, const string& s)
- {
- for (size_t i = 0; i < s.size(); ++i)
- {
- out << s[i];
- }
- return out;
- }
② 流插入操作符
- 每次插入数据都要清空当前对象中_str存放的数据。
- 因为cin 会将' ' (空格)或 '\n' 视为分隔符,不会读入其中,所以我们无法通过检测' ' (空格)或 '\n' 来停止数据。这时我们便使用istream中的一个成员函数get()来帮我们获取分隔符。
- 使用一个字符 ch来存储数据,并将其不断插入到类中。
- 返回 in,因为流插入操作符要支持连续插入。
- void clear()
- {
- _str[0] = '\0';
- _size = 0;
- }
-
- istream& operator>>(istream& in, string& s)
- {
- s.clear();
- char ch;
- ch = in.get();
- while (ch != ' ' && ch != '\n')
- {
- s += ch;
- ch = in.get();
- }
- return in;
- }
当输入的字符串很长的时候,不断+=,频繁的扩容,会使效率变得底下,所以我们可以使用一个类似内存池的机制来优化以上这种场景。
开辟一个大小合适的数组用来临时存储数据,临时数组存放满了之后再将其插入到 string 对象中。
- istream& operator >> (istream& in, string& s)
- {
- s.clear();
- char ch;
- ch = in.get();
- const size_t N = 32;
- char buff[N];
- size_t i = 0;
- while (ch != ' ' && ch != '\n')
- {
- buff[i++] = ch;
- //将数据插入到对象中
- if (i == N - 1)
- {
- buff[i] = '\0';
- s += buff;
- i = 0;
- }
- ch = in.get();
- }
- buff[i] = '\0';
- s += buff;
- return in;
- }
11. find 和 substr
在上一篇博客中我们使用了find和substr实现了网页链接切割,接下来我们就来模拟实现一下这两个功能。
find函数的实现:
- 如果是字符,遍历_str即可。
- 字符串可以使用 strstr 函数,其作用是返回字串在字符串中第一次出现的位置。
- 然后用 strstr 返回的值减去_str,即可得该子串得起始位置。
- //使用find 和 substr 进行域名字符串的切割
- size_t find(char ch, size_t pos = 0) const
- {
- assert(pos < _size);
- for (size_t i = pos; i < _size; i++)
- {
- if (ch == _str[i])
- return i;
- }
- return npos;
- }
-
- //子串查找
- size_t find(const char* sub, size_t pos = 0) const
- {
- assert(sub);
- assert(pos < _size);
- //查找
- const char *ptr=strstr(_str + pos, sub);
- if (ptr == nullptr)
- return npos;
- return ptr - _str;
- }
substr 函数的实现:
- 检查是否切割到字符串结尾,如果是直接将_pos位置后的数据全部放入临时变量中
- 如果不是切割到字符串结尾,则一个一个放入到临时变量中。
- string substr(size_t pos, size_t len = npos) const
- {
- assert(pos < _size);
- size_t realLen = len;
- //如果切割的部分大于字符串的大小
- if (len == npos || pos + len > _size)
- {
- realLen = _size - pos;
- }
- //创建一个临时变量,返回切割后的数据。
- string sub;
- for (size_t i = 0; i < realLen; ++i)
- {
- sub += _str[pos + i];
- }
- return sub;
- }
12. 比较函数重载
这就是实现一些大于、等于、小于此类的操作符重载了。
- bool operator>(const string& s) const
- {
- return strcmp(_str, s._str) > 0;
- }
- bool operator==(const string& s) const
- {
- return strcmp(_str, s. _str)==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);
- }