【C++ • STL】探究string的源码
时间:2024-04-17 15:50:27 来源:网络cs 作者:利杜鹃 栏目:建站工具 阅读:
文章目录
一、深浅拷贝二、传统版写法的string类(简单)三、string类的模拟实现四、现代版写法的string类五、总结
ヾ(๑╹◡╹)ノ" 人总要为过去的懒惰而付出代价ヾ(๑╹◡╹)ノ"
一、深浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
浅拷贝:(1)析构两次,造成程序崩溃(2)一个对象修改影响另外一个
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
编译器默认生成的拷贝构造,是浅拷贝,会是两个对象指向同一块空间,当程序结束的时候,那么两个对象都会进行销毁,那么一块空间就会进行多次释放,从而引起崩溃。
深拷贝:给每一个对象分配资源,保证多个对象之间不会因为共享资源而导致多次释放造成程序崩溃。
二、传统版写法的string类(简单)
#pragma once#include <iostream>using namespace std;#include <assert.h>namespace yyqx//为了与库里面的string进行区分{//仅仅实现一个简单的string,仅仅考虑资源管理深浅拷贝问题class string{public://构造函数string(const char* str):_str(new char[strlen(str) + 1])//这里的+1,是为了'\0'开辟空间{strcpy(_str, str);//拷贝的时候'\0'也拷贝了}//拷贝构造(深拷贝)//s2(s1)string(const string& s):_str(new char[strlen(s._str) + 1]){strcpy(_str, s._str);}//赋值,也会有深浅拷贝的问题string& operator=(const string& s){if (this != &s)//避免自己给自己赋值,会导致值被释放,就会变成随机值{//delete[] _str;//首先进行释放//_str = new char[strlen(s._str) + 1];//C++的new是不需要检查是否开辟空间会抛异常//strcpy(_str, s._str);//为了避免开辟空间失败,而本来的空间也被我们释放,可以先开启空间,//进行拷贝,然后再释放char* tmp = new char[strlen(s._str) + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;}return *this;}//析构函数~string(){if (_str){delete[] _str;}}//目的为了输出字符串const char* c_str() const{return _str;}//返回c格式的字符串//重载[]char& operator[](size_t pos){assert(pos < strlen(_str));//注意这里的范围return _str[pos];}size_t size(){return strlen(_str);}private:char* _str;};}
赋值运算符重载也会有深浅拷贝的问题。赋值,对象本身是有值的【拷贝的时候,如果空间小,就会不够,空间大,就会造成资源浪费】
三、string类的模拟实现
string的增删查改以及使用string【传统】
基本框架:
#pragma once#include <iostream>using namespace std;#include <assert.h>namespace yyqx//为了与库里面的string进行区分{class string{public:
构造函数+析构函数:
写法1:
//构造函数string(const char* str):_size(strlen(str)),_capacity(_size){_str = new char[strlen(str) + 1];//这里的+1,是为了'\0'开辟空间strcpy(_str, str);//拷贝的时候'\0'也拷贝了}string()//注意,这里不是给的空,而是给了一个空的字符串//标准库里的就是给了一个"":_size(0),_capacity(0){_str = new char[1];_str[0] = '\0';}
构造函数:初始化列表,初始化的顺序并不是初始化列表的顺序,而是成员变量在类中的声明次序。构造函数:注意默认的构造函数【编译器自动生成、缺省、函数重载】,默认的构造函数这里选择写一个同名函数,注意这里并不是给一个空指针,而是给了一个空字符串。写法2:(最优写法)
string(const char* str = "")//这里默认值不能给nullptr,strlen以及拷贝strcpy会崩溃:_size(strlen(str)),_capacity(_size){_str = new char[strlen(str) + 1];//这里的+1,是为了'\0'开辟空间strcpy(_str, str);//拷贝的时候'\0'也拷贝了}//析构函数~string(){if (_str){delete[] _str;_str = nullptr;//好习惯_size = 0;_capacity = 0;}}
缺省值这不能给nullptr,strlen以及拷贝strcpy时程序会崩溃注意初始化列表strcpy注意,拷贝的时候’\0’也拷贝了new开空间的时候,一定要多开一个给’\0’ 拷贝构造+赋值重载函数+其他:
//拷贝构造(深拷贝)//s2(s1)string(const string& s):_size(strlen(s._str)),_capacity(_size){_str = new char[_capacity + 1];strcpy(_str, s._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;}//目的为了输出字符串const char* c_str() const{return _str;}//返回c格式的字符串char& operator[](size_t pos)//这里仅仅可以传入对象,不能传入const对象,如果是const对象,就会报错{assert(pos < _size);//注意这里的范围return _str[pos];}const char& operator[](size_t pos) const//这里就可以传入const对象{assert(pos < _size);return _str[pos];}//这里的const修饰的是this指针指向的对象const string s;size_t size() const//写const,普通对象以及const对象都可以调用,如果不加const对象就不可以调用{return _size;}size_t capacity() const//写const,普通对象以及const对象都可以调用{return _capacity;}
添加:
string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}void reverse(size_t n)//一个扩容的作用{if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;//注意这里的释放不是free_str = tmp;_capacity = n;}}void resize(size_t n, char ch = '\0'){if (n < _size){_size = n;_str[_size] = '\0';}else{if (n > _capacity){reverse(n);}for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;_str[_size] = '\0';}}void push_back(char ch){if (_size == _capacity){reverse(_capacity == 0 ? 4 : _capacity * 2);//如果是一个空字符串,就会导致并没有扩容,//扩容要注意刚开始没有容量的情况下}_str[_size] = ch;_size++;_str[_size] = '\0';//注意\0,容易遗漏}//append插入的字符个数是未知的,扩容二倍也不一定足够void append(const char* str){size_t len = _size + strlen(str);if (len > _capacity){reverse(len);}strcpy(_str + _size, str);_size = len;}//但是我们一般用+=
判断容量是否满,如果 _size= _ capacity,容量扩2倍,new一个新容量的空间,释放旧空间,最后指针指向新的空间。append (append插入的字符个数是未知的,扩容二倍也不一定足够:解决办法:reverse预留空间【一个扩容的作用】)reverse 为string预留空间,避免多次扩容(提高效率)resize用处:扩空间+初始化;删除数据保留前n个插入:
string& insert(size_t pos, char ch){assert(pos <= _size);//这里的=_size相当于尾插//注意,这里容易忘记,size_t就已经大于等于0了,所以在这里我们主要保证pos是小于_size即可if (_size == _capacity){reverse(_capacity == 0 ? 4 : 2 * _capacity);}//不可以用strcpy,这里不可以是同一块地址,对导致内容不是我们想要的//最后一个未知的字符移到_size然后就是倒数第二位移动,从后向前移动size_t end = _size + 1;//注意这里如果end=_size,当头插的时候,进入循环end会变成-1,因为是size_t所以又会进入循环,导致错误while (end > pos){_str[end] = _str[end - 1];--end;}_str[pos] = ch;_size++;return *this;}//插入\0,用c_str(遇到\0停止打印)打印显示在屏幕的字符串长度会减小或者不变,但是_size会变大//用范围for或者迭代器可以打印出来string& insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reverse(_size + len);}size_t end = _size + len;while (end > pos + len - 1)//这里注意{_str[end] = _str[end - len];--end;}strncpy(_str + pos, str, len);//防止为了遇见\0就不拷贝了(strcpy遇见\0就不拷贝了)_size += len;return *this;}
插入字符:
不可以用strcpy,在字符进行向后移的时候,不可以是同一块地址,对导致内容不是我们想要的,最后一个未知的字符移到_size然后就是倒数第二位移动,从后向前移动end=_size,当头插的时候,进入循环end会变成-1,因为是size_t,又是大于0所以又会进入循环,导致代码错误插入字符串:
防止为了遇见\0就不拷贝了,所以用的是strncpy(strcpy遇见\0就不拷贝了)删除:
//删除string& erase(size_t pos, size_t len = npos){assert(pos < _size);//删除的数据大于等于_sizeif (len == npos || pos + len >= npos){_str[pos] = '\0';_size = pos;}else{size_t begin = pos + len;while (begin <= _size){_str[begin - len] = _str[begin];++begin;}_size -= len;}return *this;}
注意:npos类中静态成员的初始化,必须在类外,类和对象(下)本篇文章中有详细说明。【const在定义的时候必须初始化,但是静态成员的变量初始化又在外面】
查找:
size_t find(char ch, size_t pos = 0){for (; pos < _size; ++pos){if (_str[pos] == ch){return pos;}}return npos;}size_t find(const char* str, size_t pos = 0){const char* p = strstr(_str + pos, str);if (p == nullptr){return npos;}else{return p - _str;}}void clear(){_str[0] = '\0';_size = 0;}private:char* _str;size_t _size;//有效字符的个数size_t _capacity;//存储有效字符的空间大小const static size_t npos;//正确的写法是在类外进行初始化//const static size_t npos = -1;//这种写法也可以,但是违背了正确的写法,要注意};const size_t string::npos = -1;
strstr返回的是指针,没有找到返回空指针。 流插入和流提取:
/流插入和流提取//在类外//不可以用c_str(),因为遇见\0会停止//'\0'是不可以见字符,不会显示//流插入ostream& operator<<(ostream& out, const string& s){for (auto ch : s){out << ch;}return out;}//流提取,字符从面板提取到sistream& operator>>(istream& in, string& s){s.clear();//要把对象里面的字符清理掉,否则当对象不是空的时候,会导致字符直接加到已有对象的后面。//但是我们想要的是,对象是我们输入的字符串//第一种思路(缺点:频繁的+=,字符串过大,会导致频发的扩容,影响效率)/*char ch;ch = in.get();if (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in*///第二种思路(这种思路比较优,无论大小都可以避免频繁扩容)char ch;ch = in.get();char buff[128] = { '\0' };size_t i = 0;if (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){s += buff;memset(buff, '\0', 128);i = 0;}ch = in.get();}s += buff;return in;}
'\0’是不可以见字符,不会显示clear()要把对象里面的字符清理掉,否则当对象不是空的时候,会导致字符直接加到已有对象的后面。但是我们想要的是,对象是我们输入的字符串 运算符重载:
//运算符重载//比较大小//全局函数.可以类比日期类bool operator<(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) < 0;}bool operator==(const string& s1, const string& s2){return strcmp(s1.c_str(), s2.c_str()) == 0;}bool operator<=(const string& s1, const string& s2){return s1 < s2 || s1 == s2;}bool operator>(const string& s1, const string& s2){return !(s1 <= s2);}bool operator>=(const string& s1, const string& s2){return s1 > s2 || s1 == s2;}bool operator!=(const string& s1, const string& s2){return !(s1 == s2);}}//这个是yyqx的大括号
这里是在全局变量,没有在类里面,是在类外
迭代器:
string类private里面:
public://迭代器typedef char* iterator;typedef const char* const_iterator;const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}iterator begin() {return _str;}iterator end() {return _str + _size;}
四、现代版写法的string类
拷贝构造和赋值的现代写法
//拷贝构造(深拷贝)//s2(s1)//现代写法,剥削行为,要完成深拷贝,void swap(string& s){std::swap(_str, s._str);std::swap(_size, s._size);std::swap(_capacity, s._capacity);}string(const string& s):_str(nullptr),_size(0),_capacity(0)//这里要进行初始化,否则交换后,局部变量的销毁(随机值销毁,不可以){//构造一个对象tmp,tmp里所有的东西this想要。this和tmpstring tmp(s._str);//局部变量,出了作用域会销毁swap(tmp);//tmp出了作用域会销毁}//赋值,也会有深浅拷贝的问题//现代写法//第一种//string& operator=(const string& s)//{//if (this != &s)//避免自己给自己赋值,会导致值被释放,就会变成随机值//{//string tmp(s._str);//swap(tmp);//把tmp给this,出了作用域把this给tmp的值进行销毁//}//return *this;//}//第二种string& operator=(string s)//传值传参,拷贝构造,拷贝的值给this,并不会导致s的实参发生变化{swap(s);return *this;}//掌握现代写法
补充知识点
遍历方式中有一个是范围for(范围for的底层实现是迭代器,如果没有迭代器的程序,代码会进行报错)
代码展示:
yyqx::string s("hello 12345");for (auto ch : s){cout << ch << " ";}cout << endl;
在c语言中,我们用atoi。
string中的两个常用函数
五、总结
以上就是今天要讲的内容,本文详细的介绍了浅拷贝、浅拷贝和string的模拟实现。本文以及一文带你走进string详细的介绍了string的相关知识,希望给友友们带来帮助!
本文链接:https://www.kjpai.cn/news/2024-04-17/159808.html,文章来源:网络cs,作者:利杜鹃,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!
下一篇:返回列表