面向对象语言三大特性:封装、继承、多态。在OC中多态是用Runtime
实现的,在C++中用虚表实现多态,今天我们了解一下Swift中的多态及其原理(和**C++**类似,都是使用虚表)。
什么是多态?父类指针指向子类对象就是多态。
一、结构体和类函数调用比较
1.1. 结构体
通过汇编分析可以看到,因为不存在继承重写行为,调用的函数地址都是在编译时期确定的。
1.2. 类
speak
函数调用栈:
eat
函数调用栈:
sleep
函数调用栈:
类生成的汇编代码非常多,相比结构体复杂了很多,并且通过函数调用发现,函数地址是动态变化的。所以,如果没有继承行为或简单的类,建议使用结构体,效率更高。
类的函数调用地址之所以变化是为因为子类继承父类会导致函数实际调用地址发生变化,这也是多态的体现。
二、继承类汇编分析
示例代码:
class Animal {
func speak() {
print("Animal speak")
}
func eat() {
print("Animal eat")
}
func sleep() {
print("Animal sleep")
}
}
class Dog: Animal {
override func speak() {
print("Dog speak")
}
override func eat() {
print("Dog eat")
}
func run() {
print("Dog run")
}
}
var animal = Animal()
animal.speak()
animal.eat()
animal.sleep()
/*
输出:
Animal speak
Animal eat
Animal sleep
*/
animal = Dog()
animal.speak()
animal.eat()
animal.sleep()
/*
输出:
Dog speak
Dog eat
Animal sleep
*/
汇编分析:
分析: 类的实例前8个字节保存的是类的信息,所以上面的汇编代码会一值围绕着实例animal
的前8个字节去查找函数地址。而animal
最后一次指向的是对象Dog
在堆空间的内存,所以最终调用的是Dog
中的speak
函数。
其实就是虚表:
callq *0x50(%rcx)
中的0x50
就是偏移量,跳过0x50
就是函数speak
的地址。
总结起来其实很简单:
- 先找到全局变量
animal
的地址; animal
地址保存的是堆空间Dog
对象的内存地址;Dog
对象前8个字节保存的是对象类型信息地址;- 对象类型信息地址保存着类中函数的地址。
注意: 无论创建多少个同类型对象,对象的类型信息都指向同一块内存地址。对象类型信息保存在全局区。