编程语言
首页 > 编程语言> > c++多态学习小记

c++多态学习小记

作者:互联网

联编

首先先了解啥是联编,先看书上的概念
联编: 是指确定函数调用和函数代码段之间的映射关系。
静态联编:是只在编译时确定了函数调用的具体操作对象;
动态联编:是只在程序运行过程中动态确定函数调用的具体对象。

换句话说
联编就是找到应该调用哪个函数的过程;
看个例子

#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++ 默认的类不采用虚析构函数呢,因为虚函数会导致额外的开销,对于不需要继承的类,声明为虚函数会浪费内存,故默认没有采用。。
由上可见,当需要该类作为基类时,一般采用虚析构函数;这样会导致内存泄漏。

标签:虚表,函数,联编,fish,多态,c++,animal,指针,小记
来源: https://blog.csdn.net/qq_35027690/article/details/117154412