1 类中成员变量的声明
通过上一篇文章对string类的简单使用相信大家对于string类中成员变量已经很熟悉了,这里就不再多说了:
class string { private: char* _str; int _size;//当前有效数据个数 int _capacity;//存储有效数据的最大容量,不包括'\0' static const size_t npos; typedef char* iterator; typedef const char* const_iterator; };
但是为了测试时避免与库里面的冲突我们就把该类放在一个独立的命名空间中:
namespace grm { class string { private: char* _str; int _size;//当前有效数据个数 int _capacity;//存储有效数据的最大容量,不包括'\0' static const size_t npos; typedef char* iterator; typedef const char* const_iterator; }; }
2 迭代器
iterator begin() { return _str; } iterator end() { return _str + _size; } const_iterator begin()const { return _str; } const_iterator end()const { return _str + _size; }
3 一些常用接口
size_t size()const //无论是不是const对象都能够调用 { return _size; } char* c_str()const { return _str; } char& operator[](size_t pos) { assert(pos < _size); return _str[pos]; } const char& operator[](size_t pos)const { assert(pos < _size); return _str[pos]; } void clear()const { _str[0] = '\0'; _size = 0; } size_t capacity()const { return _capacity; } bool empty()const { return _size == 0; }
4 六大默认函数
4.1 构造
string(const char* s) :_size(strlen(s)) ,_capacity(_size) { _str = new char[_capacity + 1];//多开出一个空间用来存储'\0' strcpy(_str, s); }
但是我们发现如果是无参又该怎么办?
有人会说像这样特殊处理一下就好了:
string() :_str(new char[1]) ,_size(0) ,_capacity(0) { _str[0] = '\0'; }
这样处理是没有问题的,但是我们学过缺省参数,在这里能不能够给一个缺省值呢?缺省值又该给那个呢?
C++中是这样处理的:
string(const char* s="")//给一个空字符串 :_size(strlen(s)) ,_capacity(_size) { _str = new char[_capacity + 1];//多开出一个空间用来存储'\0' strcpy(_str, s); }
缺省值给的是一个空字符串。
4.2 拷贝构造
传统写法:
//传统写法 string(const string& s) :_size(s._size) ,_capacity(s._capacity) { _str = new char[_capacity+1]; strcpy(_str, s._str); }
这种写法就是自己手动开空间拷贝。
现代写法:我们发现上面我们已经实现好了默认的构造函数,我们可以直接用构造函数来解决
void swap(string& s) { std::swap(s._str, _str); std::swap(s._capacity, _capacity); std::swap(s._size, _size); } //现代写法 string(const string& s) :_str(nullptr)//必须要给初始值,否则交换后调用析构函数就会析构随机值会崩溃 ,_size(0) ,_capacity(0) { string tmp(s._str); swap(tmp); }
其中需要注意的是使用这种方式用初始化列表将_str初始化成nullptr,不然交换后调用tmp的析构函数就会释放随机地址而出错。
4.3 赋值运算符重载
与拷贝构造一样,赋值运算符重载也分为传统版本和现代版本。
传统版本:
//传统写法 string& operator=(const string& s) { if (this != &s) { //这种方式如果new失败了抛异常就会有问题,我们并不想_str维护的空间被释放 delete[] _str; _str = new char[s._capacity+1]; strcpy(_str, s._str); _size = s._size; _capacity = s._capacity; } return *this; }
大家可以从注释中看见传统写法都是要自己手动开空间拷贝,而且大家也发现了代码中还多了一个判断this是否与&s相等,否则当直接delete_str后由于s与_str指向的是同一块空间,再对s解引用就是典型的野指针问题了。而且还有一个问题就是当我们new空间失败时我们并不想_str被释放,所以可以用下面这种写法:
//传统写法 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; }
而且大家发现没有这样写即使不用写上面的if判断程序依旧可以正常运行,但是加上判断当自己给自己赋值时可以减少拷贝。
现代写法 方法1:
//现代写法 方法1: string& operator=(const string& s) { if (this != &s) //这里加上判断条件在自己给自己赋值能够减少拷贝,不加也是没有问题的 { string tmp(s); swap(tmp); } return *this; }
现代写法 方法2:
//现代写法 方法2: string& operator=(string s) { swap(s); return *this; }
这种方式就更加巧妙了,直接用了传值传参,而传值传参就是一个拷贝构造。
4.4 析构
~string() { if (_str) { delete[] _str; _str = nullptr; _size = 0; _capacity = 0; } }
5 开空间&&增删查改
void reserve(size_t n) //这里要求开n个有效空间,不包括'\0' { if (_capacity < n) { char* tmp = new char[n + 1]; strcpy(tmp, _str); delete[] _str; _str = tmp; _capacity = n; } } void resize(size_t n, char ch = '\0') { if (n > _size) { if (n > _capacity) { reserve(n); } memset(_str + _size, ch, n - _size); } _size = n; _str[_size] = '\0'; }
void push_back(char ch) { if (_capacity == _size) reserve(_capacity == 0 ? 4 : _capacity * 2); _str[_size] = ch; ++_size; _str[_size] = '\0'; } void append(const char* s) { int len = strlen(s); if (_size + len > _capacity) reserve(_size + len); strcpy(_str + _size, s); _size += len; } string& operator+=(char ch) { push_back(ch); return *this; } string& operator+= (const char* s) { append(s); return *this; } size_t find(char ch) { for (size_t i = 0; i < _size; i++) if (_str[i] == ch) return i; return -1; } size_t find(const char* s, size_t pos=0) { int i = pos, j = 0;//i是主串遍历,j是子串遍历 while (i < _size && j < strlen(s)) { if (s[j] == _str[i]) { i++; j++; } else { i = i - j + 1; j = 0; } } if (j == strlen(s)) return i - j; else return -1; }
string& insert(size_t pos, const char* s)//在pos位置插入,字符串从pos位置开始插 { assert(pos <= _size); int len = strlen(s); if (_size + len > _capacity) reserve(_size + len); int end = _size + len - 1; int gap = _size - pos ; while (gap--) { _str[end] = _str[end - len]; end--; } strncpy(_str + pos, s, len);//不要拷贝'\0' _str[_size + len] = '\0'; _size+=len; return *this; } string& erase(size_t pos, size_t len = npos) { assert(pos < _size); if (len == npos || pos + len >= _size) { _str[pos] = '\0'; _size = pos; return *this; } strcpy(_str + pos, _str + len + pos); _size -= len; return *this; }
这些代码我们之前上顺序表都已经比较详细的介绍了,大家可以参考参考。
我们可以自己测试一下来看看:
可以发现是没有多大问题的。
6 其他运算符重载
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 strcmp(_str, s._str) <= 0; } bool operator>=(const string& s)const { return strcmp(_str, s._str) >= 0; }bool operator!=(const string& s)const { return strcmp(_str, s._str) != 0; } ostream& operator<<(ostream& out, const string& s) { //out << s.c_str();//能这么写吗? 不能,也许我们会在字符中间插入一个'\0' for (auto& e : s) { out << e; } /*for (int i = 0; i < s.size(); i++) { out << s[i]; }*/ return out; } istream& operator>>(istream& in, string& s) { s.clear();//要加上这个,否则可能出错 char ch = in.get(); while (ch != '\0' && ch != '\n') { s += ch; ch = in.get(); } return in; }
上面这几个比较可以自己重载成成员方法,当然也可以重载成全局函数。下面流提取运算符和流插入运算符我们在之前已经讲过了,里面需要注意的细节代码中都有注释。