C++ 中,动态内存的管理是通过一对运算符来完成的,new 用于申请内存空间,调用对象构造函数初始化对象并返回指向该对象的指针。delete接收一个动态对象的指针,调用对象的析构函数销毁对象,释放与之关联的内存空间。动态内存的管理在实际操作中并非易事,因为确保在正确的时间释放内存是极其困难的,有时往往会忘记释放内存而产生内存泄露;有时在上游指针引用内存的情况下释放了内存,就会产生非法的野指针(悬挂指针)。
为了更容易且更安全的管理动态内存,C++ 推出了智能指针(smart pointer)类型来管理动态对象。智能指针存储指向动态对象的指针,用于动态对象生存周期的控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露。
对动态内存的管理,可以引申为对系统资源的管理,但是 C++ 程序中动态内存只是最常使用的一种资源,其他常见的资源还包括文件描述符(file descriptor)、互斥锁(mutex locks)、图形界面中的字型和笔刷、数据库连接、以及网络sockets等,这些资源事实上都可以使用智能指针来管理。
智能指针的基本思想是以栈对象管理资源 。考察如下示例:
void remodel(std::string & str){ std::string* ps = new std::string(str); ... if (weird_thing()) { throw exception(); } str = *ps; delete ps; return;}
如果在函数 remodel 中出现异常,语句delete ps没有被执行,那么将会导致 ps 指向的 string 的堆对象残留在内存中,导致内存泄露。如何避免这种问题?有人会说,这还不简单,直接在throw exception();之前加上delete ps;不就行了。是的,你本应如此,问题是很多人都会忘记在适当的地方加上delete语句(连上述代码中最后的那句delete语句也会有很多人忘记吧),如果你要对一个庞大的工程进行review,往往会发现内存泄露时有发生,对于程序而言,这无疑是一场灾难!这时我们会想:当 remodel 这样的函数终止(不管是正常终止,还是由于出现了异常而终止),函数体内的局部变量都将自动从栈内存中删除,因此指针 ps 占据的内存将被释放,如果 ps 指向的内存也被自动释放,那该有多好啊。我们知道析构函数有这个功能。如果 ps 有一个析构函数,该析构函数将在 ps 过期时自动释放它指向的内存。但 ps 的问题在于,它只是一个常规指针,不是有析构凼数的类对象指针。如果 ps 是一个局部的类对象,它指向堆对象,则可以在 ps 生命周期结束时,让它的析构函数释放它指向的堆对象。
通俗来讲, 智能指针就是模拟指针动作的类。所有的智能指针都会重载 -> 和 * 操作符。智能指针的主要作用就是用栈智能指针离开作用域自动销毁时调用析构函数来释放资源。当然,智能指针还不止这些,还包括复制时可以修改源对象等。智能指针根据需求不同,设计也不同(写时复制,赋值即释放对象拥有权限、引用计数、控制权转移等)。
shared_ptr 是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,定义在 memory 文件中,命名空间为 std。shared_ptr最初实现于Boost库中,后由 C++11 引入到 C++ STL。shared_ptr 利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个 shared_ptr 共同管理同一个对象。像 shared_ptr 这种智能指针,《Effective C++》称之为“引用计数型智能指针”(reference-counting smart pointer,RCSP)。
1 引用计数
智能指针有时需要将其管理的对象的所有权转移给其它的智能指针,使得多个智能指针管理同一个对象,比如C++ STL中的shared_ptr支持多个智能指针管理同一个对象。这个时候智能指针就需要知道其引用的对象总共有多少个智能指针在引用在它,也就是说智能指针所管理的对象总共有多少个所有者,我们称之为引用计数(Reference Counting),因为智能指针在准备释放所引用的对象时,如果有其他的智能指针同时在引用这个对象时,则不能释放,而只能将引用计数减一。
大多数 C++ 类用三种方法之一来管理指针成员:
(1)不管指针成员。复制时只复制指针,不复制指针指向的对象实体。当其中一个指针把其指向的对象的空间释放后,其它指针都成了悬挂指针。这是一种极端做法。
(2)当复制的时候,即复制指针,也复制指针指向的对象。这样可能造成空间的浪费。因为指针指向的对象的复制不一定是必要的。
(3) 第三种就是一种折中的方式。利用一个辅助类来管理指针的复制。原来的类中有一个指针指向辅助类对象,辅助类的数据成员是一个计数器和一个指针(指向原来的对象)。
引用计数,是资源管理的一种技巧和手段,智能指针使用了引用计数,STL中的string也同样使用了引用计数并配合“写时复制”来实现存储空间的优化。总的来说,使用引用计数有如下两个目的:
(1)节省内存,提高程序运行效率。如何很多对象拥有相同的数据实体,存储多个数据实体会造成内存空间浪费,所以最好做法是让多个对象共享同一个数据实体。
(2)记录引用对象的所有者数量,在引用计数为0时,让对象的最后一个拥有者释放对象。
其实,智能指针的引用计数类似于Java的垃圾回收机制:Java的垃圾的判定很简单,如果一个对象没有引用所指,那么该对象为垃圾,系统就可以回收了。
2 等值对象具有多份拷贝的情况
一个未使用引用计数计数实现的String类伪代码示例如下:
class String{public:String(const char* value="");String& operator=(const String& rhs){if(this==&rhs) //防止自我赋值return *this;delete[] data; //删除旧数据data=new char[strlen(rhs.data)+1] //=1strcpy(data,rhs.data);return *this;}...private:char* data;};String a,b,c,d,e;a=b=c=d=e="Hello";
很显然对象a~e都有相同的值"hello",这就是等值对象存在多份拷贝。这样的多份副本是空间和时间的双重性能损耗。
3 以引用计数实现String
3.1 含有引用计数的字符串数据实体
引用计数实现String需要额外的变量来描述数据实体被引用的次数,即描述字符串值被多少个String对象所共享。这里重新设计一个结构体StringValue来描述字符串和引用计数。StringValue设计如下:
Struct StringValue{int refCount;char* data;};
3.2含有引用计数的字符串数据实体的String
新的String类的大致定义可描述如下:
class String { private: Struct StringValue { int refCount; char* data; StringValue(const char* initValue); ~StringValue(); }; StringValue* value; public: String(const char* initValue="");//constructor String(const String& rhs);//copy constructor String& operator=(const String& rhs); //assignment operator ~String(); //destructor };
关于StringValue的构造函数和析构函数可定义如下:
String::StringValue::StringValue(const char* initValue):refCount(1){data=new char[strlen(initValue)+1];strcpy(data,initValue);}String::StringValue::~StringValue(){delete[] data;}
String的成员函数可定义如下:
3.2.1 String的构造函数
String::String(const char* initValue):value(new StringValue(initValue)){}
在这种构造函数的作用下String s1("lvlv");和String s2=("lvlv"),分开构造相同初值的字符串在内存中存在相同的拷贝,并没有达到数据共享的效果。其数据结构为:
事实上可以令String追踪到现有的StringValue对象,并仅仅在字符串独一无二的情况下才产生新的StringValue对象,上图所显示的重复内存空间便可消除。这样细致的考虑和实现需要增加额外的代码。
3.2.2 String拷贝构造函数:
当String对象被复制时,产生新的String对象共享同一个StringValue对象,其代码实现可为:
String::String(const String& rhs):value(rhs.value){ ++valus->refCount;}
如果以图示表示,下面的代码:
String s1("lvlv");String s2=s1;
会产生如下的数据结构:
这样就会比传统的non-reference-counted String类效率高,因为它不需要分配内存给字符串的第二个副本使用,也不要再使用后归还内存,更不需要将字符串值复制到内存中。这里只需要将指针复制一份,并将引用计数加1。
3.2.3 String析构函数
String的析构函数在绝大部分调用中只需要将引用次数减1,只有当引用次数为1时,才会去真正销毁StringValue对象:
String::~String(){ if(--value->refCount==0) delete value;}
3.2.4 String的赋值操作符(assignment)
当用户写下s2=s1;时,这是String对象的相互赋值,s1和s2指向同一个StringValue对象,该对象的引用次数应该在赋值过程中加1。此外,赋值动作之前s2所指向的StringValue对象的引用次数应该减1,因为s2不再拥有该值。如果s2是原本StringValue对象的最后一个引用者,StringValue对象将被s2销毁。String的赋值操作符实现如下:
String& String::operator=(const String& rhs){if (this->value == rhs.value) //自赋值return *this;//赋值时左操作数引用计数减1,当变为0时,没有指针指向该内存,销毁if (--value->refCount == 0)delete value;//不必开辟新内存空间,只要让指针指向同一块内存,并把该内存块的引用计数加1value = rhs.value;++value->refCount;return *this;}
3.3 String的写时复制(Copy-on-Write)
字符串应该支持以下标读取或者修改某个字符,需要重载方括号操作符。String应该有
const char& operator[](size_t index) const;//重载[]运算符,针对const Stringschar& operator[](size_t index);//重载[]运算符,针对non-const Strings
对于const版本,因为是只读动作,字符串内容不受影响:
const char& String::operator[](size_t index) const{ return value->data[index];}
对于non-const版本,该函数可能用来读取,也可能用来写一个字符,C++编译器无法告诉我们operator[]被调用时是用于写还是取,所以我们必须假设所有的non-const operator[]的调用都用于写。此时,我能就要确保没有其他任何共享的同一个StringValue的String对象因写动作而改变。也就是说,在任何时候,我们返回一个字符引用指向String的StringValue对象内的一个字符时,我们必须确保该StringValue对象的引用次数为1,没有其他的String对象引用它。
//重载[]运算符,针对non-const Stringschar& String::operator[](size_t index){if (value->refCount>1){--value->refCount;value = new StringValue(value->data);}if (indexdata))return value->data[index];}
和其他对象共享一份数据实体,直到必须对自己拥有的那份实值进行写操作,这种在计算机科学领域中存在了很长历史。特别是在操作系统领域,各进程(processes)之间往往允许共享某些内存分页(memory pages),直到它们打算修改属于自己的那一分页。这项技术非常普及,就是著名的写时复制(copy-on-write)。
**注意:**实现了String的写时复制,但存在一个问题,比如:
String s1="Hello";char* p=&s1[1];String s2=s1;
这样就会出现如下数据结构:
这表示下面的语句会导致其他的String对象也被修改。
*p='d';
这个不问题不限于指针,如果有人以引用的方式将String的non-const operator[]返回值存储起来,也会发生同样的问题。
解决这种问题主要有三种方法。
(1)忽略
允许这种操作,即使出错也不错处理。这种方法很不幸被那些实现reference-counted字符串的类库所采用。考察如下程序,
#include #include using namespace std;std::string a="lvlv";int main(){char* p=&a[1];*p='a';std:: string b=a;std::cout<
上面代码在VS2017中编译运行输出"lalv"。
(2)警告
有些编译器知道会有这种问题,并给出警告。虽然无力解决,却会说明不要那么做,如果违背,后果不可预期。
(3)避免
彻底解决这种问题,采取零容忍态度。但是会降低对象之间共享的数据实体的个数。基本解决办法是:为每一个StringValue对象加上一个flag标志,用以指示是否可被共享。一开始,我们先树立此标志为true,表示对象可被共享,但只要non-const operator[]作用于对象值时就将标志清除。一旦标志被设为false,那么数据实体可能永远不会再被共享了。
下面是StringValue的修改版,包含一个可共享标志flag。
Struct StringValue{int refCount;char* data;bool shareable;StringValue(const char* initValue);~StringValue();};String::StringValue::StringValue(const char* initValue):refCount(1),shareable(true){data=new char[strlen(initValue)+1];strcpy(data,initValue);}String::StringValue::~StringValue(){delete[] data;}
相比之前的StringValue的构造函数和析构函数,并没有什么大的修改。当然String member functions也要做相应的修改。以copy constructor为例,修改如下:
String::String(const String& rhs){if(rhs.value->shareable){value=rhs.value;++valus->refCount;}}
其他的String的成员函数都应该以类似的方法检查shareable。对于Non-const operator[]是唯一将shareable设为false者,其实现代码可为:
char& String::operator[](size_t index){if (value->refCount>1){--value->refCount;value = new StringValue(value->data);}value->shareable=false;//新增此行if (indexdata))return value->data[index];}
以上描述了引用计数的作用和使用引用计数来实现自定义的字符串类String。使用引用计数来实现自定义类时,需要考虑很多细节问题,尤其是写时复制是提升效率的有效手段。
要几本掌握引用计数这项技术,需要我们明白引用计数是什么,其作用还有如何在自定义类中实现引用计数,如果这些都掌握了,那么引用计数也算是基本掌握了。
5 一个引用计数(Reference-Counting)基类
Reference-counting可用于字符串以外的场合,任何class如果其不同的对象可能拥有相同的值,都适用此技术。但是如果重写class以便适用reference counting可能需要大量的工作。
我们可以设计一个引用计数基类RCObject,供想拥有引用计数的类继承。RCObject将“引用计数器”本身以及用以增减引用数值的函数封装起来。此外,还包括销毁对象值的函数,设置不可共享标志函数,返回共享标志的函数,查询是否在被共享的函数,查询引用计数的数目。没有必要提供一个设定共享标志位true的成员函数,因为所有的对象值在默认情况下都是可共享的。这里设定一旦某个对象被贴上”不可共享”标签,其永远都将是不可共享。
RCObject定义如下:
//引用计数基类 class RCObject{ public: RCObject();//构造函数 RCObject(const RCObject& rhs);//拷贝构造函数 RCObject& operator=(const RCObject& rhs);//拷贝赋值运算符 virtual ~RCObject() = 0;//析构函数 void addReference();//增加引用计数 void removeReference();//减少引用计数,如果变为0,销毁对象 void markUnshareable();//将可共享标志设为false bool isShareable() const;//判断其值是否可共享 bool isShared() const;//判断其值是否正在被共享 int getRefCount();//返回引用计数 private: int refCount;//保存引用计数 bool shareable;//保存其值是否可共享的状态 }; //构造函数,这里refCount设为0,让对象创建者自行或将refCoun设为1 RCObject::RCObject(void) :refCount(0), shareable(true){} //拷贝构造函数,总是将refCount设为0,因为正在产生一个新对象,只被创建者引用 RCObject::RCObject(const RCObject&) : refCount(0), shareable(true){} //拷贝赋值运算符,这里只返回*this,因为左右两方RCObject对象的外围对象个数不受影响 RCObject& RCObject::operator=(const RCObject& rhs){ return *this; } //析构函数 RCObject::~RCObject(){} //增加引用计数 void RCObject::addReference(){ ++refCount; } //减少引用计数,如果变为0,销毁对象 void RCObject::removeReference(){ if (--refCount == 0) delete this; } //将追踪其值是否可共享的成员设为false void RCObject::markUnshareable(){ shareable = false; } //判断其值是否可共享 bool RCObject::isShareable() const{ return shareable; } //判断其值是否正在被共享 bool RCObject::isShared() const{ return refCount>1; } //返回引用计数 int RCObject::getRefCount(){ return refCount; }
注意:
(1)RCObject的赋值运算符opeator=()什么也没有做,实际上可共享的实值实际不太可能被赋值。例如在自定义String类中,实值StringValue并不不会被赋值,而是String对象的赋值。
(2)RCObject::removeReference的责任不只在于将对象的refCount递减,而有当引用计数refCount为0时,销毁实值对象。使用delete this来销毁实值对象,那就要求*this是heap对象。
6 基于引用计数基类的String
基于引用计数的基类String设计如下:
class String{private: Struct StringValue:public RCObject{ char* data; StringValue(const char* initValue); ~StringValue(); }; StringValue* value; public: String(const char* initValue="");//constructor String(const String& rhs);//copy constructor String& operator=(const String& rhs); //assignment operator ~String(); //destructor};//StringValue的构造函数String::StringValue::StringValue(const char* initValue):refCount(1){ data=new char[strlen(initValue)+1]; strcpy(data,initValue); }//StringValue的析构函数String::StringValue::~StringValue(){ delete[] data;}
这一版本的StringValue几乎与前一版本完全相同,唯一的改变是StringValue的member functions不再处理引用计数refCount字段,改由RCObject掌握。
7 自动操作引用次数(Reference Count)
RCObject class存放了引用次数,也给出了操作引用次数的member fucntions,这些函数的调用动作还是得用户手动写到其他的class内,并且通过String constructor和String assignment operator调用StringValue对象所提供的addReference和removeReference。这里,我们使用可复用的类,不必让用户类去操作引用次数。这里可复用的类产生的对象我们呢称之为smart pointer。
下面使用template来实现smart pointers,指向reference-counted实值对象。
//智能指针模板类,用来自动执行引用计数实值类成员的操控动作 template class RCPtr{ public: RCPtr(T* realPtr = 0);//构造函数 RCPtr(const RCPtr& rhs);//拷贝构造函数 ~RCPtr();//析构函数 RCPtr& operator=(const RCPtr& rhs);//拷贝赋值运算符 T* operator->() const;//重载->运算符 T& operator*() const;//重载*运算符 private: T* pointee; //dumb pointer void init(); //共同的初始化操作 }; //共同的初始化操作 template void RCPtr::init(){ if (pointee == 0) return; if (pointee->isShareable() == false) { pointee = new T(*pointee); } pointee->addReference(); } //构造函数 template RCPtr::RCPtr(T* realPtr) :pointee(realPtr){ init(); } //拷贝构造函数 template RCPtr::RCPtr(const RCPtr& rhs) : pointee(rhs.pointee){ init(); } //析构函数 template RCPtr::~RCPtr(){ if (pointee) pointee->removeReference(); } //赋值运算符 template RCPtr& RCPtr::operator=(const RCPtr& rhs){ if (pointee != rhs.pointee) { if (pointee) pointee->removeReference(); pointee = rhs.pointee; init(); } return *this; } //重载成员选取运算符 -> template T* RCPtr::operator->() const { return pointee; } //重载解引用运算符* template T& RCPtr::operator*() const { return *pointee; }
8 最终String
在上面的基础之上,我们利用具有服用性质的RCObject和RCPtr classes为基础,建造一个reference-counted String class。每一个具有引用计数功能的String对象均以此数据结构实现出来:
最终的String描述如下:
class String { public: String(const char *value = "");//构造函数 const char& operator[](int index) const;//重载[]运算符,针对const Strings char& operator[](int index);//重载[]运算符,针对non-const Strings private: struct StringValue : public RCObject {//继承自引用计数基类 char *data; StringValue(const char *initValue);//构造函数 StringValue(const StringValue& rhs);//拷贝赋值运算符 void init(const char *initValue); ~StringValue();//析构函数 }; RCPtr value;//智能指针对象 }; //String::StringValue实现代码 void String::StringValue::init(const char *initValue){ data = new char[strlen(initValue) + 1]; strcpy(data, initValue); } //StringValue类的构造函数 String::StringValue::StringValue(const char *initValue){ init(initValue); } //StringValue类的拷贝赋值运算符 String::StringValue::StringValue(const StringValue& rhs){ init(rhs.data); } //StringValue类的析构函数 String::StringValue::~StringValue(){ delete[] data; } //String实现代码 //String类的构造函数 String::String(const char *initValue): value(new StringValue(initValue)) {} //重载[]运算符,针对const Strings const char& String::operator[](int index) const{ return value->data[index]; } //重载[]运算符,针对non-const Strings char& String::operator[](int index){ if (value->isShared()) { value = new StringValue(value->data); } value->markUnshareable(); return value->data[index]; }
注意,这里使用智能指针对象的String并不需要显示定义copy constructor和assignment operator,因为这些编译器为默认生成,并且会自动调用String内RCPtr member的copy constructor和assignment operator,而后者又会自动执行对StringValue对象的所有处理,包括引用次数。
9 智能指针
智能指针就是一种资源管理对象,提供的功能主要有如下几种:
(1)以指针的行为方式访问所管理的对象,需要重载指针->操作符;
(2)解引用(Dereferencing),获取所管理的对象,需要重载解引用*操作符;
(3)智能指针在其声明周期结束时自动销毁其管理的对象;
(4)引用计数、写时复制、赋值即释放对象拥有权限、控制权限转移。
第4条是可选功能,拥有不同的功能对应着不同类型的智能指针,比如C++11在STL中引入的shared_ptr就实现了引用计数的功能,已经被C++11摒弃的auto_ptr实现了赋值即释放对象拥有权限,C++11引入的unique_ptr则实现了控制权限的转移功能。
根据智能指针的功能,通常用类模板实现如下:
template class SmartPointer{ private: T *_ptr; public: SmartPointer(T *p) : _ptr(p) //构造函数 { } T& operator *() //重载*操作符 { return *_ptr; } T* operator ->() //重载->操作符 { return _ptr; } ~SmartPointer() //析构函数 { delete _ptr; } };
STL 一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr 和 weak_ptr,auto_ptr 是 C++98 提供的解决方案,C+11 已将其摒弃,并提出了 unique_ptr 作为 auto_ptr 替代方案。虽然 auto_ptr 已被摒弃,但在实际项目中仍可使用,但建议使用较新的 unique_ptr,因为 unique_ptr 比 auto_ptr 更加安全。shared_ptr 和 weak_ptr 则是 C+11 从准标准库 Boost 中引入的两种智能指针。此外,Boost 库还提出了 boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,虽然尚未得到 C++ 标准采纳,但是在开发实践中可以使用。
先来看下面的赋值语句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”);auto_ptr vocation; vocaticn = ps;
上述赋值语句将完成什么工作呢?如果 ps 和 vocation 是常规指针,则两个指针将指向同一个 string 对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是 ps 过期时,另一次是 vocation 过期时。要避免这种问题,方法有多种:
(1)定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
(2)建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于 auto_ptr 和 unique_ptr 的策略,但 unique_ptr 的策略更严格。因为 auto_ptr 有拷贝语义,拷贝后原对象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr 则禁止了拷贝语义,但提供了移动语义,即可以使用std::move() 进行控制权限的转移。
(3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加 1,而指针过期时,计数将减 1,。当减为 0 时才调用 delete。这是 shared_ptr 采用的策略。
weak_ptr 被设计为与 shared_ptr 共同工作,可以从一个 shared_ptr 或者另一个 weak_ptr 对象构造而来。weak_ptr 是为了配合 shared_ptr 而引入的一种智能指针,它更像是 shared_ptr 的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载 operator* 和 operator-> ,因此取名为 weak,表明其是功能较弱的智能指针。它的最大作用在于协助 shared_ptr 工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着 weak_ptr 只对 shared_ptr 进行引用,而不改变其引用计数,当被观察的 shared_ptr 失效后,相应的 weak_ptr 也相应失效。
在了解 STL 中的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
(a)有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;
(b)两个对象都包含指向第三个对象的指针;
(c)STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为 unique_ptr 是不错的选择。这样,所有权转让给接受返回值的 unique_ptr,而该智能指针将负责调用 delete。可将 unique_ptr 存储到 STL 容器中,只要不调用将一个 unique_ptr 复制或赋值给另一个的算法(如 sort())。
参考:
https://blog.csdn.net/K346K346/article/details/50610631
https://blog.csdn.net/K346K346/article/details/50623486
-End-