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)这个方法必须在派生类(derived class)中被实现;
  如果满足以上两点,可以考虑将该方法申明为纯虚函数。
 
在基类中定义虚函数,以便在派生类中重新定义该函数(方法覆盖更好地适用于对象,但在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
纯虚函数是指被表明为不具体实现的虚拟成员函数,定义一个基类时,遇到无法定义基类中虚函数的具体实现,其实现依赖于不同的派生类。

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()),但是却根据自身情况调用不同的面积公式(执行了不同的行为,它是通过虚函数实现的)。

我们可以理解为,继承同一基类的不同派生对象,对来自基类的同一消息执行了不同的行为,这就是多态,它是通过继承和虚函数实现的。而接受同一消息的实现就是基于基类指针

posted @ 2020-04-22 22:19  何梦吉他  阅读(855)  评论(0编辑  收藏  举报