菜鸟学习历程【31】多态

多态

概念:同样的调用语句有多种不同的表现形式。
通俗的来说:根据传入的对象类型的不同,调用不同的派生类的相应函数。

多态实现的条件
1.要存在继承关系;
2.对虚函数的重写;
3.基类指针(引用)指向派生类对象

静态联编与动态联编

静态联编:程序匹配,连接在编译阶段实现,也称为早期联编(在编译的时候,就知道了该去调用谁)
例如:函数重载

动态联编:程序联编推迟到运行时进行,也称为晚期联编(在执行时,才会知道调用那一个函数)
例如:switch 和 if 语句

我们通过动物类来说明:

#include <iostream>

using namespace std;

// 动物: 基类
class Animal
{
public:
    Animal(int age, char *name)
    {
        this->age  = age;
        this->name = name;
    }
    void sleep()
    {
        printf ("动物睡觉\n");
    }

    void print()
    {
        printf ("age = %d, name = %s\n", age, name);
    }
private:
    int age;
    char *name;
};

class Cat:public Animal
{
public:
    Cat(int age, char *name):Animal(age, name)
    {

    }
    void sleep()
    {
        printf ("猫 趴着睡觉\n");
    }
};

class Fish:public Animal
{
public:
    Fish(int age, char *name):Animal(age, name)
    {

    }
    void sleep()
    {
        printf ("鱼 睁着眼睡觉\n");
    }
};

void func(Animal *p)
{
    p->sleep();
}

int main()
{
    Animal *pa = new Animal(2, "动物");
    Cat    *pc = new Cat(3, "猫");
    Fish   *pf = new Fish(2, "鱼");

    func(pa);   //动物睡觉
    func(pc);   //动物睡觉
    func(pf);   //动物睡觉

    return 0;
}

分析:
基类中的 sleep()函数是一个普通类型的函数,并且 func 的形参为 Animal 类的指针,虽然 pc 和 pf 是一个指向 猫类 和 鱼类 的指针,调用函数 func 时,基类指针指向了派生类的对象,基类指针的本质依旧是 Animal*。因此,在编译的时候就明确了,该调用哪一个函数,这是一个静态联编。
实际上,我们希望能根据传入的指针的不同,调用不同的sleep();
那么,我们需要使用 virtual 来修饰基类的成员函数,以此来实现多态。

#include <iostream>

using namespace std;

// 动物: 基类
class Animal
{
public:
    Animal(int age, char *name)
    {
        this->age  = age;
        this->name = name;
    }
    virtual void sleep()
    {
        printf ("动物睡觉\n");
    }

    void print()
    {
        printf ("age = %d, name = %s\n", age, name);
    }
private:
    int age;
    char *name;
};

class Cat:public Animal
{
public:
    Cat(int age, char *name):Animal(age, name)
    {

    }
    void sleep()
    {
        printf ("猫 趴着睡觉\n");
    }
};

class Fish:public Animal
{
public:
    Fish(int age, char *name):Animal(age, name)
    {

    }
    void sleep()
    {
        printf ("鱼 睁着眼睡觉\n");
    }
};

void func(Animal *p)
{
    p->sleep();
}

int main()
{
    Animal *pa = new Animal(2, "动物");
    Cat    *pc = new Cat(3, "猫");
    Fish   *pf = new Fish(2, "鱼");

    func(pa);  // 动物睡觉
    func(pc);  // 猫 趴着睡觉
    func(pf);  // 鱼 睁着眼睡觉

    return 0;
}

此时,我们将基类中的 sleep() 函数声明为虚函数后,再通过基类指针去调用函数时,会根据传入对象的不同,找到相应的函数来执行。
多态的原理,随后为大家讲解。


虚析构函数

通过基类指针释放派生类对象, 基类的析构函数一定要是 虚析构函数。
举个例子说明一下:

#include <iostream>

using namespace std;

// 基类
class A
{
public:
    A()
    {
        printf ("A 的构造函数\n");
    }
    ~A()
    {
        printf ("A 的析构函数\n");
    }
};

class B:public A
{
public:
    B()
    {
        p = new char[20];
        printf ("B 的构造函数\n");
    }
    ~B()
    {
        if (p != NULL)
            delete[] p;
        printf ("B 的析构函数\n");
    }

private:
    char *p;
};

void func(A *pa)
{
    delete pa;
}


int main()
{
    A *pa = new B;
    func(pa); 

    return 0;
}

运行结果是:
A 的构造函数
B 的构造函数
A 的析构函数
发现,没有了B的析构函数,这样会造成内存的泄漏,为什么会出现这种现象呢?

首先,我们得明确构造和析构的一个基本特性:

构造函数:从当前类往上找 父类 ⇒ 最上层的父类, 从最上层的父类开始构造(调用构造函数),类似于前序递归
析构函数:从当前类开始析构 析构完 沿着继承路径往上找父类 析构父类 ⇒ 找到最上层的父类 析构,类似于后序递归

那么,对于上面的程序的结果,原因在于,执行语句“A *pa = new B;”定义了一个 B 类的对象,那么会先调用 基类的 构造函数,再调用自身的构造函数,再调用 func()函数时,释放的是基类的指针,它认为基类的指针是当前的对象,所以调用的是基类的构造函数。

Tips:

char *ptr = "abcdefg";

void printS(char *p) 
{   
    if (*p == '\0') 
        return ;
    printf ("%c", *p);

    printS(p+1);   // 递归
    //printf ("%c", *p);
}

// 先打印,再递归的,称为后序递归,打印结果:abcdefg
// 先递归,再打印的,称为前序递归,打印结果:gfedcba

为了解决这个问题,我们只需要将基类的析构函数声明为虚函数

class A
{
public:
    A()
    {
        printf ("A 的构造函数\n");
    }
    virtual ~A()
    {
        printf ("A 的析构函数\n");
    }
};

多态的原理

多态通过一个虚函数指针(vfptr)来实现。这个虚函数指针指向一个虚函数表,这张表里存放了该类对应的虚函数。
我们还是使用动物类来说明:

class Animal
{
public:
    Animal(int age, char *name)
    {
        this->age  = age;
        this->name = name;
    }
    virtual void sleep()
    {
        printf ("动物睡觉\n");
    }
    virtual void eat()
    {
        printf ("动物吃饭\n");
    }
    void print()
    {
        printf ("age = %d, name = %s\n", age, name);
    }
private:
    int age;
    char *name;
};

class Cat:public Animal
{
public:
    Cat(int age, char *name):Animal(age, name)
    {

    }
    void sleep()
    {
        printf ("猫 趴着睡觉\n");
    }
};

我们知道 基类中的 sleep() 和 eat()函数是一个虚函数,那么基类的虚函数指针就会指向一张虚函数表,这张表由编译器保管。
这里写图片描述

那么,当 Cat 类继承 Animal 时,由于 Cat 类中有与 Animal 类中的虚函数同名的函数,因此 Cat类中的同名函数也是虚函数,但与基类有所不同的是,派生类中的虚函数指针不在指向基类中虚函数表,而是指向自己的虚函数表。
这里写图片描述
因此,当调用 func()函数时,编译器会根据对象虚函数指针找到对应的虚函数表,再找到虚函数。
注意:由于虚函数在编译的时候,并不知道谁在调用,所以会在执行的时候才会确定,因此是动态联编,并且效率会低于一般的函数


虚继承与虚函数共存的情况

(以下内存模型的结果,本人通过 VS2010 和 VS2015进行验证)
1.基类有虚函数,派生类中没有虚函数,普通继承时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    return 0;
}

执行结果:
sizeof b is 12
AA 的print函数

来看一下内存模型:
这里写图片描述
我们看到对象 b 所占内存为 12 字节,查看一下内存分布可以看出来,地址从低到高依次是 vfptr,a,b,此时虚函数表里存放的是 AA 中的 print 函数。

2.基类有虚函数,派生类中没有虚函数,虚继承时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :virtual public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    return 0;
}

执行结果:
sizeof b is 16
AA 的print函数

内存模型:

这里写图片描述
在这里,说明一下虚函数指针的存放位置,一般 vfptr 都会与自身类的成员在内存上连续存放,就像这里的 vfptr 和 a

3.基类没有虚函数,派生类中有虚函数,普通继承时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    return 0;
}

运行结果:
sizeof b is 12

内存分析:
这里写图片描述

4.基类没有虚函数,派生类中有虚函数,虚继承时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :virtual public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    return 0;
}

运行结果:
sizeof b is 16
内存分析:
这里写图片描述

5.基类有虚函数,派生类中有虚函数,普通继承时
(1)派生类 中的 虚函数 与 基类 中的虚函数不同名时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    return 0;
}

运行结果:
sizeof b is 12
AA 的print函数

内存模型:
这里写图片描述

(2)派生类 中的 虚函数 与 基类 中的虚函数同名时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    b.show();
    return 0;
}

运行结果:
sizeof b is 12
BB 的print函数
BB 的show函数
这里写图片描述

这两种情况其实是一样的,唯一的区别在于 虚函数指针 指向的虚函数列表中虚函数不同,当派生类中没有与基类虚函数同名的函数时,虚函数列表里有 基类 的虚函数,但当派生类中与基类虚函数同名的函数时,虚函数列表里有 派生类 的虚函数

6.基类有虚函数,派生类中有虚函数,虚继承时
(1)派生类 中的 虚函数 与 基类 中的虚函数不同名时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :virtual public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    b.show();
    return 0;
}

运行结果:
sizeof b is 20
AA 的print函数
BB 的show函数

内存分析:
此时的 虚基类指针 的第一个参数为 -4 ,因为当前对象的 虚函数指针 在最上面。对象 b 的虚函数表中有两个函数,一个是 基类的 print 函数,一个是 自己的 show 函数,并且 自己的 show 函数 在内存中的位置 在基类的 print 函数之前,而AA 的虚函数表中 只有自己的 print 函数。
这里写图片描述

(2)派生类 中的 虚函数 与 基类 中的虚函数同名时

#include <iostream>

using namespace std;

class AA
{
public:
    AA(int a = 0)
    {
        this->a = a;
    }
    virtual void print()
    {
        printf("AA 的print函数\n");
    }
private:
    int a;
};

class BB :virtual public AA
{
public:
    BB(int b) :AA(1)
    {
        this->b = b;
    }
    virtual void print()
    {
        printf("BB 的print函数\n");
    }
    virtual void show()
    {
        printf("BB 的show函数\n");
    }
private:
    int b;
};

int main()
{
    BB b(2);

    printf("sizeof b is %d\n", sizeof(b));

    b.print();
    b.show();
    return 0;
}

运行结果:
sizeof b is 24
BB 的print函数
BB 的show函数

内存分析:
这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值