本篇可看做是下一节虚机制的前置内容,如果您仅希望了解有关动态联编和静态联编的问题,请无视这个提示,这不影响对联编知识的介绍。
如果您也像博主一样正在学习C++,在完成本篇博客阅读后建议继续阅读有关虚机制的内容
传送门
联编
先不论动态还是静态,首先需要明确的是关于“联编”的含义,书中有这样的说明:
将源代码中的函数调用解释为执行特定函数代码块被称为函数名联编(binding)
通俗些说,就是指明调用函数的语句调用的究竟是哪一个函数,执行哪一个函数下的代码块。也可以说是明确调用关系,指明那条语句调用了那个函数。我们来看一个栗子:
#include<iostream>
using namespace std;
class A{
public:
void f(){cout << "func f in class A" << endl;}
};
void f(){cout << "global func f" << endl;}
int main()
{
A a;
f();
a.f();
}
这个例子再简单不过了,要辨别主函数中两条语句调用的是哪一个函数简直就是小学二年级的问题 。
明确f()
调用的是定义在全局的f函数
,a.f()
调用的是A类中的f成员函数
的过程就是联编。只不过这项工作实际上需要编译器来完成的,并且联编的结果不是一两句话而是一个个函数的地址。
静态联编
现在,知道了编联的含义,就可以来讨论有关静态和动态联编的意义了。
首当其冲 首先介绍的是静态联编。
静态联编指的是联编的工作在编译期间完成,书上有这样的介绍:
C/C++编译器可以在编译过程完成这种联编。在编译过程中进行联编被称为静态联编(static binding),又称为早期联编(early binding)
没错,这里的静态和其他地方出现的静态有着相同的意义指向——编译期间。
在编译期间明确具体使用哪一个函数是一个简单的事情——仅当调用的函数是非虚函数的情况下。当调用的函数是虚函数时,这个问题就变得很复杂了,甚至在编译期间是无解的。
我们来将刚刚的例子稍作改变:
#include<iostream>
using namespace std;
class A{
public:
void f(){cout << "func f in class A" << endl;}
virtual void g(){cout << "func g in class A" << endl;}
};
void f(){cout << "global func f" << endl;}
int main()
{
A* p = new A();
f();
p->g();
p->f();
}
也许在这个例子中,函数的调用关系仍旧很清晰。但是不得不指出的是,调用函数g
的联编,并不是在编译期间完成的,而是在程序运行过程中完成的。。。。
动态联编
为了解释这个问题,只需要认识到,函数g的调用时不明确的,即不能仅凭一条调用语句就确定调用的那个版本的g函数
——这是虚函数独有的特性。
刚接触到这个概念的我,会对这件事情产生质疑,认为即使是使用了虚函数,这段代码中的调用关系仍旧明确,可以在编译期间完成编联。这就是没有考虑到多态和重写的问题了。
我们来看下面的例子:
#include<iostream>
using namespace std;
class A{ //基类A
public:
void f(){cout << "func f in class A" << endl;}
virtual void g(){cout << "func g in class A" << endl;}
};
class B: public A{ //派生类B
public:
virtual void g(){cout << "func g in class B" << endl;}
};
class C:public A{ //派生类C
public:
virtual void g(){cout << "func g in class C" << endl;}
};
A* factory(char son) //A类工厂方法,根据输入产生对象
{
A* p;
switch(son)
{
case 'B':p = new B();break;
case 'C':p = new C();break;
default :p = new A();break;
}
return p;
}
void f(){cout << "global func f" << endl;}
int main()
{
char son;
cin >> son;
A* p = factory(son); //根据输入指向不同的子类对象
f(); //调用全局函数f
p->g(); //调用虚函数g
p->f(); //调用成员函数f
}
给定三个不同的输入,来测试结果:
可以看到,由于输入不同,在调用函数g时,程序的行为发生了变化——调用了不同版本的函数g。对于其他函数,不同的对象并不影响调用的函数。
同时,具体调用哪一个版本的g函数,在编译期间是不可知的——因为编译器并不能预测输入结果,更不能预测运行时的情况。因此,这项任务就交给了动态联编进行实现,我们来看下书上的定义:
编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编(dynamic binding),又称为晚期联编(late binding)
这个定义告知了我们动态编联实现的原理,是让编译器生成一段能够进行正确选择的代码,而非直接在编译期间锁定调用的函数。
由于编译器生成的是一串起作用的代码,因此在程序运行的过程中根据这段代码便可以选择正确的函数。因此我们可以说:
- 使得
那么关于这段神奇的代码是怎样的机制,他们怎么工作。就是以后再介绍的——虚机制的内容了。
看完文章,快来关注博主,一起学习C++鸭~~~~
啃书系列往期博客
语言基础部分:
- 啃书《C++ Primer Plus》之 C++ 函数指针
- 啃书《C++ Primer Plus》之 C++ 名称空间1
- 啃书《C++ Primer Plus》之 C++ 名称空间2
- 啃书《C++ Primer Plus》之 C++ 引用
- 啃书《C++ Primer Plus》之 const修饰符修饰 类对象 指针 变量 函数 引用
- 啃书《C++ Primer Plus》之 枚举 内容大全
面向对象部分:
- 啃书《C++ Primer Plus》 面向对象部分 构造函数基础及其使用 ——初始化列表 构造函数重载与调用 创建对象
- 啃书《C++ Primer Plus》 面向对象部分 类型转换——转换构造函数 与 转换函数
- 啃书《C++ Primer Plus》 面向对象部分 析构函数
- 啃书《C++ Primer Plus》 面向对象部分 深拷贝与浅拷贝问题 拷贝构造函数 赋值函数
- 啃书《C++ Primer Plus》 动态内存管理(上) new和delete的使用
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(中) 动态对象的创建 重载new和delete
- 啃书《C++ Primer Plus》 面向对象部分 动态内存管理(下) 动态成员管理