Block在使用过程中跟C语言的函数非常相似,而且其底层也是依靠C语言的结构体和函数来实现的.结构体是一个非常重要的概念,也是几乎所有高级语言中类的基础.为了更好地理解Block的实现原理,我们今天来先补习一下关于结构体的知识.
1.什么是结构体
在面向对象编程技术之前,编程语言是面对过程的.由于只有单个的基础数据类型,在需要对批量的数组进行操作和存储时,我们可以数组集合;可是当我们需要把不同的数据类型作为一个处理单位的时候,怎么处理呢?这时候集合就出现了.
结构体就是允许通过自定义行为把不同的类型进行统一存储的数据单元.使用struct来进行定义.
2 结构体如何定义
结构体其实可以算是类概念的原型,可以把我们需要的各种数据都放在一起,用来统一进行存储和操作.下边我们来定义一个简单的结构体:
struct Man { char *name; int age; };
这样我们就定义了一个名称为Man的结构体,有两个成员变量一个name和age.
结构体里的成员变量可以取任何你想要放入而当前开发语言能够支持的数据类型,比如字符,数组,指针等等.当然你也可以给你定义的结构体添加自己的方法:
struct Man { char *name; int age; void(*funPtr)(void *); };
这样我们就为结构体添加了一个无返回值,参数时任意指针类型的函数成员变量;当单一的结构体被定义之后,如果你需要创建一个链式结构,就可以在搞结构体内放入一个同类型的指针,来指向下一个同样的结构:
struct Man { char *name; int age; struct Man *next; };
这样你就可以把多个结构体使用连接起来,也就实现了可以无线存储数据的需要(在内存允许的情况下).
3. 结构如何初始化
定义完成之后就需要存储我们需要的信息了,那怎么初始化呢?
如果你定义的结构体只是临时定义的并不想以后再用,那就可以直接在定义时进行初始化,这时候结构体就成了一个没有名字的结构体:
struct { char *name; int age; } man = {"Ericydong", 30};
如果需要多次使用,可以选择先定义结构体,然后再进行使用:
void function(){ printf("function execute"); } //定义之后再使用 struct Man { char *name; int age; void(*funPtr)(void *); }; struct Man man = {"Ericydong", 30, function};
如果觉得这样写太麻烦,还可以使用typedef进行简化 :
void function(){ printf("function execute"); } typedef struct ManHereCanBeOmitted { char *name; int age; void(*funPtr)(void *); } Man; Man man = {"Ericydong", 30, function}; man.function();
struct后边的结构体名称可以有也可以没有(代码中的ManHereCanBeOmitted), 根据自己的需求来定.但是如果结构体定义内有使用到结构体自身的声明,stru
这种结构就是简单的单向链表结构,如果你想要实现双向链表,就可以将最后一个链表的nex指向头指针,从而形成闭环的双向链表.
zzh
[根据个人习惯和实际需求,head节点可以存储数据也可以不存储数据只作为一个标志].
4. 强制类型转化:
强制转化是将一种数据类型强制转化为另外一种数据类,但并不是所有的类型之间都可以进行强制转化.一般如果需要强制转化的目的类型比原有类型能表示的范围更广才可以保证强制转化不会出现异常.例如正常情况下在64位操作系统中,int类型占据四个字节空间,而long占据八个字节,那么一个int就可以直接强制转化为long类型,不会有任何异常;而long累哦行如果被强制转化为int,则有可能会丢失数据(如果long表示的数据没有超过int的范围不会有异常,如果超出则会丢失).在结构体中,强制转化也要满足这种条件.
4.1 结构体之间转化
有时候为了操作的方便,我们会需要将结构体之间进行强制转化(C语言本身不支持结构体自定义构造函数,所以如果用Xcode做模拟把文件名修改为.mm即可,以下演示均基于此环境).
我们定义两个结构体:
struct Person { char *name; int age; }; struct Student { struct Person person; char *className; Student(char *name, int age, char *className) : className(className) { person.name = name; person.age = age; } };
在这两个结构体中,结构体Person定义了两个属性,而结构体Student则包含了一个Person类型的结构体,同时新增了className成员变量.同时自定义了初始化方法来初始化结构体Student:
Student stu = Student("Ericydong", 30, "Class 1, Grade 2"); Person *per = (Person *)&stu; NSLog(@"person.name == %s, person.age == %d", per->name, per->age);
由于在结构体中成员变量会从结构体起始地址按照顺序进行存储,所以结构体变量stu的地址其实是和stu内的成员变量person的起始地址相同,所以我们可以将stu强制转化为结构体Person类型的变量指针per,然后使用变量指针per访问结构体Person的各个变量.
猜想一下,我们通过变量指针per是否可以访问到实例变量stu中的成员变量className值?
虽然使用了强制转化,但是事实上&stu和per指向了同一快内存区域,只是为了使用方便强迫其表现形式不一样,而结构体的内存空间都是连续的空间,成员变量按照顺序依次存储,所以理论上讲我们是可以通过变量指针per进行偏移来访问到per的成员变量className.
Student stu = Student("Ericydong", 30, "Class 1, Grade 2"); Person *per = (Person *)&stu; char *location = (char *)per; location += sizeof(Person); char *className = *(char **)location; NSLog(@"className == %s", className);
事实证明,我们可以通过指针的偏移来获取到指定位置存储的信息,不过在面向对象的高级语言中都对指针操作做了限制.需要注意以下几个问题:
- 为什么不能直接使用per来进行指针移动操作?因为per是结构体Person类型的指针,所以操作的单位也是以结构体Person为单位进行的,所以如果per自增1,相当于移动了一个per的偏移量,而不是一个字节.所以我们需要可以自增1相当于偏移一个字节的指针变量(在C中可以使用void *,由于C++不支持指针移位操作,所以我们使用char *来做替代);
- 由于在结构体Student中,className并不是直接存储在结构体中,而是将实际存储空间的地址存储在结构体中,我们没需要首先找到存储实际空间(*location),然后将该地址转化为字符指针(char **location),最后取出该空间存储的值(*(char **)location);
- 由于在没有字符串类型语言中,字符串是通过字符连续存储实现的,结尾会有一个特定的结尾字符,其处理逻辑和字符串对象不一样,所以输出时需要使用格式控制符%s,而不是高级语言中字符串对象的格式控制符.
- 对于强制转化从形式上来讲,你可以对任意关联类型进行转化,但是在实际使用时一定要按照其真实的存储结构来进行相应的操作,避免造成寻址混乱,例如对于上述的&stu,你可以将其转化为任意的指针表现形式:
//Person指针类型 struct Person * pointer = (struct Person *)&stu; //void *通用指针类型 void *pointer = (void *)*stu; //甚至是函数指针 void(*fun)(void) = (void(*)(void))&stu; ... ... ...
我们再尝试给Person添加一个功能函数,但是这个函数需要传入一个Student类型的结构体指针,然后通过Student初始化这个成员变量然后尝试使用结构体Person的实例来调用FunPtr函数.修改后的代码如下:
struct Person { char *name; int age; void *FunPtr; }; struct Student { struct Person person; char *className; Student(void *fp, char *name, int age, char *className) : className(className) { person.FunPtr = fp; person.name = name; person.age = age; } }; static void printDescriptionInfo(struct Student *stu) { printf("name == %s, age == %d, className == %s", stu->person.name, stu->person.age, stu->className); }
修改之后,Person的成员变量里多出了一个void *类型的FunPtr变量,用来存储函数实现的指针,同时Student构造方法里出现了一个新的参数void *,使用Person的FunPtr来接收.
Student stu = Student((void *)&printDescriptionInfo, "Ericydong", 30, "Class 1, Grade 2"); Person *per = (Person *)&stu; void(*funPtr)(Student *) = (void(*)(Student *))per->FunPtr; funPtr((Student *)per);
初始化stu之后,我们将结构体Student的实例stu转化为结构体Person类型的指针per,使用per->FunPtr来获取到函数实现,并进行强制转化为函数实现本身的类型(void(*)(Student *)),然后传入参数调用该方法.而事实上,我们上边已经说过,其实任意指针类型在形式上是可以互相转化的,而且获取到FunPtr只是一个确保编译能正常通过的过渡操作,所以完全没有必要将per再转化为(Student *)类型,只需要将函数实现的形式参数指针修改为(Person *)类型即可,只要在函数实现内正确使用该指针就可以了.
Student stu = Student((void *)printDescriptionInfo, "Ericydong", 30, "Class 1, Grade 2"); Person *per = (Person *)&stu; void(*funPtr)(Person *) = (void(*)(Person *))per->FunPtr; funPtr(per);
将上述过程进行简化,我们就可以得到:
Person *per = (Person *)&Student((void *)printDescriptionInfo, "Ericydong", 30, "Class 1, Grade 2"); ((void(*)(Person *))per->FunPtr)(per);
如果你想更加通用一些,可以将中间过程转化为通用指针:
void (*block)(void) = (void(*)())&Student((void *)printDescriptionInfo, "Ericydong", 30, "Class 1, Grade 2"); ((void(*)(Person *))(Person *)block->FunPtr)(per);
好了,block的雏形已经出现了,具体细节下次再聊.