「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」
类是面向对象变成思想的核心,在面向对象编程思想中万物皆为对象,类便是描述对象的一种方式;在Object-C语言中,结构体只能定义属性,而不能定义方法;而在Swift语言中,结构体十分强大,它可以进行属性和方法的封装。在某些情况下,结构体可以代替类来使用;
类的某些高级特性是结构体所不具备的,需要根据实际情况进行选择;
初识类与结构体
我们先来看如下代码:
在上述代码中,我们创建了一个Teacher类,类中有两个属性age和name,并创建了一个初始化器init(age: Int, name: String);
接下来,我们直接将class关键字改为struct:
此时,Teacher从一个类变成了一个结构体,并且依然能够通过编译,说明在某种程度上,类与结构体具有相似性;
类与结构体的异同
相同点:
- 定义存储值的属性;
agename - 定义方法;
func - 定义下标,用来使用下标语法提供对其值的访问;
subscript - 定义初始化器;
init - 使用
extension来拓展功能 - 遵循协议来提供某种功能
不同点:
类有继承的特性,而结构体无法继承;- 类型转换能够在
运行时检查和解释类实例的类型; - 类有
析构函数用来释放其分配的资源;deinit 引用计数允许对一个类实例有多个引用;
引用类型和值类型
在使用类与结构体时,我们首先需要注意的是:
类是引用类型,一个类的变量并不直接储存具体的实例对象,而是对存储具体实例内存地址的引用;结构体是值类型
引用类型
如上图代码所示:我们创建一个t来接收类Teacher的实例对象,但是t并没有直接存储Teacher的实例对象,而是存储对Teacher实例对象内存地址的引用;同样的,如果我们将t赋值给其他变量,比如t1,那么t和t1存储的都是Teacher实例变量内存地址的引用;
我们可以通过**withUnsafePointer**来查看t和t1的内存地址,注意t和t1需要使用var,如果使用let则无法使用**withUnsafePointer**来分析:
我们看到,t和t1在内存中的地址是挨着的,中间相差8字节;接下来我们通过**x/8g**来查看t的内存地址的存储信息:
可以看到t对应的内存地址上存储的8个字节的数据是**0x00000001072213a0**,刚好对应了Teacher实例对象的内存地址**0x1072213a0**;也就是说t和t1中间相差的8字节数据存储的是对应失恋对象的内存地址;
po和p命令的区别在于使用po只会输出对应的值,而p则会返回值的类型以及命令结果的引用;x/8g:读取内存中的值(8g是以8字节格式输出);
值类型
相比较于引用类型的变量中存储的是地址,值类型存储的就是具体的实例或者说具体的值;
如上述代码所示:s和s1是两个不同的变量,但是他们存储的是否是相同的内容呢?我们通过po执行来查看一下:
可以看到在s和s1中存储的是age和name的值,并不是地址;此时,我们修改s1中age属性的值,来看一下s中是否会同样发生变化:
修改
s1中的数据,s中的数据并没有发生变化,说明s和s1中存储的是不同的数据;
一般情况下:
引用类型存储在堆上,值类型存储在栈上;
内存区域基本概念图如下:
- 图中所示:
低地址在下,高地址在上; MachO文件从一个虚拟地址开始,只有一部分内存地址可供我们操作;- 可操作的内存区具体分为:
栈区、堆区、全局区、常量区和_text指令区;(全局区、常量区和_text指令区也可统称为全局区); 栈区的拉伸从高地址向低地址拉伸;堆区从低地址向高地址拉伸;当栈区与堆区重合将会发生堆栈溢出;栈区:存放局部变量和函数运行过程中的上下文;堆区:存放所有对象;全局区:Global存放全局变量,常量,代码区;
那么如何查看内存地址是在栈还是堆上呢?这里有个工具可以帮我们打印出内存地址的情况;
如上述代码,我们可以通过cat address指令来分析得出,局部变量age存放在栈区;
在我们的MachO文件中有多个段Segment,每个段有不同的功能;每个段又分为很多个小的Section:
TEXT.text:机器码;TEXT.cstring:硬编码的字符串;TEXT.const:初始化过的常量;DATA.data:初始化过的可变的静态/全局数据;DATA.const:没有初始化过的常量;DATA.bss:没有初始化的静态/局部变量;DATA.common:没有初始化过的符号声明;
如上述代码,根据打印结果:a是初始化过的全局数据,存放在Data.data;age还没有初始化,存放在Data.common;
类与结构体的内存分布
可以使用
frame variable -L指令来查看内存分布情况
结构体的内存分布
结构体内存空间在栈上;age在低地址,name在高地址;
类的内存分布
接下里,我们将Struct修改为class,来看一下分析结果:
类内存空间在堆上;age在低地址,name在高地址;
内存分配流程:
在
栈上分配8字节的内存大小,用来存放s;在Student初始化的过程中,在堆上寻找合适的内存区域,将该内存区域的地址返回,然后将age和name的值拷贝到堆的内存空间上;最后将栈上的内存地址,指向堆区;
类与结构体混用时内存分布
结构体s内存地址在栈区- 结构体中的
类p的内存地址在堆区;
堆和栈再分配内存的过程中,会有时间和速度的区别。我们可以通过GitHub上的StructVsClassPerformance工程来分析;
在选择数据结构类型时,尽可能优先选用
结构体;结构体在栈上,是线程安全的;在内存分配上,栈区也要比堆区快;