[C++]智能指针
本篇文章介绍一下C++里的四个智能指针 : auto_ptr、unique_ptr、shared_ptr 还有 weak_ptr,除了auto_ptr(C++98) 以外,后三者是C++11后才有。它们在使用上和普通的指针差别不大(可以使用解引用 * 和箭头 -> 来访问对象),但是它们具有管理资源的功能 :在智能指针对象的生命周期结束时可以自动释放所管理的资源。
1. 智能指针原理及其引入
1.1. 原理
智能指针所用到的原理是 RAII(Resource Acquisition Is Initialization,资源获取即初始化),是由C++之父 Bjarne Stroustrup 提出的。这里所指的资源可以是内存、文件句柄、网络连接、互斥量等等。其基本思路就是在申请资源时将该资源给一个对象托管,在对象的生命周期内该资源始终有效,在对象生命周期结束析构时将该资源进行释放。这样做的好处是可以不用我们人工干预释放资源的过程,也能以防我们忘记释放资源。
1.2. 设计一个的智能指针:SmartPtr
现在我们使用 RAII 的思想设计一个简单的智能指针,总结为下面几个步骤:
- 设计一个类来封装资源。
- 在构造函数中初始化,在对象初始化时绑定我们要管理的资源。
- 在析构函数中对我们所管理的资源进行清理释放。
我们来写一个SmartPtr类:
#include <iostream>
using namespace std;
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
private:
T* _ptr; //步骤一:封装资源
public:
SmartPtr(T* ptr = nullptr) //步骤二:在构造函数中初始化
: _ptr(ptr)
{}
~SmartPtr() //步骤三:在析构函数中清理资源
{
delete _ptr;
}
};
//测试使用的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
SmartPtr<A> ptr(new A(6)); //使用智能指针对获取的对象资源进行管理
return 0;
}
运行结果:
A(int data)
~A() and its _data = 6
我们在 new 了一个对象后,并将其使用智能指针进行管理,这样我们就可以不显式调用 delete 来手动释放资源了,可以防止我们忘记手动释放资源而导致内存泄漏的情况发生。
为了让上面的 SmartPtr 的行为更像普通的指针,我们还应该重载operator*() 和 operator->() 函数,让智能指针对象能够像普通指针一样通过 * 和 -> 访问所指向的资源。 下面是写了操作符重载后的代码:
#include <iostream>
using namespace std;
// 使用RAII思想设计的SmartPtr类
template<class T>
class SmartPtr {
private:
T* _ptr; //步骤一:封装资源
public:
SmartPtr(T* ptr = nullptr) //步骤二:在构造函数中初始化
: _ptr(ptr)
{}
~SmartPtr() //步骤三:在析构函数中清理资源
{
delete _ptr;
}
//重载operator* 和 operator-> 来访问资源
T& operator*() const
{
return *_ptr;
}
T* operator->() const
{
return _ptr;
}
//该接口用于获得底层的指针
T* get() const
{
return _ptr;
}
};
//测试使用的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
SmartPtr<A> ptr(new A(666)); //使用智能指针对获取的对象资源进行管理
//使用智能指针对象来访问资源
cout << (*ptr)._data << endl;
cout << ptr->_data << endl;
(*ptr)._data = 777;
cout << (*ptr)._data << endl;
ptr->_data = 888;
cout << ptr->_data << endl;
return 0;
}
运行结果:
A(int data)
666
666
777
888
~A() and its _data = 888
我们下面来看C++里的4个智能指针, 其使用方法和功能和我们自我实现的 SmartPtr 大差不差 ,下面只讲一下它们的特性和使用上需要注意的点。
2. auto_ptr
官方文档: auto_ptr - C++ Reference (cplusplus.com)
2.1. auto_ptr的缺陷
我们来看下面的代码:
#include <iostream>
#include <memory> //包含auto_ptr的头文件
using namespace std;
//喜闻乐见的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
A* ptrA = new A(10);
auto_ptr<A> auPtr1(ptrA);
auto_ptr<A> auPtr2 = auPtr1; //使用auPtr1对auPtr2进行赋值
cout << auPtr1->_data << endl;
cout << auPtr2->_data << endl;
return 0;
}
我们乍一看这段代码, auPtr1 和 auPtr2 指向同一个对象的内存空间,这很合理,我们使用 auPtr1 和 auPtr2 输出一下A对象的_data 时,发生了严重报错,报错内容为:auto_ptr not dereferenceable。
这是因为 auto_ptr 的拷贝或赋值操作会转移资源的特性导致的问题,这叫做管理权转移,我们将 auPtr1 赋值给 auPtr2 时,并不是像平常的指针一样,两个指针指向同一块内存空间,而是将 auPtr1 的资源管理权交给 auPtr2 管理,而 auPtr1 指针置为空,上面的报错就是 auPtr1 的空引用造成的。
我们修改代码,并调用 auto 对象的 get 方法获取底层的原始指针,查看 auPtr1 在赋值后是否被置空了。
修改代码:
int main()
{
A* ptrA = new A(10);
auto_ptr<A> auPtr1(ptrA);
auto_ptr<A> auPtr2(auPtr1); //使用auPtr1对auPtr2进行赋值
if (auPtr1.get() == nullptr)
cout << "auPtr1.get() == nullptr" << endl;
else
cout << auPtr1->_data << endl;
cout << auPtr2->_data << endl;
return 0;
}
运行结果:
A(int data)
auPtr1.get() == nullptr
10
~A() and its _data = 10
我们发现确实如此,auPtr1 在赋值给 auPtr2,相当于将资源的管理权转给了 auPtr2。。
auto_ptr 的拷贝和赋值操作具有迷惑性,用不好容易导致严重错误,因此最佳实践就是尽量不要使用 auto_ptr,auto_ptr 的存在是由于历史原因。 于是C++11就有了更靠谱的 unique_ptr,来解决这个问题。
2.2. auto_ptr的模拟实现
//------------------------ auto_ptr ------------------------------
// 描述:模拟实现一个auto_ptr
// 缺陷:管理权转移后旧的指针对象会被置空,继续使用会出现严重错误
//--------------------------------------------------------------------
template<class T>
class auto_ptr
{
private:
T* _ptr;
public:
auto_ptr(T* ptr)
:_ptr(ptr)
{}
//拷贝构造:管理权转移
auto_ptr(auto_ptr<T>& other)
{
// 管理权转移
_ptr = other._ptr;
other._ptr = nullptr;
}
//赋值重载:管理权转移
auto_ptr<T>& operator=(auto_ptr<T>& other)
{
// 检测是否为自己给自己赋值
if (this != &other)
{
// 释放当前对象中资源
delete _ptr;
// 转移other中资源到当前对象中
_ptr = other._ptr;
other._ptr = nullptr;
}
return *this;
}
~auto_ptr()
{
delete _ptr;
}
// 重载 * 和 ->操作符
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
3. unique_ptr
官方文档: unique_ptr - C++ Reference (cplusplus.com)
3.1. unique_ptr特性
unique_ptr 为了解决 auto_ptr 的问题,直接禁用了拷贝构造和赋值重载函数,禁止指针对象的拷贝(防拷贝), 这样就不会出现 auto_ptr 那样导致的严重错误的情况了。在使用和功能上没有什么特别的。我们在这着重讲一下 unique_ptr 的 定制删除器该如何使用。
3.2. unique_ptr自定义删除器的使用
unique_ptr 默认的删除器是使用 delete 来清理资源的 ,如果我们的对象是 new 出来的,那么没问题,我们可以不自己制定资源的 删除器,但如果我们申请的资源是通过 fopen(C语言中打开文件) 或者是 new[] 申请的,则需要使用对应的 fclose 和 delete[] 来释放资源,这个时候我们需要给 unique_ptr 传入一个清理资源的方法,即在指针对象实例化时传入一个重载了 operator() 删除方法的类。
我们先来看一下unique_ptr模板类的声明:
//non-specialized
template <class T, class D = default_delete<T>> class unique_ptr;
//array specialization
template <class T, class D> class unique_ptr<T[],D>;
下面那个是数组的特化 ,如果是在实例化对象时指明类型为 T[] 则底层会调用 delete[] 帮助我们释放数组资源。
下面这个例子定制了删除器:
#define _CRT_SECURE_NO_WARNINGS 1 //解除VS对不安全函数的警告
#include <iostream>
#include <cstdio> //包含fopen以及fclose的头文件
#include <memory> //包含unique_ptr的头文件
using namespace std;
//喜闻乐见的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
//自定义删除器:
template<class T>
struct DeleteArr //用于删除对应new []的对象数组
{
void operator()(T* ptr)
{
delete[] ptr;
cout << "delete[] ptr" << endl;
}
};
struct FileClose //用于关闭文件的FileClose
{
void operator()(FILE* pFile)
{
fclose(pFile);
cout << "fclose(pFile)" << endl;
}
};
int main()
{
A* pArray = new A[2];
unique_ptr<A,DeleteArr<A>> uPtr1(pArray); //传入删除器DeleteArr
//或者使用下面的特化版本
//unique_ptr<A[]> uPtr1(pArray); //T[]的特化,调用对应delete[]
FILE* pFile = fopen("test.txt", "w"); //打开一个文件进行写入
unique_ptr<FILE,FileClose> uPtr2(pFile); //传入删除器FileClose
return 0;
}
运行结果:
A(int data)
A(int data)
fclose(pFile)
~A() and its _data = 0
~A() and its _data = 0
delete[] ptr
3.3. unique_ptr的模拟实现
//------------------------ default_delete ------------------------------
// 描述:默认的删除器
//------------------------------------------------------------------------
template<class T>
struct default_delete
{
void operator()(T* ptr)
{
delete ptr; //默认使用delete作为删除器
}
};
//------------------------ unique_ptr ------------------------------
// 描述:模拟实现一个unique_ptr指针
// 特性:不能够被拷贝,解决了auto_ptr管理权转以后出现的不安全的问题
//--------------------------------------------------------------------
template<class T,class D = default_delete<T>>
class unique_ptr
{
private:
T* _ptr; //指向所维护的空间
public:
unique_ptr(T* ptr) :_ptr(ptr) {}
//禁止拷贝
unique_ptr(const unique_ptr<T>&) = delete;
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;
//重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
~unique_ptr()
{
D del; //删除器对象
del(_ptr); //使用删除器释放资源
_ptr = nullptr;
}
};
4. shared_ptr(共享指针)
官方文档: shared_ptr - C++ Reference (cplusplus.com)
4.1. shared_ptr的特性和原理
- 共享指针之间允许互相拷贝 ,即共享指针可以像普通指针那样同时指向一个资源。
- 每个被指向的资源都有一个引用计 ,记录该资源被多少个共享指针所管理,若有一个新的共享指针对象拥有该资源的管理权,那么该资源的引用计数会增加1,若有一个共享指针对象被销毁了(生命周期到了),那么就会释放对该资源的所有权,该资源的 引用计数减少1。
- 如果指向该资源的最后一个共享指针对象销毁了,那么引用计数减一, 引用计数等于0,此时该资源会被释放掉,归还操作系统。如果引用计数不为0,说明仍有共享指针 共同管理着该资源,不能释放该资源,可以类比:生活中最后一个走出房间的人要关灯。
下面是一个使用例子:
#include <iostream>
#include <memory> //包含shared_ptr的头文件
using namespace std;
//喜闻乐见的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
A* pA = new A(5);
shared_ptr<A> sPtr1(pA);
cout << sPtr1.use_count() << endl; //使用共享指针里的use_count方法可以获得该内存的引用计数
cout << endl;
// {}创建一个作用域
{
shared_ptr<A> sPtr2 = sPtr1; //共享指针允许互相拷贝,指向同一块内存空间
cout << "sPtr1.use_count() = " << sPtr1.use_count() << endl; //使用共享指针里的use_count方法可以获得该内存的引用计数
cout << "sPtr2.use_count() = " << sPtr2.use_count() << endl; //使用共享指针里的use_count方法可以获得该内存的引用计数
cout << sPtr1->_data << endl;
cout << sPtr2->_data << endl;
cout << endl;
sPtr1->_data = 999; //修改sPtr1所指向的数据
cout << sPtr1->_data << endl;
cout << sPtr2->_data << endl;
}
cout << "出了 sPtr2 的作用域,sPtr2 被销毁" << endl;
cout << "sPtr1.use_count() = " << sPtr1.use_count() << endl; //使用共享指针里的use_count方法可以获得该内存的引用计数
cout << sPtr1->_data << endl;
return 0;
}
运行结果:
A(int data)
1
sPtr1.use_count() = 2
sPtr2.use_count() = 2
5
5
999
999
出了 sPtr2 的作用域,sPtr2 被销毁
sPtr1.use_count() = 1
999
~A() and its _data = 999
4.2. shared_ptr 的模拟实现
//------------------------ shared_ptr ------------------------------
// 描述:模拟实现一个共享指针,可以像普通的指针一样拷贝指向同一块空间
// 原理:引用计数,能够知道该资源有多少个指针对象共享
// 当最后一个指针对象销毁时,该资源才被回收
//--------------------------------------------------------------------
template<class T>
class shared_ptr
{
private:
T* _ptr; //指向共享的空间
int* _pCount; //引用计数,每一个共享空间对应一个计数器
std::function<void(T*)> _del = [](T* ptr) {delete ptr; }; //函数包装:删除器,默认用delete释放
//当一个共享指针被销毁时,对引用计数--,并判断是否需要释放所共享的空间
void destroy()
{
if (--(*_pCount) == 0)
{
_del(_ptr); //指向资源的最后一个共享指针析构了,使用删除器释放资源
delete _pCount;
_ptr = nullptr;
_pCount = nullptr;
}
}
public:
//普通构造,默认使用delete来做删除器
shared_ptr(T* ptr = nullptr) :_ptr(ptr), _pCount(new int(1)) { }
template<class D> //模板参数D:用来订制del删除器
shared_ptr(T* ptr, D del) :_ptr(ptr), _pCount(new int(1)),_del(del) { }
//拷贝构造和赋值重载
shared_ptr(const shared_ptr<T>& other)
{
_ptr = other._ptr;
_pCount = other._pCount;
(*_pCount)++;
}
//赋值重载
shared_ptr<T>& operator=(const shared_ptr<T>& other)
{
destroy(); //共享指针要管理别的资源了,让原有的资源的引用计数--再指向别的空间
_ptr = other._ptr;
_pCount = other._pCount;
(*_pCount)++;
return *this;
}
//重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
//返回这块空间被共享指针所共享的个数
int use_count() const
{
return *_pCount;
}
//返回底层指向内存空间的指针
T* get() const
{
return _ptr;
}
~shared_ptr()
{
destroy();
}
};
4.3. shared_ptr自定义删除器的使用
shared_ptr 的删除器使用方法和 unique_ptr 不一样 ,在unique_ptr 中,删除器是以实例化 unique_ptr 指针对象时以具体类型传入的,而 shared_ptr 则是在构造函数中传入一个函数对象的方式传入的,在内部使用了包装器将这个函数对象包装起来,可在别的成员函数里使用。
下面来看一下如何使用订制删除器:
#include <iostream>
#include <memory> //包含shared_ptr的头文件
using namespace std;
//喜闻乐见的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
A* pArr = new A[2];
shared_ptr<A> ptr(pArr,
[](A* ptr)
{
delete[] ptr;
cout << "delete[] ptr;" << endl;
}); //传入一个带有删除方法的函数对象
return 0;
}
运行结果:
A(int data)
A(int data)
~A() and its _data = 0
~A() and its _data = 0
delete[] ptr;
4.4. make_shared模板函数
功能:传入构建某个对象的参数来构建一个动态开辟的对象资源,返回一个管理该资源的智能指针。
这样做的目的是可以实现动态开辟的引用计数内存紧邻着资源对象内存的,这样可以减少内存碎片的产生 。
使用示例:
#include <iostream>
#include <memory> //包含shared_ptr的头文件
using namespace std;
//喜闻乐见的A类
class A
{
public:
int _data;
A(int data = 0) :_data(data)
{
cout << "A(int data)" << endl;
}
~A()
{
cout << "~A() and its _data = " << _data << endl;
}
};
int main()
{
//传入构造A类对象需要的参数,动态开辟一个A类对象,并返回一个共享指针管理该对象资源
shared_ptr<A> sPtr1 = make_shared<A>(10);
cout << sPtr1->_data << endl;
(*sPtr1)._data++;
cout << sPtr1->_data << endl;
return 0;
}
运行结果:
A(int data)
10
11
~A() and its _data = 11
4.5. shared_ptr的问题:循环引用
来看下面这个例子:
#include <iostream>
#include <memory> //包含shared_ptr的头文件
using namespace std;
//共享指针的循环引用问题
//双向链表结点:
struct ListNode
{
int _data;
shared_ptr<ListNode> _pPrev; //指向前一个头结点
shared_ptr<ListNode> _pNext; //指向后一个头结点
ListNode(int data)
:_data(data)
,_pPrev(nullptr)
,_pNext(nullptr)
{
cout << "ListNode(int data)" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
}
};
int main()
{
//动态开辟两个结点
shared_ptr<ListNode> pNode1(new ListNode(1));
shared_ptr<ListNode> pNode2(new ListNode(2));
//将这两个结点链接起来
pNode1->_pNext = pNode2;
pNode2->_pPrev = pNode1;
//打印这两个结点的引用计数
cout << pNode1.use_count() << endl;
cout << pNode2.use_count() << endl;
return 0;
}
运行结果:
ListNode(int data)
ListNode(int data)
2
2
我们动态开辟的两个链表结点并没有正确的析构(程序结束时析构函数没有被调用),下面来解释一下原因。
当智能指针 pNode1 和 pNode2 刚开始接手管理数据为1和2的链表结点时,其引用计数都为1 。
对应代码:
//动态开辟两个结点
shared_ptr<ListNode> pNode1(new ListNode(1)); // pNode1.use_count() 值为 1
shared_ptr<ListNode> pNode2(new ListNode(2)); // pNode2.use_count() 值为 1
示意图:
当我们前后连接这两个结点时,其内部的共享指针互相指向对方, 数据为1和2的链表结点引用计数都增加为2 。
对应代码:
//将这两个结点链接起来
pNode1->_pNext = pNode2; // pNode1.use_count() 值为 2
pNode2->_pPrev = pNode1; // pNode2.use_count() 值为 2
示意图:
当main函数结束时,智能指针对象 pNode1 和 pNode2 销毁,数据为1和2的结点的引用计数都减少为1 。
如果想要数据为1的结点析构,则需要数据为2的结点里的智能指针_pPrev 销毁。如果想要数据为2的结点析构,则需要数据为1的结点的里的智能指针 _pNext 销毁。这就是循环引用,一方的析构需要另外一方析构,而另外一方析构则需要这一方析构了才行,这导致了双方都无法析构。
而循环引用的解决方法需要使用我们下面介绍的 weak_ptr 指针。
5. weak_ptr
官方文档: weak_ptr - C++ Reference (cplusplus.com)
5.1. weak_ptr的特性
- 可以用
shared_ptr的指针对象来拷贝构造一个weak_ptr对象,指向同一块资源。 weak_ptr的指针对象指向shared_ptr所管理的资源时,不会引起该共享资源的引用计数增加1。weak_ptr会存在过期问题,如果weak_ptr指针对象所指向的资源已经没有共享指针所管理时,该weak_ptr会被视为过期,继续使用会出现问题。判断一个weak_ptr是否过期,调用weak_ptr对象里的expired()方法即可。
5.2. weak_ptr的模拟实现
//------------------------ weak_ptr -----------------------------------------------------------------------------------
// 描述:模拟实现一个weak指针,解决共享指针中循环引用导致无法释放资源的情况
// 该指针指向共享指针所指向的内存时,不会增加其资源的引用计数
// 但weak_ptr指针对象可能会出现过期问题,即weak_ptr指向的内存空间已经没有任何共享指针所管理了,这段内存空间被释放掉了
// 可以使用expired()方法检查一个weak_ptr指针对象是否过期,该方法实现起来复杂,下面的模拟实现并没有实现该功能
//-----------------------------------------------------------------------------------------------------------------------
template<class T>
class weak_ptr
{
private:
T* _ptr;
public:
weak_ptr(T* ptr = nullptr) :_ptr(ptr) { }
//拷贝构造和赋值重载,用共享指针给weak指针赋值,不会增加引用计数
weak_ptr<T> operator=(const shared_ptr<T>& sharedPtr)
{
_ptr = sharedPtr.get();
return *this;
}
weak_ptr(const shared_ptr<T>& sharedPtr)
{
_ptr = sharedPtr.get();
}
//重载 * 和 ->
T& operator*()
{
return *_ptr;
}
T* operator->()
{
return _ptr;
}
};
5.3. shared_ptr的循环引用问题的解决
weak_ptr 设计来只是辅助 shared_ptr 的,用来解决其循环引用导致的资源无法释放的问题。在上面的问题中,我们只需要将链表结点中的_pPrev 和 _pNext 指针对象的类型从 shared_ptr 改为 weak_ptr 即可。注意,weak_ptr 有默认构造函数,会自动初始化为空状态,因此构造函数中可省略显式置空。但为了代码清晰,保留初始化列表中的 nullptr 也是常见的写法。
#include <iostream>
#include <memory> //包含shared_ptr和weak_ptr的头文件
using namespace std;
//共享指针的循环引用问题
//双向链表结点:
struct ListNode
{
int _data;
weak_ptr<ListNode> _pPrev; //指向前一个头结点
weak_ptr<ListNode> _pNext; //指向后一个头结点
ListNode(int data)
:_data(data)
{
cout << "ListNode(int data)" << endl;
}
~ListNode()
{
cout << "~ListNode()" << endl;
// 判断 weak_ptr 是否失效
if (_pPrev.expired()) cout << "_data = " << _data << " ,_pPrev.expired()" << endl;
if (_pNext.expired()) cout << "_data = " << _data << " ,_pNext.expired()" << endl;
}
};
int main()
{
//动态开辟两个结点
shared_ptr<ListNode> pNode1(new ListNode(1));
shared_ptr<ListNode> pNode2(new ListNode(2));
//将这两个结点链接起来
pNode1->_pNext = pNode2;
pNode2->_pPrev = pNode1;
//打印这两个结点的引用计数
cout << pNode1.use_count() << endl;
cout << pNode2.use_count() << endl;
// 判断 weak_ptr 是否失效
if (!pNode1->_pNext.expired()) cout << "!pNode1->_pNext.expired()" << endl;
if (!pNode2->_pPrev.expired()) cout << "!pNode1->_pPrev.expired()" << endl;
return 0;
}
运行结果:
ListNode(int data)
ListNode(int data)
1
1
!pNode1->_pNext.expired()
!pNode1->_pPrev.expired()
~ListNode()
_data = 2 ,_pNext.expired()
~ListNode()
_data = 1 ,_pPrev.expired()
_data = 1 ,_pNext.expired()
使用了weak_ptr后的引用计数的情况:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)