C++的静态联编和动态联编、虚函数、纯虚函数、
一、静态联编和动态联编:
联编就是将 模块或者函数 合并在一起生成可执行代码的处理过程(也可以叫做绑定),同时对每个模块或者函数调用分配内存地址,并且对外部访问也分配正确的内存地址,它是计算机程序彼此关联的过程。按照联编所进行的阶段不同,可分为两种不同的联编方法:静态联编和动态联编。
1.静态联编
是指在编译阶段就将函数实现和函数调用关联起来,因此静态联编也叫早绑定,在编译阶段就必须了解所有的函数或模块执行所需要检测的信息,它对函数的选择是基于指向对象的指针(或者引用)的类型,C语言中,所有的联编都是静态联编,并且任何一种编译器都支持静态联编。
动态联编
指在程序执行的时候才将函数实现和函数调用关联,因此也叫运行时绑定或者晚绑定,动态联编对函数的选择不是基于指针或者引用,而是基于对象类型,不同的对象类型将做出不同的编译结果。 C++中一般情况下联编也是静态联编,但是一旦涉及到多态和虚函数就必须要使用动态联编了。虚函数是实现动态联编的基础
多态
字面的含义是具有多种形式、形态。C++多态有两种形式,动态多态和静态多态;动态多态是指一般的多态,动态多态是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。
模板函数
参考https://blog.csdn.net/mr_h9527/article/details/82598237
注意:
2、实例
动态多态--通过类的继承和虚函数实现
#include <stdio.h> #include <iostream> class CShape//图形类、基类 { public: CShape(){} virtual ~CShape(){}//析构函数也声明为虚函数,为了析构彻底! virtual void Draw() = 0;//把想要多态的函数声明为虚函数 }; class CPoint : public CShape//点类 { public: CPoint(){} ~CPoint(){} void Draw() {printf("Hello! I am Point!\n");} }; class CLine : public CShape //线类 { public: CLine(){} ~CLine(){} void Draw() {printf("Hello! I am Line!");} }; int main() { CShape* shape = new CPoint();//手动分配内存,new 生成了一个点类对象,基类指针可以指向派生类的对象(多态性)!!! //draw point shape->Draw();//shape将会调用CPoint的Draw()函数,而不是基类的Draw方法 delete shape; //并不是删除指针,而是删除指针指向的对象,因为基类的析构函数被声明为虚函数,因此会调用该指针指向的派生类的析构函数, //而派生类的析构函数又会自动调用基类的析构函数,这样整个派生类的对象会被完全释放 shape = new CLine(); //draw Line shape->Draw();//shape将会调用CLIne 的Draw()函数 delete shape; return 0; }
一个Draw() 可以有两种实现,并且是在运行时决定的,在编译阶段不知道,只有在运行的时候才能知道我们生成的shape是那种图形,
当然要实现这种效果就需要动态联编了,在基类我们会把想要多态的函数声明为虚函数(前面加上 virtual关键字),而虚函数的实现原理就使用了动态联编。
静态多态----通过模板实现
在上面例子的基础之上添加模板函数:
template <class T> void DrawShape(T* t) { t->Draw(); }
main函数修改为
void main() { CShape* shape = new CPoint();
//draw point shape->Draw(); DrawShape<CPoint>((CPoint*)shape); //模板函数的调用方法:模板名<用来实例化模板的类型>(参数)delete shape;
shape = new CLine(); //draw Line shape->Draw(); DrawShape<CLine>((CLine*)shape); delete shape; return ; }
在程序编译main函数的时候,编译器就已经指定了DrawShape函数里面的Draw要调用那个实现了,这就是静态多态,在编译时就已经知道了要调用的函数。
三、虚函数的使用
1.基类析构函数为虚函数
(1)基类析构函数定义为虚函数时:基类指针可以指向派生类的对象(多态性),如果删除该基类指针delete [ ]p(并不是删除指针,而是删除指针所指向的对象);就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
(2)基类析构函数不定义为虚函数时:编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全
2.为了区分重载函数,把一个派生类中重定义基类的虚函数称为 覆盖
覆盖和重载的区别:
覆盖是子类和父类之间的关系,垂直关系; 重载同一个类之间方法之间的关系,是水平关系。
覆盖是根据对象类型(对象对应存储空间类型)来决定的; 而重载关系是根据调用的实参表和形参表来选择方法体的。
函数重载的条件:函数名相同,参数列表不同(形参数量不同,形参类型不一样,形参顺序不一样)
3.C++构造函数和析构函数调用虚函数时都不使用动态联编,在构造函数和析构函数中调用虚函数时,采用 静态联编。
如果在构造函数或析构函数中调用虚函数,则运行的是为构造函数或析构函数自身类型定义的版本。
详情看https://www.cnblogs.com/bonelee/p/5826196.html
四、纯虚函数
1.什么时候使用?
(1)当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;
2.定义格式
virtual 返回值类型 函数名(参数表)= 0;
含有纯虚函数的基类是不可以定义对象。纯虚函数无实现部分,只是作为一个占位符!!(告诉编译器,我先不去实现,在后面进行实现函数),不能产生对象,所以含有虚函数的类是抽象类。
子类中该虚方法前面的virtual 可加可不加 !
//Test1.h #include<iostream> using namespace std; class Fish { public: virtual void water() = 0; virtual void eat() = 0; }; class Shark : public Fish { public: void water();//子类中该虚方法前面的virtual 可加可不加 ! void eat(); }; void Shark::eat(){cout<<"Shark eat. "<<endl;} void Shark::water(){cout<<"Shark water. "<<endl;} void fun(Fish *f) { f->eat(); f->water(); }
3.纯虚函数需要注意
a.定义纯虚函数时,不能定义纯虚函数的实现部分。即使是函数体为空也不可以,函数体为空就可以执行,只是什么也不做就返回。而纯虚函数不能调用。
(其实可以写纯虚函数的实现部分,编译器也可以通过,但是永远也无法调用。因为其为抽象类,不能产生自己的对象,而且子类中一定会重写纯虚函数,因此该类的虚表内函数一定会被替换掉,所以可以说永远也调用不到纯虚函数本身)
b."=0"表明程序将不定义该函数,函数声明是为派生类保留一个位置。“=0”的本质是将指向函数体的指针定为NULL。
c.在派生类中必须重写虚函数,进行覆盖,这样的派生类才能用来定义对象。(如果不重写进行覆盖,程序会报错)
d.含有纯虚函数的基类不能实例化,因为是抽象类,不能产生自己的对象;但可以创建抽象类的对象变量,这个变量只能用来指向他的非抽象子类对象,
//Test.cpp #include"Test1.h" void main() { Shark s;//实例化一个派生类对象 Fish *f = &s; //基类指针指向派生类对象,而不是派生类的指针! 为了实现多态性 fun(f); }
运行结果:
参考
https://www.cnblogs.com/area-h-p/p/10374162.html ;
五、为什么要用基类指针指向派生类对象?
在基类与派生类之间,有一个规定:派生类对象的地址可以赋给指向基类对象的指针变量(简称基类指针),即基类指针也可以指向派生类对象。为什么有这一规定呢?因为它可以实现多态性,即向不同的对象发送同一个消息,不同的对象在接受时会产生不同的行为。而接受同一消息的实现就是基于基类指针。
例如
#include <iostream> using namespace std; class Shape { public: virtual double area() const = 0; //纯虚函数 }; class Square : public Shape { double size; public: Square(double s) { size = s; } virtual double area() const { return size * size; } }; class Circle : public Shape { double radius; public: Circle(double r) { radius = r; } virtual double area() const { return 3.14159 * radius * radius; } }; int main() { Shape* array[2]; //定义基类指针数组 Square Sq(2.0); //实例化派生类对象 Circle Cir(1.0); array[0] = &Sq; array[1] =&Cir; for (int i = 0; i < 2; i++) / { cout << array[i]->area() << endl; } return 0; }
上面的不同对象Sq,Cir(来自继承同一基类的不同派生类)接受同一消息(求面积,来自基类的成员函数area()),但是却根据自身情况调用不同的面积公式(执行了不同的行为,它是通过虚函数实现的)。
我们可以理解为,继承同一基类的不同派生对象,对来自基类的同一消息执行了不同的行为,这就是多态,它是通过继承和虚函数实现的。而接受同一消息的实现就是基于基类指针。