String类各函数接口总览
默认成员函数
构造函数
构造函数设置为缺省函数,若不传入函数,则默认构造为空字符串。字符串的初始大小和容量均设为传入C字符串的长度。(不包含'\0');
string s1("hello world");//构造函数
//构造函数 string(const char* str = "") { _size = strlen(str); //初始时,字符串大小设置为字符串长度 _capacity = _size; //初始时,字符串容量设置为字符串长度 _str = new char[_capacity + 1]; //为存储字符串开辟空间(多开一个用于存放'\0') strcpy(_str, str); //将C字符串拷贝到已开好的空间 }
拷贝构造函数
模拟实现拷贝构造之前,我们需要先了解深浅拷贝:
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以 当继续对资源进项操作时,就会发生发生了访问违规.
string s1("hello world"):string s2(s1); //拷贝构造
下面来看一下浅拷贝这个例子:
string s1("hello world"); string s2(s1);
运行结果:
报错了?为什么会这样?我们根据上面给的定义简单分析下:
通过调试也会发现,这里他们共同用一块空间
因此,为了避免两次释放同一块空间,我们需要进行深拷贝,深拷贝就是重新开出来一块空间.
下面提供两种深拷贝的两种写法:
写法一:传统的写法
传统写法的思想比较简单:先开辟一块足以容纳源对象字符串的空间,然后将源对象的字符串拷贝过去,然后将源对象的字符串拷贝过去,接着把源对象的其他成员变量也赋值过去即可。拷贝对象的_str与源对象的_str指向的并不是同一块空间,所以拷贝出来的对象与源对象是相互独立的。
//拷贝构造函 string(const string& str) :_str(new char(strlen(str._str)+1)) ,_size(0) ,_capacity(0) { strcpy(_str, str._str);//将str._str拷贝一份到_str _size = str._size;//_size赋值 _capacity = str._capacity;//_capacity赋值 }
会发现,深拷贝会重新开辟一个空间出来,这样就不会出现一个空间被释放两次的错误
写法二:现代的写法
现代写法与传统写法的思想不同:先根据源字符串的C字符串调用构造函数构造一个tmp对象,然后再将tmp对象与拷贝对象的数据交换即可。拷贝对象的_str与源对象的_str指向的也不是同一块空间,是互相独立的。
//现代写法 void swap(string& s) { //调用库里的swap ::swap(_str, s._str);//交换两个对象的字符串 ::swap(_size, s._size);//交换两个对象的大小 ::swap(_capacity, s._capacity);//交换两个对象的容量 } string(const string& s) :_str(nullptr) ,_size(0) ,_capacity(0) { string tmp(s._str);//调用构造函数 swap(tmp);//交换这两个对象 }
赋值运算符重载函数
与拷贝构造函数类似,赋值运算符重载函数的模拟实现也涉及深浅拷贝问题,我们同样需要采用深拷贝。下面也提供深拷贝的两种写法:
string d1;
string d2(2022,2,20);
d1=d2;
写法一:传统写法
赋值运算符重载函数的传统写法与拷贝构造函数的传统写法几乎相同,只是左值的_str在开辟新空间之前需要先将原来的空间释放掉,并且在进行操作之前还需判断是否是自己给自己赋值,若是自己给自己赋值,则无需进行任何操作。
//传统写法 string& operator=(const string& s) { if (this != &s) //防止自己给自己赋值 { delete[] _str; //将原来_str指向的空间释放 _str = new char[strlen(s._str) + 1]; //重新申请一块刚好可以容纳s._str的空间 strcpy(_str, s._str); //将s._str拷贝一份到_str _size = s._size; //_size赋值 _capacity = s._capacity; //_capacity赋值 } return *this; //返回左值(支持连续赋值) }
写法二:现代写法
赋值运算符重载函数的现代写法与拷贝构造函数的现代写法也是非常类似,但拷贝构造函数的现代写法是通过代码语句调用构造函数构造出一个对象,然后将该对象与拷贝对象交换;而赋值运算符重载函数的现代写法是通过采用“值传递”接收右值的方法,让编译器自动调用拷贝构造函数,然后我们再将拷贝出来的对象与左值进行交换即可。
//现代写法 void swap(string& s) { //调用库里的swap ::swap(_str, s._str);//交换两个对象的字符串 ::swap(_size, s._size);//交换两个对象的大小 ::swap(_capacity, s._capacity);//交换两个对象的容量 } string& operator=(const string& s) { if (this != &s) //防止自己给自己赋值 { string tmp(s); //用s拷贝构造出对象tmp swap(tmp); //交换这两个对象 } return *this; //返回左值(支持连续赋值) }
析构函数
string类的析构函数需要我们进行编写,因为每个string对象中的成员_str都指向堆区的一块空间,当对象销毁时堆区对应的空间并不会自动销毁,为了避免内存泄漏,我们需要使用delete手动释放堆区的空间。
//析构函数 ~string() { delete[] _str; //释放_str指向的空间 _str = nullptr; //及时置空,防止非法访问 _size = 0; //大小置0 _capacity = 0; //容量置0 }
容量和大小相关函数
size和capacity
size函数用于获取字符串当前的有效长度(不包括’\0’)。
//大小 size_t size()const { return _size; //返回字符串当前的有效长度 }
capacity函数用于获取字符串当前的容量。
//容量 size_t capacity()const { return _capacity; //返回字符串当前的容量 }
reserver和resize
reserve和resize这两个函数的执行规则一定要区分清楚。
reserve规则:
1、当n大于对象当前的capacity时,将capacity扩大到n或大于n。
2、当n小于对象当前的capacity时,什么也不做。
//改变容量,大小不变 void reserve(size_t n) { if (n > _capacity) //当n大于对象当前容量时才需执行操作 { char* tmp = new char[n + 1]; //多开一个空间用于存放'\0' strncpy(tmp, _str, _size + 1); //将对象原本的C字符串拷贝过来(包括'\0') delete[] _str; //释放对象原本的空间 _str = tmp; //将新开辟的空间交给_str _capacity = n; //容量跟着改变 } }
注意:代码中使用strncpy进行拷贝对象C字符串而不是strcpy,是为了防止对象的C字符串中含有有效字符’\0’而无法拷贝(strcpy拷贝到第一个’\0’就结束拷贝了)。
resize规则:
1、当n大于当前的size时,将size扩大到n,扩大的字符为ch,若ch未给出,则默认为’\0’。
2、当n小于当前的size时,将size缩小到n。
//改变大小 void resize(size_t n, char ch = '\0') { if (n <= _size) //n小于当前size { _size = n; //将size调整为n _str[_size] = '\0'; //在size个字符后放上'\0' } else //n大于当前的size { if (n > _capacity) //判断是否需要扩容 { reserve(n); //扩容 } for (size_t i = _size; i < n; i++) //将size扩大到n,扩大的字符为ch { _str[i] = ch; } _size = n; //size更新 _str[_size] = '\0'; //字符串后面放上'\0' } }
empty
empty是string的判空函数,我们可以调用strcmp函数来实现,strcmp函数是用于比较两个字符串大小的函数,当两个字符串相等时返回0。
//判空 bool empty() { return strcmp(_str, "") == 0; }