C++ 模拟实现string类
目录
在C++中,string类是一个非常重要的数据类型,它提供了一系列的方法来处理字符串。然而,你有没有想过string类是如何实现的呢?在这篇文章中,我们将模拟实现一个简化版的string类,这可以更好地理解其内部工作原理。
一、类的定义
我们首先定义了一个名为Byte::string
的类,它包含了一些私有成员变量和公有成员函数。
namespace Byte
{
class string
{
private:
char* _str;
size_t _capacity;
size_t _size;
static const size_t npos;
//static const size_t npos = -1;//也可以
public:
// ...
};
const size_t string::npos = -1;
}
在这个类中,我们有三个私有成员变量:_str
,_capacity
和_size
。_str
是一个字符指针,用于存储字符串的内容;_capacity
表示字符串的容量,即可以存储的字符数量;_size
表示当前字符串的长度。此外,我们还定义了一个静态常量npos
,它的值为-1,通常用于表示“不存在”的位置。
二、初始化&销毁
1、构造函数
string(const char* str = "")
:_size(strlen(str))
{
_capacity = _size == 0 ? 3 : _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
-
构造函数
string(const char* str = "")
使用了全缺省的方式,使用全缺省的构造函数,可以有以下几种调用方式:-
不传递任何参数:
string s;
,这将创建一个空字符串的string
对象。 -
传递一个空字符串:
string s("");
,这将创建一个空字符串的string
对象。 -
传递一个非空字符串:
string s("Hello");
,这将创建一个包含指定字符串的string
对象。 -
传递一个空指针:
string s(nullptr);
,这将创建一个空字符串的string
对象。
-
-
_size(strlen(str))
: 这是一个初始化列表,它将_size
成员变量初始化为传入字符串str
的长度。这是通过使用C标准库函数strlen
来实现的。 -
_capaicty = _size == 0 ? 3 : _size;
: 这行代码设置了_capacity
成员变量的值。如果_size
为0(也就是说,如果传入的字符串为空),那么_capacity
被设置为3。否则,_capacity
被设置为_size
的值。 -
_str = new char[_capaicty + 1];
: 这行代码在堆上为_str
分配了足够的内存来存储字符串。分配的内存大小为_capacity + 1
,额外的1用于存储字符串的空字符终止符\0
。 -
strcpy(_str, str);
: 这行代码将传入的字符串str
复制到_str
指向的内存中。这是通过使用C标准库函数strcpy
来实现的。
2、辨析三种定义?
string(const char* str = nullptr) 不可以
string(const char* str = '\0') 不可以
string(const char* str = "\0") 可以
-
string(const char* str = nullptr)
: 这个构造函数版本不可行,因为nullptr
不能被strlen
和strcpy
处理,这会导致未定义的行为。 -
string(const char* str = '\0')
: 这个构造函数版本也不可行,因为'\0'
是一个字符,而不是一个字符串。它不能被赋值给const char*
类型的参数。 -
string(const char* str = "\0")
: 这个构造函数版本是可行的,因为"\0"
是一个包含空字符的字符串。然而,它与string(const char* str = "")
的行为是相同的,因为在C++中,字符串总是以空字符终止。
3、析构函数
析构函数则负责释放
_str
指向的内存,并将所有成员变量重置为初始状态。?
~string()
{
delete[] _str;
_str = nullptr;
_size = _capaicty = 0;
}
?
三、赋值?
1、拷贝构造函数
拷贝构造函数的目的是创建一个与参数对象
s
具有相同字符串和相同容量的新string
对象。
string(const string& s)
:_size(s._size)
, _capacity(s._capacity)
{
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
}
-
string(const string& s)
: 这是复制构造函数的声明。它接受一个对已存在的字符串对象的引用作为参数。使用引用是为了避免无限递归,因为如果不使用引用,那么在创建新的字符串对象时会再次调用复制构造函数,如此无限循环下去。 -
_size(s._size)
: 这是初始化列表的一部分,它将新字符串对象的?_size
?成员变量设置为传入的字符串对象的?_size
。 -
_capaicty(s._capaicty)
: 同样是初始化列表的一部分,它将新字符串对象的?_capaicty
?成员变量设置为传入的字符串对象的?_capaicty
。 -
_str = new char[s._capaicty + 1];
: 这行代码为新字符串对象的?_str
?成员变量分配内存。它创建一个新的字符数组,大小为传入的字符串对象的?_capaicty
?加 1。这里加 1 是为了留出空间存储字符串的结束字符 '\0'。 -
strcpy(_str, s._str);
: 这行代码将传入的字符串对象的?_str
?成员变量的内容复制到新字符串对象的?_str
?成员变量中。
2、赋值运算符
这段代码是C++中的赋值运算符重载函数,用于将一个字符串对象赋值给另一个字符串对象。这也是一个深拷贝操作,因为它创建了新的内存空间来存储复制的字符串,而不是简单地复制指针。
string& operator=(const string& s)
{
if (this != &s)
{
char* tmp = new char[s._capaicity + 1];
strcpy(tmp, s._str);
delete[] _str;
_str = tmp;
_size = s._size;
_capaicity = s._capacity;
}
return *this;
}
-
string& operator=(const string& s)
: 这是赋值运算符重载函数的声明。它接受一个对已存在的字符串对象的引用作为参数,并返回对当前对象的引用,以便支持链式赋值(例如 a = b = c)。 -
if (this != &s)
: 这是一个保护性检查,用于防止自我赋值。如果尝试将一个对象赋值给它自己,那么这个检查将阻止后续的代码执行。 -
char* tmp = new char[s._capaicity + 1];
: 这行代码创建一个新的字符数组,大小为传入的字符串对象的?_capaicity
?加 1。这里加 1 是为了留出空间存储字符串的结束字符 '\0'。 -
strcpy(tmp, s._str);
: 这行代码将传入的字符串对象的?_str
?成员变量的内容复制到新创建的字符数组中。 -
delete[] _str;
: 这行代码释放当前对象的?_str
?成员变量所占用的内存。这是为了避免内存泄漏,因为?_str
?将被重新赋值。 -
_str = tmp;
: 这行代码将?_str
?成员变量设置为新创建的字符数组的地址。 -
_size = s._size;
: 这行代码将当前对象的?_size
?成员变量设置为传入的字符串对象的?_size
。 -
_capaicity = s._capaicity;
: 这行代码将当前对象的?_capaicity
?成员变量设置为传入的字符串对象的?_capaicity
。 -
return *this;
: 这行代码返回对当前对象的引用,以支持连续赋值。
四、成员访问
我们为这个类重载了一些运算符,以便可以像使用内置类型一样使用我们自己定义的string类。
?operator[ ]
const char& operator[](size_t pos) const
{
assert(pos < _size);
return _str[pos];
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
-
const char& operator[](size_t pos) const
: 这是一个重载的下标运算符,它接受一个位置参数,并返回该位置的字符的引用。这个版本的函数是常量版本,它不能用于修改字符串的内容,只能用于读取。如果位置超出字符串的大小,它会触发一个断言错误。 -
char& operator[](size_t pos)
: 这也是一个重载的下标运算符,它接受一个位置参数,并返回该位置的字符的引用。这个版本的函数可以用于修改字符串的内容。如果位置超出字符串的大小,它也会触发一个断言错误。
使用[ ]时会自动选取合适的函数重载,这两种重载版本的存在,使得我们可以在保证类型安全的同时,对常量和非常量字符串对象进行适当的操作。
五、比较大小&判断相等
这些函数都是常量的,这意味着它们不能修改字符串对象的状态。这些函数都接受一个对?
string
?对象的常量引用作为参数,这可以避免不必要的复制操作。
bool operator>(const string& s) const
{
return strcmp(_str, s._str) > 0;
}
bool operator==(const string& s) const
{
return strcmp(_str, s._str) == 0;
}
bool operator>=(const string& s) const
{
//return *this > s || *this == s;
return *this > s || s == *this;
}
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);
}
-
bool operator>(const string& s) const
: 这个函数重载了大于运算符?>
。它使用?strcmp
?函数比较两个字符串,如果当前字符串大于参数字符串,strcmp
?会返回一个大于0的值,所以函数返回?true
。 -
bool operator==(const string& s) const
: 这个函数重载了等于运算符?==
。它使用?strcmp
?函数比较两个字符串,如果两个字符串相等,strcmp
?会返回0,所以函数返回?true
。 -
bool operator>=(const string& s) const
: 这个函数重载了大于等于运算符?>=
。它使用已经重载的?>
?和?==
?运算符来判断当前字符串是否大于或等于参数字符串。注释掉的代码有误,应该是?return *this > s || *this == s;
。 -
bool operator<(const string& s) const
: 这个函数重载了小于运算符?<
。它使用已经重载的?>=
?运算符来判断当前字符串是否不大于等于参数字符串,即小于参数字符串。 -
bool operator<=(const string& s) const
: 这个函数重载了小于等于运算符?<=
。它使用已经重载的?>
?运算符来判断当前字符串是否不大于参数字符串,即小于或等于参数字符串。 -
bool operator!=(const string& s) const
: 这个函数重载了不等于运算符?!=
。它使用已经重载的?==
?运算符来判断当前字符串是否不等于参数字符串。
六、容量操作?
1、size()
size_t size() const
{
return _size;
}
size_t size() const
: 这个函数返回字符串的大小,即字符串中的字符数量。这个函数是常量函数,这意味着它不能修改字符串的状态。
2、reserve
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
-
if (n > _capacity)
: 检查新的容量?n
?是否大于当前的容量?_capacity
。如果不是,那么函数立即返回,不做任何操作。 -
char* tmp = new char[n + 1];
: 如果新的容量大于当前的容量,那么首先创建一个新的字符数组,大小为新的容量加 1。这里加 1 是为了留出空间存储字符串的结束字符 '\0'。 -
strcpy(tmp, _str);
: 然后将当前字符串的内容复制到新的字符数组中。 -
delete[] _str;
: 然后释放当前字符串的内存。这是为了避免内存泄漏,因为?_str
?将被重新赋值。 -
_str = tmp;
: 然后将?_str
?指向新的字符数组。这样,_str
?就指向了一个更大的内存区域,可以存储更多的字符。 -
_capacity = n;
: 最后更新字符串的容量为新的容量。
3、push_back
void push_back(char ch)
{
if (_size + 1 > _capacity)
{
reserve(_capacity * 2);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
void push_back(char ch)
: 这个函数在字符串的末尾添加一个字符。如果添加新字符后字符串的大小超过了当前的容量,那么它会首先调用?reserve
?函数来增加字符串的容量。然后它将新字符添加到字符串的末尾,并更新字符串的大小,最后不要忘记补充‘ \0 ’在新的字符串末尾。?
4、append
void append(const char* str)
{
size_t len = strlen(str);
if (_size+len > _capacity)
{
reserve(_size + len);
}
strcpy(_str + _size, str);
_size += len;
}
void append(const char* str)
: 这个函数在字符串的末尾添加一个C风格字符串。它首先计算要添加的字符串的长度,如果添加新字符串后字符串的大小超过了当前的容量,那么它会首先调用?reserve
?函数来增加字符串的容量。然后它将新字符串添加到当前字符串的末尾,并更新字符串的大小。
5、加等运算符?
这两个函数提供了一种简洁的方式来修改字符串的内容,它们可以用来代 push_back 和 append 函数。
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string& operator+=(const char* str)
{
append(str);
return *this;
}
-
string& operator+=(char ch)
: 这个函数接受一个字符作为参数,并在字符串的末尾添加这个字符。它调用了?push_back
?函数来完成这个操作。然后它返回对当前对象的引用,以支持链式操作,例如?str += 'a' += 'b'
。 -
string& operator+=(const char* str)
: 这个函数接受一个C风格字符串作为参数,并在字符串的末尾添加这个字符串。它调用了?append
?函数来完成这个操作。然后它也返回对当前对象的引用,以支持链式操作,例如?str += "hello" += " world"
。
6、C风格
const char* c_str()
{
return _str;
}
const char* c_str()
: 这个函数返回一个指向字符串内部字符数组的指针。这个函数通常用于与需要C风格字符串的函数进行交互。返回类型为?const char*
,意味着你不能通过这个指针修改字符串的内容。?
7、insert
这两个?
insert
?函数用于在?string
?对象的指定位置插入字符或字符串。
插入字符?
?下面的代码有什么问题呢?
void insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size;
while (end >= pos)
{
_str[end + 1] = _str[end];
--end;
}
_str[pos] = ch;
++_size;
}
- 这段代码的主要问题在于?
while
?循环中的条件判断。在 C++ 中,size_t
?是无符号整数类型,当?end
?为 0 时,--end
?会导致?end
?变为最大的无符号整数,而不是 -1。因此,当?pos
?为 0 时,end >= pos
?的判断条件始终为?true
,这会导致无限循环。 - 在这个无限循环中,程序会尝试访问?
_str[end]
,其中?end
?是一个非常大的数。这将导致数组越界,可能会引发运行时错误,或者导致未定义的行为。
对上述代码进行改进:?
string& insert(size_t pos, char ch)
{
assert(pos <= _size);
if (_size + 1 > _capacity)
{
reserve(2 * _capacity);
}
size_t end = _size + 1;
while (end > pos)
{
_str[end] = _str[end-1];
--end;
}
_str[pos] = ch;
++_size;
return *this;
}
-
string& insert(size_t pos, char ch)
: 这个函数在指定位置?pos
?插入一个字符?ch
。 -
首先,它检查位置是否有效,然后检查是否需要增加字符串的容量。
-
接下来,它将从位置?
pos
?开始的所有字符向后移动一位,为新字符腾出空间。 -
然后,它在位置?
pos
?插入新字符,并更新字符串的大小。 -
最后,它返回对当前对象的引用,以支持链式操作。
插入字符串
string& 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 + len;
while (end > pos + len - 1)
{
_str[end] = _str[end - len];
--end;
}
strncpy(_str + pos, str, len);
_size += len;
return *this;
}
-
string& insert(size_t pos, const char* str)
: 这个函数在指定位置?pos
?插入一个C风格字符串?str
。 -
首先,它检查位置是否有效,然后计算要插入的字符串的长度,然后检查是否需要增加字符串的容量。
-
然后,它将从位置?
pos
?开始的所有字符向后移动?len
?位,为新字符串腾出空间。然后,它在位置?pos
?插入新字符串,并更新字符串的大小。 -
最后,它也返回对当前对象的引用,以支持链式操作。
insert实现push_back&append
void push_back(char ch)
{
//if (_size + 1 > _capacity)
//{
// reserve(_capacity * 2);
//}
//_str[_size] = ch;
//++_size;
//_str[_size] = '\0';
insert(_size, ch);
}
void append(const char* str)
{
//size_t len = strlen(str);
//if (_size+len > _capacity)
//{
// reserve(_size + len);
//}
//strcpy(_str + _size, str);
strcat(_str, str);
//_size += len;
insert(_size, str);
}
8、resize?
这个函数提供了一种灵活的方式来改变字符串的大小,无论是增大还是减小。如果需要增大字符串的大小,那么可以选择一个填充字符。如果需要减小字符串的大小,那么将删除多余的字符。
void resize(size_t n, char ch = '\0')
{
if (n < _size)
{
// 删除数据--保留前n个
_size = n;
_str[_size] = '\0';
}
else if (n > _size)
{
if (n > _capacity)
{
reserve(n);
}
size_t i = _size;
while (i < n)
{
_str[i] = ch;
++i;
}
_size = n;
_str[_size] = '\0';
}
}
这个?resize
?函数用于改变?string
?对象的大小。它接受两个参数:新的大小?n
?和用于填充的字符?ch
,默认为 '\0'。
-
if (n < _size)
: 如果新的大小?n
?小于当前的大小?_size
,那么将删除多余的字符。这通过将?_size
?设置为?n
,并在新的末尾位置添加结束字符 '\0' 来实现。 -
else if (n > _size)
: 如果新的大小?n
?大于当前的大小?_size
,那么将添加额外的字符。 if (n > _capacity)
: 如果新的大小?n
?大于当前的容量?_capacity
,那么将调用?reserve
?函数来增加字符串的容量。while (i < n)
: 然后使用一个循环来添加额外的字符。每个额外的字符都被设置为?ch
。_size = n;
: 最后更新字符串的大小为新的大小?n
,并在新的末尾位置添加结束字符 '\0'。
9、erase
string& erase(size_t pos, size_t len = npos)
{
assert(pos < _size);
if (len == npos || pos + len >= _size)
{
_str[pos] = '\0';
_size = pos;
}
else
{
strcpy(_str + pos, _str + pos + len);
_size -= len;
}
return *this;
}
-
函数定义:?
string& erase(size_t pos, size_t len = npos)
?定义了一个名为?erase
?的成员函数,它属于某个?string
?类型。它接受两个参数:pos
?和?len
。pos
?是要开始删除的位置索引(从零开始),而?len
?是要删除的字符数。如果?len
?没有指定,默认值是?npos
,这通常表示字符串的最大长度,意味着从?pos
?开始删除到字符串末尾。 -
assert(pos < _size);
?这一行是一个调试时使用的断言,确保?pos
?小于当前字符串的大小?_size
。如果?pos
?大于等于?_size
,程序会在调试模式下终止。 -
检查?
len
?和删除操作:- 如果?
len
?等于?npos
?或?pos + len
?大于等于?_size
(意味着要删除的范围超出了字符串的末尾),那么只需将位置?pos
?处的字符设置为?\0
(空字符),表示字符串在这里结束。然后更新?_size
?为?pos
,相当于删除了从?pos
?到字符串末尾的所有字符。 - 如果不是上述情况,则执行?
else
?里的代码。这里使用?strcpy(_str + pos, _str + pos + len);
?来删除指定的字符。这个操作实际上是将?pos + len
?之后的字符串复制到?pos
?处,从而覆盖掉要删除的部分。然后更新?_size
?来反映新的字符串长度,即减去删除的字符数。
- 如果?
-
返回值: 函数返回?
*this
,即返回经过修改后的字符串对象的引用。这允许链式调用,例如?str.erase(1, 3).append("abc");
。
10、swap
通过使用std::swap
函数,它交换了两个对象的成员变量,包括字符串内容、容量和大小。
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_capacity, s._capacity);
std::swap(_size, s._size);
}
11、find?
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;
}
}
return npos;
}
size_t find(const char* str, size_t pos = 0)
{
assert(pos < _size);
// kmp
char* p = strstr(_str + pos, str);
if (p == nullptr)
{
return npos;
}
else
{
return p - _str;
}
}
这两个函数都是?string
?类的?find
?方法的实现,用于在字符串中查找特定的字符或子字符串。我将逐步解释它们的作用和逻辑:
-
函数定义:
size_t find(char ch, size_t pos = 0)
?和?size_t find(const char* str, size_t pos = 0)
?定义了两个重载的?find
?函数。第一个函数用于查找单个字符?ch
,第二个函数用于查找子字符串?str
。两个函数都接受一个可选的起始位置?pos
,默认值为 0,表示从字符串的开始位置进行查找。 -
断言:
assert(pos < _size);
?这一行是一个调试时使用的断言,确保?pos
?小于当前字符串的大小?_size
。如果?pos
?大于等于?_size
,程序会在调试模式下终止。 -
查找字符:在第一个?
find
?函数中,从位置?pos
?开始,遍历字符串中的每个字符,如果找到与?ch
?相等的字符,就返回其位置。如果遍历完整个字符串都没有找到,就返回?npos
,通常表示字符串的最大长度,这里用作查找失败的标志。 -
查找子字符串:在第二个?
find
?函数中,使用?strstr
?函数从位置?pos
?开始查找子字符串?str
。strstr
?是 C/C++ 标准库中的一个函数,用于在一个字符串中查找另一个字符串的首次出现位置。如果找到,strstr
?返回第一次出现的位置的指针,否则返回?nullptr
。如果?strstr
?返回?nullptr
,表示没有找到子字符串,函数返回?npos
。否则,返回找到的位置,即?p - _str
。
七、迭代器
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;
}
-
typedef char* iterator;
: 这行代码定义了一个类型别名?iterator
,它是?char*
?类型的别名。这意味着你可以使用?iterator
?来代替?char*
。 -
typedef const char* const_iterator;
: 这行代码定义了一个类型别名?const_iterator
,它是?const char*
?类型的别名。这意味着你可以使用?const_iterator
?来代替?const char*
。 -
iterator begin()
: 这个函数返回一个指向字符串开始的迭代器。在这个?string
?类中,字符串的开始就是?_str
?成员变量的地址。 -
iterator end()
: 这个函数返回一个指向字符串结束的迭代器。在这个?string
?类中,字符串的结束就是?_str
?成员变量的地址加上字符串的大小?_size
。 -
const_iterator begin() const
: 这是?begin()
?函数的常量版本,它返回一个指向常量字符串开始的常量迭代器。这个函数是常量的,这意味着它不能修改字符串对象的状态。 -
const_iterator end() const
: 这是?end()
?函数的常量版本,它返回一个指向常量字符串结束的常量迭代器。这个函数也是常量的。
八、流输出流输入
ostream& operator<<(ostream& out, const string& s)
:这是输出流操作符<<
的重载函数,用于将string
对象s
输出到输出流out
。
ostream& operator<<(ostream& out, const string& s)
{
for (auto ch : s)
{
out << ch;
}
return out;
}
for (auto ch : s)
:这是一个范围for循环,用于遍历字符串s
中的每一个字符ch
。out << ch
:这行代码将字符ch
输出到输出流out
。return out
:最后,函数返回输出流out
,以便进行链式操作。
在类中定义clear()成员函数用于清空?
string
?对象中的字符串。
void clear()
{
_str[0] = '\0';
size = 0;
}
?
istream& operator>>(istream& in, string& s)
:这是输入流操作符>>
的重载函数,用于从输入流in
读取数据到string
对象s
。?
istream& operator>>(istream& in, string& s)
{
s.clear();
char 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
,另一个是字符串s
。这个函数的目的是从输入流in
中读取字符,然后将这些字符存储到字符串s
中。 -
在函数开始时,首先清空字符串
s
,以确保它是空的。 -
然后,使用
in.get()
函数从输入流中获取一个字符,并将其存储在变量ch
中。 -
定义一个字符数组
buff
,用于临时存储从输入流中读取的字符。同时定义一个变量i
,用于跟踪buff
中的当前位置。 -
接下来是一个循环,该循环会一直执行,直到从输入流中读取的字符是空格或换行符。在循环中,首先将从输入流中读取的字符存储到
buff
中,然后增加i
的值。 -
如果
i
的值达到127(即buff
的最大容量),则将buff
的最后一个字符设置为\0
(表示字符串的结束),然后将buff
中的内容添加到字符串s
中,并将i
重置为0。 -
在每次循环的结束,都会从输入流中获取一个新的字符。
-
循环结束后,如果
buff
中还有未添加到s
的字符(即i
不等于0),则将buff
的最后一个字符设置为\0
,然后将buff
中的内容添加到s
中。 -
最后,函数返回输入流对象
in
,以便可以链式地进行更多的输入操作。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!