前言:
在之前的学习过程中,我们已经对继承进行了详细的学习和了解。今天,我将带领大家学习的是关于 多态 的基本知识。
目录
(一)多态的概念
1、概念
(二)多态的定义及实现
1、多态的构成条件
2、虚函数
1️⃣纯虚函数
2️⃣ 面试题:虚函数与纯虚函数的区别
3、虚函数的重写
1️⃣虚函数重写的两个例外:
2️⃣析构函数的重写(基类与派生类析构函数的名字不同)
4、C++11 override 和 final
5、重载、覆盖(重写)、隐藏(重定义)的对比(面试题)
(三)抽象类
1、概念
2、接口继承和实现继承
(四)多态的原理
1、虚函数表
2、多态的原理
3、动态绑定与静态绑定
总结
【小结】
1、这些例子中,不同的对象根据自身的特性和行为对相同的消息做出了不同的响应,展现了多态的概念;
2、通过多态性,我们可以灵活地处理不同的对象,并针对每个对象的特点执行适当的操作,提高代码的可扩展性和复用性。
【小结】
要实现多态性,需要满足以下条件:
继承关系:存在一个继承关系的类层次结构,包括基类和派生类。派生类继承了基类的属性和方法。
方法重写:在派生类中重新定义(重写)与基类中相同名称的方法。子类通过重写基类方法来赋予自己独特的行为。
向上转型:将派生类的对象赋值给基类的引用变量。这样可以使得基类引用指向派生类的对象,从而可以调用派生类中重写的方法。
运行时绑定:在运行时确定调用哪个方法,实现动态绑定。由于基类引用指向的是派生类对象,因此根据实际的对象类型来决定调用哪个子类的方法。
当满足以上条件时,就实现了多态性。通过使用多态,可以提高代码的灵活性、可扩展性和可维护性,同时减少了代码的重复编写。
【注意】
这里有一个大家容易混淆的点:那就是虚函数与之前学到的虚继承之间有什么联系吗?(强调:二者之间无联系,只是公用一个关键字而已)
C++中的虚函数和虚继承是两个不同的概念,它们在面向对象编程中发挥不同的作用。
虚函数(Virtual Functions):
虚继承(Virtual Inheritance):
关系:
纯虚函数(Pure Virtual Function)是一种在基类中声明但不进行实现的虚函数。它通过在函数声明末尾添加 = 0
来标识,例如 virtual void func() = 0;
。
纯虚函数在基类中起到以下作用:
派生类必须实现基类中的纯虚函数,如果未能实现,则派生类也成为了抽象类。只有当派生类实现了基类的所有纯虚函数时,才能实例化派生类的对象。
以下是一个展示纯虚函数的代码示例:
#include
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;
}
【说明】
Animal
是一个抽象基类,其中声明了一个纯虚函数 makeSound()
。而 Dog
和 Cat
是派生类,它们必须实现 makeSound()
函数才能被实例化。makeSound()
,并要求派生类提供它们特定的实现。在 main()
函数中,我们创建了 Dog
和 Cat
对象,并调用它们的 makeSound()
函数,分别输出对应的结果!!总结来说,纯虚函数是一种没有具体实现的函数,用于定义基类的接口和要求派生类提供实现。
首先,给大家先抛出概念性的东西,大家有个认识:
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;
}
输出结果如下:
【说明】
virtual void funtion1()=0
概念:
接下来,我简单的用代码展示一下:
#include
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;
}
输出显示:
【注意】
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;
}
输出显示:
而当我们想返回的是对象的时候,此时编译器就会发生报错:
【说明】
Person
的虚函数f()
返回类型是A*
,而派生类Student
的重写函数f()
的返回类型是B*
,这违反了上述规则,因为B*
不是A*
的派生类。B*
转换为 A*
,然后在函数中返回一个派生类对象的指针。首先,我们先看这样的场景:
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person {
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person p;
Student s;
return 0;
}
经过我们的分析,发现上述代码并没有问题。紧接着,我把代码改动一下,看最终的结果是什么!
【说明】
Student
是基类 Person
的子类,并且在派生类中定义了析构函数。在 main()
函数中,使用了动态内存分配来创建了两个对象 p1
和 p2
,分别指向 Person
类型和 Student
类型;delete
关键字,却没有使用虚析构函数。由于基类 Person
的析构函数不是虚函数,因此在通过基类指针 p2
删除指向派生类对象的指针时,将不会调用派生类 Student
的析构函数,可能导致资源泄露。为了解决这个问题:只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函 数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
【说明】
class Car
{
public:
virtual void Drive() final
{}
};
class Benz :public Car
{
public:
virtual void Drive()
{
cout << "Benz-舒适" << endl;
}
};
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;
}
上述对于重写我已经实现。接下来,我简单的实现一下剩下的两类:
// 重载函数
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;
}
输出展示:
隐藏(重定义)
//函数隐藏
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()
函数,而派生类中的函数被隐藏而不可访问。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();
}
输出显示:
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
首先,这里常考一道笔试题: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;
}
输出显示:
【说明】
那么派生类中这个表放了些什么呢?我们接着往下分析:
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. 这里还有一个童鞋们很容易混淆的问题: 虚函数存在哪的?虚表存在哪的?
- 虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。
- 但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
- 另外对象中存的不是虚表,存的是虚表指针。
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;
}
接下来,我们对代码进行调试观察:
【说明】
其次,我在带大家看下底层的汇编指令,看多态状态和不是多态状态下的场景:
以下是一个示例,展示了如何在 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++ 中如何实现静态绑定的方式。可以看出,在没有使用虚函数或基类指针/引用的情况下,默认使用的是静态绑定。
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()
调用了基类 Base
的 display()
函数,而 ptr2->display()
调用了派生类 Derived
的 display()
函数。这就是在 C++ 中实现动态绑定的方式。通过使用虚函数和基类指针/引用,我们能够在运行时根据对象的实际类型确定要调用的函数版本。
最后给大家推荐一篇文章,帮助大家更好的理解:
到此,关于多态相关的知识便讲解结束了。感谢大家的观看与支持!!!