目录
- 判断string大小关系
- bool operator==(const string&s1,const string s2)
- 代码
- bool operator<(const string& s1, const string& s2)
- 代码
- bool operator<=(const string& s1, const string& s2)
- 代码
- bool operator>(const string& s1, const string& s2)
- 代码
- bool operator>=(const string& s1, const string& s2)
- 代码
- bool operator!=(const string& s1, const string& s2)
- 代码
- void clear()清除数据
- 代码
- ostream& operator<<(ostream& out, const string& s)实现string的流插入
- 代码
- istream& operator>>(istream& in, string& s)实现string的流提取
- 代码
- istream& getline(istream& in, string& s)获取输入的一行
- 代码
- string(const string& s)拷贝构造现代写法
- 传统写法
- 代码
- 代码
- 总结
- 拓展1
- 拓展2
- 引用技术
感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接
🐒🐒🐒 个人主页
🥸🥸🥸 C语言
🐿️🐿️🐿️ C语言例题
🐣🐣🐣 python
🐓🐓🐓 数据结构C语言
🐔🐔🐔 C++
🐿️🐿️🐿️ 文章链接目录
判断string大小关系
bool operator==(const string&s1,const string s2)
代码
bool operator==(const string& s1, const string& s2){int ret = strcmp(s1.c_str(), s2.c_str());return ret == 0;}
bool operator<(const string& s1, const string& s2)
代码
bool operator<(const string& s1, const string& s2){int ret = strcmp(s1.c_str(), s2.c_str());return ret < 0;}
bool operator<=(const string& s1, const string& s2)
代码
bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;}
bool operator>(const string& s1, const string& s2)
代码
bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}
bool operator>=(const string& s1, const string& s2)
代码
bool operator>=(const string& s1, const string& s2){return !(s1 < s2);}
bool operator!=(const string& s1, const string& s2)
代码
bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}
void clear()清除数据
代码
void clear(){_size = 0;_str[_size] = '\0';
}
ostream& operator<<(ostream& out, const string& s)实现string的流插入
代码
ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}
istream& operator>>(istream& in, string& s)实现string的流提取
代码
istream& operator>>(istream& in, string& s){s.clear();char ch;ch = in.get();char buff[128];size_t i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
in.get()是遇到什么字符就直接取,因为有时候我们想要得到空格字符,但是用其他的输入会将空格视为结束
注意这里的string& s还没有开辟空间,所以ch = in.get()不能写成s[i]这样的形式
istream& getline(istream& in, string& s)获取输入的一行
代码
istream& getline(istream& in, string& s){s.clear();char ch;ch = in.get();char buff[128];size_t i = 0;while (ch != '\n'){buff[i++] = ch;if (i == 127){buff[127] = '\0';s += buff;i = 0;}ch = in.get();}if (i > 0){buff[i] = '\0';s += buff;}return in;}
string(const string& s)拷贝构造现代写法
拷贝构造的传统写法就是先开辟一块同样大小的空间,然后将数据拷贝到那块空间里
传统写法
string(const string& s){_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}
代码
//现代写法
string(const string& s)
{string tmp(s._str);swap(tmp);
}
这个写法的过程如下图,首先this指针是表示的s2,s2会先指向空(不指向空会出现一些问题)
然后进入函数后string tmp(s._str)会调用下面这个构造函数
string(const char* str = ""):_size(strlen(str)){_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);}
所以tmp会拷贝s1的数据
然后swap(tmp)就是让s2和tmp进行交换,因为this指针是s2
原因是string(const string& s)这个拷贝构造函数中的this指针就是s2,所以函数里面的this指针也默认是s2,交换后因为s2一开始就是指向的空,所以交换后tmp也指向空,而s2就指向tmp开出来的新空间
因为tmp是一个局部对象,出了作用域后就会调用析构函数销毁
现在回到之前的问题为什么s2最开始是指向空的
因为如果s2不指向空,那么s2就是一个随机值,当s2和tmp交换后,tmp就指向一个随机值,tmp调用析构函数后会delete他的_str,但是tmp是一个随机值,delete一个随机值就会出现问题
string& operator=(const string& s)现代写法
现在需要将s3的值赋值给s1,注意赋值是两个已经存在的对象,所以他们的空间是已经开辟好了的
如果我们想直接将s3拷贝给s1的话需要分情况
第一种情况就是s3的size大于s1的capacity,这样s1就需要扩容
第二种情况就是s3的size小于s1的capacity,但是如果s1的capacity远大于s1,比如capacity=1000,size=1
在实际当中一般不会这样拷贝数据,而是重新开一块新的空间,然后拷贝s3的数据,之后让s1指向新开的空间,然后释放s1之前的空间
string& operator=(const string& s){char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[]_str;_str = tmp;_size = s._size;_capacity = s._capacity;return *this;}
这是上面的传统写法
现代写法过程如下,先创造一个对象s2拷贝s3的数据
然后s1和s2交换,交换完之后因为s2是局部对象,出了作用域后会调用析构函数,所以s1之前的空间会被释放掉
代码
//简化版
string& operator=(string& s2)
{swap(s2);return *this;
}
这个函数和第一个函数的区别是
第一个函数的传引用,所以s就是s3的别名,然后通过s3拷贝构造出s2,再将s2和s1进行交换
第二个函数是传值传参,传值传参会调用拷贝构造函数(形参是实参的拷贝)(这里要和之前的拷贝构造死循环问题要区分开,两个是不一样的),然后s2和s1进行交换
总结
现代写法相对于传统写法在二插树 链表等场景优势比较明显
因为树的节点如果用传统写法需要一个节点一个节点的开空间,然后再释放,而现代写法就是直接调用拷贝构造函数,然后就交换,并且省了自己去释放空间
拓展1
现在string这个类中的成员变量有下面这几个
我们假设现在的环境是32位,现在需要计算他的sizeof()的大小
32位环境下指针的大小是4个字节,size_t的大小是4个字节,所以根据内存对齐规则,他的大小是12个字节,对于static修饰的变量不算进类里面,因为类里面的静态成员变量存的地方是在静态区里的,这个变量的生命周期是全局的,也就是我们可以把npos看成在类外面定义的全局变量
int main()
{std::string s1("11111");cout << sizeof(s1) << endl;jack::string s2("11111");cout << sizeof(s2) << endl;return 0;
}
上面代码中我们用了两个不同的string,一个是库里的string,另一个是我们自己写的string,我们自己写的大小是12,而库里的string却是28
下面我们来讲解一下为什么库里的string是28
通过调试我们可以看到s1多了原始视图
这里的_Mysize和_Myres其实就是上面的size和capacity
那_Bx又是什么呢
_Bx点开后有三个部分_Buf ,_Ptr, _Alias(其实只有两个部分,编译器做了一下优化所以就有三个部分了)
我们再点开_Buf
发现这个_Buf存的是字符,这些字符正好是我们输入
所以我们可以推断出这个_Buf可能就是多出来的那部分空间,因为_Buf是一个字符数组,里面有16个元素,他的大小就是16个字节,那么16+12正好就是28
也就是说我们定义的成员变量里面是有一个_Buf数组的,只是我们看不到而已
那这个数组是固定大小的吗
其实当_size<16的时候数据就存在_Buf里面(_size不能等于16,因为需要留一个位置存放\0),我们来验证一下
这次的s1字符串里面有15个字符
当变成16个字符的时候这个_Buf里面就没有字符了,因为装不下
那会存在哪呢?
_Ptr是一个指针,他会指向一块空间,而这块空间就是存放数据的地方
注意不同的编译器情况是不一样的,这是VS的环境下运行的,这种方法就是空间换时间,当_size<16的时候就不需要在堆上开空间了,而是在对象里面开空间,对象里面开的空间是在栈上的,因为栈上开空间要比堆上开更快,所以才这样搞
拓展2
在之前深拷贝的时候我们都需要先开一块同样大小的空间,然后拷贝数据
那我们能不能就让他们指向同一块空间,这样就省了开空间了
这样做有两个问题:第一个问题是会多次析构,第二个问题是其中一个修改会影响另一个
引用技术
为了解决这个问题我们需要用到一个引用技术
在了解到这个方法的时候我第一反应想到的是之前学过的一种做题方法,我们需要将arr1各个数字出现的次数统计出来
这个方法会需要有一个arr2的数组来记录,arr2的下标就对应arr1元素的值,arr2的下标对应的元素就是arr1元素出现的次数
就上面这个例子
0一共就在arr1里出现了1次,所以我们arr2[0]++(这里arr2中的所以元素初始化为0),
1一共出现了三次,所以arr2[1]++
2一共出现了1次,所以arr2[2]++
…
就这样遍历一遍arr1后就可以得到他每个数字出现的次数了
引用技术和这个比较类似
就是统计指向这块空间的对象有多少个,每增加一个对象就让计数的数字加1
当有对象要析构的时候就先让计数的数字减1
当计数的数字为1时表示这块空间只属于他自己,那么就可以放心的析构了
只不过这个方法只解决了第一个问题,第二个问题还是解决不了,所以当要修改的时候我们需要检查一下记录的数字,如果为1就直接修改,如果大于1就先拷贝再修改,然后数字减1