简介
在 MQL5 中,您可能会创建自己的类以在代码中进一步使用该类类型的变量。我们已从前文 MQL5 中对象创建和析构的顺序中学到,结构和类可以通过两种方式创建 – 自动和动态。
要自动创建对象,只需声明一个类类型变量 – 系统将自动创建对象并初始化对象。要动态创建对象,必须显式地将运算符 new 应用至对象指针。
然而,自动创建的对象和动态创建的对象之间有何区别,哪些情况下必须使用对象指针而哪些情况下自动创建对象即已足够?本文正是旨在回答上述问题。首先,我们探讨使用对象时易犯的错误,以及相应的解决方法。
访问无效指针时产生的关键性错误
首先,您应当记住,在使用对象指针时必须先初始化对象然后才能使用对象。在您访问无效指针时,MQL 程序会因关键性错误而停止执行,因此程序被移除。我们将一个简单的“EA 交易”作为示例,该“EA 交易”具有类 CHello 并在此声明。类实例的指针声明为全局级别。
//+------------------------------------------------------------------+ //| GetCriticalError.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" //+------------------------------------------------------------------+ //| 一个简单的类 | //+------------------------------------------------------------------+ class CHello { private: string m_message; public: CHello(){m_message="开始...";} string GetMessage(){return(m_message);} }; //--- CHello *pstatus; //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 调用方法显示状态 Print(pstatus.GetMessage()); //--- 如果EA被成功初始化则打印消息 Print(__FUNCTION__," OnInit() 函数完成"); //--- return(0); } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
pstatus变量是对象指针,但我们故意“忘记”使用new运算符创建对象本身。如果您要在 EURUSD 图表上启动该“EA 交易”,您将会看到这一必然的结果 – “EA 交易”在 OnInit() 函数的执行阶段立即卸载。随后“EA 交易”日志中显示如下所示的消息:
14:46:17 Expert GetCriticalError (EURUSD, H1) loaded successfully(14:46:17 Expert GetCriticalError (EURUSD, H1) 成功加载)
14:46:18 Initializing of GetCriticalError (EURUSD, H1) failed(14:46:18 GetCriticalError (EURUSD, H1) 初始化失败)
14:46:18 Expert GetCriticalError (EURUSD, H1) removed(14:46:18 Expert GetCriticalError (EURUSD, H1) 已移除)
该示例极为简单,错误容易捕捉。然而,如果您的 MQL5 程序包含成百上千甚至成千上万行代码,这些错误的捕捉将会极其复杂。这对于程序行为的紧急情况条件取决于非可预测因素 – 例如特定的市场结构 – 的情形尤为重要。
使用前检查指针
是否有可能避免关键性程序的终止?是的,毫无疑问!在对象指针的使用前加入检查程序便已足够。我们通过添加 PrintStatus 函数来修改示例:
//+------------------------------------------------------------------+ //| 使用 CHello 类型对象的方法打印一条消息 | //+------------------------------------------------------------------+ void PrintStatus(CHello *pobject) { if(CheckPointer(pobject)==POINTER_INVALID) Print(__FUNCTION__," 变量 '对象' 没有被初始化!"); else Print(pobject.GetMessage()); }
现在该函数调用 GetMessage() 方法,CHello 类型对象的指针传递至该函数。首先,它使用函数 CheckPointer() 检查指针。让我们添加一个外部参数并将“EA 交易”的代码保存至 GetCriticalError_OnDemand.mq5 文件。
//+------------------------------------------------------------------+ //| GetCriticalError_OnDemand.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" input bool GetStop=false;// 获得关键性错误 //+------------------------------------------------------------------+ //| 一个简单的类 | //+------------------------------------------------------------------+ class CHello { private: string m_message; public: CHello(){m_message="开始...";} string GetMessage(){return(m_message);} }; //--- CHello *pstatus; //+------------------------------------------------------------------+ //| 使用CHello类型对象的方法打印消息 | //+------------------------------------------------------------------+ void PrintStatus(CHello *pobject) { if(CheckPointer(pobject)==POINTER_INVALID) Print(__FUNCTION__," 变量 '对象' 没有被初始化!"); else Print(pobject.GetMessage()); } //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 调用方法显示状态 if(GetStop) pstatus.GetMessage(); else PrintStatus(pstatus); //--- 如果EA被成功初始化则打印一条消息 Print(__FUNCTION__," OnInit() 函数完成"); //--- return(0); } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
现在我们可以通过两种方式来启动“EA 交易”:
- 通过关键性错误 (GetStop = true)
- 不通过错误,而是通过关于无效指针的消息 (GetStop = false)
默认情况下,“EA 交易”将成功执行,且以下消息将出现在“EA 交易”日志中:
GetCriticalError_OnDemand (EURUSD, H1) 15:01:57 PrintStatus the variable ‘object’ isn’t initialized!(GetCriticalError_OnDemand (EURUSD, H1) 15:01:57 PrintStatus 变量“对象”未初始化!)
GetCriticalError_OnDemand (EURUSD, H1) 15:01:57 OnInit function OnInit () is completed(GetCriticalError_OnDemand (EURUSD, H1) 15:01:57 OnInit 函数 OnInit () 已完成)
因此,使用前检查指针可避免关键性错误。
使用前始终在函数中检查指针的正确性
通过引用传递未初始化的对象
如果将未初始化的对象作为函数的输入参数传递会发生什么?(引用对象本身,而不是对象指针)。复杂对象,如类和结构,使用&符号通过引用传递。让我们改写 GetCriticalError_OnDemand.mq5 的部分代码。我们将重命名 PrintStatus() 函数,并以不同的方式改写其代码。
//+------------------------------------------------------------------+ //| 使用CHello类型对象的一个方法打印消息 | //+------------------------------------------------------------------+ void UnsafePrintStatus(CHello &object) { DebugBreak(); if(CheckPointer(GetPointer(object))==POINTER_INVALID) Print(__FUNCTION__," 变量'对象' 没有被初始化!"); else Print(object.GetMessage()); }
现在不同之处在于,该类型的变量本身而不是CClassHello类型的对象指针通过引用作为输入参数传递。我们将新版本的“EA 交易”另存为 GetCriticalError_Unsafe.mq5。
//+------------------------------------------------------------------+ //| GetCriticalError_Unsafe.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" input bool GetStop=false;// 获得关键性错误 //+------------------------------------------------------------------+ //| 一个简单的类 | //+------------------------------------------------------------------+ class CHello { private: string m_message; public: CHello(){m_message="开始...";} string GetMessage(){return(m_message);} }; //--- CHello *pstatus; //+------------------------------------------------------------------+ //| 使用CHello类型对象的一个方法打印消息 | //+------------------------------------------------------------------+ void UnsafePrintStatus(CHello &object) { DebugBreak(); if(CheckPointer(GetPointer(object))==POINTER_INVALID) Print(__FUNCTION__," 变量'对象'没有被初始化!"); else Print(object.GetMessage()); } //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 调用方法显示状态 if(GetStop) pstatus.GetMessage(); else UnsafePrintStatus(pstatus); //--- 如果EA被成功初始化则打印消息 Print(__FUNCTION__," OnInit() 函数完成"); //--- return(0); } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
我们可以看到 GetCriticalError_OnDemand.mq5 和 GetCriticalError_Unsafe.mq5“EA 交易”的不同在于参数传递给函数的方式。对于前者是对象指针传递给函数,而后者是对象本身通过引用传递。对于两者,在使用对象和其指针前,函数检查了指针的正确性。
这是否意味着两个版本的“EA交易”将以同样的方式工作?事实并非如此!让我们启动含参数 GetStop=false 的“EA 交易”,我们将再次得到一个关键性错误。事实是,如果对象通过引用传递,关键性错误将在函数调用阶段发生,因为未初始化的对象作为参数进行了传递。您可以使用 F5 按钮直接从 MetaEditor5 以调试模式启动脚本来进行验证。
要避免使用手动断点,我们需要通过在函数内部添加 DebugBreak () 断点来修改函数。
//+------------------------------------------------------------------+ //| 使用CHello类型对象的一个方法打印消息 | //+------------------------------------------------------------------+ void UnsafePrintStatus(CHello &object) { DebugBreak(); if(CheckPointer(GetPointer(object))==POINTER_INVALID) Print(__FUNCTION__," 变量'对象' 没有被初始化!"); else Print(object.GetMessage()); }
在调试模式下,该“EA 交易”将在调用 DebugBreak() 函数前卸载。如果未初始化的对象通过引用传递,我们如何针对这种情况来编写安全的代码?答案很简单 – 使用函数重载。
如果未初始化对象的指针作为函数的参数通过引用传递,将会导致关键性错误并停止 MQL5 程序。
使用重载函数构建安全代码
如果开发人员在使用外部库时不得不针对输入对象的正确性执行检查,无疑将带来极大的不便。 可以利用函数重载,在库中执行所有必要的检查会更好。
让我们来实施重载函数UnsafePrintStatus()的方法并编写该函数的两个版本 – 其一使用传递的对象指针而不是对象本身,其二使用通过引用的对象传递。两个函数具有相同的函数名 “PrintStatus”,但该实施将不再具有潜在的危险性。
//+------------------------------------------------------------------+ //| 使用CHello类型对象指针安全打印一条消息 | //+------------------------------------------------------------------+ void SafePrintStatus(CHello *pobject) { if(CheckPointer(pobject)==POINTER_INVALID) Print(__FUNCTION__,"变量'对象'没有被初始化!"); else Print(pobject.GetMessage()); } //+------------------------------------------------------------------+ //| 使用引用传递的CHello对象打印一条消息 | //+------------------------------------------------------------------+ void SafePrintStatus(CHello &pobject) { DebugBreak(); SafePrintStatus(GetPointer(pobject)); }
现在,如果函数通过传递对象指针调用,正确性检查将执行,且关键性错误不会出现。如果您通过引用使用传递对象来调用重载函数,首先我们使用 GetPointer 函数获得对象指针,然后我们调用通过指针使用对象传递的安全代码。
我们可以重写一个更短的函数安全版本,来代替
void SafePrintStatus (CHello & pobject) ( DebugBreak (); CHello * p = GetPointer (pobject); SafePrintStatus (p); )
我们来编写一个简洁版本:
void SafePrintStatus (CHello & object) ( DebugBreak (); SafePrintStatus (GetPointer (object)); )
两种版本是等效的。我们使用名称 GetCriticalError_Safe.mq5 来保存第二个版本。
//+------------------------------------------------------------------+ //| GetCriticalError_Safe.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "2009, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" input bool GetStop=false;// 得到关键性错误 //+------------------------------------------------------------------+ //| 一个简单的类 | //+------------------------------------------------------------------+ class CHello { private: string message; public: CHello(){message="开始...";} string GetMessage(){return(message);} }; //--- CHello *pstatus; //+------------------------------------------------------------------+ //| 使用CHello对象的指针安全打印一条消息 | //+------------------------------------------------------------------+ void SafePrintStatus(CHello *pobject) { if(CheckPointer(pobject)==POINTER_INVALID) Print(__FUNCTION__," 变量 '对象' 没有被初始化!"); else Print(pobject.GetMessage()); } //+------------------------------------------------------------------+ //| 使用引用传递的CHello对象的指针打印一条消息 | //+------------------------------------------------------------------+ void SafePrintStatus(CHello &pobject) { DebugBreak(); SafePrintStatus(GetPointer(pobject)); } //+------------------------------------------------------------------+ //| EA初始化函数 | //+------------------------------------------------------------------+ int OnInit() { //--- 调用方法显示状态 if(GetStop) pstatus.GetMessage(); else SafePrintStatus(pstatus); //--- 如果EA被初始化成功则打印一条消息 Print(__FUNCTION__," OnInit() 函数完成"); //--- return(0); } //+------------------------------------------------------------------+ //| EA订单函数 | //+------------------------------------------------------------------+ void OnTick() { //--- } //+------------------------------------------------------------------+
如果使用不同实施的两个函数(通过传递引用和通过传递对象指针),这可确保重载函数的安全工作。
最终,我们学习了如何使用对象、作为参数传递至函数,现在是时候学习:
哪些情况下需要指针?
使用对象指针,您可以灵活管理对象的创建和销毁过程,并创建更多复杂的抽象对象。它使得程序更加灵活。
链表
但在某些情况下,我们需要一些不同的数据组织方式,链表正是其中之一。标准库中含有链表类 CList,在这里我们将使用我们自己的示例。链表意味着,每个列表项都与前后的列表项连接在一起,如果它们存在的话。要组织此类链接,使用列表项 (ListItem) 的对象指针极为方便。
让我们创建一个表示列表中项目的类,如下所示:
class CListItem { private: int m_ID; CListItem *m_next; CListItem *m_prev; public: ~CListItem(); void setID(int id){m_ID=id;} int getID(){return(m_ID);} void next(CListItem *item){m_next=item;} void prev(CListItem *item){m_prev=item;} CListItem* next(){return(m_next);} CListItem* prev(){return(m_prev);} };
列表本身将在一个单独的类中组织:
//+------------------------------------------------------------------+ //| 链表 | //+------------------------------------------------------------------+ class CList { private: int m_counter; CListItem *m_first; public: CList(){m_counter=0;} ~CList(); void addItem(CListItem *item); int size(){return(m_counter);} };
CList 类包含第一个列表项的指针first,而对其他列表项的访问始终可通过 CListItem() 类的 next () 和 prev() 函数实现。CList 类有两个有趣的功能。第一个是添加新项至列表的功能。
//+------------------------------------------------------------------+ //| 把一个项目添加到链表中 | //+------------------------------------------------------------------+ CList::addItem(CListItem *item) { //--- 检查指针,它应该是正确的 if(CheckPointer(item)==POINTER_INVALID) return; //--- 增加链表项目数量 m_counter++; //--- 如果链表中没有任何项目 if(CheckPointer(m_first)!=POINTER_DYNAMIC) { m_first=item; } else { //--- 将前一个项目的指针设为第一个 m_first.prev(item); //--- 保存当前第一个项目的指针 CListItem *p=m_first; //--- 把新增项目作为第一个项目 m_first=item; //--- 把原来第一个项目作为新增项目的后一个项目 m_first.next(p); } }
每个添加到列表的项成为第一项,原来的第一项的指针存储在next 字段中。因此,最早添加到列表的项将位于列表的末尾。最后添加的项将成为列表的第一项,并且其指针存储在 first 变量中。另一个有趣的功能是~CList()析构函数。该析构函数在对象销毁时调用,这将确保正确销毁列表对象。它的实现极为简单:
//+------------------------------------------------------------------+ //| 列表析构函数 | //+------------------------------------------------------------------+ CList::~CList(void) { int ID=first.getID(); if(CheckPointer(first)==POINTER_DYNAMIC) delete(first); Print(__FUNCTION__," 第一个项目 ID =",ID," 被删除"); }
我们可以看到,如果 first 包含列表项的正确指针,只有第一个列表项会被移除。所有其他列表项将逐个移除,因为 CListItem() 类析构函数依次生成正确的对象取消初始化。
//+------------------------------------------------------------------+ //| 项目析构函数 | //+------------------------------------------------------------------+ CListItem::~CListItem(void) { if(CheckPointer(m_next)==POINTER_DYNAMIC) { delete(m_next); Print(__FUNCTION__," 移除项目 ID =",m_ID); } else Print(__FUNCTION__," 此项目的后一个项目没有定义ID=",m_ID); }
指针的正确性在析构函数的内部验证,如果析构函数指定,则具有指针 next 的对象将使用 delete() 运算符销毁。销毁前,第一个列表项调用析构函数,它将删除第二项,接下来它将导致第三项的删除,依次进行直至到达链的末尾。
列表的工作在脚本 SampleList.mq5 中显示。列表(CListType 变量)在 OnStart() 函数中声明。该列表将自动创建和初始化。列表的填充在列表中执行,首先,系统使用 new 运算符动态创建每个列表项,然后将它们添加到列表中。
void OnStart() { //--- CList list; for(int i=0;i<7;i++) { CListItem *item=new CListItem; item.setID(i); list.addItem(item); } Print("列表中有 ",list.size()," 个项目"); }
启动脚本,您将在“EA 交易”日志中看到以下消息:
2010.03.18 11:22:05 SampleList (EURUSD, H1) CList::~ CList 第一个项目 ID=6 被删除
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 6
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 5
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 4
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 3
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 2
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 移除了一个项目 ID = 1
2010.03.18 11:22:05 SampleList (EURUSD, H1) CListItem::~ CListItem 该项目的下一个项目没有被定义 ID=0(~ CListItem 没有为 ID=0 的项定义下一项)
2010.03.18 11:22:05 SampleList (EURUSD, H1) 列表中 7 个项目(2010.03.18 11:22:05 SampleList (EURUSD, H1) 列表中包含 7 个列表项)
如果您仔细研读代码,您不会对 ID=0 的项没有后续项感到奇怪。然而,我们仅考虑了一个原因,即指针的使用令程序的编写较为便利和使程序具有较好的可读性。还有另一个原因,称之为
多态
我们常常需要为同一类型的不同对象实施相同的功能性。例如,有一些简单的对象,如直线、三角形、矩形和圆。虽然看上去形态各异,但它们有一个共同特征-可以绘制。我们创建一个基类 CShape,然后针对每一几何形状类型,将该基类用于其后代的创建。
出于教学目的,基类及其后代仅具有最低限度的功能性。
//+------------------------------------------------------------------+ //| 形状对象的基类 | //+------------------------------------------------------------------+ class CShape { private: int m_type; public: CShape(){m_type=0;} void Draw(); string getTypeName(){return("Shape");} }; //+------------------------------------------------------------------+ //| 线性对象的类 | //+------------------------------------------------------------------+ class CLine:public CShape { private: int m_type; public: CLine(){m_type=1;} void Draw(); string getTypeName(){return("Line");} }; //+------------------------------------------------------------------+ //| 三角形对象的类 | //+------------------------------------------------------------------+ class CTriangle:public CShape { private: int m_type; public: CTriangle(){m_type=2;} void Draw(); string getTypeName(){return("Triangle");} }; //+------------------------------------------------------------------+ //| 长方形对象的类 | //+------------------------------------------------------------------+ class CRectangle:public CShape { private: int m_type; public: CRectangle(){m_type=3;} void Draw(); string getTypeName(){return("Rectangle");} }; //+------------------------------------------------------------------+ //| 圆形对象的类 | //+------------------------------------------------------------------+ class CCircle:public CShape { private: int m_type; public: CCircle(){m_type=4;} void Draw(); string getTypeName(){return("Circle");} };
父类 CShape 包含两个在其子类中被覆盖的函数 – Draw() 和 getTypeName()。Draw() 函数用于绘制形状,getTypeName() 函数返回形状的字符串说明。
我们创建一个包含基类 CShape 的指针的数组 *shapes[],然后为不同的类指定指针的值。
//+------------------------------------------------------------------+ //| 脚本程序起始函数 | //+------------------------------------------------------------------+ void OnStart() { //--- CShape 类型对象指针的数组 CShape *shapes[]; //--- 修改数组大小 ArrayResize(shapes,5); //--- 填充指针数组 shapes[0]=new CShape; shapes[1]=new CLine; shapes[2]=new CTriangle; shapes[3]=new CRectangle; shapes[4]=new CCircle; //--- 打印数组元素类型 for(int i=0;i<5;i++) { Print(i,shapes[i].getTypeName()); } //--- 删除数组中的所有对象 for(int i=0;i<5;i++) delete(shapes[i]); }
在 for() 循环内,我们为 *shapes[] 数组的每个元素调用 getTypeName () 方法。第一次您可能会对基类的 getTypeName() 函数为列表中的每个对象调用的事实感到惊讶,尽管事实上每个派生类具有自己的 getTypeName() 函数实施。
2010.03.18 14:06:18 DemoPolymorphism (EURUSD, H1) 4 Shape
2010.03.18 14:06:18 DemoPolymorphism (EURUSD, H1) 3 Shape
2010.03.18 14:06:18 DemoPolymorphism (EURUSD, H1) 2 Shape
2010.03.18 14:06:18 DemoPolymorphism (EURUSD, H1) 1 Shape
2010.03.18 14:06:18 DemoPolymorphism (EURUSD, H1) 0 Shape
对于该事实的解释如下:*shapes [] 数组作为 CShape 类型的指针数组进行声明,因此每个数组对象调用基类的 getTypeName() 方法,即使后代具有不同的实施。要在程序执行时调用与实际对象类型(后代)一致的 getTypeName() 函数,必须将该函数在基类中作为虚拟函数进行声明。
让我们在父类 CShape 的声明中添加虚拟关键字到 getTypeName() 函数。
class CShape ( private: int m_type; public: CShape () (m_type = 0;) void Draw (); virtual string getTypeName () (return ("Shape");) )
然后再次启动脚本。现在结果和预期一致:
2010.03.18 15:01:11 DemoPolymorphism (EURUSD, H1) 4 Circle
2010.03.18 15:01:11 DemoPolymorphism (EURUSD, H1) 3 Rectangle
2010.03.18 15:01:11 DemoPolymorphism (EURUSD, H1) 2 Triangle
2010.03.18 15:01:11 DemoPolymorphism (EURUSD, H1) 1 Line
2010.03.18 15:01:11 DemoPolymorphism (EURUSD, H1) 0 Shape
因此,通过在基类中声明虚拟函数,我们可以在程序执行过程中调用后代的相同函数。现在,我们可以为每个派生类实施完全功能的 Draw() 函数。
该函数的执行示例请参见随附的 DrawManyObjects.mq5 脚本,其结果是在图表上显示一个随机形状。
总结
接下来又到了我们总结的时间。在 MQL5 中,对象的创建和销毁均自动执行,因此,只有在有实际需要时才使用指针,并且您应知道如何使用指针。
然而,如果指针非用不可,确保在使用指针前使用 CheckPointer() 检查其正确性 – 它正是针对这些情形特地而设。
最后一件事:在 MQL5 中,指针并非是在 C++ 中使用的实际内存指针,因此不要将它们作为输入参数传递给 DLL。
本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/36
(3.21 KB)
(10.4 KB)
(2.34 KB)
(2.05 KB)
(2.08 KB)
(3.75 KB)
(3.14 KB)
MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。