C++类和对象_06----多态

多态是C++面向对象三大特性之一,即具有多种形态。可以理解为,同一种行为会产生不同的具体状态/操作。具体到C++中,就是函数调用的多种形态。
多态分为两类
静态多态: 函数重载运算符重载属于静态多态,复用函数名
动态多态: 派生类和虚函数实现运行时多态,主要指父类的指针或引用调用、重写虚函数。(如果父类的指针或引用指向父类,就调用父类的虚函数;如果父类的指针或引用指向某个子类,就调用那个子类的虚函数)
静态多态和动态多态区别:
静态多态的函数地址早绑定 - 编译阶段确定函数地址
动态多态的函数地址晚绑定 - 运行阶段确定函数地址

1 多态的基本概念

1.1 多态的构成条件

多态是指不同继承关系的类对象,调用同一函数时产生了不同的行为。

在继承中要构成多态需满足两个条件:
① 必须通过父类的指针或者引用调用虚函数
被调用的函数必须是虚函数且派生类必须对父类的虚函数进行重写

1.2 静态多态

静态多态:主要指函数重载。

class Animal
{
public:
    void Speak()
    {
        cout << "父类 --- 动物类在说话" << endl;
    }
};
class Cat : public Animal
{
public:
    void Speak()   //这里是函数重写:函数返回值相同、函数名相同、参数列表相同
    {
        cout << "子类--- 猫类在说话  --- 静态多态" << endl;
    }
};
void DoSpeak(Animal & animal)
{
	animal.speak();
}
void polymorphism_basic()
{
    Cat cat1;
    doSpeak(cat1);
    // 在该函数中, Animal &animal = cat1, 输出为"父类 --- 动物类在说话"
    // 是因为doSpeak()函数属于静态多态的函数,地址早绑定 - 编译阶段确定函数地址,因此,不管传什么参数,始终走得就是“父类”
}

在这里插入图片描述

注:在doSpeak(cat1)中:Animal &animal = cat1, 输出为"父类 — 动物类在说话"。是因为doSpeak()函数属于静态多态的函数,地址早绑定 - 编译阶段确定函数地址,因此,不管传什么参数,始终走得就是“父类”

1.3 动态多态

class Animal
{
public:
    //函数前面加上virtual关键字,变成虚函数
    virtual void Speak()
    {
        cout << "父类 --- 动物类在说话 --- 动态多态" << endl;
    }
};
class Dog : public Animal
{
public:
    /*
    
    */
    virtual void Speak()
    {
        cout << "子类--- 狗类在说话 --- 动态多态" << endl;
    }
};
void doSpeak(Animal &Animal)
{
    Animal.Speak();
}
void polymorphism_basic()
{
    Dog dog1;
    doSpeak(dog1);
}

在这里插入图片描述
多态的两个条件
①virtual关键字和虚函数:在父类成员函数Speak() 前加virtual关键字后,该函数就变为虚函数;
② 在子类Dog中重写父类虚函数(子类重写时,virtual关键字可加可不加)

注意
① 只有类的非静态成员函数才可以成为虚函数。
② **虚函数和虚继承都用到了virtual关键字,但二者之间没有任何关系。**虚函数使用virtual是为了实现多态,而虚继承是为了解决菱形继承中产生的数据冗余和二义性问题,它们之间没有关联。
虚函数重写
派生类中有一个跟基类完全相同的虚函数,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,这时称子类的虚函数重写了基类的虚函数。

1.4 虚函数重写注意事项

class Animal
{
public:
    virtual void Speak()
    {
        cout << "父类 --- 动物类在说话" << endl;
    }
};
class Dog : public Animal
{
public:
    virtual void Speak()   //虚函数重写
    {
        cout << "子类 --- 狗狗类在说话" << endl;
    }
};
void DoSpeak(Animal &animal)
{
    animal.Speak();   // 调用"同一个"成员函数(实际不是同一个)
}
int test06()
{
    Animal a;
    Dog d;

    //父类的引用接收不同类型的对象,实现了多态
    DoSpeak(a);
    DoSpeak(d);

    return 0;
}

在这里插入图片描述
上面代码中DoSpeak()函数的参数a是父类的引用,接收不同类型的对象时调用虚函数产生了不同的结果,如果将参数改为父类的指针也可以实现同样的效果,但是如果是父类的对象则不可以。

class Animal
{
public:
    virtual void Speak()
    {
        cout << "父类 --- 动物类在说话" << endl;
    }
};
class Dog : public Animal
{
public:
    virtual void Speak()   //虚函数重写
    {
        cout << "子类 --- 狗狗类在说话" << endl;
    }
};

void func1(Animal &animal) //父类引用
{
    animal.Speak(); // 调用"同一个"成员函数(实际不是同一个)
}
void func2(Animal *animal) //父类指针
{
    animal->Speak();
}
void func3(Animal animal) //父类对象
{
    animal.Speak();
}

int test06()
{
    Animal a;
    Dog d;

    //父类的引用接收不同类型的对象,实现了多态
    func1(a);
    func1(d);
    cout << endl;

    //父类的指针接收不同类型的对象,实现了多态
    func2(&a);
    func2(&d);
    cout << endl;

    //父类的对象接收不同类型的对象,无法实现多态
    func3(a);
    func3(d);

    return 0;
}

在这里插入图片描述

很显然以父类的对象为参数无法实现多态,这也印证了最开始提到的,只有父类的引用或指针才可以实现多态。

1.5 总结

1.5.1 多态满足条件

① 有继承关系
子类重写父类中的虚函数

1.5.2 多态使用条件

父类指针或引用 指向子类对象

1.5.3 重写

函数返回值类型 函数名 参数列表 完全一致称为重写

2 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数

2.1 纯虚函数

语法: virtual 返回值类型 函数名 (参数列表)= 0 ;

当类中有了纯虚函数,这个类也称为抽象类

2.2 抽象类特点

(1)无法实例化对象
(2)子类必须重写抽象类中的纯虚函数,否则也属于抽象类

class Base  //抽象类
{
public:
    virtual void func() = 0; //纯虚函数
};
class Son :public Base
{
public:
    virtual void func()
    {
        cout << "子类重写父类中的纯虚函数" << endl;
    }
};
void polymorphism_basic()
{
    //利用多态调用
    Base *base = new Son;
    base->func();
    delete base;
}

2.2.1 抽象类特点(1)无法实例化对象

void polymorphism_basic()
{
    //抽象类特点:(1)无法实例化对象
    Base b1; //ERR
    Base * base = new Base; // 错误,抽象类无法实例化对象
}

在这里插入图片描述

2.2.2 抽象类特点(2)子类必须重写抽象类中的纯虚函数,否则也属于抽象类

void polymorphism_basic()
{
    //抽象类特点:(2)子类必须重写抽象类中的纯虚函数,否则也属于抽象类
    Son s1;
    s1.func();
}

在这里插入图片描述

2.3 接口继承和实现继承

普通函数的继承是一种实现继承,子类继承了父类函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。尤其是抽象类的产生,更是强制子类重写父类(否则子类也无法实例化出对象,类的功能大打折扣)。

3虚析构和纯虚析构

问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构语法
virtual ~类名(){}

纯虚析构语法
virtual ~类名() = 0;
类名::~类名(){}

3.1 普通析构函数

class MyAnimal
{
public:
    MyAnimal()
    {
        cout << "MyAnimal 构造函数调用" << endl;
    }

    virtual void Speak() = 0;

    ~MyAnimal() //利用虚析构可以解决父类指针释放子类对象时,释放不干净的问题
    {
        cout << "MyAnimal 析构函数调用" << endl;
    }

};
class MyCat : public MyAnimal
{
public:
    MyCat(string name)
    {
        cout << "MyCat 构造函数调用" << endl;
        m_Name = new string(name);
    }
    virtual void Speak()
    {
        cout << *m_Name << "小猫在说话" << endl;
    }
    ~MyCat()
    {
        if (m_Name != NULL)
        {
            cout << "MyCat 析构函数调用" << endl;
            delete m_Name;
            m_Name = NULL;
        }
    }
    string *m_Name;
};
void test06()
{
    MyAnimal *animal = new MyCat("Tom");
    animal->Speak();
    delete animal;
}

在这里插入图片描述
从打印结果来看,animal指针在delete时仅调用了父类的析构函数,而它在new时申请的是Cat的空间,这样就可能造成内存泄漏。
问题:通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
解决方式:给基类增加一个虚析构函数/纯虚析构函数
虚析构函数就是用来解决通过父类指针释放子类对象

3.2 给基类增加虚析构函数

class MyAnimal
{
public:
    MyAnimal()
    {
        cout << "MyAnimal 构造函数调用" << endl;
    }
    virtual void Speak() = 0;

    virtual ~MyAnimal() //利用虚析构可以解决父类指针释放子类对象时,释放不干净的问题
    {
        cout << "MyAnimal 析构函数调用" << endl;
    }
};

在这里插入图片描述

3.3 给基类增加纯虚析构函数

注意:纯虚析构 — 需要声明,也需要类外实现;有纯虚析构时,这个类也属于抽象类(无法实例化对象)

class MyAnimal
{
public:
    MyAnimal()
    {
        cout << "MyAnimal 构造函数调用" << endl;
    }

    virtual void Speak() = 0;

    virtual ~MyAnimal() = 0; 
};
MyAnimal:: ~MyAnimal()
{
    cout << "MyAnimal 纯虚析构函数调用" << endl;
}

在这里插入图片描述

3.4 虚析构和纯虚析构共性&区别

虚析构和纯虚析构共性:
① 可以解决父类指针释放子类对象
② 都需要有具体的函数实现
虚析构和纯虚析构区别:
如果是纯虚析构,该类属于抽象类,无法实例化对象

4 多态原理

在虚函数实现时,内存空间会有一个虚函数表(vftable)和虚函数表指针(vfptr)

4.1 虚函数表指针

class A
{
public:
	virtual void Func()
	{
		cout << "virtual void Func()" << endl;
	}
private:
	int _b = 0;
	char _ch = '\0';
};

int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

根据结构体 中内存对齐的规则可得该类定义出的对象的大小为8字节,但下面的运行结果是12字节。
在这里插入图片描述
通过调试看到对象a中不仅仅有类中定义的两个变量,还有一个void类型的指针,vfptr指针是虚函数表指针,对象a大小为12字节正是因为多了这一个指针变量。
在这里插入图片描述
只要类中有虚函数,就一定会存在虚函数表指针,它就是用来实现多态的。

4.2 虚函数表

一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(本质是一个数组)中,虚表指针指向的就是这个数组的起始地址。

class A
{
public:
	virtual void Func() //虚函数
	{
		cout << "virtual void Func()" << endl;
	}
	
	virtual void Func2() //虚函数
	{}

	void Func3() //普通函数
	{}
private:
	int _b = 0;
	char _ch = '\0';
};

int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

可以看到,类中定义了两个虚函数,则对象中含有虚表指针指向的数组中有两个元素,而普通函数则没有对应的虚表指针
在这里插入图片描述

4.3 多态原理

在这里插入图片描述
① 父类的虚函数表内记录虚函数的地址;
② 当不同子类重写父类的虚函数时,各个子类的虚函数表内会替换成各个子类的虚函数的地址;
③ 当父类指针或引用指向子类对象的时候,发生多态
在这里插入图片描述在这里插入图片描述

结论
通过调试可以看到,animal1和animal2中的虚表指针指向的地址不同,也就是说两个类中定义的speak()和析构函数不是同一个,所以在调用时调用各自定义的函数,实现了多态。

总结
1、满足多态条件、构成多态后,父类的指针或引用调用哪个虚函数,不是在编译时确定的,而是运行到指定位置、需要从指针或引用指向的对象的虚表中找虚函数地址时才确定的。所以如果指向的是父类对象,则调用父类的虚函数;指向的是子类对象,则调用子类的虚函数。

2、如果不满足多态条件,那么调用哪个函数是在编译是就可以确定的,调用函数的对象是什么类型,就调用哪个类型定义的虚函数,与传什么类型的参数无关。

5 静态绑定和动态绑定

静态绑定又称为前期绑定(早绑定),即在程序编译期间就确定了程序的行为,也称为静态多态比如本文最开始提到的函数重载
动态绑定又称后期绑定(晚绑定),即在程序运行期间根据具体的类型来确定程序的具体行为,并确定具体调用哪个函数,也称为动态多态

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值