重学iOS——1.从C角度看OC

面向对象

面向对象的编程思维,简而言之就是:一切皆对象。面向对象的语言区别于C语言(面向过程),C语言是按照代码的执行顺序去实现项目功能,而高级编程语言是从对象的角度去把控,详细解释就是:任何事件都是由事物构成,事物拥有自己特有的属性(对象的固有属性和行为属性),大事件通过各种事物之间的相互作用(行为调用)实现。OC是从C语言的基础之上发展而来的,是C语言的面向对象。OC完全兼容C语言,可以实现OC与C的混合编程,一起生成可执行文件。

OC与C的部分不同点

参考代码:

#import <Foundation/Foundation.h>
#import <objc/objc.h>

/*
 *要求定义一个人类
 *事物名称:人
 *属性:年龄(age),身高(height),体重(weight)
 *行为:吃饭(eat),睡觉(sleep),散步(walk)
 */

@interface Person : NSObject
{
    @public
    //属性
    int _age;
    double _height;
    double _weight;
}
//行为
-(void)eat:(char *)food;

-(void)sleep;

-(void)walk;
+(void)about;
@end


@implementation Person

-(void)eat:(char *)food
{
    NSLog(@"吃%s",food);
}

-(void)walk
{
    NSLog(@"开始遛弯");
}

-(void)sleep
{
    NSLog(@"开始睡觉");
}

+(void)about
{
    NSLog("Hello");
}
@end


int main(int argc, const char * argv[]) {
    //1. 通过类创建对象
    /*
     *1.开辟存储空间
     *2.初始化所有属性
     *3.返回指针地址
     */

    //因为在堆栈中创建的对象,实质是一个结构体,所以可以通过结构体指针直接访问属性
    /*
     *创建对象的时候,返回的地址其实就是类的第0个属性的地址
     *结构体创建完成以后,结构体的地址其实就是结构体第0个成员的首地址
     *但是需要注意的是:类的第0个属性并不是我们编写的_age,而是一个叫做isa的属性
     *isa是一个指针,占8个字节
     *
     *其实类也是一个对象,也就意味着Person也是一个对象
     *平时我们所说的创建对象,就是通过一个类对象创建一个新的对象
     *类对象是系统自动帮我们创建的,里面保存了当前对象的所有方法,准确说是一个方法列表,包括方法体
     *而实际对象是程序自己员手动通过new来创建的,而实例对象中有一个isa指针就指向了创建它的那个类对象
     */
    Person *p=[Person new];
    p->_age=30;
    p->_height=1.75;
    p->_weight=65.0;
    [p walk];
    [Person about];
    NSLog(@"p=%p",p);
    NSLog(@"&age=%p",&(p->_age));
    /*
     *结构体创建完成以后,结构体的地址其实就是结构体第0个成员的首地址
     */
    //    struct Person
    //    {
    //        int age;
    //        char *name;
//    };
//    struct Person sp;
//    NSLog(@"&sp=%p",&sp);
//    NSLog(@"&age=%p",&sp.age);
    return 0;
}

1.#import和#include的区别
import的功能和include的功能是一样的,都是导入文件,但是OC为了避免C语言中的重复导入问题(避免使用复杂的头文件卫士)为import集成了避免重复导入的机制。下面介绍一下C语言中头文件卫士的写法:

#ifndef _TEST_H
#define _TEST_H//一般是文件名的大写
头文件结尾写上一行:
#endif

这样一个工程文件里同时包含两个test.h时,就不会出现重定义的错误了。
分析:当第一次包含test.h时,由于没有定义_TEST_H,条件为真,这样就会包含(执行)#ifndef _TEST_H和#endif之间的代码,当第二次包含test.h时前面一次已经定义了_TEST_H,条件为假,#ifndef _TEST_H和#endif之间的代码也就不会再次被包含,这样就避免了重定义了。
当然以上这些内容都是关于C语言-预处理的知识内容,具体细节参考:C语言再学习——C预处理器和头文件为什么要加#ifndef #define #endif

2.OC实现面向对象的原理
OC是对C语言进行了面向对象的封装,即通过C语言实现面向对象,那么是如何实现的?其实是结构体+指针的方式实现的。OC中的对象在内存中其实是以结构体的形式进行存储的。这一点的内容就涉及到了runtime运行时的内容。在这里由于目前还没有涉及到运行时内容,所以对本部分内容进行了适当的简化,同时也增进了后期运行时内容的进一步学习与理解。
OC中的对象是通过结构体来实现的,即在堆中对象的存储数据结构是struct,创建的对象变量,其实是一个指向相应类结构体的指针。由于对象的实际结构是结构体,那么我们可以通过"->”这样的方式去访问实例对象中的属性,代码如下:

IPhone *p=[IPhone new];
p->_color=0;//默认@interface中的属性都是@protected的,如果想这样子直接访问,需要使用@public
p->_model=0;

在实际的实例对象存储过程中,实例对象内部是不存在实例方法的,只包含属性信息和isa指针信息,isa指针指向该类的类对象,类对象是在程序编译阶段,系统自动创建的,包含了类的所有信息(属性列表、方法列表、类名等信息),是创建实例对象的依据(模板)。需要注意的是:结构体创建完成以后,结构体的地址其实就是结构体第0个成员的首地址,但是类对象的首地址是isa指针属性的存储值,isa是一个指针,占8位。具体在内存中的存在方式如下图所示:

对象创建过程.png

所以从上图中我们可以联想到,[Person new]方法,其实是调用了Person类对象的new方法。参考代码中的[p walk]和 [Person about]对比会发现,类方法的执行效率高于实例方法。图解如下:

[p walk]分析.png
[Person about]分析.png

两幅图的分析,故意将元类的概念略去,因为类方法实际是存储在元类中的,但是在此为了方便理解,同时也为后期进阶做准备。
上图中涉及到了堆、栈、代码区等内容,具体可以参考iOS中的内存管理C语言中内存分配linux内存管理深入浅出-iOS内存分配与分区

3.OC中的类方法和实例方法以及函数
OC和C中的函数的对比,其实很简单,在这里直接上代码,我觉得对于身为程序员的自己来说,可能会更加清晰,注意点:在Java中对象可以直接调用类方法,但是在OC中对象不能调用类方法,必须通过类调用类方法。静态方法、类方法和对象方法的区别

//
//  main.m
//  OC_day1_04_类方法
//
//  Created by 刘旭辉 on 2017/10/13.
//  Copyright © 2017年 刘旭辉. All rights reserved.
//

#import <Foundation/Foundation.h>

typedef enum
{
    KIColorBlace,//黑色
    KIColorWhite,//白色
    KIColorTuHaoJin//土豪金
}IColor;
//1.编写类的声明
@interface IPhone : NSObject
{
@public
    double _size;
//    int _color; //颜色 0代表黑色 1代表白色 2代表土豪金
    int _cpu;
    int _model;
    IColor _color;
}
/*
 *行为
 *OC中的行为和C语言中的函数一样,都是用来保存一段特定功能的代码
 *OC中定义一个方法,也分为声明和实现,声明在@interface中,实现写在@implementation
 *
 *C语言中的函数分为两种:内部函数和外部函数
 *OC中的函数也分为两种:类方法和对象方法
 *类方法只能用类名调用,对象方法只能用对象调用
 *OC中的类方法用+表示,OC中的对象方法用-表示
 *
 *编写C语言函数的规律:1.确定函数名称;2.确定形参;3.确定返回值;4.确定返回值类型
 *编写OC方法也有规律,规律和C语言一模一样
 */

/*C语言方法
 void about();
 void about()
 {
     printf("model=cpu=size=color");
 }
 */

//注意:OC中的方法,如果没有形参不需要写(),而是直接写一个;
//为什么OC中没有形参不需要写()呢?因为OC方法中的()有特殊用途,OC方法中的()是用来括住数据类型的
//行为的声明
-(void)about;

/*
 *C语言中的函数:1.没有返回值没有参数的函数;2.有返回值没有参数的;3.有返回值有参数的;4.没有返回值有参数的
 *OC中的方法也有四种
 */

//有返回值没有参数的。例如:读取短信
-(char *)loadMessage;

//有返回值有参数的,例如:打电话
//int signal(int number); C语言中的函数
//注意:OC中的方法如果有参数,那么每个参数的数据类型前面必须加一个:
//注意:当前的有参数的方法的方法名称是  signal: 冒号也是方法名称的一部分
-(int)signal:(int)number;

//有返回值,并且有多个参数的,发短信
//int sendMessage(int number,char* content);
//为了提高阅读性,OC方法允许我们给每一个参数添加一个标签来说明当前参数的含义
-(int)sendMessageWithNumber:(int)number andContent:(char*) content;
//-(int)sendMessage:(int)number :(char*) content; //当前的方法名称叫做:sendMessage::

//没有返回值,有参数的函数
-(void)callWithNumber:(int) number;


//计算器功能(类方法)
//如果你不想每次使用方法都需要创建对象并且开辟存储空间
//并且如果该方法中没有使用到属性(成员变量),那么你可以把这个方法定义为类方法
//对象方法用对象调用 类方法用类名调用
//-(int)sumWithNumber1:(int)number1 andNumber2:(int) number2;
//只需要将对象方法的-号转换成+,那么就定义了一个类方法
//注意:如果实现了一个类方法,就必须去实现类方法
//如果实现的是一个对象方法,那么就必须去实现对象方法




/*
 *类方法和对象方法的区别
 *0.对象方法以-开头;类方法以+开头
 *1.对象方法必须用对象调用,类方法必须用类调用
 *2.对象方法中可以直接访问属性(成员变量),类方法中不可以直接访问属性(成员变量)
 *3.类方法的优点,调用类方法的效率会比调用对象方法高
 *4.类方法和对象方法可以进行相互调用
 *4.1对象方法中可以直接调用类方法
 *4.2可以在类方法中间接调用对象方法
 *4.3在类方法中可以直接调用其他方法
 *4.4对象方法中可以直接调用对象方法
 */


/*
 *类方法的应用场景
 *如果方法中没有使用到属性(成员变量),那么能用类方法就用类方法
 *类方法的执行效率比对象方法高
 *
 *类方法一般用于定义工具方法
 *字符串查找
 *文件操作
 *数据库操作
 */
+(int)sumWithNumber1:(int)number1 andNumber2:(int) number2;
@end

//2.编写类的实现
@implementation IPhone
//行为的实现
-(void)about
{
    //    NSLog(@"打印本机信息");
    //如果在对象方法中想访问该对象的属性,可以直接写上_属性名称即可
    [IPhone sumWithNumber1:50 andNumber2:50];
    NSLog(@"size=%f,color=%i,model=%i,cpu=%i",_size,_color,_model,_cpu);
}

-(char *)loadMessage
{
      return "老婆我们家我做主";
 }

-(int)signal:(int)number
{
    NSLog(@"打电话给%i",number);
    return 1;
}

-(int)sendMessage:(int)number :(char *)content
{
    NSLog(@"发短信给%i,内容是%s",number,content);
    return 0;
}

-(int)sendMessageWithNumber:(int)number andContent:(char *)content
{
    NSLog(@"发短信给%i,内容是%s",number,content);
    return 0;
}
-(void)callWithNumber:(int)number
{
    NSLog(@"打电话给%i",number);
}

+(int)sumWithNumber1:(int)number1 andNumber2:(int)number2
{
    IPhone *p1=[IPhone new];
    [p1 about];//在类方法中,可以简介调用对象方法,但是在企业开发中,不建议这样子使用
    return number1+number2;
}

@end
int main(int argc, const char * argv[]) {
    //1.通过类创建对象
    IPhone *p=[IPhone new];
    p->_color=KIColorBlace;
    p->_model=4;
    p->_cpu=1;
    p->_size=3.5;
    //3.获取对象的属性
    //    NSLog(@"size=%f,color=%i,model=%i,cpu=%i",p->_size,p->_color,p->_model,p->_cpu);
    //4.如果给对象发消息(如果调用对象的方法)
    [p about];
    //注意:OC中的NSLog对C语言中的字符串支持不是很好,如果返回的是中文的C语言字符串可能输出是乱码,也可能什么都不输出
    NSLog(@"%s",[p loadMessage]);
    [IPhone sumWithNumber1:1 andNumber2:2];
    //    [p sendMessage:110 :"help,help"];
    [p sendMessageWithNumber:110 andContent:"help"];
    [p sendMessage:110 :"help,help"];
    [p signal:110];
    [p callWithNumber:119];
    return 0;
}

void test()
{
    //1.创建一个对象
    IPhone *p1=[IPhone new];
    //2.利用对象调用假发运算方法
//    [p1 sumWithNumber1:1 andNumber2:2];
}

4.OC成员变量、局部变量、全局变量的区别以及内存分析

#import <Foundation/Foundation.h>

@interface Person : NSObject
{
    //写在类声明的大括号中的变量,我们称之为成员变量(或者属性,实例变量)
    //注意:
    //1.成员变量不能离开类,离开类以后就不是成员变量;
    //2.成员变量不能在定义的同时进行初始化;
    //3.成员变量只能通过对象来访问
    //存储在:堆(当前对应的堆的内存空间中)
    //存储在堆中的数据,不会被自动释放,只能程序员手动释放
    int age;
}

@end

@implementation Person



@end
//写在函数和大括号外部的变量,我们称之为全局变量
//作用域:从定义的那一行开始,一直到文件末尾
//全局变量可以先定义再初始化,也可以定义的同时初始化
//存储在:静态区
//程序一启动就会分配内存空间,知道程序结束才会释放
int a;
int b=10;
int main(int argc, const char * argv[]) {
    //写在函数或者代码块中的变量,我们称之为局部变量
    //作用域:从定义的哪一行开始,一直到遇到大括号或者return
    //局部变量可以先定义再初始化,也可以定义的同时初始化
    //存储在栈中
    //存储在栈中的数据有一个特点,系统会自动给我们释放
    int num=10;
    return 0;
}
#import <Foundation/Foundation.h>

@interface Person : NSObject
{
    //写在类声明的大括号中的变量,我们称之为成员变量(或者属性,实例变量)
    //注意:
    //1.成员变量不能离开类,离开类以后就不是成员变量;
    //2.成员变量不能在定义的同时进行初始化;
    //3.成员变量只能通过对象来访问
    //存储在:堆(当前对应的堆的内存空间中)
    //存储在堆中的数据,不会被自动释放,只能程序员手动释放
    int age;
}

@end

@implementation Person



@end
//写在函数和大括号外部的变量,我们称之为全局变量
//作用域:从定义的那一行开始,一直到文件末尾
//全局变量可以先定义再初始化,也可以定义的同时初始化
//存储在:静态区
//程序一启动就会分配内存空间,知道程序结束才会释放
int a;
int b=10;
int main(int argc, const char * argv[]) {
    //写在函数或者代码块中的变量,我们称之为局部变量
    //作用域:从定义的哪一行开始,一直到遇到大括号或者return
    //局部变量可以先定义再初始化,也可以定义的同时初始化
    //存储在栈中
    //存储在栈中的数据有一个特点,系统会自动给我们释放
    int num=10;
    return 0;
}

在以上的代码中已经对成员变量全局变量局部变量做了详细的区分,但是在内存中具体是如何存储的?还没有一个明确的概念。通过阅读一些博客,对物理存储有了一定的了解,分析如下:

  • RAM和ROM区分
    我们所说的内存,其实指的就是RAM,在程序运行期间系统将ROM中的源代码加载到RAM中,我们所说的栈、堆、全局区/静态区、常量区、代码区其实都是位于RAM中。

    RAM:运行内存,不能掉电存储。
    ROM:存储性内存,可以掉电存储,例如内存卡、Flash。
    由于RAM类型不具备掉电存储能力(即一掉电数据消失),所以app程序一般存放于ROM中。RAM的访问速度要远高于ROM,价格也要高。App程序启动,系统会把开启的那个App程序从Flash或ROM里面拷贝到内存(RAM),然后从内存里面执行代码。另一个原因是CPU不能直接从内存卡里面读取指令(需要Flash驱动等等)。

  • RAM中五大分区详解

  1. 栈区(stack):
    存放的局部变量、先进后出、一旦出了作用域就会被销毁;函数跳转地址,现场保护等;
    程序猿不需要管理栈区变量的内存。主要负责函数模块内申请,函数结束时自动释放,存放局部变量,函数变量
    ~栈区地址从高到低分配;
  2. 堆区(heap):
    通过malloc函数或new函数等操作符操作的得到,需要程序员自己管理
    ARC的内存的管理,是编译器再便宜的时候自动添加 retain、release、autorelease;
    ~堆区的地址是从低到高分配
  3. 全局区/静态区(static):
    包括两个部分:未初始化过(.bss段区域) 、初始化过(data段区域),即全局区/静态区在内存中是放在一起的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。
  4. 常量区:存放常量字符串;
  5. 代码区: 存放编写的源代码,由系统自动加载进入;
  6. 注意:也有人将全局区/静态区(static)和常量区联合在一起成为数据区。

具体图示如下图所示:

内存分区图解

5.函数与方法的区别
首先需要理解的一点是:方法是对象的行为,函数是整个文件的一个功能块。方法只能存在于类内部,函数可以存在于整个文件的任何地方。具体代码如下:

#import <Foundation/Foundation.h>


/*
 *函数和方法的区别
 *1.函数属于整个文件,方法属于某一个类,方法如果离开类就不行
 *2.函数可以直接调用,方法必须用对象或者类来调用
 *注意:虽然函数属于整个文件,但是如果把函数写在类的声明中就会不识别
 *3.不能把函数当做方法调用,也不能将方法当做函数调用
 *
 *
 *方法的注意点:
 *方法可以只有声明没有实现,也可以只有实现没有声明,编译不会报错,但是运行会报错
 *如果方法只有声明而没有实现,则会报错:1.reason:'+[Person demo]:unrecognized selector sent to class 0x100001140',发送了一个不能识别的消息,在Person类中没有+开头的方法
 *reason:'-[Person test]:unrecognized selector sent to instance 0x100001140'
 *类也可以只有实现没有声明,例如:@implementation Person : NSObjec ... @end
 */

@interface Person : NSObject
//对象方法的声明
-(void)test;
//类方法声明
+(void)demo;
@end

@implementation Person
//对象方法的实现
-(void)test
{
    NSLog(@"test");
}
//类方法的实现
+(void)demo
{
    NSLog(@"demo");
}

@end
//外部函数的声明
extern void sum();
//内部函数的声明
static void minus();
//外部函数
extern void sum()
{
    printf("sum");
}
//内部函数
static void minus()
{
    printf("minus");
}

int main(int argc, const char * argv[]) {
    
    return 0;
}

6.结构体作为对象属性的细节
当结构体作为函数(C语言中的称呼)参数或者作为对象属性的情况下,结构体是以拷贝结构体属性的方式进行传递的,此过程传递的不是指针。
结构体作为属性的代码示例:
类模型

#import <Foundation/Foundation.h>

/*
 *合理的设计一个“学生”类
 *学生有*姓名*生日另个属性和说出自己姓名生日的方法
 *要求利用设计的学生类创建学生对象,并且说出自己的姓名和年龄
 */

//定义一个新类型Date
typedef struct
{
    int year;
    int month;
    int day;
}Date;

@interface Student : NSObject
{
    @public
    NSString *_name;
    
    Date _birthday;
}
-(void)say;
@end

@implementation Student

-(void)say
{
    NSLog(@"name=%@;year=%i,month=%i,day=%i",_name,_birthday.year,_birthday.month,_birthday.day);
}

@end

结构体属性修改范例

int main(int argc, const char * argv[]) {
    //1.创建学生对象
    Student *student=[Student new];
    [student say];
    //2.设置学生对象的属性
    student->_name=@"lxh";
    /*
     *student->_birthday={1992,12,23};错误原因
     *1.结构体只能在定义的时候初始化
     *2.系统并不清楚它({1992,12,23})是数组还是结构体
     */
    student->_birthday=(Date){1992,12,23};
    //student->_birthday=(Date){1992,12,23};这句话的本质其实是:结构体拷贝,将结构体{1992,12,23}强制拷贝给了_birthday
    /*
    Date d1={1999,1,5};
    Date d2;
    d2=d1; //当结构体作为函数的参数的时候,赋值其实是将结构体内部的属性,进行了一次拷贝
           //这里是将d1内部的属性都拷贝了一份赋值给了d2
    d2.year=2000;//修改d2的值并不影响d1的值
    printf("d1=year=%i\n",d1.year);
    printf("d2=year=%i\n",d2.year);
    */
    
    /*
     *这种方式赋值也可以,单个赋值
     */
    student->_birthday.year=1992;
    student->_birthday.month=12;
    student->_birthday.day=23;
    
    /*
     *第三种赋值方式
     */
    Date d={1992,12,23};
    student->_birthday=d;
    return 0;
}

欢迎关注我的个人微信公众号,免费送计算机各种最新视频资源!你想象不到的精彩!


0.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容