C++ ---》string类的模拟实现
1.成员变量
本质上是一个char*指针指向的字符数组,以及size_t size,和int _capacity .
基于这样的成员变量为了进一步方便操作我们的string类,我们其实把string也当成容器不过他是char的实例化模板。 所以我们肯定要有对应的迭代器来访问咯。
2.迭代器
迭代器分为两类 一个是begin 一个是end 分别指向当前字符串的头部,和字符串最后一个字符的下一个(end就是最后一个字符的下一个)。
我们把这个string类的图示出来了,begin指针本质上就是char*,但是为了和其他容器统一规划,我们会自定义类型位iterator。
下面是 迭代器的实现:
简单的迭代器的使用
模拟实现的跟库里面的用起来差不多哈哈哈毕竟是模拟。
简单的构造函数和析构函数
我们这里析构开辟了内存然后将字符串进行复制用的是strcpy 其实还可以用memcpy这个也很好的。
my_string(const char* str = ""):_size(std::strlen(str)),_capacity(_size){_str = new char[_capacity];std::strcpy(_str,str);}~my_string(){delete[] _str;_capacity = 0;_size = 0;}
tip补充 strcpy 和memcpy 和c字符串是什么意思:
strcpy 和 memcpy 都是 C 语言中用于复制数据的函数,主要区别如下:
操作对象不同:strcpy 专门用于字符串复制,以 '\0' 作为结束标志;memcpy 可复制任意类型数据,需指定长度。
复制方式不同:strcpy 遇到 '\0' 就停止复制;memcpy 严格按照指定长度复制,不关心内容。
用途不同:strcpy 适合字符串操作,依赖结束符;memcpy 适用于各种数据类型,更通用安全,需显式指定长度避免越界。
函数原型不同:strcpy 原型为 char* strcpy (char* dest, const char* src);memcpy 原型为 void* memcpy (void* dest, const void* src, size_t n)
小结:
strcpy(char*des,char*src)它会去src源字符串中找到\0 然后把这之前的所有字符都复制到des字符的\0中去,存在的隐患就是des(我们也可以理解它是一个缓冲区)这个缓冲区的长度不够,可能会复制到\0的后面去,导致越界了。
所以为了减小隐患有memcpy(void*des,void*src,size_t n) 和strncpy(void*des,void*src,size_t n) 二者的作用都是把src的n个内容复制到指定的数组中去,使用者要自己确保n的大小可以适应缓冲区大小。 (意思是还是可能会出现溢出导致崩溃),stryncpy当字符串 n的长度小于原来字符串的长度的时候其实是不会补上\0的所以需要自己补上 des[n]='\0'
c风格的字符串
c字符串是指""双引号括起来的字符串这是末尾自动补上\0的 所以哪怕是空串""也是会包含\0,
所以c++为了兼容c字符串其实默认空串是""。c++的字符串规定其实不一定必须包含\0的,这是因为string并不依靠\0来判断字符串的末尾包含size 或者length这样的成员变量。当然如果想将string类的字符串转换为c类的可以用c_str这样的成员函数。
验证末尾会不会加上\0:
三种 strcpy,strnpy,memcpy
现在就发现其实这里我们用是复制过去的size个字符 但是还有一个\0其实补不上的,它就会越界补上所以这里是有问题的,我们最好把_capacity的大小改为_size+1
strcpy 末尾补上了\0
memcpy和strncpy都是 没有初始化的所以都是未知内存
3.扩容和修改
void reserve(int n)
插入 void insert(size_t pos, char*str)
在指定的位置插入 字符串 str 个数是len个字符
insert实现
实现的思路:
因为要在pos的位置插入一个字符串,所以从pos位置的字符开始每一个字符向后移动len个位置,这样就是为插入的字符提供空间。(当然要判断是否需要扩容)
之后再用strncpy(_str+pos ,str,len) 把这len个字符在指定的位置插入进去.
具体实现
void insert(size_t pos, const char* str){size_t len = std::strlen(str);if (_size + len > _capacity){reserve(_size+len);}int end = _size;while (end >= (int)pos){_str[end + len ] = _str[end];end--;}strncpy(_str+pos,str,len);_size += len;}
其实这里有一个需要注意的地方就是当pos等于0之后下一次end变成-1,当-1跟无符号整形比较的时候,-1可就是最大的无符号整形数了。所以不应该这样做。我们可以强转一下pos。
删除 void earaser(size_t pos,size_t len)
eraser的实现
我们现在的目的就是把pos到pos+len之间的数删除。
具体做法就是从pos+len开始往前覆盖就行了。 我们创建一个start指针指向pos+len 然后循环开始往前覆盖即可。
一些细节: 我们的函数输入的参数是 pos 和len,所以这个len可能输入的远远超过数组的元素所以一个是对pos的输入断言一下至少pos<_size
其次 没有对len进行缺省处理为npos(-1 转换成无符号整形为最大的无符号整数)
当len大于size之间全部覆盖即可不要出现越界处理。
void eraser(size_t pos, size_t len = npos)
{assert(pos<_size);if (pos + len > _size || len == npos){_str[pos] = '\0';_size =0;}else {int start = pos + len;while (start <= _size){_str[start - len] = _str[start];start++;}_size -= len;}}
void resize(size_t size,char c='\0')
实现的思路
resize 当输入的size小于当前_size 会让size变小,没有其他影响。
当size大于_capacity的时候是需要扩容的,这个时候就需要reserve了。
其次对于新增的内容是需要初始化的默认是\0你可以写一个缺省当然也可以输入。
这里用一个分支可以区别情况,
对于初始化新增内容可以使用一个循环当扩容以后变成size了,扩容最后传参是size+1 补上\0然后从原来的_size的位置开始一直到size初始化为\0。
最后更新_size.
有一个细节就是最后对_str[size] 这个位置补上一个\0
运算符重载
[],+,+=
void append(const char* str){if (str != nullptr){int len = std::strlen(str);if (len + _size > _capacity){reserve(len+_size);}strcpy(_str+_size,str);_size += len;}}char& operator[](size_t pos){return _str[pos]; }void push_back(char ch){if (_size == _capacity){reserve(_capacity==0?4:2*_capacity);}_str[_size++] = ch;}my_string operator+=(char*&str) {this->append(str);return *this;}my_string operator+=(char &ch) {this->push_back(ch);return *this;}
4. 浅拷贝问题
substr(size_t pos,size_t len =npos)
实现思路
substr的实现输入参数pos(位置)len,从pos开始的个数。 设计思路: 我们可以设置两个标记end, 和len长度(可能需要调整)就像eraser的实现一样要判断len。
end=pos+len (最后一个字符的下一个位置)。
对end和len进行调整分支判断:len=npos|| end>=_size 则有多少取多少 调整
len = _size-pos ;end=_size;
然后进行拷贝获取子串:提前创建一个string s;
用一个循环 从pos到end之间的数拷贝进s中 最后返回s (我们这里还没有实现拷贝构造会出现默认拷贝构造的浅拷贝问题)
默认的拷贝对象进行拷贝的时候是按照字节序一一拷贝的,所以返回s对象的时候返回的中间拷贝对象和原来栈上的s的_str成员指向的是相同的地址值,导致指向的内存是同一块。最后这块内存会出现二次析构的问题,这样未定义的行为,导致程序崩溃。
所以我们要对拷贝构造进行重载手写,完成深拷贝:
但是还有一种写法,比较简洁和实用:
思路就是 让栈上自己去调用构造函数, 通过传进来的s 传递它的字符输出构造出来一个tmp对象,然后将tmp对象和当前对象进行swap一下,更改他们分别指向的字符数组的地址。 这样栈上创建的会交给编译器自动调用析构来管理,拷贝的工作被交给构造函数来解决。
类似的我们实现赋值运算符重载也是类似的思路 如上。
补充一个细节 :
这里新的写法这里 使用了swap的时候,一定要记得初始化成安全的值,不然这些成员变量的值都是一些随机化的不安全的值,当swap之后,编译器进行delete处理的_str这样的野指针就会被定义为未定义的行为,程序再次崩溃。 所以真的是很容易报错啊!。
5. 常量引用 和非常量引用 左值和右值
左值: 一个变量名,可以取地址的而且有具体名字的一个变量。
右值: 指一些临时变量,或者一些字面量(字符串,单个字符这些常量)
const修饰的情况; 常量引用既可以传递右值也可以传递左值
非常量引用只可以传递左值
看一组例子:
void test_none_const_site(const char*&a)// 这里是非常量引用
{ ...}void test_const(const char ch)// 这里是常量引用
{...}int main(
{// 这里是非常量引用 char* str = "hello";test_none_const_site(str);// 这是允许的 调用左值test_none_const_site("hello"); // 这是不允许的,非常量引用不能调用右值// // 这里是常量引用 char ch = 'c';test_const(ch);// 左值传递引用是允许的test_const('c');// 常量引用传递一个右值引用也是允许的}
解析: 这里的const char* &a 这个为什么不是常量引用 , 因为这里接受的参数 是const char* 然后的引用。 这里的 const char* 是指这个指针是可以改变的,但是指针的内容不能改变,本质上传递的是一个指针的指,而且这个指针还是能改变的,所以这当然不是一个常量啦。 至于为什么
非常量引用不能接受右值我简单一说你就懂了: 这里传递参数的时候是 test("hello") 把右边:hello这个字符串传递过去 本质上传递会创建一个临时变量字符指针指向这个字符串,转换出来的指针是临时的、没名字的右值,意思是说这个传递的右值是可以被修改的,但是呢引用&本身是不能保证它不能被修改的,所以如果你写一个常量的右值引用,这个右值可修改同时跟引用的要求相冲。为了避免这样的情况c++从根源上就要求不允许出现非常量的右值引用。
同理,常量引用同样会创建一个临时变量。如上这里的 'c' 这个右值传递的时候同样会创建一个临时变量 char的 但是 它会变成const char 这个变量本身是不能被修改的是跟常量引用是合法的。
6. string的流插入<< 和 流提取>>
先了解一下输入流和输出流二者的关系,在c++有两个流对象:一个输出流,还有就是输入流。
OutStream和InputStream 当我们需要将缓冲区中的内容插入到输出流对象的时候我们使用的是:
ostream out ;out<<buffer ; 将缓冲区buffer中的内容插入到输出流中去。
另一个我们使用istream对象的时候,我们会读取输入流中的内容。 也就是
istream in buffer 接受: in.get()
总之,简单来说流插入就是把缓冲区的内容插入到输出流对象中,流提取就是把输入流中的内容读取到缓冲区中。 一个是写,一个是读权限。
string中的实现:
看下面两组声明
my_string{public:ostream& operator<<(ostream&out){for(int i=0;i<_size;i++){out<<_str[i];}
return out;}
private:char* _str;size_t _size;size_t _capacity;}
和:
my_string{public:private:char* _str;size_t _size;size_t _capacity;}
ostream& operator<<(ostream&out,my_string str){for(int i=0;i<str.size();i++){out<<_str[i];}
return out;}
区别就在于 使用的时候分别是: s.cout<<; 和 cout<<s 这是因为作为成员函数的时候默认第一个元素就是 this 当前对象这是隐式默认的。 你无法改变,所以为了满足习惯我们的string的流插入和流提取一般会在类外声明作为全局函数。
具体实现:
ostream& operator<<(ostream& out, my_string& str)
{for (auto ch: str){cout << ch;}return out;
}
istream& operator >>(istream& in, my_string& str)
{str.clear();// 创建 缓冲区 buffer 以及标记位置 i 还有每次读取的临时变量 chchar buffer[129];size_t i = 0;char ch = in.get();while (ch != ' ' && ch != '\n'){buffer[i++] = ch;ch = in.get();if (i == 128){buffer[i] = '\0';str += buffer;i = 0;}}if (i != 0){buffer[i] = '\0';str += buffer;}return in;}
为什么返回的是流对象的引用是因为这样你就可以连续写好几个咯:
cout<<str1<<str2<<str3
或者cin>>s1>>s3>>s4