【C++】—— 多态的基本介绍

news2024/11/25 20:33:00

前言:

在之前的学习过程中,我们已经对继承进行了详细的学习和了解。今天,我将带领大家学习的是关于 多态 的基本知识。

目录

(一)多态的概念

1、概念

(二)多态的定义及实现

1、多态的构成条件

2、虚函数

1️⃣纯虚函数

2️⃣ 面试题:虚函数与纯虚函数的区别

3、虚函数的重写

1️⃣虚函数重写的两个例外:

2️⃣析构函数的重写(基类与派生类析构函数的名字不同)

4、C++11 override 和 final

5、重载、覆盖(重写)、隐藏(重定义)的对比(面试题)

 (三)抽象类

1、概念

2、接口继承和实现继承

(四)多态的原理

1、虚函数表

2、多态的原理

3、动态绑定与静态绑定

总结


(一)多态的概念

1、概念

  • 多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态

💨  举个栗子:比如买票这个行为 ,当 普通人 买票时,是全价买票; 学生 买票时,是半价买票; 军人
买票时是优先买票。

💨  再举个栗子: 最近为了 争夺在线支付市场 ,支付宝年底经常会做诱人的 扫红包 - 支付 - 给奖励金 的 活动。那么大家想想为什么有人扫的红包又大又新鲜8 块、 10 ... ,而有人扫的红包都是 1 毛, 5
.... 。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如
你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你 去使用支付宝,那么就你扫码金额 = random()%1 ;总结一下: 同样是扫码动作,不同的用户扫 得到的不一样的红包,这也是一种多态行为。 ps :支付宝红包问题纯属瞎编,大家仅供娱乐。

【小结】

1、这些例子中,不同的对象根据自身的特性和行为对相同的消息做出了不同的响应,展现了多态的概念;

2、通过多态性,我们可以灵活地处理不同的对象,并针对每个对象的特点执行适当的操作,提高代码的可扩展性和复用性。


(二)多态的定义及实现

1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了
Person Person 对象买票全价, Student 对象买票半价。

那么在继承中要 构成多态还有两个条件
  • 1. 必须通过基类的指针或者引用调用虚函数
  • 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

【小结】

要实现多态性,需要满足以下条件:

  1. 继承关系:存在一个继承关系的类层次结构,包括基类和派生类。派生类继承了基类的属性和方法。

  2. 方法重写:在派生类中重新定义(重写)与基类中相同名称的方法。子类通过重写基类方法来赋予自己独特的行为。

  3. 向上转型:将派生类的对象赋值给基类的引用变量。这样可以使得基类引用指向派生类的对象,从而可以调用派生类中重写的方法。

  4. 运行时绑定:在运行时确定调用哪个方法,实现动态绑定。由于基类引用指向的是派生类对象,因此根据实际的对象类型来决定调用哪个子类的方法。

当满足以上条件时,就实现了多态性。通过使用多态,可以提高代码的灵活性、可扩展性和可维护性,同时减少了代码的重复编写。


2、虚函数

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

【注意】

这里有一个大家容易混淆的点:那就是虚函数与之前学到的虚继承之间有什么联系吗?(强调:二者之间无联系,只是公用一个关键字而已)

C++中的虚函数和虚继承是两个不同的概念,它们在面向对象编程中发挥不同的作用。

  • 下面是对这两个概念的解释以及它们之间的关系:
  1. 虚函数(Virtual Functions):

    • 虚函数是用于实现动态多态性的机制。通过将基类中的函数声明为虚函数,可以在派生类中重写(override)该函数来实现不同的行为。
    • 当通过基类指针或引用调用虚函数时,实际执行的函数取决于指针或引用指向的对象的类型,而不是指针或引用本身的类型。
    • 通过使用虚函数,可以在运行时决定调用哪个函数,实现对象的多态行为。
  2. 虚继承(Virtual Inheritance):

    • 虚继承是解决多继承带来的问题的一种机制。当一个类需要从多个基类继承时,如果其中的一些基类有共同的基类,那么在继承关系中就会产生多个对共同基类的实例。
    • 通过使用虚继承,可以确保在继承关系中只有一个对共同基类的实例。这样可以避免派生类中对共同基类的成员访问和命名冲突。
    • 虚继承会在派生类对象中引入虚基类指针(Virtual Base Pointer)和虚基类表(Virtual Base Table),用于管理共享基类的访问。

关系

  • 虚函数和虚继承是C++语言提供的两种不同的特性,它们在语法和作用上是独立的。
  • 虚函数通过动态绑定实现运行时多态性,而虚继承通过调整继承关系来解决多继承带来的问题。
  • 在某些情况下,我们可能需要同时使用虚函数和虚继承。例如,在存在多继承关系的类层次结构中,如果基类中有虚函数,并且派生类需要重写这些函数,那么可以通过虚继承来消除多个对共同基类的实例,同时实现动态多态性。

1️⃣纯虚函数

纯虚函数(Pure Virtual Function)是一种在基类中声明但不进行实现的虚函数。它通过在函数声明末尾添加 = 0 来标识,例如 virtual void func() = 0;

纯虚函数在基类中起到以下作用:

  1. 接口定义:纯虚函数可以被视为基类对于派生类的接口定义,定义了派生类必须实现的方法。
  2. 强制继承类实现:通过在基类中声明纯虚函数,强制要求派生类必须提供实现,确保每个派生类都具备相同的接口。
  3. 抽象类:包含一个或多个纯虚函数的类称为抽象类,无法实例化对象。抽象类通常用作基类,用于定义通用接口和行为。

派生类必须实现基类中的纯虚函数,如果未能实现,则派生类也成为了抽象类。只有当派生类实现了基类的所有纯虚函数时,才能实例化派生类的对象。

以下是一个展示纯虚函数的代码示例:

#include<iostream>
using namespace std;


// 抽象基类 Animal
class Animal 
{
public:
    // 纯虚函数,用于定义接口
    virtual void makeSound() = 0;
};

// 派生类 Dog
class Dog : public Animal 
{
public:
    // 实现基类的纯虚函数
    void makeSound() override 
    {
       cout << "汪汪!" << endl;
    }
};

// 派生类 Cat
class Cat : public Animal 
{
public:
    // 实现基类的纯虚函数
    void makeSound() override 
    {
        cout << "喵喵!" << endl;
    }
};

int main() 
{
    Dog dog;
    Cat cat;

    dog.makeSound(); // 输出:汪汪!
    cat.makeSound(); // 输出:喵喵!

    return 0;
}

【说明】

  1. 在上面的代码中,Animal 是一个抽象基类,其中声明了一个纯虚函数 makeSound()。而 DogCat 是派生类,它们必须实现 makeSound() 函数才能被实例化。
  2. 通过上述代码,我们可以看到通过纯虚函数的方式,我们定义了一个通用的接口 makeSound(),并要求派生类提供它们特定的实现。在 main() 函数中,我们创建了 DogCat 对象,并调用它们的 makeSound() 函数,分别输出对应的结果!!

总结来说,纯虚函数是一种没有具体实现的函数,用于定义基类的接口和要求派生类提供实现。


2️⃣ 面试题:虚函数与纯虚函数的区别

首先,给大家先抛出概念性的东西,大家有个认识:

  • 1. 虚函数和纯虚函数可以定义在同一个类中,含有纯虚函数的类被称为抽象类,而只含有虚函数的类不能被称为抽象类。
  • 2. 虚函数可以被直接使用,也可以被子类重载以后,以多态的形式调用,而纯虚函数必须在子类中实现该函数才可以使用,因为纯虚函数在基类有声明而没有定义。
  • 3. 虚函数和纯虚函数都可以在子类中被重载,以多态的形式被调用。
  • 4. 虚函数和纯虚函数通常存在于抽象基类之中,被继承的子类重载,目的是提供一个统一的接口。
  • 5. 虚函数的定义形式: virtual{} 纯虚函数的定义形式: virtual { } = 0 ;
  • 6. 在虚函数和纯虚函数 的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时要求前期绑定,然而虚函数却是动态绑定,而且被两者修饰的函数生命周期也不一样。

💨  举个虚函数的例子:
class A
{
public:
	virtual void foo()
	{
		cout << "A::foo() is called" << endl;
	}
};
class B :public A
{
public:
	void foo()
	{
		cout << "B::foo() is called" << endl;
	}
};
int main(void)
{
	A* a = new B();
	a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
	return 0;
}

输出结果如下:

 【说明】

  1. 这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓推迟联编或者动态联编上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时 刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以 被成为函数。
  2. 虚函数只能借助于指针或者引用来达到多态的效果。
💨  纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”
virtual void funtion1()=0
为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀
等子类,但动物本身生成对象明显不合常理。
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法: virtual ReturnType
Function()= 0; ),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类 称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。
声明了纯虚函数的类是一个抽象类。所以,用户不能创建类的实例,只能创建它的派生类的实例。
纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的= 0 ,否则该派生类也
不能实例化),而且它们在抽象类中往往没有定义。
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。
纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为
纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者, 你必须提
供一个纯虚函数的实现,但我不知道你会怎样实现它


3、虚函数的重写

概念:

  • 是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重 写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数;
  • 重写的基类中被重写的函数必须有virtual修饰。

接下来,我简单的用代码展示一下:

#include<iostream>
using namespace std;


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

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

void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

输出显示:

【注意】

  • 在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

1️⃣虚函数重写的两个例外:

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

  • 例如以下代码: 
class a {};
class b : public a 
{

};

class person 
{
public:
	virtual a* f() 
	{ 
		cout << "new A" << endl;
		return nullptr;
	}
};
class student : public person 
{
public:
	virtual b* f() 
	{ 
		cout << "new B" << endl;
		return nullptr;
	}
};

void Func(person* p)
{
	p->f();
	delete p;
}

int main()
{
	Func(new person);
	Func(new student);

	return 0;
}

输出显示:

 而当我们想返回的是对象的时候,此时编译器就会发生报错:

 

 

【说明】

  1. 基类Person的虚函数f()返回类型是A*,而派生类Student的重写函数f()的返回类型是B*,这违反了上述规则,因为B*不是A*的派生类。
  2. 在C++中,如果要进行虚函数重写,返回类型必须是完全匹配的,或者是基类返回类型的派生类。在这种情况下,你可以将 B* 转换为 A*,然后在函数中返回一个派生类对象的指针。

2️⃣析构函数的重写(基类与派生类析构函数的名字不同)

首先,我们先看这样的场景:


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

int main()
{
	Person p;
	Student s;


	return 0;
}
  • 输出结果:

 经过我们的分析,发现上述代码并没有问题。紧接着,我把代码改动一下,看最终的结果是什么!

  • 具体如下:

 【说明】

  1. 上述代码存在内存泄露问题。
  2. 在代码中,派生类 Student 是基类 Person 的子类,并且在派生类中定义了析构函数。在 main() 函数中,使用了动态内存分配来创建了两个对象 p1p2,分别指向 Person 类型和 Student 类型;
  3. 然而,在释放这些动态分配的对象时,只调用了 delete 关键字,却没有使用虚析构函数。由于基类 Person 的析构函数不是虚函数,因此在通过基类指针 p2 删除指向派生类对象的指针时,将不会调用派生类 Student 的析构函数,可能导致资源泄露。

为了解决这个问题:只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函 数,才能构成多态,才能保证p1p2指向的对象正确的调用析构函数。

  • 修正后的代码如下所示:

 【说明】

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

4、C++11 override final

从上面可以看出, C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数
名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有
得到预期结果才来 debug 会得不偿失,因此: C++11 提供了 override final 两个关键字,可以帮
助用户检测是否重写。
  • final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
	virtual void Drive() final 
	{}
};
class Benz :public Car
{
public:
	virtual void Drive() 
	{ 
		cout << "Benz-舒适" << endl; 
	}
};
  • override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	void Drive()
	{}
};

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

int main()
{
	Car cc;
	Benz bb;

	cc.Drive();
	bb.Drive();

	return 0;
}

输出显示:

 

因此,想要达到相应的效果,我们需要在基类中用虚函数实现:

class Car {
public:
	virtual void Drive()
	{}
};

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

int main()
{
	Car cc;
	Benz bb;

	cc.Drive();
	bb.Drive();

	return 0;
}


5、重载、覆盖(重写)、隐藏(重定义)的对比(面试题)

首先,给大家一张表格让大家直观的感受:

 

上述对于重写我已经实现。接下来,我简单的实现一下剩下的两类:
💨  重载
  1. 我们在平时写代码中会用到几个函数但是他们的实现功能相同,但是有些细节却不同;
  2. 函数重载是 指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
代码演示:

// 重载函数
void print(int num) {
    cout << "Integer: " << num << endl;
}

void print(float num) {
    cout << "Float: " << num << endl;
}

void print(const char* str) {
    cout << "String: " << str << endl;
}

int main() {
    print(10);               // 调用 print(int) 重载
    print(3.14f);            // 调用 print(float) 重载
    print("Hello, World!");  // 调用 print(const char*) 重载

    return 0;
}

输出展示:

💨 隐藏(重定义)

  1. 在C++中,函数隐藏(Function Hiding)也称为函数重定义(Function Redefinition),指的是派生类中的函数隐藏了基类中的同名函数;
  2. 这种情况下,无法使用基类指针或引用调用派生类中隐藏的函数。
以下是一个C++代码示例来演示函数隐藏的情况:
//函数隐藏
class Base 
{
public:
    void print() 
    {
        cout << "Base::print()" << endl;
    }
};

class Derived : public Base 
{
public:
    void print() 
    {
        cout << "Derived::print()" << endl;
    }
};

int main() 
{
    Base base;
    Derived derived;

    base.print();    // 调用基类 Base 的 print()
    derived.print(); // 调用派生类 Derived 的 print()

    // 使用基类指针或引用调用派生类中隐藏的函数
    Base* basePtr = &derived;
    basePtr->print(); // 调用基类 Base 的 print(),派生类的函数被隐藏

    return 0;
}

输出演示:

【说明】

  • 在上述示例中,我们创建了一个基类 Base 和一个派生类 Derived。两个类中都定义了名为 print() 的函数,其中派生类 Derived 的 print() 函数隐藏了基类 Base 中的同名函数。
  • 在 main() 函数中,我们分别创建了一个基类对象 base 和派生类对象 derived。然后通过调用 base.print() 和 derived.print(),可以分别看到基类和派生类中的 print() 函数的输出结果。
  • 接下来,我们使用基类指针 basePtr 指向派生类对象 derived,然后通过 basePtr->print() 调用 print() 函数。这时,会发现调用的是基类 Base 中的 print() 函数,而派生类中的函数被隐藏而不可访问。


 (三)抽象类

1、概念

在虚函数的后面写上 =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();
}

输出显示:

 


2、接口继承和实现继承

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

(四)多态的原理

1、虚函数表

首先,这里常考一道笔试题:sizeof(Base)是多少?

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

int main()
{
	Base b;
	cout << sizeof(Base) << endl;

	return 0;
}

输出显示:

 

 

【说明】

通过观察测试我们发现 b 对象是 8bytes 除了 _b 成员,还多一个 __vfptr 放在对象的前面 ( 注意有些
平台可能会放到对象的最后面,这个跟平台有关 ) ,对象中的这个指针我们叫做 虚函数表指针 (v
virtual f 代表 function);
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。

 

那么派生类中这个表放了些什么呢?我们接着往下分析:

// 针对上面的代码我们做出以下改造:
  • 1.我们增加一个派生类Derive去继承Base
  • 2.Derive中重写Func1
  • 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

通过调试,我们可以得到以下这正图片:

【说明】

 

1. 派生类对象 d 中也有一个虚表指针, d 对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表 中存的是重写的 Derive::Func1 ,所以虚函数的重写也叫作覆盖 ,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外 Func2 继承下来后是虚函数,所以放进了虚表, Func3 也继承下来了,但是不是虚函
数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr
5. 总结一下派生类的虚表生成:
  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有一个童鞋们很容易混淆的问题: 虚函数存在哪的?虚表存在哪的?
  • 虚函数存在虚表,虚表存在对象中。注意上面的回答的错的
  • 但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
  • 另外对象中存的不是虚表,存的是虚表指针。

2、多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里 Func 函数传 Person 调用的
Person::BuyTicket ,传 Student 调用的是 Student::BuyTicket

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

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

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person mike;
	Func(mike);

	Student john;
	Func(john);

	return 0;
}

 接下来,我们对代码进行调试观察:

 

 

【说明】

  • 1. 观察上图的红色箭头我们看到,p是指向mike对象时,p->BuyTicketmike的虚表中找到虚 函数是Person::BuyTicket
  • 2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicketjohson的虚表中 找到虚函数是Student::BuyTicket
  • 3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  • 4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调 用虚函数。反思一下为什么?
  • 5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行 起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的

💨 其次,我在带大家看下底层的汇编指令,看多态状态和不是多态状态下的场景:

 


3、动态绑定与静态绑定

由于 派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多 态 。 多态分为 静态多态和动态多态

静态多态 编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数 ,如果有对应
的函数,就调用,没有则在编译时报错。

以下是一个示例,展示了如何在 C++ 中实现静态绑定:

class Base 
{
public:
    void display() 
    {
        cout << "买票-全价" << endl;
    }
};

class Derived : public Base 
{
public:
    void display() 
    {
        cout << "买票-半价" << endl;
    }
};

int main() {
    Base baseObj;
    Derived derivedObj;

    baseObj.display();      // 静态绑定,调用 Base 类的 display() 函数
    derivedObj.display();   // 静态绑定,调用 Derived 类的 display() 函数

    return 0;
}

输出显示:

 【说明】

  • 由于函数调用使用的是静态绑定,编译器在编译时就知道要调用哪个函数。因此,baseObj.display() 调用了 Base 类的 display() 函数,而 derivedObj.display() 调用了 Derived 类的 display() 函数。

这就是 C++ 中如何实现静态绑定的方式。可以看出,在没有使用虚函数或基类指针/引用的情况下,默认使用的是静态绑定。

动态多态: 其实要实现动态多态,需要几个条件 —— 即动态绑定条件:
  • 1. 虚函数。基类中必须有虚函数,在派生类中必须重写虚函数。
  • 2. 通过基类类型的指针或引用来调用虚函数

class Base {
public:
    virtual void display() {
        cout << "Base::display()" << endl;
    }
};

class Derived : public Base {
public:
    void display() override // 使用 override 关键字指明这是一个重写的虚函数
    {  
        cout << "Derived::display()" << endl;
    }
};

int main() 
{
    Base baseObj;
    Derived derivedObj;

    Base* ptr1 = &baseObj;      // 基类指针指向基类对象
    Base* ptr2 = &derivedObj;   // 基类指针指向派生类对象

    ptr1->display();      // 动态绑定,调用 Base 类的 display() 函数
    ptr2->display();      // 动态绑定,调用 Derived 类的 display() 函数

    return 0;
}

输出展示:

【说明】

  • 由于函数调用使用的是动态绑定,当我们通过指针调用 display() 函数时,实际调用的函数版本根据指针指向的对象类型来确定;
  • 因此,ptr1->display() 调用了基类 Basedisplay() 函数,而 ptr2->display() 调用了派生类 Deriveddisplay() 函数。

这就是在 C++ 中实现动态绑定的方式。通过使用虚函数和基类指针/引用,我们能够在运行时根据对象的实际类型确定要调用的函数版本。


最后给大家推荐一篇文章,帮助大家更好的理解:

  • C++ 虚函数表解析

 

总结

到此,关于多态相关的知识便讲解结束了。感谢大家的观看与支持!!!

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/819230.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【力扣每日一题】2023.8.1 英雄的力量

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 题目给我们一个数组&#xff0c;让我们找出数组的每个非空子数组&#xff08;不用连续&#xff09;&#xff0c;然后按照公式算出子数组的…

CNN、数据预处理、模型保存

目录 CNN代码读取数据搭建CNN训练网络模型 数据增强迁移学习图像识别策略数据读取定义数据预处理操作冻结resnet18的函数把模型输出层改成自己的设置哪些层需要训练设置优化器和损失函数训练开始训练再训练所有层关机了&#xff0c;再开机&#xff0c;加载训练好的模型 CNN 代码…

计算机网络(2) --- 网络套接字UDP

计算机网络&#xff08;1&#xff09; --- 网络介绍_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/131967378?spm1001.2014.3001.5501 目录 1.端口号 2.TCP与UDP协议 1.TCP协议介绍 1.TCP协议 2.UDP协议 3.理解 2.网络字节序 发送逻辑…

Node.js之express框架学习心得

Node.js:颠覆传统的服务器端开发 Node.js是基于Chrome V8引擎构建的JavaScript运行时,它采用了完全不同的开发模型。Node.js使用事件驱动和非阻塞I/O的方式处理请求,通过单线程和异步机制,实现高效的并发处理。这意味着在Node.js中,一个线程可以处理数千个并发连接,大大提…

Debian 12.1 “书虫 “发布,包含 89 个错误修复和 26 个安全更新

导读Debian 项目今天宣布&#xff0c;作为最新 Debian GNU/Linux 12 “书虫 “操作系统系列的首个 ISO 更新&#xff0c;Debian 12.1 正式发布并全面上市。 Debian 12.1 是在 Debian GNU/Linux 12 “书虫 “发布六周后推出的&#xff0c;目的是为那些希望在新硬件上部署操作系统…

从内核源码看 slab 内存池的创建初始化流程

slab cache 机制确实比较复杂&#xff0c;涉及到的场景又很多&#xff0c;大家读到这里&#xff0c;我想肯定会好奇或者怀疑笔者在上篇文章中所论述的那些原理的正确性&#xff0c;毕竟 talk is cheap &#xff0c;所以为了让大家看着安心&#xff0c;理解起来放心&#xff0c;…

让SpringBoot不需要Controller、Service、DAO、Mapper,卧槽!这款工具绝了!

Dataway介绍 Dataway 是基于 DataQL 服务聚合能力&#xff0c;为应用提供的一个接口配置工具。使得使用者无需开发任何代码就配置一个满足需求的接口。整个接口配置、测试、冒烟、发布。一站式都通过 Dataway 提供的 UI 界面完成。UI 会以 Jar 包方式提供并集成到应用中并和应…

在windows下安装ruby使用gem

在windows下安装ruby使用gem 1.下载安装ruby环境2.使用gem3.gem换源 1.下载安装ruby环境 ruby下载地址 选择合适的版本进行下载和安装&#xff1a; 在安装的时候&#xff0c;请勾选Add Ruby executables to your PATH这个选项&#xff0c;添加环境变量&#xff1a; 安装Ruby成…

vue-print-nb使用(实现分页打印)

参考链接&#xff1a;vue-print-nb - npm (npmjs.com)https://www.npmjs.com/package/vue-print-nb 一、安装 1、Vue2安装 npm install vue-print-nb --save <!-- 全局配置&#xff1a;main.js --> import Print from vue-print-nb // Global instruction Vue.use(P…

解码“平台工程”,VMware 有备而来

随着全球数字化进程加快&#xff0c;企业使用前沿技术加快商业创新&#xff0c;以提高竞争力。其中如何加快开发效率&#xff0c;为客户创造更多价值成为新的关注焦点。 继DevOps后&#xff0c;“平台工程”&#xff08;Platform Engineering&#xff09; 一词引发热议。平台工…

Redis的安装部署以及基本的使用

目录 一、Linux下直接安装Redis &#xff08;1&#xff09;下载Redis安装包 &#xff08;2&#xff09;安装GCC编译环境 &#xff08;3&#xff09;安装Redis &#xff08;4&#xff09;服务启动 &#xff08;5&#xff09;后台启动 二、使用Docker安装部署Redis &…

火车头采集器免费版【php源码】

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python turtle circle 画半圆圆心在哪&#xff0c;python中用turtle画一个圆形&#xff0c;现在让我们一起来看看吧&#xff01; 1、t.circle(100,180)的意思&#xff1f; t.circle(100, 180)是Python中turtle库中的一…

18- C++ 强制类型转换-6 (C++)

第八章 强制类型转换 c提供了 隐式类型转换&#xff0c;所谓隐式类型转换&#xff0c;是指不需要用户干预&#xff0c;编译器默认进行的类型转换行为&#xff08;很多时候用户可能都不知道到底进行了哪些转换&#xff09;。例如&#xff1a; int nValue 8; double dValue 10…

评估修改后的YOLOv8模型的参数量和速度

YOLOv8公布了自己每个模型的速度和参数量 那么如果我们自己对YOLOv8做了一些修改&#xff0c;又怎么样自己写代码统计一下修改后的模型的参数量和速度呢&#xff1f; 其实评估这些东西&#xff0c;大多数情况下不需要我们从头自己写一个函数来评估 一般来说&#xff0c;只要…

【云存储】使用OSS快速搭建个人网盘教程(阿里云)

使用OSS快速搭建个人网盘 一、基础概要1. 主要的存储类型1.1 块存储1.2 文件存储1.3 对象存储 2. 对象存储OSS2.1 存储空间2.2 地域2.3 对象2.4 读写权限2.5 访问域名&#xff08;Endpoint&#xff09;2.6 访问密钥2.7 常用功能&#xff08;1&#xff09;创建存储空间&#xff…

HCIP-datacom-831题库

考取HCIP数通证书可以胜任中到大型企业网络工程师岗位&#xff0c;需要掌握中到大型网络的特点和通用技术&#xff0c;具备使用华为数通设备进行中到大型企业网络的规划设计、部署运维、故障定位的能力&#xff0c;并能针对网络应用设计出较高安全性、可用性和可靠性的解决方案…

RedisJava的Java客户端

目录 1.Jedis的使用 前置工作-ssh进行端口转发 JedisAPI的使用 Jedis连接池 2.SpringDataRedis的使用 1.创建项目 2.配置文件 3.注入RedisTemplate对象 4.编写代码 3.SpringRedisTemplate 哈希结构用法 ​总结 1.Jedis的使用 Jedis&#xff1a;以Redis命令作为方法…

途乐证券:沪指强势拉升涨0.63%,券商等板块走强,传媒板块活跃

31日早盘&#xff0c;两市股指全线走高&#xff0c;沪指一度涨超1%收复3300点&#xff0c;上证50指数盘中涨逾2%&#xff1b;随后涨幅有所收窄&#xff1b;两市成交额显着放大&#xff0c;北向资金净买入超90亿元。 到午间收盘&#xff0c;沪指涨0.63%报3296.58点&#xff0c;深…

Python多线程与GIL锁

Python多线程与GIL锁 python多线程 Python的多线程编程可以在单个进程内创建多个线程来同时执行多个任务&#xff0c;从而提高程序的效率和性能。Python的多线程实现依赖于操作系统的线程调度器&#xff0c;并且受到全局解释器锁&#xff08;GIL&#xff09;的限制&#xff0c…