STL篇一:string
可能很多人都不太清楚STL是什么,它是用来干什么的,大家在看完这篇文章之后相信能有个大概的了解,目前我只能说STL是C++的中十分重要的部分之一,是大家学习C++必须掌握的部分。
文章目录
前言
可能很多人都不太清楚STL是什么,它是用来干什么的,大家在看完这篇文章之后相信能有个大概的了解,目前我只能说STL是C++的中十分重要的部分之一,是大家学习C++必须掌握的部分。
1. STL的简单理解
1.1 什么是STL
STL(standard template libaray-标准模板库):是C++标准库的重要组成部分,不仅是一个可复用的组件库,而且是一个包罗数据结构与算法的软件框架。
1.2 STL的版本
- 原始版本
Alexander Stepanov、Meng Lee 在惠普实验室完成的原始版本,本着开源精神,他们声明允许任何人任意运用、拷贝、修改、传播、商业使用这些代码,无需付费。唯一的条件就是也需要向原始版本一样做开源使用。 HP 版本–所有STL实现版本的始祖。
- P. J. 版本
由P. J. Plauger开发,继承自HP版本,被Windows Visual C++采用,不能公开或修改,缺陷:可读性比较低,符号命名比较怪异。
- RW版本
由Rouge Wage公司开发,继承自HP版本,被C+ + Builder 采用,不能公开或修改,可读性一般。
- SGI版本
由Silicon Graphics Computer Systems,Inc公司开发,继承自HP版本。被GCC(Linux)采用,可移植性好, 可公开、修改甚至贩卖,从命名风格和编程风格上看,阅读性非常高。我们后面学习STL要阅读部分源代码, 主要参考的就是这个版本。
1.3 STL的六大组件
1.4 STL的重要性
- 在笔试中
- 在面试中
3.在工作中
网上有句话说:“不懂STL,不要说你会C++”。STL是C++中的优秀作品,有了它的陪伴,许多底层的数据结构以及算法都不需要自己重新造轮子,站在前人的肩膀上,健步如飞的快速开发。
1.5 STL的缺陷
- STL库的更新太慢了。这个得严重吐槽,上一版靠谱是C++98,中间的C++03基本一些修订。C++11出来已经相隔了13年,STL才进一步更新。
- STL现在都没有支持线程安全。并发环境下需要我们自己加锁。且锁的粒度是比较大的。
- STL极度的追求效率,导致内部比较复杂。比如类型萃取,迭代器萃取。
- STL的使用会有代码膨胀的问题,比如使用vector/vector/vector这样会生成多份代码,当然这是模板语法本身导致的。
2. string类
string类是一个容器,它是我们学习STL的第一部分,我们的主线是讲解STL的容器,其余部分会在讲解容器时进行穿插讲解。
2.1 为什么学习string类?
2.1.1 C语言中的字符串
C语言中,字符串是以’\0’结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问.
2.1.2 两个面试题
1. 字符串转整形数字
2. 字符串相加
在OJ中,有关字符串的题目基本以string类的形式出现,而且在常规工作中,为了简单、方便、快捷,基本都使用string类,很少有人去使用C库中的字符串操作函数。
2.2 标准库中的string类
2.2.1 string类(了解)
- 字符串是表示字符序列的类
- 标准的字符串类提供了对此类对象的支持,其接口类似于标准字符容器的接口,但添加了专门用于操作单字节字符字符串的设计特性。
- string类是使用char(即作为它的字符类型,使用它的默认char_traits和分配器类型(关于模板的更多信息,请参阅basic_string)。
- string类是basic_string模板类的一个实例,它使用char来实例化basic_string模板类,并用char_trai ts和allocator作为basic_string的默认参数(根于更多的模板信息请参考basic_string)。
- 注意,这个类独立于所使用的编码来处理字节:如果用来处理多字节或变长字符(如UTF-8)的序列,这个类的所有成员(如长度或大小)以及它的迭代器,将仍然按照字节(而不是实际编码的字符)来操作。
总结:
- string是表示字符串的字符串类
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作string的常规操作。
- string在底层实际是:basic_string模板类的别名,typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列。
在使用string类时,必须包含#include头文件以及using namespace std;
2.2.2 string类的常用接口说明
上面就是关于string修改部分的一些接口,比如插入、删除等等。
上面就是关于string容量部分的一些接口,比如计算长度、计算容量等等。
此外还有大量的其他接口,再后面我会将用的比较多的接口进行模拟实现,如果有小伙伴对其他接口也十分感兴趣,可以通过下面的链接进行跳转观看string的全部接口,对于每一个接口点进去都会有详细的文档说明和测试用例,十分便于大家理解。
2.3 string类的模拟实现中的一些问题
2.3.1 经典的string类问题
上面已经对string类进行了简单的介绍,大家只要能够正常使用即可。在面试中,面试官总喜欢让学生自己来模拟实现string类,最主要是实现string类的构造、拷贝构造、赋值运算符重载以及析构函数。大家看下以下string类的实现是否有问题?
// 为了和标准库区分,此处使用String
class String
{
public:
/*String()
:_str(new char[1])
{*_str = '\0';}
*/
//String(const char* str = "\0") 错误示范
//String(const char* str = nullptr) 错误示范
String(const char* str = "")
{
// 构造String类对象时,如果传递nullptr指针,可以认为程序非
if (nullptr == str)
{
assert(false);
return;
}
_str = new char[strlen(str) + 1];
strcpy(_str, str);
}
~String()
{
if (_str)
{
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
};
// 测试
void TestString()
{
String s1("hello bit!!!");
String s2(s1);
}
我们会发现程序运行崩溃,这是因为上述String类没有显式定义其拷贝构造函数与赋值运算符重载,此时编译器会合成默认的,当用s1构造s2时,编译器会调用默认的拷贝构造。最终导致的问题是,s1、s2共用同一块内存空间,在释放时同一块空间被释放多次而引起程序崩溃,这种拷贝方式,称为浅拷贝。这个知识点我在C++类和对象中的赋值运算符部分进行过详细的讲解,有不懂的小伙伴可以点击链接进行跳转阅读。
2.3.2 浅拷贝
浅拷贝:也称位拷贝,编译器只是将对象中的值拷贝过来。如果对象中管理资源,最后就会导致多个对象共享同一份资源,当一个对象销毁时就会将该资源释放掉,而此时另一些对象不知道该资源已经被释放,以为还有效,所以当继续对资源进项操作时,就会发生发生了访问违规。
可以采用深拷贝解决浅拷贝问题,即:每个对象都有一份独立的资源,不要和其他对象共享。
2.3.3 深拷贝
如果一个类中涉及到资源的管理,其拷贝构造函数、赋值运算符重载以及析构函数必须要显式给出。一般情况都是按照深拷贝方式提供。
深拷贝的具体如何实现在下一小节模拟实现中进行讲解。
2.4 string的模拟实现
2.4.1 构造函数
//声明,这一行是放在String.h文件中的
//string(const char* str = "");
WY::string::string(const char* str)
:_size(strlen(str))
,_capacity(strlen(str))
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
在实现拷贝构造时一般都会采用缺省值的方式,是为了避免使用者在实例化对象时忘记赋予初始值。对于缺省参数,在声明和定义中,一般是在声明时书写缺省值。并且在开辟空间时是要开辟容量+1 个空间,因为参数的字符串中是包含一个字符零的,因此在开辟时要给字符零预留一个空间,
2.4.2 拷贝构造
WY::string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
拷贝构造一定需要注意的点是参数必须进行引用传参,负责会出现无限递归的问题,具体过程在C++类和对象有关拷贝构造的小节有详细说明。
初次之外这里也是涉及到了深拷贝,在拷贝构造中,我们是重新开辟了一块新空间进行拷贝,因此原来的对象与新对象指向的是不同的空间,在析构时是不会发生对一块空间析构两次的情况。
2.4.3 析构函数
//析构
WY::string::~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
就是释放空间,并将对象中的成员变量都进行归零。需要注意的是在释放空间是要加 [ ]中括号,对于单个变量是不需要加中括号的,但是对于多个变量,比如数组在释放时需要加中括号。详细原理见C/C++动态内存管理。
2.4.4 赋值运算符重载
WY::string& WY::string::operator = (const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_capacity = s._capacity;
_size = s._size;
}
return *this;
}
首先需要说明的是,这里的引用传参仅仅只是为了提高效率,因为在实现了拷贝构造函数之后再进行传值传参就不会发生无穷递归了。为了避免浅拷贝问题,需要重新开辟一块空间,将数据进行拷贝过去,然后将被赋值的对象的原数据进行释放,将新开辟的空间给了被赋值对象,再将其余成员变量依次进行修改。
2.4.5 [ ]的实现
char& WY::string::operator[](size_t index)
{
return _str[index];
}
const char& WY::string::operator[](size_t index)const
{
return _str[index];
}
中括号的实现可以让用户更加方便的查询数据,但是const成员是无法调用非const 的成员函数的,因此要将const成员函数也要实现一下。
2.4.6 扩容
//扩容
void WY::string::reserve(size_t n)
{
if (_capacity < n)
{
_capacity = n;
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
}
}
重新开辟一块空间再将原数据进行拷贝就可以了,要记得释放原对象的空间噢!!
2.4.7 尾插
void WY::string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
尾插无法避免的情况就是当容量已满时就需要进行扩容。
2.4.8 插入一个字符串
void WY::string::append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size +=len;
}
上面的尾插只是插入了一个字符,这个函数是用来专门插入一个字符串的。在库函数中这个函数是可以实现在各个位置进行插入,这里我只实现了尾部插入,需要更加详细了解的可以去看相关的需求文档。
2.4.9 += 的运算符重载
WY::string& WY::string::operator+=(char c)
{
push_back(c);
return *this;
}
WY::string& WY::string::operator+=(const char* str)
{
append(str);
return *this;
}
这里的逻辑跟尾插一样,直接调用尾插就好了,这也是进行分模块实现功能的好处,可以进行代码复用,十分方便。
2.4.10 插入
void WY::string::insert(size_t pos, char c)
{
assert(pos >= 0);
if (_size == _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
}
只要插入数据就需要进行扩容判断,然后从后往前不断挪动数据,一直到pos位置再进行插入,这一块的时间复杂度是O(n)。
void WY::string::insert(size_t pos, const char* str)
{
assert(pos >= 0);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end + len - 1] = _str[end - 1];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
}
插入一个字符串和插入一个字符的逻辑是一样的。
2.4.11 删除
void WY::string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos ||pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos + len;
while (begin <= _size)
{
_str[pos++] = _str[begin++];
}
_size -= len;
}
}
删除有两种情况,一种是将pos位置之后的数据全部删除,一种是只删除pos位置之后长度为len的数据。这里的npos是一个全局变量,是为了与库函数中的实现方式相对应,同时它也是参数len的缺省值。
如果用户没有具体说明删除多长的数据,默认为删除pos位置之后的所有数据,而当你删除的数据长度加上pos位置大于整个数据的大小说明此时也是删除了pos位置之后的所有数据。另一种情况就是将需要删除的数据后面所剩的数据全部挪移到前面即可。
2.4.12 重新设置大小
void WY::string::resize(size_t n, char c)
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
while (_size < n)
{
_str[_size++] = c;
}
_str[_size] = '\0';
}
}
如果小于原数据大小,直接在n位置填上’\0’即可,如果大于原数据,就需要先扩容,再多余的空间上填上字符c,这里的c的默认参数是’\0’。
2.4.13 << 与 >> 的重载
ostream& WY::operator<<(ostream& _cout, const WY::string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
_cout << s[i];
}
return _cout;
}
istream& WY::operator>>(istream& _cin, WY::string& s)
{
char ch = _cin.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = _cin.get();
}
return _cin;
}
ostream与istream也是一个对象,一个是输出流(cout),一个是输入流对象(cin)。这个运算符重载需要注意的是这个是写在类外面的,并不是一个类的成员函数,因为如果写成成员函数的话,就是这个样子operator>>(this,istream& _cin),由于this是在前面,因此在外面调用的话就需要书写成 s << cout,而我们正常写的是cout << s,所有需要在类的外面进行声明定义。但是类外面的函数要是无法访问类内部的成员变量的,所以还需要将这两个函数声明为该类的友元函数,有关友元函数的讲解在C++类和对象中有详细解释。当一个函数被声明为友元函数时,这个函数就可以访问该类的成员变量。
还需要注意的一个点是这两个函数一般都是引用返回,一是为了提高效率(引用传参、引用返回都不需要再进行拷贝构造,因此可以提高效率),二是可以实现连续输入输出。比如:cout << s << s1 ------>operator << (opeartor <<(cout,s),s1)。
2.4.14 < 、>、==运算符重载
bool WY::string::operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool WY::string::operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool WY::string::operator<=(const string& s)
{
return (*this < s) || (*this == s);
}
bool WY::string::operator>(const string& s)
{
return !(*this <= s);
}
bool WY::string::operator>=(const string& s)
{
return !(*this < s);
}
bool WY::string::operator!=(const string& s)
{
return !(*this == s);
}
实现上两个其他的就可以直接进行复用了。
2.4.15 查找函数
size_t WY::string::find(char c, size_t pos) const
{
for (size_t i = pos; i < size(); i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t WY::string::find(const char* s, size_t pos) const
{
const char* p = strstr(_str + pos, s);
if (p)
{
return p - _str;
}
else
{
return npos;
}
}
pos是用户可以指定从哪里开始进行查找,查找单个字符依次便利尽心比较即可。在查找子串时,可以调用库函数的strstr函数,如果找到它返回的是子串的首地址,如果没找到返回的是空指针。还需要注意的是指针减指针的含义是两个指针之间的数据个数,这里也就是子串首字母的位置。
3.全部代码
3.1 String.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
namespace WY
{
class string
{
friend ostream& operator<<(ostream& _cout, const WY::string& s);
friend istream& operator>>(istream& _cin, WY::string& s);
public:
typedef char* iterator;
typedef const char* const_iterator;
public:
string(const char* str = "");
string(const string& s);
string& operator = (const string& s);
~string();
//
// iterator
iterator begin();
const_iterator begin()const;
iterator end();
const_iterator end()const;
/
// modify
void push_back(char c);
string& operator+=(char c);
void append(const char* str);
string& operator+=(const char* str);
void clear();
void swap(string& s);
const char* c_str()const;
/
// capacity
size_t size()const;
size_t capacity()const;
bool empty()const;
void resize(size_t n, char c = '\0');
void reserve(size_t n);
/
// access
char& operator[](size_t index);
const char& operator[](size_t index)const;
/
//relational operators
bool operator<(const string& s);
bool operator<=(const string& s);
bool operator>(const string& s);
bool operator>=(const string& s);
bool operator==(const string& s);
bool operator!=(const string& s);
// 返回c在string中第一次出现的位置
size_t find(char c, size_t pos = 0) const;
// 返回子串s在string中第一次出现的位置
size_t find(const char* s, size_t pos = 0) const;
// 在pos位置上插入字符c/字符串str,并返回该字符的位置
void insert(size_t pos, char c);
void insert(size_t pos, const char* str);
// 删除pos位置上的元素,并返回该元素的下一个位置
void erase(size_t pos, size_t len = npos);
private:
char* _str;
size_t _capacity;
size_t _size;
const static size_t npos;
};
ostream& operator<<(ostream& _cout, const WY::string& s);
istream& operator>>(istream& _cin, WY::string& s);
void test_string1();
void test_string2();
void test_string3();
void test_string4();
void test_string5();
void test_string6();
};
3.2 String.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"String.h"
const size_t WY::string::npos = -1;
//构造
WY::string::string(const char* str)
:_size(strlen(str))
,_capacity(strlen(str))
{
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//拷贝构造
WY::string::string(const string& s)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_capacity = s._capacity;
_size = s._size;
}
WY::string& WY::string::operator = (const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capacity + 1 ];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_capacity = s._capacity;
_size = s._size;
}
return *this;
}
//析构
WY::string::~string()
{
delete[] _str;
_str = nullptr;
_capacity = _size = 0;
}
char& WY::string::operator[](size_t index)
{
return _str[index];
}
const char& WY::string::operator[](size_t index)const
{
return _str[index];
}
//扩容
void WY::string::reserve(size_t n)
{
if (_capacity < n)
{
_capacity = n;
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
}
}
void WY::string::push_back(char c)
{
if (_size == _capacity)
{
reserve(_capacity == 0 ? 4 : _capacity * 2);
}
_str[_size] = c;
_size++;
_str[_size] = '\0';
}
void WY::string::append(const char* str)
{
int len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size +=len;
}
const char* WY::string::c_str()const
{
return _str;
}
WY::string& WY::string::operator+=(char c)
{
push_back(c);
return *this;
}
WY::string& WY::string::operator+=(const char* str)
{
append(str);
return *this;
}
void WY::string::clear()
{
_str[0] = '\0';
}
void WY::string::swap(string& s)
{
char* tmp = new char[_capacity + 1];
strcpy(tmp, _str);
strcpy(_str, s._str);
strcpy(s._str, tmp);
}
size_t WY::string::size()const
{
return _size;
}
size_t WY::string::capacity()const
{
return _capacity;
}
bool WY::string::empty()const
{
return _size == 0;
}
void WY::string::insert(size_t pos, char c)
{
assert(pos >= 0);
if (_size == _capacity)
{
reserve(_capacity * 2);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end - 1];
end--;
}
_str[pos] = c;
_size++;
}
void WY::string::insert(size_t pos, const char* str)
{
assert(pos >= 0);
size_t len = strlen(str);
if (_size + len > _capacity)
{
reserve(_size + len);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end + len - 1] = _str[end - 1];
end--;
}
strncpy(_str + pos, str, len);
_size += len;
}
void WY::string::erase(size_t pos, size_t len)
{
assert(pos < _size);
if (len == npos ||pos + len > _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
size_t begin = pos + len;
while (begin <= _size)
{
_str[pos++] = _str[begin++];
}
_size -= len;
}
}
void WY::string::resize(size_t n, char c)
{
if (n <= _size)
{
_str[n] = '\0';
_size = n;
}
else
{
reserve(n);
while (_size < n)
{
_str[_size++] = c;
}
_str[_size] = '\0';
}
}
WY::string::iterator WY::string::begin()
{
return _str;
}
WY::string::const_iterator WY::string::begin()const
{
return _str;
}
WY::string::iterator WY::string::end()
{
return _str + _size;
}
WY::string::const_iterator WY::string::end()const
{
return _str + _size;
}
// cout << s ----> operator <<(cout,s)
ostream& WY::operator<<(ostream& _cout, const WY::string& s)
{
for (size_t i = 0; i < s.size(); i++)
{
_cout << s[i];
}
return _cout;
}
istream& WY::operator>>(istream& _cin, WY::string& s)
{
char ch = _cin.get();
while (ch != ' ' && ch != '\n')
{
s += ch;
ch = _cin.get();
}
return _cin;
}
bool WY::string::operator<(const string& s)
{
return strcmp(_str, s._str) < 0;
}
bool WY::string::operator==(const string& s)
{
return strcmp(_str, s._str) == 0;
}
bool WY::string::operator<=(const string& s)
{
return (*this < s) || (*this == s);
}
bool WY::string::operator>(const string& s)
{
return !(*this <= s);
}
bool WY::string::operator>=(const string& s)
{
return !(*this < s);
}
bool WY::string::operator!=(const string& s)
{
return !(*this == s);
}
size_t WY::string::find(char c, size_t pos) const
{
for (size_t i = pos; i < size(); i++)
{
if (_str[i] == c)
return i;
}
return npos;
}
size_t WY::string::find(const char* s, size_t pos) const
{
const char* p = strstr(_str + pos, s);
if (p)
{
return p - _str;
}
else
{
return npos;
}
}
3.3 Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include"String.h"
void WY::test_string1()
{
string s("hello world");
cout << s.c_str() << endl;
string s1(s);
cout << s1.c_str() << endl;
string s2;
s2 = s;
cout << s2.c_str() << endl;
s.push_back('x');
cout << s.c_str() << endl;
s1.append("xxx");
cout << s1.c_str() << endl;
s2 += 'x';
cout << s2.c_str() << endl;
s2 += "xxx";
cout << s2.c_str() << endl;
}
void WY::test_string2()
{
string s("hello world");
s.insert(0, 'x');
cout << s.c_str() << endl;
string s1("hello world");
s1.insert(0, "xxx");
cout << s1.c_str() << endl;
s.erase(0, 1);
cout << s.c_str() << endl;
s1.erase(0, 3);
cout << s1.c_str() << endl;
s1.erase(1);
cout << s1.c_str() << endl;
}
void WY::test_string3()
{
string s("hello world");
s.resize(5);
cout << s.c_str() << endl;
s.resize(20,'x');
cout << s.c_str() << endl;
}
void WY::test_string4()
{
string s;
cin >> s;
cout << s << endl;
}
void WY::test_string5()
{
string s("hello world");
string s1("hello world");
cout << (s == s1) << endl;
string s2("hello");
cout << (s == s2) << endl;
string s3("a");
string s4("b");
cout << (s3 < s4) << endl;
}
void WY::test_string6()
{
string s("hello world");
size_t pos = s.find('w');
cout << pos << endl;
}
int main()
{
WY::test_string1();
//WY::test_string2();
//WY::test_string3();
//WY::test_string4();
//WY::test_string5();
//WY::test_string6();
return 0;
}
4. 总结
STL是学习是C++中必不可少的一部分,必须熟练掌握才行,我建议大家可以通过刷题来快速掌握STL的使用,熟练使用是最重要的,其次才是了解它的底层实现。
如果大家发现有什么错误的地方,可以私信或者评论区指出喔。我会继续深入学习C++,希望能与大家共同进步,那么本期就到此结束,让我们下期再见!!觉得不错可以点个赞以示鼓励!!
更多推荐
所有评论(0)