联编
首先先了解啥是联编,先看书上的概念
联编: 是指确定函数调用和函数代码段之间的映射关系。
静态联编:是只在编译时确定了函数调用的具体操作对象;
动态联编:是只在程序运行过程中动态确定函数调用的具体对象。
换句话说
联编就是找到应该调用哪个函数的过程;
看个例子
#include <iostream>
using namespace std;
class animal{
public:
void breathe() {
cout << "animal breathe" << endl;
}
};
在编译以上代码后,编译器会记住函数 breathe() 的地址,每次对animal的实例调用该函数时,编译器都会按照固定的调用路径执行,为静态联编,即在编译时就已确定函数的调用路径。例子1就是静态联编。
多态的引入
多态的目的即一种接口,多种实现,如下例子1,
#include <iostream>
using namespace std;
class animal{
public:
void breathe() {
cout << "animal breathe" << endl;
}
};
class fish: public animal{
void breathe(){
cout<<"fish bubble"<<endl;
}
};
int main() {
fish f;
animal *p = &f;
p->breathe();
return 0;
}
//输出:animal breathe
上例的本意是,通过父类指针 去访问子类的成员方法,即应输出 “fish bubble”,但其结果仍调用的是父类的成员方法。
原因在于:
在编译器编译时,需要确定每个对象调用函数的地址,称为早期绑定。在构造fish类的对象时,先要调用animal的构造函数,再调用fish类的构造函数。二者拼成一个完整的fish对象。
当将fish类的对象f 的地址赋给animal类的指针p时,需要进行类型转换(可以类比 int a = 1.5),即p此时会指向animal的地址,调用的就是animal的breathe()函数了。
多态的实现
1.函数重载–静态联编
看下面例子2
#include <iostream>
using namespace std;
class point{
private:
int x;
int y;
public:
point(int xx, int yy)
{
x = xx;
y = yy;
}
double area() {
return 0.0;
}
};
class circle: public point{
private:
int r;
public:
circle(int xx, int yy, int rr):point(xx,yy){
r = rr;
}
double area() {
return 3.14 * r * r;
}
};
int main() {
point p(1,1);
circle c(2,2,2);
cout<<p.area()<<endl;
cout<<c.area()<<endl;
cout<<c.point::area()<<endl;
return 0;
}
输出
0
12.56
0
这里,采用跳转指令,告诉了编译器该调用哪个类的实例的成员方法。
2 虚函数-动态联编
同样是还是上面的例子2修改如下
#include <iostream>
using namespace std;
class animal{
public:
virtual void breathe() {
cout << "animal breathe" << endl;
}
};
class fish: public animal{
void breathe(){
cout<<"fish bubble"<<endl;
}
};
int main() {
fish f;
animal *p = &f;
p->breathe();
return 0;
}
输出 fish bubble
只是声明基类的成员方法为虚函数,即实现了我们想要的功能,即:通过基类指针访问子类的成员变量和成员函数。注意,如果不声明为虚函数,则基类指针只能访问子类的成员变量,并不能访问成员方法。
虚函数的实现方法:
当brearhe() 被声明为虚函数时,编译器在编译时会为每个包含虚函数的类创建一个虚表(为1维数组),这个虚表里会存放虚函数的地址,并为每个类提供一个虚表指针,指向各自对应的虚表。进而,在程序运行的时候,编译器根据对象的类型去调用各自对应的函数。对于上例,当fish类的实例f创建后,其内部的虚表指针会指向fish类的虚表,进而会调用fish类的breathe方法。
上述描述刚看起来可能有点不好理解,借用别人博客上的一张图:
以base基类和derive子类为例,当类中有虚函数时,一个对象的构成会包括一个虚表指针vfptr 和这个对象本身,32位编译器下这个指针占4个字节。这个虚表指针会指向某一个虚表, 虚表是一个一维数组,里面包括各个虚函数的地址。当调用虚函数时,编译器首先会查看对象中的虚表指针,然后转到相应的虚函数地址表。这里需要注意, 无论类中有多少个虚函数,只需要在类中添加一个虚表指针即可,只是这个虚表的大小不同了。,可以想到,对于多重继承,会存在多个虚表指针和多个虚表,每个虚表指针指向各自对应的虚表。
由上可见,在使用虚函数时也会带来一定的成本,包括:
1)每个对象都会增大,增大一个存储地址的空间;
2 )创建类的时候会额外创建一个虚表;
3) 调用函数时会增加额外的在虚表中查找地址的操作。
总结,对于虚函数的调用来说,每个对象内部都会有一个虚表指针,被初始化为指向本类的虚表。不管对象类型如何去转换,该虚表指针是固定的,因此实现了动态的对象函数的调用。
基类的构造函数不允许定义为虚函数
自己测试如上。这个问题网上有很多回答:包括,虚函数的调用需要虚函数表指针,而该指针存放在对象的内容空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚函数
我个人比较认同的一个回答是:
假如基类的构造函数是虚的,那么我们在实例化一个子类对象时,由于虚函数的特性,子类会调用子类自己的构造函数。又由于继承机制,子类实例化时,会先调用基类的构造函数,再调用子类的构造函数。这样下来二者冲突。故 这样做没啥意义。
析构函数一般定义为虚函数
如下例子
#include <iostream>
using namespace std;
class animal{
public:
animal(){
cout<<"animal"<<endl;
};
~ animal(){
cout<<"~animal"<<endl;
};
};
class fish: public animal{
public:
fish(){
cout<<"fish"<<endl;
};
~ fish(){
cout<<"~fish"<<endl;
};
};
int main() {
fish f;
animal *p = &f;
delete p;
return 0;
}
输出:
animal
fish
~animal
如果析构函数不采用虚函数,释放指针p只是释放了基类的内存,没有释放子类的内存;这样会导致内存泄漏。原因同静态联编。
为什么c++ 默认的类不采用虚析构函数呢,因为虚函数会导致额外的开销,对于不需要继承的类,声明为虚函数会浪费内存,故默认没有采用。。
由上可见,当需要该类作为基类时,一般采用虚析构函数;这样会导致内存泄漏。