1. 概念
动态绑定又名动态联编;我们分开解释
1.1 绑定
绑定的定义出现在编译器中,主要是指源码中的变量名或者函数替换为内存地址;
1.2 动态
有动态肯定有静态
静态发生在编译期,即编译为可执行文件的阶段
动态发生在运行期,即可执行文件运行时的阶段
2. 原因
2.1 为什么需要绑定
1. 源代码写的都是类,变量,方法
2. cpu 执行的是指令,指令的执行需要数据的内存地址;
如 cpu 执行一个int c = a + b;需要先加载 a b的值,这个加载指令需要 a b 的内存地址,
其实这个时候 CPU 压根儿不知道 a b, 它只知道 a 和 b 的内存地址,
比如 a 的内存地址为0x1,b 的内存地址为 0x2,上述表达式可以翻译为(0x1)+ (0x2)
看到没,CPU 压根儿不知道 a b 这两个符号
3. 编译和连接器需要把源码符号 a b 转为 CPU 可识别的地址(0x1 0x2)
4. 这个过程就是绑定过程
2.2 为什么需要动态绑定
废话,因为有需求呗,比如?????
c++是一个面向对象的语言,这里面有个基本需求就是基类指针指向子类对象,这样通过继承和多态提高代码的可扩展性。再具体一点,来个例子:
class Animal{
virtual void eat(){}
}
class Cat: pulic Animal{
virtual void eat(){
cout << "cat eat";
}
}
class Dog: pulic Animal{
virtual void eat(){
cout << "dog eat";
}
}
Animal *p;
int n;
cin >> n;
if(n > 10){
p = new Cat("cat", 16);
}else{
p = new Dog("dog", 22);
}
p -> eat();
上面代码在编译的时候,需要解决 P 指向谁的问题;
发现 p 的类型只有在运行时才可以知道,进而也没有办法确定调用哪个eat
3. 原理
当然上面只是列举了动态绑定的一个使用场景,我们来看看它的具体实现
对于上面实例,编译和链接阶段如何做的才达到这个效果呢
首先对于某个类,如果它的成员方法有 virtual 或者它的父类有 virtual 方法,就会生成一个虚函数表
该虚函数表里存放的是个每个虚函数的内存地址
编译器在编译时会给每个类增加一个隐藏属性,使该属性指向这个虚函数表
当对象类型确定后,比如上例中 P,假设为 Dog 类型
那么通过对象 P 的隐藏属性就可以找到它的虚函数表
从它的虚函数表中即可查到 它的 eat 内存地址,进而发生调用
其实我们可以发现,对于 Dog 类型增加的隐藏属性,它是在编译的时候就加入了,而且是在编译期时就指向了这个虚函数表
只是因为我们在编译的时候不知道 P 到底是 Cat 类型还是 Dog 类型,进而我们无法绑定 p -> eat()的内存地址
现在编译器中,其实对于虚函数无论在编译期能不能确定它的类型,都是采用这种方式进行的,估计是为了统一吧
有个问题,就是自动增加的这个隐藏属性可不可以为静态的????
答:不能是静态的,假设存在继承关系,那么这个成员只能指向某个虚函数表,而如果子类也有其他虚函数,导致基类和子类的虚函数表是不同的,进而需要指向不同的虚函数表
4. 问题
运行时是如何确定 P 的类型的呢,我也不知道,留着以后学习吧