C/C++ 用变量来存储数据,用函数来定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。CPU 通过地址来取得内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。
CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,那么加法运算c = a + b;将会被转换成类似下面的形式:
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果赋值给地址为 0X3000 的内存。
变量名和函数名为我们提供了方便,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址,那场景简直让人崩溃。
我们不妨将变量名和函数名统称为符号(Symbol),找到符号对应的地址的过程叫做符号绑定。本节只讨论函数名和地址的绑定,变量名也是类似的道理。
我们知道,函数调用实际上是执行函数体中的代码。函数体是内存中的一个代码段,函数名就表示该代码段的首地址,函数执行时就从这里开始。说得简单一点,就是必须要知道函数的入口地址,才能成功调用函数。
找到函数名对应的地址,然后将函数调用处用该地址替换,这称为函数绑定。
一般情况下,在编译期间(包括链接期间)就能找到函数名对应的地址,完成函数的绑定,程序运行后直接使用这个地址即可。这称为静态绑定(Static binding)。
但是有时候在编译期间想尽所有办法都不能确定使用哪个函数,必须要等到程序运行后根据具体的环境或者用户操作才能决定。这称为动态绑定(dynamic binding)。
C++ 是一门静态性的语言,会尽力在编译期间找到函数的地址,以提高程序的运行效率,但是有时候实在没办法,只能等到程序运行后再执行一段代码(很少的代码)才能找到函数的地址。
上节我们讲到,通过p -> display();语句调用 display() 函数时会转换为下面的表达式:
这里的 p 有可能指向 People 类的对象,也可能指向 Student 或 Senior 类的对象,编译器不能提前假设 p 指向哪个对象,也就不能确定调用哪个函数,所以编译器干脆不管了,p 爱指向哪个对象就指向哪个对象,等到程序运行后执行一下这个表达式自然就知道了。
有读者可能会问,对于下面的语句:
p = new Senior("李智", 22, 92.0, true);
p -> display();
p 不是已经确定了指向 Senior 类的对象吗,难道编译器不知道吗?对,编译器编译到第二条语句的时候如果向前逆推一下,确实能够知道 p 指向 Senior 类的对象。但是,如果是下面的情况呢?
int n;
cin>>n;
if(n > 100){
p = new Student("王刚", 16, 84.5);
}else{
p = new Senior("李智", 22, 92.0, true);
}
p -> display();
如果用户输入的数字大于 100,那么 p 指向 Student 类的对象,否则就指向 Senior 类的对象,这种情况编译器如何逆推呢?鬼知道用户输入什么数字!所以编译器干脆不会向前逆推,因为编译器不知道前方是什么情况,可能会很复杂,它也无能为力。
这就是动态绑定的本质:编译器在编译期间不能确定指针指向哪个对象,只能等到程序运行后根据具体的情况再决定。