多态【C++】机制详解

概念

什么是多态?多态简单来说就是多种形态,同一个事情去完成某个行为,在面向不同的对象的时候会产生不同的状态。

就比方说:一些游戏中游戏会有用相同的接口不同的角色上调用的时候就会产生不同的行为(不同类型的角色会产生不同的行为如火元素,水元素,风场等)

多态的定义和实现

多态的构成

这个就是一个多态的简单例子:

#include <iostream>
using namespace std;

class Fyro
{
public:
	virtual void effect()
	{
		cout << "火球" << endl;
	}
};

class Water:public Fyro
{
public:
	virtual void effect()
	{
		cout << "水龙" << endl;
	}
};

void User(Fyro &person)
{
	person.effect();
}

int main()
{
	Fyro A;
	Water B;

	User(A);
	User(B);
	return 0;
}

当元素类型不同的A/B调用effect()这个函数会对应大新不同的状态
在这里插入图片描述

构成多态需要两个条件

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

虚函数

虚函数就是被virtual修饰的函数

class Fyro
{
public:
	virtual void effect()
	{
		cout << "火球" << endl;
	}
};

effect()就是一个虚函数

虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Fyro
{
public:
	virtual void effect()
	{
		cout << "火球" << endl;
	}
};

class Water:public Fyro
{
public:
	virtual void effect()
	{
		cout << "水龙" << endl;
	}
};

:在派生类中不写virtual的时候虚函数也重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是不写virtual容易产生误会并且书写不规范不建议省略virtual

虚函数重写的两个例外

协变(基类与派生类虚函数返回值类型不同) 注:很少遇到了解下就可以

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

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;}
};
析构函数的重写(基类与派生类析构函数的名字不同)

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

#include <iostream>
using namespace std;

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

class Water :public Fyro
{
public:
	virtual ~Water()
	{
		cout << "~Water()" << endl;
	}
};

int main()
{
	Fyro *A = new Fyro;
	Fyro *B = new Water;

	delete A;
	delete B;
	return 0;
}

在这里插入图片描述
~Fyro析构两次的原因是:只有派生类Water的析构函数重写了Fyro的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证AB指向的对象正确的调用析构函数。

两个新的关键字(C++11)

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
 virtual void Drive() final {}
};
class Benz :public Car
{
public:
 virtual void Drive() {cout << "Benz-舒适" << endl;}
};
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
 virtual void Drive(){}
};
class Benz :public Car {
public:
 virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

在这里插入图片描述

重载、覆盖(重写)、隐藏(重定义)的对比

在这里插入图片描述

抽象类

到这里不知道大家尝试写多态的时候有没有注意就是基类的函数往往不知道让其实现什么好,往往显得冗杂或者多余,那么就引出了抽象类

概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 virtual void Drive()
 {
 cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 virtual void Drive()
 {
 cout << "BMW-操控" << endl;
 }
};
void Test()
{
Car* pBenz = new Benz;
 pBenz->Drive();
 Car* pBMW = new BMW;
 pBMW->Drive();
}

接口继承和实现继承(注意)

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

多态的实现原理

多态的实现原理是面向对象编程中的一个核心概念,它允许使用一个接口来定义多个类的共同行为,并通过这个接口来调用不同类中具体的实现。多态性可以分为编译时多态和运行时多态。

编译时多态(静态多态)

编译时多态是在编译阶段由编译器确定的多态性。它主要包括函数重载和模板。

  1. 函数重载:允许在同一个作用域内定义多个同名函数,但它们的参数列表(参数类型、数量或顺序)必须不同。编译器会根据参数列表的不同来选择正确的函数版本。
  2. 模板:模板是一种参数化类型,它允许编写与类型无关的代码。编译器在编译时会根据传入的参数类型来确定模板的具体实现。

运行时多态(动态多态)

运行时多态是在运行时由运行时系统确定的多态性。它主要通过虚函数来实现。

  1. 虚函数:在基类中声明为虚的成员函数,可以在派生类中被覆盖。通过基类指针或引用调用虚函数时,会根据对象的实际类型来决定调用哪个函数版本。
  2. 虚表:每个包含虚函数的类都有一个虚表(vtable),它是一个函数指针数组,存储了类中所有虚函数的地址。每个对象都有一个指向其类虚表的指针(vptr)。
  3. 动态绑定:当通过基类指针或引用调用虚函数时,会根据对象的 vptr 来查找虚表,并调用虚表中对应的函数地址。

示例

class Animal {
public:
    virtual void makeSound() {
        std::cout << "Unknown sound" << std::endl;
    }
};
class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "Woof!" << std::endl;
    }
};
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "Meow!" << std::endl;
    }
};
int main() {
    Animal* animal = new Dog();
    animal->makeSound(); // 输出: Woof!
    animal = new Cat();
    animal->makeSound(); // 输出: Meow!
    return 0;
}

在这个例子中,Animal 类有一个虚函数 makeSoundDogCatAnimal 的派生类,它们覆盖了 makeSound 方法。通过基类指针 animal 调用 makeSound 函数时,会根据 animal 指向的实际对象类型来决定调用哪个版本的 makeSound 函数。
多态的实现原理使得程序能够以统一的方式处理不同类型的对象,提高了代码的灵活性和可扩展性。

继承和多态常见的面试问题

概念查考:

  1. 下面哪种面向对象的方法可以让你变得富有( )
    A: 继承 B: 封装 C: 多态 D: 抽象
  1. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
    A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
  1. 面向对象设计中的继承和组合,下面说法错误的是?()
    A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用
    B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用
    C:优先使用继承,而不是组合,是面向对象设计的第二原则
    D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现
  1. 以下关于纯虚函数的说法,正确的是( )
    A:声明纯虚函数的类不能实例化对象
    B:声明纯虚函数的类是虚基类
    C:子类必须实现基类的纯虚函数
    D:纯虚函数必须是空函数
  1. 关于虚函数的描述正确的是( )
    A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型
    B:内联函数不能是虚函数
    C:派生类必须重新定义基类的虚函数
    D:虚函数可以是一个static型的函数
  1. 关于虚表说法正确的是( )
    A:一个类只能有一张虚表
    B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
    C:虚表是在运行期间动态生成的
    D:一个类的不同对象共享该类的虚表
  1. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
    A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
    B:A类对象和B类对象前4个字节存储的都是虚基表的地址
    C:A类对象和B类对象前4个字节存储的虚表地址相同
    D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
  1. 下面程序输出结果是什么? ()
    A:class A class B class C class D
    B:class D class B class C class A
    C:class D class C class B class A
    D:class A class C class B class D
#include<iostream>
using namespace std;
class A{
public:
 A(char *s) { cout<<s<<endl; }
 ~A(){}
};
class B:virtual public A
{
public:
 B(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class C:virtual public A
{
public:
 C(char *s1,char*s2):A(s1) { cout<<s2<<endl; }
};
class D:public B,public C
{
public:
 D(char *s1,char *s2,char *s3,char *s4):B(s1,s2),C(s1,s3),A(s1)
 { cout<<s4<<endl;}
};
int main() {
 D *p=new D("class A","class B","class C","class D");
 delete p;
 return 0;
}
  1. 多继承中指针偏移问题?下面说法正确的是( )
    A:p1 == p2 == p3
    B:p1 < p2 < p3
    C:p1 == p3 != p2
    D:p1 != p2 != p3
class Base1 {  public:  int _b1; };
class Base2 {  public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0;
}
  1. 以下程序输出结果是什么()
    A: A->0
    B: B->1
    C: A->1
    D: B->0
    E: 编译出错
    F: 以上都不正确
class A
   {
   public:
       virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
 virtual void test(){ func();}
   };
   
   class B : public A
   {
   public:
       void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
   };
   
   int main(int argc ,char* argv[])
   {
       B*p = new B;
       p->test();
       return 0;
   }

参考答案:

  1. A
  2. D
  3. C
  4. A
  5. B
  6. D
  7. D
  8. A
  9. C
  10. B

点个赞嘿嘿
在这里插入图片描述

相关推荐

  1. C++基础-详解

    2024-07-15 15:52:05       54 阅读
  2. C++ 详解(14)

    2024-07-15 15:52:05       67 阅读
  3. c++详细学习

    2024-07-15 15:52:05       16 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-15 15:52:05       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-15 15:52:05       72 阅读
  3. 在Django里面运行非项目文件

    2024-07-15 15:52:05       58 阅读
  4. Python语言-面向对象

    2024-07-15 15:52:05       69 阅读

热门阅读

  1. 关于c语言在内存中的分配管理

    2024-07-15 15:52:05       18 阅读
  2. Scala之基础面向对象编程

    2024-07-15 15:52:05       19 阅读
  3. Linux入侵排查

    2024-07-15 15:52:05       18 阅读
  4. 短剧app系统开发

    2024-07-15 15:52:05       22 阅读
  5. Vue响应式源码解析

    2024-07-15 15:52:05       18 阅读
  6. qt 创建一个左侧边线,可以向左侧拖拽的矩形

    2024-07-15 15:52:05       23 阅读
  7. 关于vue环境变量的使用

    2024-07-15 15:52:05       13 阅读
  8. 软件测试中Bug分析的艺术:方法与实践

    2024-07-15 15:52:05       20 阅读
  9. go 的HTTP请求处理

    2024-07-15 15:52:05       16 阅读
  10. 洛谷 P1162 填涂颜色

    2024-07-15 15:52:05       23 阅读