类 —— 多态、抽象类

多态

通常说的多态,是指发生在类之间的多态。即相同的代码,实现不同的功能。

函数重载 ------ 静态多态/编译时多态。

类之间的多态 ------ 动态多态/运行时多态。

前提

继承、虚函数、函数重写。

函数重写(override)

在子类中重写父类的虚函数就是函数重写的过程,可以实现多态。

1、必须有继承关系;

2、父类中必须有虚函数。

虚函数(virtual)

只要基类中的某函数是虚函数,则所有继承自此基类的子类中该函数都是虚函数。

只有 普通成员函数 与 析构函数 可以声明为虚函数。

常规来说,给父类中的函数加上 virtual 关键字,定义成一个虚函数,那么在子类中,可以对父类的虚函数进行函数重写(override)。在 C++11中,可以在派生类的新覆盖的函数上使用一个关键字 override 来验证覆盖是否成功。

只要有虚函数的类,都会有一个虚函数表和一个虚(函数表)指针。

虚指针:指向虚函数表的指针;

虚函数表:存储所有的虚函数的信息(每个有虚函数的类都有专属的虚函数表)

虚函数表 :保存所有虚函数的入口地址,每一个包含虚函数的类都会有一张虚函数表。

如果发生继承关系,子类会先复制父类的虚函数表,如果子类对某个虚函数重写,就会更新虚函数表中该函数的入口地址。未被重写的虚函数,保持原样(和父类的虚函数一样)。
虚函数表指针:指向虚函数表的指针,父类中有一个虚函数表指针,子类中的虚函数表指针是从父类中继承下来的虚函数表指针,指向子类的虚函数表(虚函数表指针存在类中的第一个位置,占 4 字节)。

cpp 复制代码
#include <iostream>
using namespace std;

class Father
{
public:
    string secret;
    virtual void act_1()
    {
        cout << "Earn money. " << endl;
    }
};

class Son:public Father
{
public:
    string secret;
    void act_1() override		// 覆盖基类中的虚函数,virtual 可写可不写,override 验证覆盖是否成功
    {
        cout << "Save money. " << endl;
    }
};

int main()
{
    Father f;
    cout << &f << endl;				// 类中存在虚函数时,起始位置存放虚指针
    cout << &f.secret << endl;		// 类中第一个成员的地址,偏移首地址 4 个字节

    Son s;
    cout << &s << endl;				// 子类继承父类的虚指针(1 个)
    cout << &s.secret << endl;		// 类中第一个成员的地址,偏移首地址 8 个字节

    return 0;
}

虚析构函数

问题:由于实现多态 需要使用父类的指针 指向 子类的空间,父类指针可以操作的空间只能是父类自己的那部分,所以,在 delete 父类指针时,并不会释放掉子类的空间。

解决方法:给基类(父类)的析构函数前面加上 virtual 关键字,只要基类是虚析构函数,后面继承的所有子类都是虚析构函数,虚析构函数会引导父类的指针释放掉子类的空间。

cpp 复制代码
#include <iostream>
using namespace std;

class Father
{
    string secret;
public:
    virtual void act_1();	// 虚函数声明
    
    virtual ~Father()		
    {
        cout << "Destructor of father class. " << endl;
    }
};

void Father::act_1()		// 虚函数声明与定义分离时,virtual 只需要修饰在声明处
{
    cout << "Earn money. " << endl;
}

class Son:public Father
{
    string secret;

public:
    void act_1()			// 前面可以加 virtual,后面可以加 override
    {
        cout << "Save money. " << endl;
    }
    void act_2()			// 这里不能加 virtual 和 override,此函数非虚函数
    {
        cout << "Inherit money. " << endl;
    }
    virtual ~Son()			// 此 virtual 可以不加
    {
        cout << "Destructor of son class. " << endl;
        // 父类的析构前,不加 virtual,则 delete p 时,此函数不执行
    }
};

int main()
{
    Father *p = new Son;
    p->act_1();
    p->Father::act_1();
    delete p;

    return 0;
}

多态的实现

多态可以理解为"一种接口,多种状态"。只需要编写一个函数接口,根据传入的参数类型,执行不同的策略代码。

多态的使用时需要具有三个前提条件:

● 公有继承

● 函数覆盖

● 基类的 引用/指针 指向派生类对象

在代码运行时,通过对象的虚函数指针找到虚函数表,在表中定位到虚函数的调用地址,从而执行对应的虚函数内容。

使用时会产生一些额外的开销,优点是代码的编写更加灵活高效,缺点是会降低代码的执行速度。

cpp 复制代码
#include <iostream>
using namespace std;

class Animal
{
public:
    virtual void eat();
};

// 声明与定义分离。virtual 只需要修饰声明处
void Animal::eat()
{
    cout << "动物爱吃坤柳。" << endl;
}

class Dog:public Animal
{
public:
    // 覆盖基类中虚函数,派生类中 virtual 关键字可写可不写
    void eat() override 		// 验证虚函数覆盖是否成功
    {
        cout << "狗吃骨头。" << endl;
    }
};

class Cat:public Animal
{
public:
    void eat() // override
    {
        cout << "猫吃鱼。" << endl;
    }
};

void test_eat1(Animal &al)			// 基类的引用指向派生类对象
{
    al.eat();		
}

void test_eat2(Animal *al)			// 基类的指针指向派生类对象
{
    al->eat();		
}	

int main()
{
    Animal a1;
    Dog d1;
    Cat c1;
    test_eat1(a1);  		// 动物爱吃坤柳,基类的引用指向了基类的对象
    test_eat1(d1);  		// 狗吃骨头,基类的引用指向了 Dog类 的对象
    test_eat1(c1);  		// 猫吃鱼,基类的引用指向了 Cat类 的对象

    Animal *a2 = new Animal;    // 基类的指针指向了基类的对象
    Dog *d2 = new Dog;
    Cat *c2 = new Cat;
    test_eat2(a2);  // 动物爱吃坤柳
    test_eat2(d2);  // 狗吃骨头
    test_eat2(c2);  // 猫吃鱼

    return 0;
}

💡 练习

全局变量:int monster = 10000;

定义英雄类 hero,受保护的属性:string name,int hp,int attck;

公有的无参构造,有参构造,虚成员函数 void Atk(){blood-=0;};

法师类继承自英雄类,私有属性 int ap_atk=50;重写虚成员函数 void Atk(){blood-=(attck+ap_atk);};射手类继承自英雄类,私有属性 int ac_atk = 100;重写虚成员函数 void Atk(){blood-=(attck+ac_atk);}实例化类对象,判断怪物何时被杀死。

cpp 复制代码
#include <iostream>
using namespace std;

int He_Yanwei = 10000;

class Hero
{
protected:
    string name;
    int hp;
    int attack;
public:
    Hero(string name, int hp, int attack):name(name), hp(hp), attack(attack) { }
    virtual void atk()
    {
        He_Yanwei -= 0;
    }
};

class Mages:public Hero
{
    int ap_atk = 50;
public:
    Mages():Hero("You_Changzhi", 10000000000, 1000) { }
    Mages(string name, int hp, int attack, int ap_atk):Hero(name, hp, attack), ap_atk(ap_atk) { }
    void atk()
    {
        He_Yanwei -= (attack + ap_atk);
        cout << "-" << (attack + ap_atk) << endl;
    }
};

class Shooters:public Hero
{
    int ac_atk = 100;
public:
    Shooters():Hero("Zou_Jinqi", 100000000, 1000) { }
    Shooters(string name, int hp, int attack, int ac_atk):Hero(name, hp, attack), ac_atk(ac_atk) {}
    void atk()
    {
        He_Yanwei -= (attack + ac_atk);
        cout << "-" << (attack + ac_atk) << endl;
    }
};

int main()
{
    Mages mage("You_Changzhi", 10000000000, 1000, 50);
    Shooters shooter("Zou_Jinqi", 100000000, 1000, 100);
    int i = 1;
    cout << "He_Yanwei: " << He_Yanwei << endl;
    cout << "-----------------------" << endl;

    while (He_Yanwei >= 0)
    {
        cout << "Round " << i++ << ": " << endl;

        shooter.atk();
        if (He_Yanwei <= 0)
            break;
        cout << "He_Yanwei: " << He_Yanwei << endl;

        mage.atk();
        if (He_Yanwei <= 0)
            break;
        cout << "He_Yanwei: " << He_Yanwei << endl;

        shooter.atk();
        if (He_Yanwei <= 0)
            break;
        cout << "He_Yanwei: " << He_Yanwei << endl;

        cout << "-----------------------" << endl;
    }
//    cout << "-----------------------" << endl;			// 加上就有问题,离谱
    cout << "You killed He Yanwei! " << endl;

    return 0;
}

纯虚函数和抽象类

纯虚函数

virtual 返回值 函数名(参数列表) = 0;

抽象类

包含纯虚函数的类就是抽象类,抽象类不允许实例化类对象。抽象类的析构函数必须是虚析构函数。

抽象类通常作为一个类型的模板(作为基类存在),不能实例化类对象(没有意义)。在继承后的子类中,加上具体的属性后,重写父类中的纯虚函数就可以实例化类对象了;如果子类中不重写父类的纯虚函数,那么子类也不能实例化类对象。

抽象类的子类重写纯虚函数:该子类不再是抽象类。(水果 ------> 苹果、香蕉、草莓...)

抽象类的子类不重写纯虚函数:该子类依然是抽象类。(水果 ------> 热带水果、温带水果...)

● 抽象类支持多态,可以存在引用或指针的声明格式。

● 因为抽象类的作用是指定算法框架,因此在一个继承体系中,抽象类的内容相对丰富且重要。

cpp 复制代码
#include <iostream>
using namespace std;

class Test
{
public:
    virtual void show() = 0;     		// show 函数是一个纯虚函数
};

class T:public Test
{
public:
    void show()
    {
        cout << "T" << endl;
    }
};

int main()
{
    T t1;			// 可以定义了;若不对 父类的纯虚函数 重写,则此行报错
    
    return 0;
}

💡 练习

定义抽象类 Animal,私有成员 string name,string colour,公有纯虚函数 void sound(),

定义 Cat 类,继承自 Animal 类,重写 sound 函数,

定义 Dog 类,继承自 Animal 类,重写 sound 函数,实现多态现象的测试。

(定义一个全局函数,可以实现两个子类 sound 功能的测试,要求:全局函数只有一个参数)

cpp 复制代码
#include <iostream>
using namespace std;

class Animal
{
    string species;
    string color;
public:
    virtual void sound() = 0;
};

class Cat:public Animal
{
public:
    void sound()
    {
        cout << "喵~喵~" << endl;
    }
};

class Dog:public Animal
{
public:
    void sound()
    {
        cout << "汪~汪~" << endl;
    }
};

class Sheep:public Animal				// 1、公有继承
{
public:
    void sound()						// 2、重写父类的虚函数
    {
        cout << "咩~咩~" << endl;
    }
};

void choice(Animal *ani)				
{
    ani->sound();						// 3、基类的 指针或引用 指向 派生类对象
}

int main()
{
    Cat c;
    choice(&c);
    Dog d;
    choice(&d);
    Sheep sh;
    choice(&sh);

    return 0;
}
相关推荐
XiaoLeisj41 分钟前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师2 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉2 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer2 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq2 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java4 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山4 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
青花瓷4 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~4 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程4 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust