• oc语言学习之基础知识点介绍(五):OC进阶


    一、点语法介绍

    /*
     以前封装后,要给属性赋值,必须调用方法
     这样做,有两个缺点:
                    1.代码量多,调用方法要写的东西多。
                    2.看起来并不像是给属性赋值,也不像取值。
    
     我们用点语法就可以更好的解决!
     
        点语法的语法:
                    对象.属性;
                注意:这里的属性不需要加_
     
                    对象.属性 = 值;
     
        点语法的本质:
                其实就是调用getter或者setter方法。
    
     点语法注意事项:
            1.在类的方法里面如果用self.age 这个是调用方法,self->_age 是直接调用成员变量。
            2.千万记得不要在setter或者getter方法里面用点语法,否则,死循环。
            3.用点语法之前必须保证你的setter和getter方法的方法名符合命名的规范。
     提示:点语法,也可以称之为 语法糖。
            本质可以理解为是编译器的特性。
            因为其他面向对象的语言都是用.来访问属性。
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *p = [Person new];
    //        [p setName:@"刘德华"];
            p.name = @"张学友"; //这一句就相当于上一句,编译后也是编译成 [p setName:@"张学友"];
    //        [p setAge:16];
            p.age = 16; //这一句就相当于上一句
    //        [p setWeight:100];
            p.weight = 100; //这一句就相当于上一句
    //        NSLog(@"名字=%@ 年龄:%d",[p name],[p age]);
             NSLog(@"名字=%@ 年龄:%d",p.name,p.age);
        }
        return 0;
    }

    二、@property和synthesize关键字

    /*
     @property
            作用:帮我们生成setter和getter方法的声明。
            语法:
                @property  类型  属性名;
                注意:属性名不加_
    如果只用@property关键字的话,在实现类中,setter和getter方法需要自己实现。
    
    既然有关键字可以生成setter和getter方法的声明,肯定就有关键字可以生成setter和getter方法的实现。
     @synthesize:
            作用:自动生成getter和setter方法的实现
    
            语法:
                @synthesize 特性名(属性名)
    
        @synthesize做了多少事:
                    1.生成getter和setter方法的实现
     
                    2.还会生成一个私有的成员变量,成员变量名字就是特性名
     
                    3.实现的方法里是给自己生成的那个私有成员变量赋值
     
                    4.如果想指定给某个成员变量赋值,就在特性名后面加一个 =  成员变量名;
                            例:@synthesize name = _name;
        
                    5.如果你想过滤,就自己重写
     
        批量生成方法的实现:
                    语法:
                        @synthesize  特性名1,特性名2………………特性名n
     
        批量生成方法的声明:
                    语法:@property  类型  特性名1,特性名2………………特性名n
                    例:@property NSString *name,*nickName;
     
                注意:批量生成时,类型必须一致
    */
    #import <Foundation/Foundation.h>
    @interface Person : NSObject{
        NSString *_name;
        int _age;
    }
    
    //这样就会给_name封装getter和setter的声明
    @property NSString *name;
    /*
      -(void)setName:(NSString *)name;
      -(NSString *)name;
    */
    //这样就会给_age封装getter和setter的声明
    @property int age;
    /*
     -(void)setAge:(int)age;
     -(int)age;
    */
    -(void)sayHi;
    @end
    
    //
    //  Person.m
    //  OC第五天
    //
    //  Created by iOS001 on 15/11/5.
    //
    //
    
    #import "Person.h"
    
    @implementation Person
    
    //@synthesize name;//自动生成了_name的getter和setter的实现
    //
    //@synthesize age;//自动了_age的getter和setter的实现
    
    //@synthesize name = _name;//自动生成了_name的getter和setter的实现
    //
    //@synthesize age = _age;//自动了_age的getter和setter的实现
    
    @synthesize name = _name,age=_age; //批量操作。
    
    -(void)setAge:(int)age{
        if (age < 0 || age > 120) {
            _age = 18;
        }else{
            _age = age;
        }
    }
    
    /*
    -(void)setName:(NSString *)name{
        _name = name;
    }
    
    
    -(NSString *)name{
        return _name;
    }
    
    -(void)setAge:(int)age{
        _age = age;
    }
    
    -(int)age{
        return _age;
    }
    */
    @end

    上面的语法是在Xcode 4.4以前的用法,在现在的新版XCode中,可以不这么复杂。

    /*
     @property增强使用:
                用法跟之前的@property用法,完全一样
                但是,它除了能生成声明以外,还能声明实现
                而且,它还能帮我们生成私有的成员变量!
        做的事:1.声明属性的setter和getter方法的声明
                        2.实现属性的setter和getter方法
                        3.生成一个以特性名加下划线的私有成员变量
    
        注意:如果类的属性中已经存在同名的带下划线的成员变量,那么不会帮我们在生成了,操作的就是这个带下划线的成员变量。
     @property封装对象类型
     @property不能封装C语言数组
    
    */
    
    #import <Foundation/Foundation.h>
    @interface Person : NSObject{
        NSString *_name;
    }
    @property NSString *name;
    
    @end
    
    #import "Person.h"
    
    @implementation Person
    
    @end
    
    
    /*
     1.@property帮我们生成的实现,是没有任何逻辑过滤,如果我们要过滤,就重写。
     
     2.如果getter方法和setter方法都有重写的话,那么@property不会帮我们生成私有成员变量了。
        如果非要两个都重写的话,就自己定义一个成员变量。
    */
    
    #import <Foundation/Foundation.h>
    @interface Person : NSObject{
        int _age;
    }
    
    @property NSString *name;
    
    @property int age;
    @end
    
    #import "Person.h"
    @implementation Person
    -(void)setAge:(int)age{
        if(age<0 ||age>150){
            _age = 16;
        }else{ 
            _age = age;
        }
    }
    
    -(int)age{
        return _age;
    }
    @end
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            Person *p = [Person new];
            
            p.name = @"草稚京";
            
            p.age = -30;
            
            NSLog(@"p.name=%@    p.age=%d",p.name,p.age);
            
        }
        return 0;
    }

    三、动态类型和静态类型的概念

    /*
     动态类型:
        简单来说就是编译的时候,并不知道具体是什么类型的对象,直到运行的时候才知道是什么类型的对象
                所以,运行的时候确定了对象的类型,然后才会去找到对应的类型里看有没有这个方法,如果有这个方法就执行,没有这个方法就报错
    
     静态类型:
        就是编译的时候就知道是什么类型了
            比如说 int  char  float double
            int nums[10];  一旦定义就不可能再调用char *数组,或者改成char *数组
    
    动态类型的好处:
                        1.灵活
                        2.为了实现多态
    */
    //比如说我现在有3个类,Person、Student、Teacher,三个类中都有SayHi方法。Student和Teacher是继承并重写了Person类中的SayHi方法。
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Student.h"
    #import "Teacher.h"
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            /*
            Person *p = [Person new]; //动态类型
            
    //      [p study];//因为编译阶段,它并不知道你是什么对象,所以不可能让你调用这个方法
            [p performSelector:@selector(study)];//因为这是一个运行时才绑定的方法
            */
            /*
            printf("请输入序号--0--Person  1--Student  2--Teacher
    ");
            int num;
            scanf("%d",&num);
            Person *p;
            switch (num) {
                case 0:
                    p = [Person new];
                    break;
                case 1:
                    p = [Student new];
                    break;
                    
                case 2:
                    p = [Teacher new];
                    break;
                default:
                    p = [Person new];
                    break;
            }
            [p sayHi];//现在编译器知道是哪个对象的sayHi吗???只能运行时才知道??
            */
        }
        return 0;
    }

    四、id类型的使用

    /*
     动态类型:
            在编译的时候并不知道具体是什么类型的对象,在运行的时候知道是什么类型的对象。
     NSObject:根类。所有的人直接或者间接的继承自NSObject。
    
     两个检查:
            编译期检查:
                    因为OC是动态类型语言,那么也就是说类型只有在运行的时候才会被确定,所以在编译的时候没法检查真正指向的对象是类型,所以更加没法知道你指向的对象类型是否包含某个方法
                    检查范围:1.去检查它的指针类型里面是否有这个成员(属性、方法),如果有就让你编译通过,如果没有
                            2.就去父类的父类找,如果有,就编译通过,如果没有,再继续找
                            3.直到找到NSObject都还没有的话,就报错。
                一句话概括:就是只会去赋值号左边的类型里找。
     
            运行期检查:
                    就是运行的时候类型就确定了,所以就可以检查真正的那个对象里面包不包含这个成员了。
                    1.先去真正的对象类型里面找,如果找到就执行,如果没找到就继续往父类找。
                    2.去父类找,找到就执行,没找到就继续往父类的父类找,找到就执行,没找到继续往上找。
                    3.直到找到NSObject还没有,那么就程序运行的时候崩掉(闪退)。
     
     我希望有一种类型的指针可以指向我任意的对象,并且,我用中括号的方式来调用方法,又不会经过编译期检查(编译时不会报错),有没有呢?
    
     id类型:
        其实就是一个万能指针,所以任何对象都可以指向。
        非常方便的体现什么叫动态类型。
        任意对象都可以指向!
    
     好处:灵活,任意类型都可以指向,哪怕上一步指向Student对象,下一步可以立即再指向NSString
    
     缺点:因为编译器没有检查类型有没有方法,所以如果直接调用可能会报错
     
     id是万能指针,任意类型的对象都能指向.
     
     然后用id类型的变量可以调用方法,但是编译器不一定会报错,只能在运行时如果没有才报错。
    
     id需要注意的地方:
                    1.用id类型指向的对象,不能用点语法
                    2.那能不能直接调用方法来赋值呢?可以!能不能用方法取值?也可以!
     id的作用: 
            1.指向任意类型
            2.当做参数传递
                注意:虽然任意类型的对象都可以传,但是运行时可能会报错
            3.当做返回值返回
                 注意:任意类型都可以接收,那么很可能运行时会出问题
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Student.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            /*
            Person *p = [Student new]; //动态类型,只有运行的时候才直到指向的是Student对象
            
            //[p sayHi];//编译器的时候也是去Person找,只不过正好这时候Person有
            
            [p study]; //此时会报错,因为这时候是编译期检查,像这种中括号调用方法,都是编译期检查
            */
            
            /*
            id p = [Student new];
            
            [p study]; //有没有经历运行期检查??肯定有!
            
            [p sayHi];
            
            NSLog(@"%@",p);
            
            p = @"哈哈哈哈哈哈";
            
            NSLog(@"%@",p);
             
             p = [Person new];
            */
            
            id p = [Person new];
            
            [p study];//运行的时候才知道是Person类型,但是Person及其父类都没有study方法,所以运行报错
            
            /*
            [p performSelector:@selector(test)]; //编译的时候没法检查
            
            //[p sayHi];//而是先在真正指向的对象——student里面先找,找到了就执行
            
    //        NSLog(@"%@",p);
    //        
    //        [p study]; //Xcode编译的时候只会根据你的指针类型来检查,而并不会根据你具体指向的对象类型来检查
    //        
    //        NSObject *obj = [Student new];
    //        
    //        NSLog(@"%@",obj);
    //        
            */
        }
        return 0;
    }

    五、动态类型和动态方法的检测以及响应方法的方法

       1、态类型的检测

    /*
     
     动态类型检测:我们自己写代码来检测对象是否属于一个类。
      
     因为我们之前用id类型是可以调用任意方法的,编译器不会报错,但是运行时报错。
     
     所以我们就在想,能不能在调用方法之前,我们自己写代码判断一下id类型指向的对象是不是某一个类,如果是我才让你调用。
      
     动态类型检测的方法:
        1)
                语法:[对象  isKindOfClass:Class];
            注意:这个方法是用来检测一个对象是不是右边这个类的对象,或者是这个类子类的对象,如果是则返回YES,如果不是返回NO
                例: [p  isKindOfClass:[Person class] ];
                解释:判断p这个对象是不是Person类的对象,或者是Person子类的对象,如果是返回YES,如果不是返回NO。
            注意:包括子类的子类,子类的子类的子类…………………………
     
        2)
            [对象  isMemberOfClass:Class];
            判断 对象 是不是某个类的对象,不包括子类。
            跟上面的区别只在于,判断的时候不包括子类。
        总结:isKindOfClass不能确切的确定是哪一个具体的类的对象(因为还能包括子类)。
             isMemberOfClass 可以确切的知道是不是某一个类(因为不包括子类)。
     
        3)
            注意,接下来是类方法
            [类名  isSubOfClass:Class];
            判断某一个类,是否是另外一个类的子类,包括子类的子类,以及自己类。
            也就是说一个类如果是右边这个的类的本类,或者子类或者子类的子类都会返回YES,否则返回NO。
    */
    
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Student.h"
    #import "GoodStudent.h"
    
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            /*
            id obj = [Person new];
            
            //我们能不能在调用这个方法之前,自己写代码判断一下,obj是不是Student类型,如果是,我就让你调用,不是,就
            //[obj study];//编译不报错,运行报错
            
            if([obj isKindOfClass:[Student class]]){//判断obj是不是student类型
                
                //如果是就执行
                [obj study];
                
            }else{
                
                NSLog(@"obj不是Student类型");
            }
            */
            
            //isKindOfClass的用法
            /*
    //        id obj = [Person new];
            
    //        BOOL res = [obj isKindOfClass:[Student class] ];
    //        
    //        NSLog(@"%d",res); //0
            
    //        BOOL res = [obj isKindOfClass:[Person class] ];
    //        
    //        NSLog(@"%d",res); //1
            
    //        BOOL res = [obj isKindOfClass:[NSObject class] ];
    //        
    //        NSLog(@"%d",res);//1
            
            id obj = [GoodStudent new];
            
            BOOL res = [obj isKindOfClass:[Person class]];
            
            NSLog(@"%d",res);//也是1
            */
            
            //isMemberOfClass的用法
            /*
            id obj = [GoodStudent new];
            
            BOOL res1 = [obj isKindOfClass:[Person class] ];//这种方法包括子类
            
            
            
            BOOL res2 = [obj isMemberOfClass:[Person class] ];//这种方法不包括子类
            
            NSLog(@"res1=%d   res2=%d",res1,res2);//1   0
            */
            
            //isSubOfClass的用法(类方法)
            /*
    //       BOOL res  = [Student isSubclassOfClass:[Person class]];
    //        
    //        NSLog(@"%d",res);//1
            
    //        BOOL res  = [GoodStudent isSubclassOfClass:[Person class]];
    //        
    //        NSLog(@"%d",res);//1
            
    //        BOOL res  = [Person isSubclassOfClass:[Person class]];
    //        
    //        NSLog(@"%d",res);//1
            
            //这一句是问Person是Student子类吗?
            BOOL res  = [Person isSubclassOfClass:[Student class]];
            
            NSLog(@"%d",res);//0
          */
        }
        return 0;
    }

     2、动态方法的检测

    /*
     因为之前用id类型调用方法,可能会出错,所以我们想先判断一下这个类型是不是拥有某个方法。
     之前的做法是,判断一下某个对象是不是某个类的。那能不能具体一点,直接判断一下某个对象有没有某个方法呢?
     动态方法检测!
        1.(重点)
            [对象 respondsToSelector:SEL];
        例:[obj respondsToSelector:@selector(study)]
            这是在判断obj有没有study这个方法,如果有返回YES,如果没有返回NO
        注意:如果传入的是类方法,那么会返回0,因为类方法不属于对象
        2.类方法(用的少)
            [类名 instancesRespondToSelector:SEL];//判断类方法的
            判断某个类是否有某个对象方法,如果有则返回YES,否则返回NO
            跟上面效果一样,只不过这是一个类方法用类名判断
     
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Student.h"
    #import "GoodStudent.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            /*
            id obj = [Student new];
        
            //[obj study];//是不是要在这之前判断一下obj指向的对象有没有study这个方法,如果有才调用
            if( [obj respondsToSelector:@selector(study)] ){
                [obj study];
            }else{
                NSLog(@"没有study方法");
            }
            */

    //respondsToSelcetor的用法 /* id p = [Person new]; BOOL res = [p respondsToSelector:@selector(clsPerson)]; NSLog(@"%d",res);//0 */ // id obj = [Person new]; // BOOL res = [Person instancesRespondToSelector:@selector(sayHi)];//只能判断对象方法 // NSLog(@"%d",res); } return 0; }

    3、响应方法的方法

    /*
     
     响应方法就叫调用方法。
        以前我们用[]
     现在我们用响应方法的方法:
           [对象  performSelector:SEL];
                调用某个方法,是没有参数的时候
                例: [obj performSelector:@selector(sayHi)];
           [对象 performSelector:SEL withObject:id];
                调用某个方法,是有1个参数的情况
                例:[obj performSelector:@selector(sayName:) withObject:@"刘德华"];
           //最多只有2个参数
           [obj performSelector:(SEL) withObject:(id) withObject:(id)
                这是有两个参数的情况 
                例:[obj performSelector:@selector(sayName:andNickName:) 
                                                    withObject:@"发哥"
                                                    withObject:@"花卷"];
     
    */
    #import <Foundation/Foundation.h>
    #import "Person.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            id obj = [Person new];
            
            //这个是没参数的时候
    //        [obj performSelector:@selector(sayHi)];
    //        if([obj respondsToSelector:@selector(sayName:)]){
    //            //这个是有1个参数的时候
    //            [obj performSelector:@selector(sayName:) withObject:@"刘德华"];
    //        }else{
    //            NSLog(@"没有这个方法");
    //        }
            
            if([obj respondsToSelector:@selector(sayName:andNickName:)]){
                //这个是有1个参数的时候
                [obj performSelector:@selector(sayName:andNickName:)
                                                withObject:@"发哥"
                                                withObject:@"花卷"];
            }else{
                NSLog(@"没有这个方法");
            }
        }
        return 0;
    }

    六、构造和重写

    1、构造
    /*
    创建一个对象:
        [类名  new]; 
     new关键字做了三件事:
                1.开辟堆空间(相当于创建对象)
                2.初始化对象
                3.返回对象(返回堆空间首地址)
     
     其实new关键字它并不能开辟堆空间和初始化对象
     而是调用了两个方法,一个是alloc,一个是init
     alloc开辟堆空间(创建对象),然后会返回这个对象
     会调用这个alloc返回的对象的init方法,就是初始化对象的
     init方法又会返回这个对象(堆空间地址)
     构造方法是什么?
            构造方法其实就是初始化对象的方法,也就是说init就是我们的构造方法
     [Person new]   相当于 [ [Person alloc] init ];
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Person *p = [Person new]; //创建了一个person对象并且初始化
            //其实上面的语句类似于下面的步骤
    //        Person *p2 = [Person alloc];//创建对象
    //        Person *p3 = [p2 init];//初始化对象
            //上面两句可以合并到一句
            Person *p2 = [[Person alloc] init];
        }
        return 0;
    }
    2、重写
    /*
     init方法会帮我们初始化对象为0,对象类型初始化为nil。
     因为默认的只会帮我们初始化为0,我想以后创建对象,都是初始化为我指定的值。
     那么就需要重写init方法,因为init方法,因为父类是默认初始化为0,我重写就自己重新赋值就好了。
     重写init方法必须符合写法规范
            方法体要遵从以下规则:
                        if( self = [super init] ){
                            //做想初始化的事
                        }
                        return self;
    
        因为有些属性是父类里面提供初始化的,所以一定要加[super init],因为初始化的时候可能会未知原因出错,所以需要判断一下。
    
     instancetype是什么?
        之前可以用id作为返回值,但是不安全,因为我可以返回任意类型。
        比如说:我其实想返回一个Person对象,但是你给我返回一个OC字符串。。。。
        那么编译并不会报错,但是运行时可能出现严重的问题!
        所以,问题来了:有没有一种东西,可以让我返回类型,并且安全:意思就是你返回的对象一定是我这个类的。
    就要用instancetype
        instancetype还可以代表返回的是本类或者子类
        注意:instancetype只能做方法的返回值,不能做参数,也不能做类型来定义
     总结:instancetype主要是用来返回对象的,而且返回的是本类或子类的对象
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    #import "Student.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
           
            /*
            Person *p = [Person new];
            
            [p sayHi];//null 0
            */
            
    //        Person *p = [Person dedaoPersonInstance];
    //        
    //        [p sayHi];
    //        
    //        //这里得到的究竟是Person呢还是Student??
    //        Student *s = [Student dedaoPersonInstance];
    //        
            //Person *p = [[Person alloc] init];//因为init要返回一个对象,所以用instancetype,因为子类也可能要通过int方法返回对象
            Student *s = [[Student alloc] init];
            [s sayHi];
    
        }
        return 0;
    }
    
    //在Person类的实现类中
    +(instancetype)dedaoPersonInstance{
        Person *p = [self new]; //当然可以,谁调用就是谁,self就代表Person
        return p;
    //    return @"哈哈哈哈";
    }
    
    /**
     *  重写构造方法
     *  返回初始化的对象
     */
    -(instancetype)init{
        if(self = [super init]){
            //现在初始化为以下的值了
            _name = @"刘大同";
            _age = 16;
        }
        return self;
    }
    //在Student实现类中 #import "Student.h" @implementation Student -(instancetype)init{ if(self = [super init]){ _stuNo = 1; } return self; } -(void)sayHi{ NSLog(@"name=%@,age=%d,id=%d",_name,_age,_stuNo); } @end
    3、自定义构造方法
    /*
     之前重写构造方法,初始化的值都已经固定写死了,不灵活了
     如果我们想一个对象初始化的时候按照我给的值来初始化,怎么办?
     就要自定义构造方法,自定义的构造方法里面要给参数 
     步骤:
            1.是一个对象方法
            2.以initWith开头,后面接XXXXX
            3.有参数(几个参数,根据你想初始化几个值确定)
            4.有返回值,类型是instancetype
     
        注意:方法体里面也要遵从规范:
                    if(self = [super init]){
                        //初始化的语句
                    }
                    return self; 
    */
    
    #import <Foundation/Foundation.h>
    #import "Person.h"
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
    //        Person *p = [[Person alloc] init];
    //        
    //        [p sayHi];
    
            //一定要会用,以后也会经常用到
            Person *p = [[Person alloc] initWithName:@"孙悟空" andAge:99];
                    
            [p sayHi];
            
        }
        return 0;
    }
    
    #import "Person.h"
    
    @implementation Person
    -(instancetype)initWithName:(NSString *)name andAge:(int)age{    
        if(self = [super init]){
            _name = name;
            _age = age;
        }
        return self;    
    }
    
    +(instancetype)dedaoPersonInstance{
        
        Person *p = [self new]; //当然可以,谁调用就是谁,self就代表Person
        return p;
    //    return @"哈哈哈哈";
    }
    
    /**
     *  重写构造方法
     *  返回初始化的对象
     */
    -(instancetype)init{
        if(self = [super init]){
            //现在初始化为以下的值了
            _name = @"刘大同";
            _age = 16;
        }
        return self;
    }
    -(void)sayHi{
        NSLog(@"name=%@  age=%d",_name,_age);
    }
    @end
  • 相关阅读:
    预备作业03 20162311张之睿
    [LeetCode 题解]: String to Interger (atoi)
    [LeetCode 题解]: Add Two Numbers
    [LeetCode 题解]: Interger to Roman
    [LeetCode 题解]: Longest Substring Without Repeating Characters
    [LeetCode 题解]: Roman to Interger
    [LeetCode 题解]: palindromes
    [LeetCode 题解]: Two Sum
    [LeetCode 题解]: Maximum Subarray
    [LeetCode 题解]:Gas Station
  • 原文地址:https://www.cnblogs.com/bobo-pcb/p/5054851.html
Copyright © 2020-2023  润新知