目录
前言
一、模拟实现string
1. 成员变量
2. 构造函数
2.1 构造函数
2.2 重载默认构造
2.3 合并
3. 析构函数
4. 拷贝构造函数
5. c_str
6. size
7. operator[ ]
7.1 普通版
7.2 const版本
8. 迭代器—iterator
8.1 普通版iterator
8.2 const版本iterator
9. 尾插
10. operator+=
11. insert
11.1 插入n个字符
11.2 插入字符串
12. erase
12.1 从pos位置开始删除
13. find
13.1 查找字符
13.2 查找字符串
14. substr
15. reseve && resize
16. operator<< 流插入
17. operator>> 流提取
18. string比较
19. operator= 赋值运算符重载
二、整体代码
总结
前言
学习了string的用法,为了提升自身能力,我们来模拟实现string的部分功能
一、模拟实现string
初始化列表初始化顺序是声明顺序!
我们将自己实现的string放在自己的命名空间内,防止与std中的string冲突
namespace my_string
{class string{};
};
1. 成员变量
private:char* _str;size_t _size;size_t _capacity;
2. 构造函数
2.1 构造函数
构造函数功能:为字符串开空间,并根据传入的参数进行初始化操作。
此处会出现一些问题,当我们使用初始化列表时,每个人的初始化顺序可能不同,并且成员变量的声明顺序也可能不同,编写完代码后,发现出错,代码不能跑,这就是初始化列表的知识点——初始化列表的初始化顺序由成员变量的声明顺序决定。
并且为了少次使用strlen这一O(N)级函数,我们可以只将一个变量在初始化列表初始化,其他成员在函数内赋值(以达到少次使用strlen函数),这是因为这些成员变量是内置类型(内置类型在列表或构造函数体内初始化区别不大),如果是自定义类型则不能在内部赋值,因为即使不写,它们仍然会被自动调用各自的构造函数。
这里我们将全部的成员变量都放在函数体内赋值,以便代码清晰。
namespace my_string
{class string{public:string(const char* str){_size = strlen(str);_capacity = _size;//加1,存放\0_str = new char[_capacity + 1];strcpy(_str, str);}private:size_t _size;size_t _capacity;char* _str;};
};
2.2 重载默认构造
那么,当我们不传参构造,只实例化时,编译器找不到默认构造函数,所以我们要重载一个默认构造函数。
根据监视,可以看到C++的string在无参构造时,它是有一个‘\0'的,所以我们在初始化_str时不能置空,要至少开一个空间,来存储 '\0'
string(const char* str):_size(0),_capacity(0),_str(new char[1])
{_str[0] = '\0';
}
2.3 合并
我们可以使用缺省值,将两个构造函数合并
这里的问题在于,如何设置缺省值?是 '\0' ,还是 nullptr, 还是 "\0" ?
- '\0' 不可以,问题在于左边类型为char*,而'\0'是字符,两者类型不同
- nullptr 也不可以,问题在于后面的strlen直接失效,并且没有 '\0'
- "\0" 不太合适,因为 “” 内默认有\0,所以缺省值应为“”
string(const char* str = "")
{_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];strcpy(_str, str);
}
注意:该构造函数代码之后会在operator<<处再次修改
3. 析构函数
- 释放new出的空间,并将_size与_capacity置空即可
~string()
{delete[] _str;_str = nullptr;_size = _capacity = 0;
}
4. 拷贝构造函数
//拷贝构造
string(const string& s)
{_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;
}
注意:该构造函数代码之后会在operator<<处再次修改
5. c_str
- 模拟string中的c_str函数功能,返回c形式的字符串,即返回 _str
- 在函数后面加上const,可以使被const修饰的对象也可调用该函数
const char* c_str() const
{return _str;
}
6. size
- 返回_size即可
size_t size() const
{return _size;
}
7. operator[ ]
7.1 普通版
- 为了可读可写,我们使用引用返回,因为返回的对象是数组内的元素,生命周期不在operator[ ]函数内,所以可以使用引用返回
char& operator[](size_t pos)
{assert(pos < _size);return _str[pos];
}
7.2 const版本
- const版本只能读,与普通版本形成函数重载,调用函数时,编译器会调用最匹配的函数
//const版本 只读
const char& operator[](size_t pos) const
{assert(pos < _size);return _str[pos];
}
8. 迭代器—iterator
8.1 普通版iterator
- 对于string类,迭代器可以使用指针实现,即迭代器就是指针(针对string类)
//迭代器,在string这里就是指针
typedef char* iterator;iterator begin()
{return _str;
}iterator end()
{return _str + _size;
}
测试:
#include<iostream>
#include"string模拟.h"
using namespace my_string;
using std::cout;
using std::endl;void test()
{my_string::string s1("hello world");cout << s1.c_str() << endl;string::iterator it = s1.begin();while (it != s1.end()){cout << *it << " ";++it;}cout << endl;for (auto ch : s1){cout << ch << "";}cout << endl;
}
hello world
h e l l o w o r l d
hello world
可以看到,我们在写完迭代器之后,范围for就可以直接使用了,因为它就是利用迭代器实现的,底层与使用迭代器遍历几乎一样。
如果我们把end(),改为End(),那么范围for就不可用了。所以,范围for是傻瓜式替换,找到类中的begin(),end(),字母错一个它都实现不了。
8.2 const版本iterator
- 当对象被const修饰,那么迭代器就要加上const,保证值不被修改
typedef const char* const_iterator;
const_iterator begin() const
{return _str;
}const_iterator end() const
{return _str + _size;
}
string::const_iterator cit = s1.begin();
while (cit != s1.end())
{cout << *cit << endl;++cit;
}
cout << endl;
9. 尾插
- 判断是否要扩容,对于push_back() 函数可以进行2倍扩容;对于append() 函数,要进行大小判断,如果_size + strlen(str) > _ capacity 那么至少扩容到_size + len 的容量大小。
- 对于扩容操作,我们实现reserve() 函数,进行扩容处理
void reserve(size_t n)
{if (n > _capacity){//多开一个空间留给 '\0'char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}
}void push_back(char ch)
{if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';
}void append(const char* str)
{size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}strcpy(_str + _size, str);_size += len;
}
10. operator+=
- 实现尾插之后,operator+=的实现就非常简单了,+=是有返回值的,返回的是string类型
- 可以返回*this,因为*this是对象,对象的生命周期不在operator+=中,所以可以返回*this
string& operator+=(char ch)
{push_back(ch);return *this;
}string& operator+=(const char* str)
{append(str);return *this;
}
11. insert
- 先判断下标是否合法
- 再判断是否要扩容
- 移位,移位时有“坑”,因为是size_t 类型,大于等于0,-1是最大正数,如果end改为int类型也不行,因为会发生隐式类型转换。这里有多种解决方案:可以强制类型转换、可以将end从end+n开始,进行end - n 赋给 end ,这样就不会到下标为0的地方、用npos判断(npos使用静态成员函数,赋值为-1,表示最大值)
- 插入数据
- 更新_size
11.1 插入n个字符
void insert(size_t pos, size_t n, char ch)
{//size_t大于0,不用判断assert(pos <= _size);if (_size + n > _capacity){//至少扩容到 _size + nreserve(_size + n);}size_t end = _size;//size_t因为不小于0,当pos为0时,会陷入死循环//这里有多种方案,可以强制类型转换、可以将end从end+n开始、用npos判断while (end >= pos && end != npos){_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}_size += n;
}
11.2 插入字符串
- 同理,将n替换为len即可
void insert(size_t pos, const char* str)
{assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}size_t end = _size;while (end >= pos && end != npos){_str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;
}
12. erase
12.1 从pos位置开始删除
- 先判断下标合法
- 再判断是不是要删到尾部,如果是,直接把pos处改为 '\0' ,并修改_size值即可
- 如果不是,就是在内部删除,和insert操作类似,移动
void erase(size_t pos, size_t len = npos)
{assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;_str[_size] = '\0';}else{size_t begin = pos + len;//等于size,把\0也移动过去while (begin <= _size){_str[pos++] = _str[begin++];}_size -= len;}
}
13. find
13.1 查找字符
size_t find(char ch, size_t pos = 0)
{assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch)return i;}//找不到,返回nposreturn npos;
}
13.2 查找字符串
size_t find(const char* str, size_t pos = 0)
{assert(pos < _size);const char* ptr = strstr(_str, str);if (ptr){return ptr - _str;}else{return npos;}
}
14. substr
- 注意for循环中 i < pos + n,别误写为 i < n
string substr(size_t pos, size_t len = npos)
{assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);//注意i < pos + nfor (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;
}
15. reseve && resize
void reserve(size_t n){if (n > _capacity){//多开一个放'\0'char* tmp = new char[n + 1];//strcpy(tmp, _str);memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}
void resize(size_t n, char ch = '\0'){if (n < _size){_size = n;_str[_size] = '\0';}else{//利用reserve进行扩容判断reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;//由于ch不一定是\0,而且i < n没到最后的\0位置_str[_size] = '\0';}}
16. operator<< 流插入
流插入,有多少字符就输出多少字符,中间有'\0'也不终止
注意将该函数放在命名空间域中,还是全局函数
//流插入,有多少字符就输出多少字符,中间有'\0'也不终止std::ostream& operator<<(std::ostream& out, const string& s){//要访问私有变量才用友元for (auto i : s){out << s[i];}return out;}
直接使用 c_str 和 << 在一般情况是相同的,但是c_str是c字符串,遇到\0自动终止,而string则是依靠size来判断,size有多大,operator<<就打印多长
例如 :
void test() {//与c的字符串不同,c的字符串遇到\0终止,而string是直到size终止string ss("hello world");ss += '\0';ss += "LLLLLLLLLL";cout << ss.c_str() << endl;cout << ss << endl; }int main() {test();return 0; }
hello world hello wordl LLLLLLLLLL
可以看到第二个打印有空格,空格表示了 '\0'
所以,有了\0,之前用的str系列的函数就需要修改了,因为str类型的函数遇到\0自动结束,而string是遇到size才停止,将之前的str函数改为memcpy函数
- 对于构造函数,可以不修改,因为本身构造时,用的就是c字符串,strlen以及strcpy都不需要修改,这里修改strcpy是为了与下面一致
//构造string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];memcpy(_str, str, _size + 1);}//拷贝构造string(const string& s){_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}
- 对于reserve也需要更改
- 对于append可以不更改,因为追加的是c字符串,为了一致性,改为memcpy,拷贝到时候都默认加一个1,因为strcpy默认拷贝\0
void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//strcpy(_str + _size, str);memcpy(_str + _size, str, len + 1);
17. operator>> 流提取
//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据std::istream& operator>>(std::istream& in, string& s){//不能写成 in >> s.str,因为s可能没空间来存放数据char ch;in >> ch;while (ch != ' ' && ch != '\n'){s += ch;in >> ch;}return in;}
上面的代码有错误,当我们输入连续的字符时,不能读到空格和换行,就会陷入死循环,这是in本身的设定,以空格和换行符来区分多个数据,所以我们要使用 get 来提取终端数据 ——in.get(),可以读空格和换行
当我们不想以空格为分隔符时,可以将 ch != ' ' 判断条件去掉
//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据std::istream& operator>>(std::istream& in, string& s){//不能写成 in >> s.str,因为s可能没空间来存放数据char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;}
对于cin操作,如果多次cin,应该会覆盖原先数据 ,即我们需要实现清理工作clear()
void clear(){_str[0] = '\0';_size = 0;}//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据std::istream& operator>>(std::istream& in, string& s){s.clear();//不能写成 in >> s.str,因为s可能没空间来存放数据char ch = in.get();while (ch != ' ' && ch != '\n'){s += ch;ch = in.get();}return in;}
优化:循环里的+=效率不高,因为如果字符串过长可能经历多次扩容。我们当然可以提前reserve开一定空间,但是当字符串过短,那么如果空间开的过大就浪费了。
在这里,我们开一个128的char数组,暂存从终端提取的数据,积累一些数据,当数据超过数组大小后,尾插 \0 后将该数组+=上去,并将下标置为0,再次提取数据。
当数据不够128时,跳出循环,所以要再循环外判断数组内是否还存有数据,再进行+=
std::istream& operator>>(std::istream& in, string& s){s.clear();//不能写成 in >> s.str,因为s可能没空间来存放数据char ch = in.get();char buff[128];int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
但是对于前有空格和换行,后面输入的数据就进不去,所以再进行优化,加一个while循环,将前空格和换行去除。
//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据std::istream& operator>>(std::istream& in, string& s){s.clear();//不能写成 in >> s.str,因为s可能没空间来存放数据char ch = in.get();//处理前缓冲区-流-前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}char buff[128];int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}
18. string比较
- string按照ASCII进行比较大小
- 不能用strcmp,因为当遇到 例如:hello\0xxxxx 和 hello\0yyyyy 时,比较遇到 \0 就结束了,所以我们可以使用memcmp或手写
- 加const防止改变this值,以及可以使const对象调用该函数
//方法一:bool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);return ret == 0 ? _size < s._size : ret < 0;}方法二://bool operator<(const string& s)//{// //return strcmp(_str, s._str) < 0;由于\0,所以不能用strcmp// size_t i1 = 0;// size_t i2 = 0;// while (i1 < _size && i2 < s._size)// {// if (_str[i1] < s._str[i2])// return true;// else if (_str[i1] > s._str[i2])// return false;// else// {// ++i1;// ++i2;// }// }// //在循环之外,只有一种情况是true// //return i1 == _size && i2 != s._size;// //或者判断size大小// return _size < s._size;//}
其他比较复用即可
bool operator==(const string& s) const{//先比较size过滤各种情况return _size == s._size&& memcmp(_str, s._str, _size < s._size) == 0;}bool operator<=(const string& s) const{return (*this < s || *this == s);}bool operator>(const string& s) const{return !(*this <= s);}bool operator>=(const string& s) const{return !(*this < s);}bool operator!=(const string& s) const{return !(*this == s);}
19. operator= 赋值运算符重载
对于赋值,为深拷贝,则需要释放原空间,再开空间进行赋值
方法一: 开空间,赋值,再释放,赋值
//赋值运算符重载——深拷贝string& operator=(const string& s){if (this != &s){//先开空间,赋值,再释放,赋值char* tmp = new char[s._capacity];memcpy(tmp, s._str, s._size + 1);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;}return *this;}
方法二:交换
先根据s创建一个临时对象,再将临时对象和this的各个数据进行交换,这时this就指向了拷贝对象,临时对象tmp就指向了原空间,tmp出了作用域自动销毁,完美解决
//方法二:swap交换void swap(string& s){std::swap(s._str, _str);std::swap(s._size, _size);std::swap(s._capacity, _capacity);}string& operator=(const string& s){if (this != &s){//不能直接交换两个对象,会发生栈溢出、无穷递归//std::swap(tmp, *this); 一直调用赋值运算符重载string tmp(s);//this->swap(tmp);swap(tmp);}//tmp是局部对象,出函数自动销毁return *this;}
方法三:再次优化,将形参改为string tmp,直接拷贝构造tmp,出函数作用域直接析构
void swap(string& s){std::swap(s._str, _str);std::swap(s._size, _size);std::swap(s._capacity, _capacity);}//方法三:改变行参,直接拷贝构造tmpstring& operator=(string tmp){swap(tmp);return *this;}
二、整体代码
my_string.h
#pragma once
#include<cstring>
#include<assert.h>
#include<iostream>namespace my_string
{class string{public://迭代器,在string这里就是指针typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}//构造string(const char* str = ""){_size = strlen(str);_capacity = _size;_str = new char[_capacity + 1];memcpy(_str, str, _size + 1);}//拷贝构造string(const string& s){_str = new char[s._capacity + 1];memcpy(_str, s._str, s._size + 1);_size = s._size;_capacity = s._capacity;}赋值运算符重载——深拷贝//string& operator=(const string& s)//{// if (this != &s)// {// char* tmp = new char[s._capacity];// memcpy(tmp, s._str, s._size + 1);// delete[] _str;// _str = tmp;// _size = s._size;// _capacity = s._capacity;// }// return *this;//}//方法二:swap交换void swap(string& s){std::swap(s._str, _str);std::swap(s._size, _size);std::swap(s._capacity, _capacity);}//string& operator=(const string& s)//{// if (this != &s)// {// //不能直接交换两个对象,会发生栈溢出、无穷递归// //std::swap(tmp, *this); 一直调用赋值运算符重载// string tmp(s);// //this->swap(tmp);新手才写this o.0// swap(tmp);// }// //tmp是局部对象,出函数自动销毁// return *this;//}//方法三:改变行参,直接拷贝构造tmpstring& operator=(string tmp){swap(tmp);return *this;}//析构~string(){delete[] _str;_str = nullptr;_size = _capacity = 0;}const char* c_str() const{return _str;}size_t size() const{return _size;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}//const版本 只读const char& operator[](size_t pos) const{assert(pos < _size);return _str[pos];}void reserve(size_t n){if (n > _capacity){//多开一个放'\0'char* tmp = new char[n + 1];//strcpy(tmp, _str);memcpy(tmp, _str, _size + 1);delete[] _str;_str = tmp;_capacity = n;}}void resize(size_t n, char ch = '\0'){if (n < _size){_size = n;_str[_size] = '\0';}else{//利用reserve进行扩容判断reserve(n);for (size_t i = _size; i < n; i++){_str[i] = ch;}_size = n;//由于ch不一定是\0,而且i < n没到最后的\0位置_str[_size] = '\0';}}void push_back(char ch){if (_size == _capacity){reserve(_capacity == 0 ? 4 : _capacity * 2);}_str[_size] = ch;++_size;_str[_size] = '\0';}void append(const char* str){size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}//strcpy(_str + _size, str);memcpy(_str + _size, str, len);_size += len;}string& operator+=(char ch){push_back(ch);return *this;}string& operator+=(const char* str){append(str);return *this;}void insert(size_t pos, size_t n, char ch){//size_t大于0,不用判断assert(pos <= _size);if (_size + n > _capacity){//至少扩容到 _size + nreserve(_size + n);}size_t end = _size;//size_t因为不小于0,当pos为0时,会陷入死循环//这里有多种方案,可以强制类型转换、可以将end从end+n开始、用npos判断while (end >= pos && end != npos){_str[end + n] = _str[end];--end;}for (size_t i = 0; i < n; i++){_str[pos + i] = ch;}_size += n;}void insert(size_t pos, const char* str){assert(pos <= _size);size_t len = strlen(str);if (_size + len > _capacity){reserve(_size + len);}size_t end = _size;while (end >= pos && end != npos){_str[end + len] = _str[end];--end;}for (size_t i = 0; i < len; i++){_str[pos + i] = str[i];}_size += len;}void erase(size_t pos, size_t len = npos){assert(pos < _size);if (len == npos || pos + len >= _size){_str[pos] = '\0';_size = pos;_str[_size] = '\0';}else{size_t begin = pos + len;//等于size,把\0也移动过去while (begin <= _size){_str[pos++] = _str[begin++];}_size -= len;}}string substr(size_t pos, size_t len = npos){assert(pos < _size);size_t n = len;if (len == npos || pos + len > _size){n = _size - pos;}string tmp;tmp.reserve(n);//注意i < pos + nfor (size_t i = pos; i < pos + n; i++){tmp += _str[i];}return tmp;}size_t find(char ch, size_t pos = 0){assert(pos < _size);for (size_t i = pos; i < _size; i++){if (_str[i] == ch)return i;}//找不到,返回nposreturn npos;}size_t find(const char* str, size_t pos = 0){assert(pos < _size);const char* ptr = strstr(_str, str);if (ptr){return ptr - _str;}else{return npos;}}void clear(){_str[0] = '\0';_size = 0;}//方法一:bool operator<(const string& s) const{int ret = memcmp(_str, s._str, _size < s._size ? _size : s._size);return ret == 0 ? _size < s._size : ret < 0;}方法二://bool operator<(const string& s)//{// //return strcmp(_str, s._str) < 0;由于\0,所以不能用strcmp// size_t i1 = 0;// size_t i2 = 0;// while (i1 < _size && i2 < s._size)// {// if (_str[i1] < s._str[i2])// return true;// else if (_str[i1] > s._str[i2])// return false;// else// {// ++i1;// ++i2;// }// }// //在循环之外,只有一种情况是true// //return i1 == _size && i2 != s._size;// //或者判断size大小// return _size < s._size;//}bool operator==(const string& s) const{//先比较size过滤各种情况return _size == s._size&& memcmp(_str, s._str, _size < s._size) == 0;}bool operator<=(const string& s) const{return (*this < s || *this == s);}bool operator>(const string& s) const{return !(*this <= s);}bool operator>=(const string& s) const{return !(*this < s);}bool operator!=(const string& s) const{return !(*this == s);}private:size_t _size;size_t _capacity;char* _str;static size_t npos;};size_t string::npos = -1;//流插入(向终端插入数据),有多少字符就输出多少字符,中间有'\0'也不终止std::ostream& operator<<(std::ostream& out, const string& s){//要访问私有变量才用友元for (auto ch : s){out << ch;}return out;}//流提取(从终端提取数据),这里的s不能使用const,因为要往s里存放数据std::istream& operator>>(std::istream& in, string& s){s.clear();//不能写成 in >> s.str,因为s可能没空间来存放数据char ch = in.get();//处理前缓冲区前面的空格或者换行while (ch == ' ' || ch == '\n'){ch = in.get();}char buff[128];int i = 0;while (ch != ' ' && ch != '\n'){buff[i++] = ch;if (i == 127){buff[i] = '\0';s += buff;i = 0;}ch = in.get();}if (i != 0){buff[i] = '\0';s += buff;}return in;}};
总结
我们模拟实现的string,具有部分功能,还有一部分没有实现,有兴趣的同学可以参照std的string再进行模拟实现,下节我们将讲解vector并进行实现。
最后,如果小帅的本文哪里有错误,还请大家指出,请在评论区留言(ps:抱大佬的腿),新手创作,实属不易,如果满意,还请给个免费的赞,三连也不是不可以(流口水幻想)嘿!那我们下期再见喽,拜拜!