string的成员
class string() { private: char *str; size_t _size; size_t _capacity; const static size_t npos=-1; };
在类和对象中提到过,静态成员变量不能给缺省值,必须要在类外定义。
但是其实有一个特例,那就是针对整形开了一个后门,静态的整形成员变量可以直接在类中定义。
一.构造,拷贝构造,赋值重载和析构
1.构造函数
在类和对象时提到过,如果要显示定义构造函数最好是给全缺省
string(const char *str="")//注意:\0和空指针一样,'\0'是字符常量(char类型),“”和“\0"一样, { _size = strlen(str);//这是成员函数,有this指针 _capacity = _size; _str = new char [_capacity + 1];//多开一个字节给'\0' strcpy(_str, str);//可能会传参过来构造 }
这里的_capacity是给有效字符预留的空间,为了给’\0’留位置在开空间的时候要多开一个。
2.拷贝构造
首先是老实人写法,构造一个新的空间,将s._str的值拷贝到新的空间,再将其他的值拷贝
//拷贝构造 string(const string& s) { //先构造一个新空间给_str _str = new char[s._capacity + 1]; //然后将s的其他值赋值给this _size = s._size; _capacity = s._capacity; strcpy(_str, s._str); }
在下面的写法中,会经常复用C语言中的字符串函数。(不为别的,就是好用)
不是谁都想当老实人,所以又有了一种现代写法(并不是为了提高效率,而是为了让代码更简洁)。
首先构造一个tmp,然后将tmp和s交换
void swap(string&s) { std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型 std::swap(_size, s._size); std::swap(_capacity, s._capacity); } string(const string& s) :_str(nullptr) //这里必须要给_str初始化,否则拿到一个非法的地址,析构会报错 ,_size(0) ,_capacity(0) { string tmp(s._str);//这里复用构造函数 swap(tmp);//有隐藏的this指针,一个参数就够了 }
如果不给_str初始化成空指针话,交换以后临时变量tmp就指向了 _str的空间,这可能是一个野指针,临时变量tmp在出这个函数就要被销毁,调用析构的时候delete一个野指针就会产生错误。
可以delete一个空指针,无论是free还是delete当接收的参数是一个空指针时就不做任何处理。
3.swap问题
标准库中的swap函数是一个模板,在交换自定义类型时有一次构造和拷贝构造,代价比较大,所以我们提供一个成员函数会比较好。在成员函数中交换内置类型时就可以使用标准库中的swap函数,**要指定域,因为编译器默认是现在局部找,局部找不到再去全局找,再找不到就报错。**如果去局部找的话,找到的swap函数参数不匹配。
4.赋值重载
复制重载也分为传统写法:
string& operator=(const string& s) { if (this != &s)//要注意不要自己给自己赋值 { //深拷贝 char* tmp = new char[s._capacity + 1]; strcpy(tmp, s._str);//为新空间赋值 _str = tmp; _capacity = s._capacity; _size = s._size; return *this; } }
现代写法,使用传值传参,然后直接使用临时变量交换,这个写法是我比较推荐的(太简洁了)
void swap(string&s) { std::swap(_str, s._str);//库中和string分别提供了一个swap函数,用库中的swap交换内置类型 std::swap(_size, s._size); std::swap(_capacity, s._capacity); } string& operator=(string s) { swap(s);//传值传参,s是一个拷贝,直接使用这个临时变量,反正临时变量出了这个函数就被销毁了 return *this; }
s是一个拷贝的临时变量,在销毁的时候会自动调用析构函数清理,不用我们做额外处理,而且直接使用临时变量调用swap还省去了我们创建临时变量。十分简洁,但是可读性不算太好
5.析构函数
直接使用delete销毁空间,再将_size和 _capacity置0就行
//析构函数 ~string() { //释放空间 delete[] _str; _str = nullptr; _size = _capacity= 0; }
二.简单接口
1.c_str
这个主要是返回一个C类型的数组指针
const char* c_str()const { return _str; }
对于只读的函数接口建议加上const,这样不但普通对象可以使用,const类型的对象也可以使用
2.size(有效字符长度)
size_t size()const { return _size; }
3.capacity(有效字符容量)
size_t capacity()const { return _capacity; }
4.operator[]
char& operator[](size_t pos) { //虽然string是自定义类型,但_str是内置类型 assert(pos < _size); return _str[pos]; } const char& operator[](size_t pos)const//const对象只读 { assert(pos < _size); return _str[pos]; }
因为const对象只能读不可写,所以这里要重载两个,重载了[]string就可以像访问数组那样访问了
5.迭代器和范围for
强调一下,迭代器虽然行为像指针,但不一定是指针
typedef char* iterator;//这里为了简单,就实现成指针 iterator begin() { return _str; } iterator end() { return _str + _size; }
有了迭代器,就可以使用范围for了,但是范围for只认识beging和end,所以如果要使用范围for,在手搓迭代器的时候就不要乱取名哦。
三.容量
1.reverse
这个是string类中用于扩容的成员函数
void reverse(size_t n) { //只扩容,所以先检查情况 if (n > _capacity) { //_str中有数据,不能直接改_str的空间,要先建立临时空间 char*tmp = new char[n + 1]; //将_str中的数据拷贝给tmp,再将_str所指的空间释放 strcpy(tmp, _str); delete[]_str; _str = tmp; _capacity = n;//更新容量 } }
reverse是控制容量的成员函数,但是缩容的代价太大了。所以只考虑缩容,其实string类中给的reverse是会缩容的。
2.resize
这个是改变有效字符长度的成员函数
void resize(size_t n,char ch='\0')//改变size可能会改变capacity,默认插入补空间的字符是'\0' { if (n > _capacity)//这里也可以写n>_size { reverse(n); //扩大空间以后,要用字符初始化后续空间 for (size_t i = _size; i < n; i++) { _str[i] = ch; } //这里还要改变size _size = n; //可能使用者会传其他字符来初始化,前面的循环没有在size位置补'\0' _str[_size] = '\0'; } else { //如果是缩小的话,就直接在n位置补'\0' _str[n] = '\0'; _size = n; } }
有看到n>_size就扩容的,但在我看来只有大于容量的时候才有必要改变有效容量。
在缩小的时候同样没必要更改容量,直接在n位置插入一个’\0’,就无法访问到n后面的元素了,间接改变了__size
3.clear
这个成员函数是将string变成一个空串,在重载流提取的时候会用到这个函数
void clear() { _size = 0; _str[0] = '\0'; }
四.插入
1.push_back
尾插单个字符,插入就要考虑扩容
void push_back(char c) { if (_size == _capacity)//容量不够,要扩容 { //扩容要调用reverse,这里还要检查一下_capacity,第一次可能是0 int newCapacity = _capacity == 0 ? 4 : 2 * _capacity; reverse(newCapacity);//这里面更新了_capacity,外面不用再更新 } //扩容完毕以后,就可以开始插入了 _str[_size] = c; _size++; _str[_size] = '\0'; }
这样尾插只能插入单个字符,所以string还提供了一个append(追加字符串)
2.append
这是一个在末尾追加字符串的成员函数
string& append(const char* s) { //这里可以使用C标准库中的strcpy,不过还是要考虑扩容的问题,要检查剩下的空间是否足够插入 int len = strlen(s); if (_size + len > _capacity) { //容量不够插入就要扩容 reverse(_capacity + len); } //复用C标准库函数 strcpy(_str + _size, s); _size += len; return *this; }
追加一个字符串开原本空间的两倍可能还不够用,最正确的写法是计算一下字符串的长度用于增加空间
3.operator+=
重载+=是string类一个非常正确的选择,在做oj的时候你将发现,+=比尾插和追加好用太多了。
string& operator+=(char c) { //插入一个字符直接_size位置插入,复用push_back push_back(c); return *this; } //还要重载一个字符串类型,可以复用append string& operator+=(const char* str) { append(str); return *this; }
先介绍push_back和append不是没有道理的,复用可以减少代码冗余还省事,还不快复用起来
4.insert
在pos位置插入一个字符或者字符串,需要挪动数据。
插入单个字符:
string& insert(size_t pos, char c) { assert(pos <= _size); //string没有单独的扩容函数,在每个插入数据的地方都要检查容量 if (_size == _capacity) { int newCapacity = _capacity == 0 ? 4 : 2 * _capacity; reverse(newCapacity); } //插入字符要挪动字符,这里要小心,在pos位置插入 size_t end = _size + 1;//指向'\0'的下一个位置 while (end > pos) { _str[end] = _str[end - 1]; --end; } _str[pos] = c; _size += 1; return *this; }
这里有一个问题,要知道end和pos都是size_t类型的数据,如果在写判断条件的时候,写成end>=pos,可能会陷入死循环。
如果pos是0,end永远无法小于零,这就死循环咯。
解决办法有两种,其一是把end写成有符号的int并且在判断的时候强转pos,也就是这样:
int end = _size; while (end >=(int) pos) { _str[end] = _str[end - 1]; --end; }
第二种就是我写的这样,把end放在_size+1的位置,这样在判断的时候可以不用取等号,也就完美避免了所有问题。我比较推荐这种写法,因为end作为下标本身取值范围就应该要大于零。
插入字符串:
string& insert(size_t pos, const char* str) { assert(pos <= _size); size_t len = strlen(str); if (_size +len > _capacity) { int newCapacity = _capacity == 0 ? 4 : 2 * _capacity; reverse(newCapacity); } //扩容完毕,开始挪动数据,这次要挪动len个位置,要考虑一下长度是否越界 size_t end = _size + len; while (end > pos + len-1) { _str[end] = _str[end - len]; end--; } //位置挪出来以后要插入字符串 /*for (size_t i = pos; i <= pos + len; i++) { _str[i] = *str; 3str++; }*/ //此外,还可以复用C库函数strncpy strncpy(_str + pos, str, len); _size += len; return *this; }
这里要用strncpy而不能使用strcpy,因为strcpy会将’\0’也拷贝过来,而字符串的结束标志就是以’\0’为准的。