【C++】 认识多态 + 多态的构成条件详细讲解

前言
C

1. 多态的概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态. 一个接口,多种行为。
重写其实是一种接口继承

2 多态的定义及实现

2 .1 虚函数:

虚函数: 即被 virtual 修饰的类成员函数称为虚函数。

class Person
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

2 .2 虚函数的重写:

虚函数的重写(覆盖):

  • 派生类中有一个跟基类完全相同的虚函数
  • 即派生类虚函数与基类虚函数的返回值类型函数名字参数列表完全相同(三同)
  • 称子类的虚函数(重写)了基类的虚函数,也叫(覆盖)
    不符合重写,就构成隐藏。。。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:子类虚函数不加Virtual,依旧构成重写(实际最好加上)*/
2 .2.1 虚函数重写的两个例外:
  1. 协变—基类与派生类虚函数返回值类型不同:

派生类重写基类虚函数时,与基类虚函数返回值类型不同。
即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变

  • [ ] 协变的返回值类型可以不同,要求必须是(父子关系)的指针和引用
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};

2.析构函数的重写

  • 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同
  • 虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};

2 .3 多态的两个条件(重点)

  • 必须通过基类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    在这里插入图片描述

2 .4 析构函数为啥写成虚函数

1.析构函数不构成多态的情况:
在这里,析构函数默认是隐藏关系

class Person {
public:
         ~ Person () 
           {
               cout << "~ Person( )" << endl;
           }
};
class Student : public Person {
public:
       ~ Student () 
           {
               cout << "~ Student ( )" << endl;
           }
 };

2.析构函数构成多态:

class Person {
public:
       virtual  ~ Person () 
           {
               cout << "~ Person( )" << endl;
           }
};
class Student : public Person {
public:
      virtual ~ Student () 
           {
               cout << "~ Student ( )" << endl;
           }
 };

其析构函数最好定义为虚函数。见下面的例子:

  • 指向谁,就调用谁析构。
    在这里插入图片描述
    在这里插入图片描述

3 新增的两个关键字

  • C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

3.1 final的使用:

final:修饰虚函数,表示该虚函数不能再被重写。

class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};

3.2 override :

检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

重载、覆盖(重写)、隐藏(重定义)的对比
在这里插入图片描述

  • 同名的成员变量也是隐藏的关系

4 抽象类

4.1 概念

  • 在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
  • 包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象
  • 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承
class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};

5 多态的原理

class Base
{
public:
    virtual void Func1()
{
   cout << "Func1()" << endl;
}
private:
int _b = 1;
};

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些
平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针
在这里插入图片描述

同一类型的对象共用一个虚表
不管是否完成重写,子类虚表和父类虚表都不是同一个

  • 普通函数调用,编译链接时确定函数的地址,运行时直接调用。
  • 多态调用,程序运行时去指向对象的虚表中找到函数的地址,进行调用。

总结:
多态的本质原理,当符合多态的两个条件时,
那么调用时,会到指向对象的虚表中找到对应的虚函数地址,进行调用

5.1 虚函数表存在那个区域

虚表存在哪个区域?

  • 虚表应该是一个类型共用一个虚表
  • 虚表应该存在一个长期存储的区域,是在常量区/代码区存着

6 inline函数可以是虚函数吗?

总结:

  • C++在这里处理很灵活
  • 只要是构成多态的时候,那么就会忽略掉内联的属性
  • 既是保存了虚函数重写的多态特性,又保存了内联 - 这是不可能的
  • 如果是多态属性的话那么内联的属性就丢了!!
  • 因为要将函数地址放在虚表里面去。

7 静态成员函数不可以是虚函数

语法直接限制死了

  • 静态成员函数不可能满足多态,不可能完成虚函数的重写,也不可能满足多态的运行时决议
  • 因为静态成员函数没有this指针

8 构造函数可以是虚函数

答案是:不可以

  • 虚函数表在编译阶段就生成的。
  • 但虚函数表是在构造函数阶段才去初始化的。(调用构造函数就需要去找vptr,但此时vptr还没有初始化,对象内存空间都还没有)
  • 调用构造函数后, 才能生成一个对象。 假设构造函数是虚函数, 虚函数存在于虚函数表中, 而去找虚函数表又需要虚函数表指针, 而虚函数表指针又存在于对象中, 这样就矛盾了: 都没有生成对象, 哪有什么虚函数表指针呢?

9 析构函数可以是虚函数

答案是:可以!并且必须是

  • 因为基类的指针有可能指向父类对象,也可能指向子类对象
  • 如果不是析构函数,就会有问题
    代码举例:
class Person
{
public:
     virtual ~Person() 
     {  cout<<"~Person()"<<endl;
     }
};
class Student:public Person
{
public:
    virtual ~Student() 
    {
       cout << "~Student()" << endl;
    }
};

int main()
{
   Person* ptr1=new Person;
   delete ptr1;
//如果析构函数不是虚函数,当基类指针指向父类时,就会报错
   Person* ptr2=new Student;
   delete ptr2;


}

尾声
看到这里,相信大家对这个C++有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦

多态C++面向对象编程的重要特性之一,它可以让我们在编写程序时更加灵活和易于扩展。下面我们来看一个简单的多态案例。 假设我们有一个动物类(Animal),其有一个虚函数makeSound(),用于输出动物的叫声。然后我们派生出几个动物子类(如狗Dog、猫Cat等),它们都重写了makeSound()函数输出自己的叫声。最后我们编写一个测试程序,用于创建不同的动物对象,并调用它们的makeSound()函数输出叫声。 下面是详细的代码及讲解: ```cpp #include <iostream> using namespace std; // 动物类 class Animal { public: virtual void makeSound() { cout << "动物叫声..." << endl; } }; // 狗类 class Dog : public Animal { public: virtual void makeSound() { cout << "汪汪汪..." << endl; } }; // 猫类 class Cat : public Animal { public: virtual void makeSound() { cout << "喵喵喵..." << endl; } }; // 测试函数 void test(Animal* animal) { animal->makeSound(); } // 主函数 int main() { Animal* animal1 = new Animal(); Animal* animal2 = new Dog(); Animal* animal3 = new Cat(); test(animal1); test(animal2); test(animal3); delete animal1; delete animal2; delete animal3; return 0; } ``` 上述代码,Animal类是一个基类,我们将makeSound()函数声明为虚函数,表示它可以被子类重写。Dog和Cat类分别继承自Animal类,并重写了makeSound()函数,输出不同的叫声。 在测试函数test(),我们将传入的动物指针参数强制转换为Animal类型,然后调用它们的makeSound()函数。这里需要注意的是,我们将动物指针参数声明为Animal*类型而不是Dog*或Cat*类型,这是因为在多态,我们通常使用基类指针或引用来指向派生类对象,这样可以让我们更加灵活地操作对象。 在主函数,我们创建了三个动物对象,分别为基类对象、狗类对象和猫类对象,并将它们作为参数传递给test()函数进行测试。由于makeSound()函数被声明为虚函数,因此在调用test()函数时会根据传入的实际对象类型来动态调用对应的函数,这就是多态的体现。 最后需要注意的是,在程序结束时我们需要释放动态分配的内存,以免出现内存泄漏。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值