1.静态联编
-
联编(binding) 又称绑定, 就是将模块或者函数合并在一起生成可执行代码的处理过程, 同时对每个模块或者函数分配内存地址,并且对外部访问也分配正确的内存地址。
-
在编译阶段,就将函数实现和函数调用绑定起来称为静态联编(static binding) 。
静态联编在编译阶段就必须了解所有的函数或模块执行所需要的信息, 它对函数的选择是基于指向对象的指针(或者引用) 的类型。
C语言中, 所有的联编都是静态联编, C++中一般情况下联编也是静态联编。 -
eg:
#include <iostream>
using namespace std;
class Point//Point类表示平面上的点
{
double x,y; //坐标值
public:
Point(double x1=0,double y1=0):x(x1),y(y1) { }//构造函数
double area()
{
return 0; //计算面积
}
};
class Circle:public Point//Circle类表示圆,由公有继承派生出来的
{
double r;//半径
public:
Circle(double x, double y,double r1):Point(x,y),r(r1) { }
double area()
{
return 3.14*r*r;//计算面积
}
}
int main()
{
Point a(2.5,2.5);
Circle c(2.5,3.5,1);
cout<<"Point area"<<a.area()<<endl;//基类对象,静态联编
cout<<"Circle area="<<c.area()<<endl;//派生类对象,静态联编,与类C绑定了
Point *pc=&c,&rc=c;//基类指针、 引用指向或引用派生类对象(复制兼容规则!!)
cout<<"Circle area="<<pc->area()<<endl;//静态联编基类调用,看类型,指针*pc与Point类型绑定了
cout<<"Circle area="<<rc.area()<<endl;//静态联编基类调用,看类型:rc和Point类型绑定了
return 0;
}
结果:
Point area=0
Circle area=3.14
Circle area=0
Circle area=0
2.动态联编
- 在程序运行的时候,才进行函数实现和函数调用的绑定称为动态联编(dynamic binding) 。
- 如果在编译“Point *pc=&c”时, 只根据兼容性规则检查它的合理性, 即检查它是否符合派生类对象的地址可以赋给基类的指针的条件。 (使用指针或者引用的时候,才会使用动态联编)
至于“pc->area()”调用哪个函数, 等到程序运行到这里再决定。 - 如果希望“pc->area()”调用Circle::area() , 也就是使类Point的指针pc指向派生类函数area的地址, 则需要将Point类的area函数设置成虚函数。(要知道:前面这里是调用基类的area()函数的。。。因为是基类指针指向派生类对象)
- 虚函数的定义形式为:
virtual double area() { return 0; } //计算面积
- eg:
#include <iostream>
using namespace std;
class Point//Point类表示平面上的点
{
double x,y;//坐标值
public:
Point(double x1=0,double y1=0):x(x1),y(y1) { }//构造函数
virtual double area()//虚函数
{
return 0;
}
};
class Circle:public Point//Circle类表示圆
{
double r;//半径
public:
Circle(double x, double y, double r1):Point(x,y),r(r1) { }//构造函数
double area()
{
return 3.14*r*r;//虚函数
}
};
int main()
{
Point a(2.5,2.5);
Circle c(2.5,2.5,1);
cout<<"Point area="<<a.area()<<endl;//基类对象,静态联编,因为连个指针或引用也没有
cout<<"Circle area="<<c.area()<<endl;//派生类对象
Point *pc=&a;//基类指针指向基类对象
cout<<"Circle area="<<pc->area()<<endl;
pc=&c;//基类指针指向派生类对象
cout<<"Circle area="<<pc->area()<<endl;//动态联编,程序运行的时候,pc指针指向对象c
//程序运行的时候,指针指向什么对象或者引用什么对象,就调用什么对象的数据成员or成员函数
return 0;
}
运行结果:
Point area=0
Circle area=3.14
Circle area=0
Circle area=3.14
-
当编译器编译含有虚函数的类时, 将为它建立一个虚函数表VTABLE(virtual table) , 它相当于一个指针数组, 存放每个虚函数的入口地址。
编译器为该类增加一个额外的数据成员, 这个数据成员是一个指向虚函数表的指针, 通常称为vptr。 -
Point类只有一个虚函数area, 所以虚函数表里只有一项。
如下图(a) 是Point对象UML示意。
-
如果派生类Circle没有重写这个虚函数area, 则派生类的虚函数表里的元素所指向的地址就是基类Point的虚函数area的地址。
如果派生类Circle重写这个虚函数area,这时编译器将派生类虚函数表里的vptr指向Circle::area(), 即指向派生类area虚函数的地址。
如图(b)
-
当调用虚函数时, 先通过vptr找到虚函数表, 然后再找出虚函数的真正地址, 再调用它。
-
派生类能继承基类的虚函数表, 而且只要是和基类同名(参数也相同) 的成员函数, 无论是否使用virtual声明, 它们都自动成为虚函数。
如果派生类没有改写继承基类的虚函数, 则函数指针调用基类的虚函数。
如果派生类改写了基类的虚函数, 编译器将重新为派生类的虚函数建立地址, 函数指针会调用改写以后的虚函数 -
eg:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void print()//虚函数
{
cout<<"Base"<<endl;
}
};
class Derived:public Base
{
public:
void print()//虚函数,这里重写了print()
{
cout<<"Derived"<<endl;
}
};
void display(Base *p)
{
p-print();
}
int main()
{
Derived d;
Base b;
display(&d);////派生类对象, 输出“Derived”
display(&b);////基类对象, 输出“Base”
return 0;
}
- 虚函数的调用规则是:
根据当前对象, 优先调用对象本身的虚成员函数。
这和名字支配规律类似, 不过虚函数是动态联编的, 是在运行时(通过虚函数表中的函数地址) “间接” 调用实际上欲联编的函数