目录
- 六、动态联编与虚函数
1、联编的概念
2、静态联编
3、动态联编
4、虚函数
5、虚析构函数
6、纯虚函数和抽象类
- 六、动态联编与虚函数
1、联编的概念
联编是指一个计算机程序自身彼此关联(使一个源程序经过编译、 连接, 成为一个可执行程序) 的过程, 在这个联编过程中, 需要确定程序中的操作调用(函数调用) 与执行该操作(函数) 的代码段之间的映射关系, 按照联编所进行的阶段不同, 可分为静态联编和动态联编。
2、静态联编
静态联编又称静态束定、 早期联编、 前期联编。
静态联编是指联编工作是在程序编译链接阶段进行的, 静态联编又称早期联编, 因为这种联编是在程序开始运行之前完成的。 在程序编译阶段进行的这种联编又称静态束定, 在编译时就解决了程序中的操作调用与执行该操作代码间的关系, 确定这种关系又被称为束定, 编译时束定又称为静态束定。
静态联编就是, 编译器在程序运行前就知道调用什么函数做什么事。
静态联编特点: 速度快效率高。
3、动态联编
动态联编又称动态关联、 动态束定、 后期联编、 晚期联编。
动态联编是指编译程序在编译阶段并不能确切地知道将要调用的函数, 只有在程序执行时才能确定将要调用的函数, 为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行, 这种在程序运行时进行的联编工作被称为动态联编。
C++规定: 动态联编是在虚函数的支持下实现的。
动态联编, 编译器在程序运行时才知道这个函数的具体作用
C++中, 动态联编必须包括以下方面:
1、 成员函数必须声明为 virtual
2、 如果基类中的成员函数声明为虚函数, 则派生类中不必再声明,虚函数其实类似于函数指针
4、虚函数
#include<iostream>
using namespace std;
class A{
public:
void fun(void)
{
cout<<"A 中的 fun"<<endl;
}
~A()
{
cout<<"A 中的析构"<<endl;
}
};
class B:public A{
public:
void fun(void)
{
cout<<"B 中的 fun"<<endl;
}
~B()
{
cout<<"B 中的析构"<<endl;
}
};
int main()
{
//p 是个指针变量, 它的类型是定义的时候决定的, 与赋什么值没有关系
A *p=new B;//改成 B*p=new B;的时候, p->fun 就会调用类 B 的 fun
p->fun();//执行结果是 A 中的 fun
delete p;
return 0;
}
以上代码执行过程中,先执行A中的fun函数,在执行B中的析构;存在风险
只析构了 A 类的部分, B 类的扩展部分没有析构回收。 造成内存泄漏
C++规定动态联编是在虚函数的支持下实现的
虚函数是动态联编的基础。 虚函数是非 static 的成员函数
说明虚函数的方法如下
virtual 类型说明符 函数名(参数表);
虚函数一般是在基类上做的, 派生类重写这个虚函数。
一旦基类中的一个成员函数设置成了虚函数, 它就变成了一个函数指针变量。如果基类没有涉及到派生的时候, 这个指针变量就指向它自身这个函数。
一旦涉及到派生了, 基类中的 function 虚函数指向了派生类的 function 函数的入口地址。 所以涉及到派生的时候, 派生类要重新实现这个虚函数
注意:
1、 基类中的虚函数, 不涉及派生的时候, 指向自己。
2、 当涉及到派生的时候, 如果派生类没有重写这个虚函数, 基类中的虚函数还是指向自身。
3、 当涉及到派生的时候, 并且派生类重写了这个虚函数, 则基类中的虚函数就指向了派生类的重写的这个虚函数。
动态联编在编译阶段不能确定函数入口, 只有在调用的时候才能确定函数入口
#include<iostream>
using namespace std;
class Point{
private:
double x;
double y;
public:
Point(double x,double y);
virtual double Area(void)const;
};
Point::Point(double x,double y)
{
this->x=x;
this->y=y;
}
double Point::Area(void)const
{
return 0.0;
}
class Rectangle:public Point{
private:
double w;
double h;
public:
Rectangle(double x,double y,double w,double h);
double Area(void)const;
};
Rectangle::Rectangle(double x,double y,double w,double h):Point(x,y)
{
this->w=w;
this->h=h;
}
double Rectangle::Area(void)const
{
cout<<"this is Rectangle Area fun"<<endl;
return w*h;
}
class Circle:public Point{
private:
double r;
public:
Circle(double x,double y,double r);
double Area(void)const;
};
Circle::Circle(double x,double y,double r):Point(x,y)
{
this->r = r;
}
double Circle::Area(void)const
{
cout<<"this is Circle Area fun"<<endl;
return 3.14*r*r;
}
void fun(Point &s)
{
cout<<s.Area()<<endl;//在编译的时候不能确定调用哪个类中的 Area
}
int main()
{
Rectangle rec(3.0,5.2,15.0,25.0);
fun(rec);
Circle ob(1.1,2.2,6);
fun(ob);
return 0;
}
如果基类中的成员函数是虚函数, 则派生类中的重载的成员函数也是虚函数, 只不过在派生类中不需要加virtual 进行修饰
只需要在派生类的基础上在派生一类就能验证
虚函数的好处, 主要是体现在函数传参的时候。 形参是基类对象指针, 实参传各种其派生类对象地址。
派生类中对基类虚函数进行替换时,要求该函数的函数名、参数个数、参数类型、返回值都一样,只有函数体不一样
5、虚析构函数
在析构函数前面加上关键字virtual进行声明,该虚构函数即为虚析构函数
构造函数不能是虚函数
如果一个基类的析构被声明为虚析构函数, 则它的派生类中的析构函数也是虚函数(二者名字不同)
声明虚析构函数的目的在于使用 delete 运算符删除一个对象时, 能确保析构函数被正确执行。 因为设置虚析构函数后, 可以采用动态联编的方式选择析构函数
只需要将A中的析构函数设置成虚析构函数就能解决以上问题
先执行B中的fun
再执行B中的析构
最后执行A中的析构
在 delete p 的时候, 执行析构函数, 虽然 p 是 A 类的, 但是还是能够执行 B 类的析构函数, 因为 A 类的析构函数是虚析构函数, 指向了派生类 B 类的析构函数。
父类的构造函数/析构函数与子类的构造函数/析构函数会形成多态, 但是当父类的析构函数即使被声明 virtual,子类的析构方法仍无法覆盖父类的析构方法。 这是由于父类的构造函数和析构函数是子类无法继承的, 也就是说每一个类都有自己独有的构造函数和析构函数。
当 delete 父类的指针时, 由于子类的析构函数与父类的析构函数构成多态, 所以得先调动子类的析构函数;之所以再调动父类的析构函数, 是因为 delete 的机制所引起的,delete 父类指针所指的空间, 要调用父类的析构函数。
继承时, 要养成的一个好习惯就是, 基类析构函数中, 加上 virtual, 在派生类对象的地址给 基类指针赋值,通过基类指针释放对象时, 防止派生类的析构函数不被调用而导致可能的内存泄露问题
6、纯虚函数和抽象类
纯虚函数是一种特殊的虚函数, 它的一般格式如下:
class 类名{
virtual 类型 函数名(参数表)=0;
};
纯虚函数没有函数体;一个类中如果有纯虚函数,那么该类为抽象类;
抽象类不允许实例化对象,但是可以用来派生
纯虚函数在派生类中一定要实现, 如果派生类中不实现基类中的纯虚函数, 派生类就会变成抽象类