Objective-C 2.0 Class类型,选择器Selector以及函数指针

Objective-C 2.0 Class类型,选择器Selector以及函数指针,第1张

概述5,Class类型,选择器Selector以及指针函数  本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里。  上一章笔者介绍了在Objective-C里面继承的概念。有了继承的知识我们可以重复的使用很多以前生效的代码,这样就大大的提高了代码开发的效率。在本章,笔者要向同学们介绍几个非常重要的概念,Class
5,Class类型,选择器Selector以及指针函数 

本系列讲座有着很强的前后相关性,如果你是第一次阅读本篇文章,为了更好的理解本章内容,笔者建议你最好从本系列讲座的第1章开始阅读,请点击这里。 

上一章笔者介绍了在Objective-C里面继承的概念。有了继承的知识我们可以重复的使用很多以前生效的代码,这样就大大的提高了代码开发的效率。在本章,笔者要向同学们介绍几个非常重要的概念,Class类型, 选择器Selector以及指针函数。 

我们在实际上的编程过程中,也许会遇到这样的场景,那就是我们在写程序的时候不能确切的知道我们需要使用什么类,使用这个类的什么方法。在这个时候,我们需要在我们的程序里面动态的根据用户的输入来创建我们在写程序不知道的类的对象,并且调用这个对象的实例方法。Objective-C为我们提供了Class类型, 选择器Selector以及指针函数来实现这样的需求,从而大大的提高了我们程序的动态性能。

在Objective-C里面,一个类被正确的编译过后,在这个编译成功的类里面,存在一个变量用于保存这个类的信息。我们可以通过一个普通的字符串取得这个Class,也可以通过我们生成的对象取得这个Class。Class被成功取得之后,我们可以把这个Class当作一个已经定义好的类来使用它。

Selector和Class比较类似,不同的地方是Selector用于表示方法。 在Objective-C的程序进行编译的时候,会根据方法的名字(包括参数列表)确定一个唯一的身份z明(实际上就是一个整数),不用的类里面的相同名字相同声明的方法的身份z明是一样的。这样在程序执行的时候,runtime就不用费力的进行方法的名字比较来确定是执行哪一个方法了,只是通过一个整数的寻找就可以马上定位到相应的方法,然后找到相应的方法的入口地址,这样方法就可以被执行了。

笔者在前面的章节里面叙述过,在Objective-C里面消息也就是方法的执行比C语言的直接找到函数入口地址执行的方式,从效率上来讲是比较低下的。尽管Objective-C使用了Selector等招数来提高寻找效率,但是无论如何寻找的过程,都是要消耗一定的时间的。好在Objective-C是完全兼容C的,它也有指针函数的概念。当我们需要执行效率的时候,比如说在一个很大的循环当中需要执行某个功能的时候,我们可以放弃向对某一个对象发送消息的手段,用指针函数取而代之,这样就可以获得和C语言一样的执行效率了。

说到这里,可能有的同学已经有些茫然了。这些概念有些令人难以理解,但是它们确实是Objective-C的核心的功能。掌握了这些核心的功能之后,同学们可以很轻松的看懂苹果的SDK里面的很多东西含义,甚至可以自己动手写一些苹果没有为我们提供的功能。所以建议大家仔细研读本章的内容,如果有什么问题,可以发个帖子大家可以共同探讨。

从笔者的观点上来看,对于有Java或者C++或者其他面向对象的语言的经验的同学来说,前面的从第1到第4章的内容也许有些平淡无奇。从第5章开始,我们将要逐渐的深入到Objective-C的核心部分。笔者的最终目的,虽然是向大家介绍iPhone开发的入门,但是笔者认为了解了Objective-C的基本概念以及使用方法之后,熟悉iPhone的应用程序的开发将是一件水到渠成的轻松的事情。否则如果你直接就深入到iPhone的开发的话,在绝大多数时间你也许因为一个小小的问题就会困扰你几个小时甚至几天,解决这些问题的唯一方法就是熟悉Objective-C和Cocoa Foundation的特性。

好了,说了很多我们从下面就要开始,我们的手法和前面几章是一样的,我们首先要介绍一下本章程序的执行结果。

5.1,本章程序的执行结果


 图5-1,第5章程序的执行结果

在本章里面,我们将要继续使用我们在前面几章已经构筑好的类cattle和Bull。为了灵活的使用cattle和Bull,我们将要构筑一个新的类,DoProxy。在DoProxy里面,我们将会引入几个我们的新朋友,他们分别是BOol,SEL,IMP,CLASS。通过这些新的朋友我们可以动态的通过设定文件取得cattle和Bull的类,还有方法以及方法指针。下面将要介绍如何构筑本章程序。同学们可以按照本章所述的步骤来构筑,也可以通过从这里下载。不过为了熟悉代码的写作,笔者强烈建议大家按照笔者所述的步骤来 *** 作。

5.2,实现步骤

第一步,按照我们在第2章所述的方法,新建一个项目,项目的名字叫做05-Hello Selector。如果你是第一次看本篇文章,请到这里参看第二章的内容。

第二步,按照我们在第4章的4.2节的第二,三,四步所述的方法,把在第4章已经使用过的“cattle.h”,“cattle.m”,“Bull.h”还有“Bull.m” 导入本章的项目里面。如果你没有第4章的代码,请到这里下载。如果你没有阅读第4章的内容,请参看这里。

第三步,把鼠标移动到项目浏览器上面的“Source”上面,然后在d出的菜单上面选择“Add”,然后在子菜单里面选择“New files”,然后在新建文件对话框的左侧选择“Cocoa touch Classes”,然后在右侧窗口选择“NSObject subclass”,选择“Next”,在“New file”对话框里面的“file name”栏内输入“DoProxy.m”。在这里笔者没有给出图例,在这里新建文件的步骤和第3章的第二步到第四步相同,只是文件名字不一样。第一次看到本篇 文章的同学可以参照第3章。

第四步,打开“DoProxy.h”做出如下修改并且保存

#import  < Foundation / Foundation.h >

#define  SET_SKIN_color @"setSkincolor:"
 BulL_CLASS @"Bull"  cattle_CLASS @"cattle"

@interface DoProxy : NSObject {
    BOol notFirstRun;
    ID cattle[
3 ];
    SEL say;
    SEL skin;
    
voID ( * setSkincolor_Func) (ID, SEL, Nsstring );
    IMP say_Func;
    Class bullClass;
}
-  ( ) doWithcattleID:(ID) acattle colorParam:(Nsstring ) color;
) setAlliVars;
) SELFuncs;
) functionPointers;
@end

第五步,打开“DoProxy.m”做出如下修改并且保存

#import  " DoProxy.h "
#import 
cattle.h Bull.h

@implementation DoProxy
) setAlliVars
{
    cattle[
0 =  [cattle  new ];
    
    bullClass 
 NSClassFromString(BulL_CLASS);
    cattle[
1  [bullClass  ];
    cattle[
2 ];
    
    say 
 @selector(saySomething);
    skin 
 NSSelectorFromString(SET_SKIN_color);
}
) SELFuncs
{
    [self doWithcattleID:cattle[
] colorParam: @" brown ];
    [self doWithcattleID:cattle[
red black ];
    [self doWithcattleID:self colorParam:
haha ];
}
) functionPointers
{
    setSkincolor_Func
)(ID,0)">)) [cattle[ ] methodForSelector:skin];
    
// IMP setSkincolor_Func = [cattle[1] methodForSelector:skin];     say_Func   [cattle[ ] methodForSelector:say];
    setSkincolor_Func(cattle[
],skin, verbose );
    NSLog(
Running as a function pointer will be more efficIEncy! );
    say_Func(cattle[
}
) color
{
    
if (notFirstRun  ==  NO)
    {
        Nsstring 
myname   NsstringFromSelector(_cmd);
        NSLog(
Running in the method of %@ , myname);
        notFirstRun 
 YES;
    }
    
    Nsstring 
cattleParamClassname   [acattle classname];
    
([cattleParamClassname isEqualToString:BulL_CLASS]  ||  
       [cattleParamClassname isEqualToString:cattle_CLASS])
    {
        [acattle setLegsCount:
4 ];
        
([acattle respondsToSelector:skin])
        {
            [acattle performSelector:skin withObject:color];
        }
        
else
        {
            NSLog(
Hi, I am a %@, have not setSkincolor!         }
        [acattle performSelector:say];
    }
    

    {
        Nsstring 
yourClassname   [acattle classname];
        NSLog(
    }
}
@end

第六步,打开“05-Hello Selector.m” 作出如下修改并且保存

int  main (  argc,  const   char  argv[]) {
    NSautoreleasePool 
 pool   [[NSautoreleasePool alloc] init];
    DoProxy 
doProxy   [DoProxy  ];
    
    [doProxy setAlliVars];
    [doProxy SELFuncs];
    [doProxy functionPointers];
    
    [pool drain];
    
return   ;
}

第七步,选择屏幕上方菜单里面的“Run”,然后选择“Console”,打开了Console对话框之后,选择对话框上部中央的“Build and Go”,如果不出什么意外的话,那么应该出现入图5-1所示的结果。如果出现了什么意外导致错误的话,那么请仔细检查一下你的代码。如果经过仔细检查发现 还是不能执行的话,可以到这里下载笔者为同学们准备的代码。 如果笔者的代码还是不能执行的话,请告知笔者。

5.3,BOol类型

我们现在打开“DoProxy.h”文件。“DoProxy.h”文件的第3行到第5行是三个预定义的三个字符串的宏。我们将在程序当中使用这3个宏,为了实现代码的独立性,在实际的程序开发当中,我们也许考虑使用一个配置的文本文件或者一个XML来替代这些宏。但是现在由于笔者的主要目的是讲解Objective-C的概念,为了避免较多的代码给大家带来理解主题的困难,所以笔者没有使用配置文件或者XML来表述这些可以设定的常量。

“DoProxy.h”的第7行对同学们来说也是老朋友了,是通知编译器,我们需要声明一个DoProxy类,从NSObject继承。

我们在第8行遇到了我们的一个新的朋友,BOol:

    BOol notFirstRun;

我们定义了一个notFirstRun的实例变量,这个变量是布尔类型的。我们的实例方法doWithcattleID需要被执行多次,我们在第一次执行doWithcattleID的时候需要向控制输出包含doWithcattleID的方法名字的字符串,关于这个字符串的内容,请参考图5-1。

好的,我们现在需要看看在Objective-C里面BOol是怎样定义的,我们把鼠标移动到BOol上面,然后单击鼠标右键选择d出菜单的“Jump to DeFinition”,然后Xcode会打开objc.h文件,我们看到下面的代码:

typedef signed               BOol;
 BOol is explicitly signed so @encode(BOol) == "c" rather than "C"
 even if -funsigned-char is used.  OBJC_BOol_defineD


 YES             (BOol)1  NO              (BOol)0

我们看到这段代码,我们可以这样理解,在Objective-C里面,BOol其实是signed char,YES是1,NO是0。我们可以这样给BOol赋值:

BOol x   YES;
BOol y 
 NO;

 

关于BOol,实际上就是一个开关的变量,但是我们需要注意下面2点:

第一点,从本质上来说BOol是一个8bit的一个char,所以我们在把其他比如说short或者int转换成为BOol的时候一定要注意。如果short或者int的最低的8位bit都是0的话,尽管除了最低的8位以外都不是0,那么经过转换之后,就变成了0也就是NO。比如说我们有一个int的值是0X1000,经过BOol转换之后就变成了NO。 

第二点,Objective-C里面的所有的逻辑判断例如if语句等等和C语言保持兼容,如果数值不是0判断为真,如果数值是0那么就判断为假,并不是说定义了BOol值之后就变成了只有1或者YES为真。所以下面的代码的判断都为真:

( 0X1000 )
- ) 5.4,SEL类型

让我们接着看“DoProxy.h”文件的下列代码:

    ID cattle[ ];
@H_401_502@2      SEL say;
@H_401_502@3      SEL skin;

其中ID cattle[3]定义了一个数组用于存储cattle或者Bull对象。这一行代码估计大家都很熟悉,笔者就不赘述了。像这样的传统的数组并不能完全满足我们的需求,当我们需要做诸如追加,删除等 *** 作的时候,会很不方便。在随后的章节里面笔者将要向大家介绍传统数组的替代解决方案NSArray。

 

上一段代码的第二行和第三行是本节所关注的,就是SEL类型。Objective-C在编译的时候,会根据方法的名字(包括参数序列),生成一个用 来区分这个方法的唯一的一个ID,这个ID就是SEL类型的。我们需要注意的是,只要方法的名字(包括参数序列)相同,那么它们的ID都是相同的。就是 说,不管是超类还是子类,不管是有没有超类和子类的关系,只要名字相同那么ID就是一样的。除了函数名字和ID,编译器当然还要把方法编译成为机器可以执 行的代码,这样,在一个编译好的类里面,就产生了如下图所示方法的表格示意图(本构造属于笔者推测,没有得到官方证实,所以图5-2为示意图仅供参考,我们可以暂时认为是这样的)。

 

图5-2,方法的表格示意图 

请注意setSkincolor后面有一个冒号,因为它是带参数的。由于存在这样的一个表格,所以在程序执行的时候,我们可以方便的通过方法的名字,获取到方法的ID也就是我们所说的SEL,反之亦然。具体的使用方法如下:

    SEL 变量名   @selector(方法名字);
 NSSelectorFromString(方法名字的字符串);
    Nsstring  变量名   NsstringFromSelector(SEL参数);

其中第1行是直接在程序里面写上方法的名字,第2行是写上方法名字的字符串,第3行是通过SEL变量获得方法的名字。我们得到了SEL变量之后,可以通过下面的调用来给一个对象发送消息:

[对象 performSelector:SEL变量 withObject:参数1 withObject:参数2];

这样的机制大大的增加了我们的程序的灵活性,我们可以通过给一个方法传递SEL参数,让这个方法动态的执行某一个方法;我们也可以通过配置文件指定需要执行的方法,程序读取配置文件之后把方法的字符串翻译成为SEL变量然后给相应的对象发送这个消息。

从效率的角度上来说,执行的时候不是通过方法名字而是方法ID也就是一个整数来查找方法,由于整数的查找和匹配比字符串要快得多,所以这样可以在某种程度上提高执行的效率。 

5.5,函数指针

在讲解函数指针之前,我们先参看一下图5-2,函数指针的数值实际上就是图5-2里面的地址,有人把这个地址成为函数的入口地址。在图5-2里面我们可以通过方法名字取得方法的ID,同样我们也可以通过方法ID也就是SEL取得函数指针,从而在程序里面直接获得方法的执行地址。或者函数指针的方法有2种,第一种是传统的C语言方式,请参看“DoProxy.h” 的下列代码片断:

     );
    IMP say_Func;

其中第1行我们定义了一个C语言里面的函数指针,关于C语言里面的函数指针的定义以及使用方法,请参考C语言的书籍和参考资料。在第一行当中,值得我们注意的是这个函数指针的参数序列:

第一个参数是ID类型的,就是消息的接受对象,在执行的时候这个ID实际上就是self,因为我们将要向某个对象发送消息。

第二个参数是SEL,也是方法的ID。有的时候在消息发送的时候,我们需要使用用_cmd来获取方法自己的SEL,也就是说,方法的定义体里面,我们可以通过访问_cmd得到这个方法自己的SEL。 

第三个参数是Nsstring*类型的,我们用它来传递skin color。在Objective-C的函数指针里面,只有第一个ID和第二个SEL是必需的,后面的参数有还是没有,如果有那么有多少个要取决于方法的声明。 

现在我们来介绍一下Objective-C里面取得函数指针的新的定义方法,IMP。

上面的代码的第一行比较复杂,令人难以理解,Objective-C为我们定义了一个新的数据类型就是在上面第二行代码里面出现的IMP。我们把鼠标移动到IMP上,单击右键之后就可以看到IMP的定义,IMP的定义如下:

typedef ID                      ( IMP)(ID, 

);

这个格式正好和我们在第一行代码里面的函数指针的定义是一样的。

我们取得了函数指针之后,也就意味着我们取得了执行的时候的这段方法的代码的入口,这样我们就可以像普通的C语言函数调用一样使用这个函数指针。当然我们可以把函数指针作为参数传递到其他的方法,或者实例变量里面,从而获得极大的动态性。我们获得了动态性,但是付出的代价就是编译器不知道我们要执行哪一个方法所以在编译的时候不会替我们找出错误,我们只有执行的时候才知道,我们写的函数指针是否是正确的。所以,在使用函数指针的时候要非常准确地把握能够出现的所有可能,并且做出预防。尤其是当你在写一个供他人调用的接口API的时候,这一点非常重要。

5.6,Class类型

到目前为止,我们已经知道了对应于方法的SEL数据类型,和SEL同样在Objective-C里面我们不仅仅可以使用对应于方法的SEL,对于类在Objective-C也为我们准备了类似的机制,Class类型。当一个类被正确的编译过后,在这个编译成功的类里面,存在一个变量用于保存这个类的信息。我们可以通过一个普通的字符串取得 这个Class,也可以通过我们生成的对象取得这个Class。Class被成功取得之后,我们可以把这个Class当作一个已经定义好的类来使用它。这样的机制允许我们在程序执行的过程当中,可以Class来得到对象的类,也可以在程序执行的阶段动态的生成一个在编译阶段无法确定的一个对象。

因为Class里面保存了一个类的所有信息,当然,我们也可以取得一个类的超类。关于Class类型,具体的使用格式如下:

    Class 变量名 
 [类或者对象  class  [类或者对象 superclass];
 NSClassFromString(方法名字的字符串);
@H_401_502@4   NsstringFromClass(Class参数);

第一行代码,是通过向一个类或者对象发送class消息来获得这个类或者对象的Class变量。

第二行代码,是通过向一个类或者对象发送superclass消息来获得这个类或者对象的超类的Class变量。

第三行代码,是通过调用NSClassFromString函数,并且把一个字符串作为参数来取得Class变量。这个在我们使用配置文件决定执行的时候的类的时候,NSClassFromString给我们带来了极大的方便。

第四行代码,是NSClassFromString的反向函数NsstringFromClass,通过一个Class类型作为变量取得一个类的名字。

当我们在程序里面通过使用上面的第一,二或者第三行代码成功的取得一个Class类型的变量,比如说我们把这个变量名字命名为myClass,那么我们在以后的代码种可以把myClass当作一个我们已经定义好的类来使用,当然我们可以把这个变量作为参数传递到其他的方法当中让其他的方法动态的生成我们需要的对象。

5.7,DoProxy.h里面的方法定义

DoProxy.h里面还有一些实例方法,关于方法的定义的格式,同学们可以参照第三章。我们现在要对DoProxy.h里面定义的方法的做一下简要的说明。

) functionPointers;

第一行的方法,是设定acattle,也就是cattle或者Bull对象的属性,然后调用saySomething方法,实现控制台的打印输出。

第二行的方法,是把我们定义的DoProxy类里面的一些变量进行赋值。

第三行的方法,是调用doWithcattleID方法。

第四行的方法,是调用了函数指针的方法。

好的,我们把DoProxy.h的内容介绍完了,让我们打开DoProxy.m。

5.8,DoProxy.m的代码说明

有了DoProxy.h的说明,同学们理解DoProxy.m将是一件非常轻松的事情,让我们坚持一下把这个轻松的事情搞定。由于篇幅所限,笔者在这里的讲解将会省略掉非本章的内容。

DoProxy.m代码如下:

 1  @H_401_502@ 2  @H_401_502@ 3  @H_401_502@ 4  @H_401_502@ 5  @implementation DoProxy
@H_401_502@ 6  ) setAlliVars
@H_401_502@ 7  {
@H_401_502@ 8      cattle[  9      
@H_401_502@10      bullClass   NSClassFromString(BulL_CLASS);
@H_401_502@11  @H_401_502@12  @H_401_502@13  @H_401_502@14      say   @selector(saySomething);
@H_401_502@15      skin   NSSelectorFromString(SET_SKIN_color);
@H_401_502@16  }
@H_401_502@17  ) SELFuncs
@H_401_502@18  @H_401_502@19      [self doWithcattleID:cattle[ 20  @H_401_502@21  @H_401_502@22      [self doWithcattleID:self colorParam: 23  @H_401_502@24  ) functionPointers
@H_401_502@25  @H_401_502@26      setSkincolor_Func ] methodForSelector:skin];
@H_401_502@27  @H_401_502@28  ] methodForSelector:say];
@H_401_502@29      setSkincolor_Func(cattle[ 30      NSLog( 31      say_Func(cattle[ @H_401_502@32  @H_401_502@33  ) color
@H_401_502@34  @H_401_502@35   NO)
@H_401_502@36      {
@H_401_502@37          Nsstring   NsstringFromSelector(_cmd);
@H_401_502@38          NSLog( @H_401_502@39          notFirstRun   YES;
@H_401_502@40      }
@H_401_502@41  @H_401_502@42   [acattle classname];
@H_401_502@43   
@H_401_502@44         [cattleParamClassname isEqualToString:cattle_CLASS])
@H_401_502@45  @H_401_502@46          [acattle setLegsCount: 47           ([acattle respondsToSelector:skin])
@H_401_502@48          {
@H_401_502@49              [acattle performSelector:skin withObject:color];
@H_401_502@50          }
@H_401_502@51  @H_401_502@52  @H_401_502@53              NSLog( @H_401_502@54  @H_401_502@55          [acattle performSelector:say];
@H_401_502@56  @H_401_502@57  @H_401_502@58  @H_401_502@59  @H_401_502@60  @H_401_502@61  @H_401_502@62  @H_401_502@63  @end

第10行代码是通过一个预定义的宏BulL_CLASS取得Bull的Class变量。

第11和12行代码是使用bullClass来初始化我们的cattle实例变量数组的第2和第3个元素。

第14行是通过@selector函数来取得saySomething的SEL变量。

第15行是通过向NSSelectorFromString传递预定义的宏SET_SKIN_color来取得setSkincolor的SEL变量。

第22行,笔者打算“戏弄”一下doWithcattleID,向传递了不合适的参数。

第26行,笔者取得了传统的C语言的函数指针,也是使用了第5.5节所述的第一种取得的方法。

第28行,笔者通过5.5节所述的第二种取得的方法得到了函数指针say_Func

第29行和31行分别执行了分别在第26行和28行取得的函数指针。

第35行是一个BOol型的实例变量notFirstRun 。当对象被初始化之后,确省的值是NO。第一次执行完毕之后,我们把这个变量设定成为YES,这样就保证了花括号里面的代码只被执行一次。

第37行我们通过_cmd取得了doWithcattleID这个方法名字用于输出。当然同学们在设计方法的提供给别人使用的时候,为了防止使用方法的人把这个方法本身传递进来造成死循环,需要使用_cmd这个系统隐藏的变量判断一下。笔者在这里没有做出判断,这样写从理论上来说存在一定的风险。

第42行,我们通过向对象发送classname消息来取得这个对象的类的名字。 

第43行和第44行,我们通过Nsstring的方法isEqualToString来判断取得的类的名字是否在我们事先想象的范围之内,我们只希望接受Bull或者cattle类的对象。

第46行,本来我们想通过SEL的方式来进行这个牛股的设定,但是由于它的参数不是从NSObject继承下来的,所以我们无法使用。我们会有办法解决这个问题的,我们将在后面的章节里面介绍解决这个问题的方法。

第47行的代码,有一个非常重要NSObject的方法respondsToSelector,通过向对象发送这个消息,加上一个SEL,我们可以知道这个对象是否可以相应这个SEL消息。由于我们的cattle无法相应setSkincolor消息,所以如果对象是cattle类生成的话,if语句就是NO所以花括号里面的内容不会得到执行。

第59行,我们通过类的名字发现了一个假冒的cattle,我们把这个假冒的家伙给揪出来,然后实现了屏幕打印。

5.9,本章总结

本章给同学们介绍了几个新的数据类型,以及使用方法,这些数据类型分别是BOol,SEL,Class,IMP。

本章的内容很重要,希望同学们花一点时间仔细的理解一下。应该说,本章的内容有些令人难以理解,或者说知道了SEL,Class,IMP之后也许不知道如何使用,遇到什么情况的时候需要使用。不过在学习Objective-C的初级阶段,不知道这些也没有关系,但是SEL,Class,IMP的概念需要掌握,否则当你遇到别人写的质量比较高的代码或者苹果官方的技术文档的时候,你会觉得理解起来比较吃力。

本章内容也是理解下一章内容的基础,下一章我们将要讲述NSObject的奥秘。

总结

以上是内存溢出为你收集整理的Objective-C 2.0 Class类型,选择器Selector以及函数指针全部内容,希望文章能够帮你解决Objective-C 2.0 Class类型,选择器Selector以及函数指针所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/1056468.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-25
下一篇 2022-05-25

发表评论

登录后才能评论

评论列表(0条)

保存