【C++】多态-最全解析(多态是什么?如何使用多态?多态的底层原理是什么?)

目录

一、前言

二、多态是什么?

三、多态的定义及实现 

🔥 多态的构成条件🔥

🔥 虚函数的重写🔥

🔥虚函数重写的两个例外 🔥

🍍 协变返回类型

🥝 析构函数的重写

🔥C++11 的 override 和 final 🔥

 🍑override

 🍉final

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

💢重载(Overloading) 

💢覆盖(重写)(Overriding) 

💢重定义(隐藏)(Hiding) 

四、抽象类 

五、多态的原理 

💢多态性的基本原理 

💧虚函数表指针💧

💧虚函数表💧

💧虚函数表和虚指针相互关联构成 --- 多态💧

💢 动态绑定和静态绑定

六、多态的常考面试题 

七、共勉 


一、前言

   多态 ----  面向对象 三大基本特征中的最后一个,多态 可以实现 “一个接口,多种方法”,比如 父类子类 中的同名方法,在增加了多态后,调用同名函数时,可以根据不同的对象(父类对象或子类对象)调用属于自己的函数,实现不同的方法,因此 多态 的实现依赖于 继承
        如果大家还不太了解 继承 可以先看看这篇文章:C++继承详解

二、多态是什么?

        多态(Polymorphism)这个词来源于 希腊语,意思是“多种形态”。在编程中,多态允许你  使用相同的接口(方法或函数)来   处理不同的 数据类型 或 对象。这使得代码更加灵活和可扩展。
        通俗一点不同对象  -----  完成同一件事情   -----  产生不同的结果


举例1:

比如 春节回家买票 (看作一件事情),当普通人买票时,是全价买票 ;学生买票时,半价买票;军人买票时是优先买票。(不同的对象,产生了不同的结果)


举例2: 

为了争夺在线支付市场,支付宝年底经常会做诱人的 扫红包-支付-给奖励金 的活动。

那么大家想想为什么有人扫的红包又大又新鲜 8块、10 块...而有人扫的红包都是1毛,5...。其实这背后也是一个多态行为。 

总结一下:同样是扫码动作(看作一件事情)不同的用户扫得到的不-样的红包(不同的对象,产生了不同的结果),这也是一种多态行为。 


代码举例引出 本篇文章所要学习的 关于多态的 十万个 为什么? 

假设你在家里有一只宠物。宠物可以是 。每种宠物都会发出叫声,但每种宠物的叫声不同。我们可以定义一个基类 Pet,然后让 CatDog Bird 继承自这个基类。 

class Pet {
public:virtual void makeSound() {cout << "Some generic pet sound" << endl;}
};class Cat : public Pet {
public:void makeSound() override {cout << "Meow" << endl;}
};class Dog : public Pet {
public:void makeSound() override {cout << "Woof" << endl;}
};class Bird : public Pet {
public:void makeSound() override {cout << "Tweet" << endl;}
};

使用 --- 多态 的性质 

现在假设你有一个 Pet 类型的指针它可以指向 任何一种 具体的宠物 对象: 

Pet* myPet = new Dog();
myPet->makeSound();  // 输出 "Woof"

尽管 myPetPet 类型的指针,但它指向的是一个 Dog 对象。因此,当你调用 makeSound() 方法时,会调用 Dog 类的 makeSound() 方法,而不是 Pet 类的(这就是多态)

如果你把 myPet 指向一个 Cat 对象: 

myPet = new Cat();
myPet->makeSound();  // 输出 "Meow"


我们先给出 总结: 

多态
1️⃣:允许你用 基类指针或引用      操作不同的派生类对象

2️⃣:当调用虚函数时,程序会 根据实际对象的类型来调用对应的方法,而不是根据指针的类型。

3️⃣:这使得程序更加灵活,易于扩展和维护。 


此时大家肯定 会对上面的 代码解释  有不同程度 的看不懂,和不理解!!

1️⃣:构成多态需要那些条件 ?
2️⃣:什么是虚函数? 代码中关键字 virtual  是干什么的 ?
3️⃣:为什么 派生类中 可以出现 和 基类 同样的函数,这样会不会构成 继承中的 隐藏呢 ?
4️⃣:为什么 指向基类(Pet)的指针可以指向派生类(Dog 、Cat)的对象呢 ?
5️⃣:代码 派生类 中出现的关键字 override 是干什么呢 ?

这些疑问,下面将会给大家详细的讲解 !! 当大家搞懂上面这些问题后,在回头看上面的这个例子就会 恍然大悟 有一种升华的感觉!!


三、多态的定义及实现 

实现多态需要借助虚表(虚函数表),而构成虚表又需要虚函数,即 virtual 修饰的函数,除此之外还需要使用虚表指针来进行函数定位、调用 。

🔥 多态的构成条件🔥

1️⃣:继承关系(Inheritance)

多态 依赖于类之间的 继承 关系。至少需要一个基类和一个或多个派生类。派生类从基类继承属性和方法,并可以覆盖基类的方法。 

2️⃣:虚函数(Virtual Functions)

基类中必须有一个或多个虚函数。这些虚函数是用 virtual 关键字声明的。虚函数允许派生类重写它们,从而实现多态性。 

// 基类
class Base {
public:virtual void display()      // 虚函数{cout << "Display from Base" << endl;}
};// 派生类
class Derived : public Base {
public:void display() override {cout << "Display from Derived" << endl;}
};

3️⃣:必须通过 基类指针引用 来调用 虚函数(Base Class Pointer or Reference) 

使用 基类指针引用 引用派生类对象。这是实现多态性的关键,因为通过基类指针或引用调用虚函数时,程序 会根据实际指向的对象类型调用对应的函数。 

Base* b = new Derived();
b->display();  // 输出 "Display from Derived"

4️⃣:虚函数表(Virtual Table, VTable)

虚函数表是编译器在处理虚函数时创建的一个数据结构。它包含类的虚函数的指针。当通过基类指针调用虚函数时,程序会查找虚函数表并调用实际的函数实现。(这个知识点大家点先了解一下,文章在讲解 多态原理的时候,会详细讲解!!) 

5️⃣:基类中 virtual 修饰后的函数形成虚函数,与派生类中的  override 修饰函数形成 重写(三同:返回值、函数名、参数均相同)


代码案例熟悉: 

我们可以按照上面 5条 多态的构成条件,例子帮助大家熟悉熟悉:

#include <iostream>
using namespace std;// 基类
class Animal {
public:// 虚函数virtual void makeSound() {cout << "Some generic animal sound" << endl;}
};// 派生类
class Dog : public Animal {
public:// 函数重写void makeSound() override {  cout << "Woof" << endl;}
};class Cat : public Animal {
public:// 函数重写void makeSound() override {cout << "Meow" << endl;}
};int main() {// 基类指针指向派生类对象Animal* animal1 = new Dog();Animal* animal2 = new Cat();// 调用虚函数,根据实际对象类型调用对应的方法animal1->makeSound();  // 输出 "Woof"animal2->makeSound();  // 输出 "Meow"delete animal1;delete animal2;return 0;
}

 详细解释:

  • 继承关系Animal 是基类,DogCat 是从 Animal 继承的派生类。
  • 虚函数:基类 Animal 中的 makeSound 函数被声明为虚函数。
  • 基类指针:在 main 函数中,Animal* animal1Animal* animal2 分别指向 DogCat 对象。
  • 虚函数表:编译器为 Animal 类创建一个虚函数表,其中包含 makeSound 函数的指针。DogCat 类覆盖了 makeSound 函数,因此它们各自的虚函数表会包含指向相应重写函数的指针。
  • 虚函数重写:Dog Cat 中的 makeSound 函数均被 override 修饰 ,并且 返回值、函数名、参数均相同,形成了虚函数重写

通过这些条件,多态性使得可以通过基类指针调用派生类的重写方法,从而实现灵活和可扩展的代码设计。这是面向对象编程的重要特性之一,广泛用于实际开发中。 


🔥 虚函数的重写🔥

在面向对象编程中,虚函数重写(覆盖)是指派生类重新定义基类中的虚函数(返回值、函数名、参数列表,均相同)。当通过基类指针或引用调用虚函数时,程序会根据实际对象类型调用对应的重写函数,而不是基类中的函数。 

具体实现步骤 

  • 定义基类和虚函数
  • 定义派生类并重写虚函数
  • 通过基类指针或引用调用虚函数

详细示例: 

基类和虚函数 

首先,我们定义一个基类 Base,其中包含一个虚函数 display 

#include <iostream>
using namespace std;class Base {
public:virtual void display() {cout << "Display from Base" << endl;}
};
  • virtual 关键字用于声明 display 是一个虚函数。
  • 这样,派生类可以选择性地重写这个函数。

派生类和函数重写 

然后,我们定义一个派生类 Derived,它继承自 Base 并重写 display 函数: 

class Derived : public Base {
public:void display() override {cout << "Display from Derived" << endl;}
};
  • override 关键字表明这个函数是 重写基类中的虚函数
  • 虽然 override 是可选的,但推荐使用它来确保函数签名正确匹配基类中的虚函数。

使用基类指针调用重写的虚函数 

main 函数中,我们使用基类指针来调用虚函数: 

int main() {Base* b = new Derived();b->display();  // 输出 "Display from Derived"delete b;return 0;
}
  • Base* b = new Derived(); 创建一个基类指针 b,指向一个 Derived 对象。
  • b->display(); 调用 display 函数,由于 b 实际上指向的是 Derived 对象,因此会调用 Derived 类中的 display 函数。

虚函数表解析 

虚函数表(VTable)是编译器用来实现运行时多态性的机制。 

  • 创建 VTable:编译器为每个包含虚函数的类创建一个虚函数表。
  • 填充 VTable:虚函数表中包含指向该类的虚函数实现的指针。
  • 对象指向 VTable:每个对象都有一个隐藏的指针,指向它所属类的虚函数表。

示例中的 VTable 工作原理 

  • Base 类的虚函数表包含指向 Base::display 的指针。
  • Derived 类的虚函数表包含指向 Derived::display 的指针。

当创建一个 Derived 对象时,它的隐藏指针指向 Derived 类的虚函数表。因此,通过基类指针调用虚函数时,程序会查找虚函数表,并调用 Derived::display 


通过这个例子,我们可以看到虚函数重写如何实现运行时多态性,使得基类指针能够根据实际对象类型调用正确的函数实现。 


🔥虚函数重写的两个例外 🔥

🍍 协变返回类型

协变返回类型:指的是在 派生类中重写基类虚函数时返回类型可以是基类返回类型的派生类。例如,如果是基类的虚函数返回一个基类指针或引用,派生类可以重写这个函数并返回派生类的指针或引用

详细示例 :

定义基类和派生类 

首先,我们定义一个基类 Base 和一个从 Base 继承的派生类 Derived。 

#include <iostream>
using namespace std;class Base {
public:virtual Base* clone() const {return new Base(*this);}virtual void print() const {cout << "This is Base" << endl;}
};class Derived : public Base {
public:Derived* clone() const override {return new Derived(*this);}void print() const override {cout << "This is Derived" << endl;}
};

在这个例子中: 

  • Base 类定义了一个虚函数 clone,返回一个 Base* 类型。
  • Derived 类重写了 clone 函数,但返回类型是 Derived*

使用协变返回类型 

main 函数中,我们可以使用协变返回类型来创建对象的副本。 

int main() {Base* b = new Derived();Base* b_clone = b->clone();  // 调用的是 Derived::clone(),返回 Derived*,但可以赋值给 Base*b->print();        // 输出 "This is Derived"b_clone->print();  // 输出 "This is Derived"delete b;delete b_clone;return 0;
}

解释: 

  • 基类定义:基类 Base 中的虚函数 clone 返回一个 Base* 类型的对象。
  • 派生类重写:派生类 Derived 重写了 clone 函数,并返回一个 Derived* 类型的对象。这是合法的,因为 Derived* Base* 的派生类指针。
  • 多态性:通过基类指针调用 clone 函数时,会实际调用 Derived 类的 clone 函数,并返回一个 Derived*。尽管返回的是 Derived*,但它可以被赋值给 Base* 类型的指针,这是C++的类型兼容性特性。

使用协变返回类型的优点

协变返回类型(Covariant Return Type)的出现带来了许多实际作用和好处,尤其是在面向对象编程中。以下是协变返回类型的一些主要作用: 

1️⃣:提高代码的类型安全性 

协变返回类型允许派生类在重写基类的虚函数时,返回派生类的类型。这避免了在外部进行类型转换,提高了类型安全性。 

class Base {
public:virtual Base* clone() const {return new Base(*this);}
};class Derived : public Base {
public:Derived* clone() const override {return new Derived(*this);}
};int main() {Base* basePtr = new Derived();// 强制类型转换Derived* derivedClone = static_cast<Derived*>(basePtr->clone());delete basePtr;delete derivedClone;return 0;
}

通过协变返回类型,可以避免在上例中使用的类型转换: 

int main() {Base* basePtr = new Derived();// 协变类型转换                     Derived* derivedClone = basePtr->clone();delete basePtr;delete derivedClone;return 0;
}

2️⃣:提高代码的可读性和可维护性 

使用协变返回类型可以使代码更清晰,避免不必要的类型转换,从而提高代码的可读性和可维护性。 


3️⃣:支持面向对象编程中的深拷贝 

协变返回类型在实现深拷贝时尤为有用。在基类中定义一个虚函数 clone 并在派生类中重写,返回派生类对象,可以确保拷贝的对象具有正确的动态类型。 

class Shape {
public:virtual Shape* clone() const {return new Shape(*this);}virtual void draw() const {cout << "Drawing a shape" << endl;}
};class Circle : public Shape {
public:Circle* clone() const override {return new Circle(*this);}void draw() const override {cout << "Drawing a circle" << endl;}
};int main() {Shape* shape = new Circle();Shape* clonedShape = shape->clone();shape->draw();          // 输出 "Drawing a circle"clonedShape->draw();    // 输出 "Drawing a circle"delete shape;delete clonedShape;return 0;
}

代码解析: 

基类 Shape 

class Shape {
public:virtual Shape* clone() const {return new Shape(*this);}virtual void draw() const {cout << "Drawing a shape" << endl;}
};

Shape 是一个基类,包含两个虚函数:

  • clone返回一个指向 Shape 的指针,用于创建对象的副本。该函数通过 new Shape(*this) 创建当前对象的拷贝。
  • draw输出 "Drawing a shape",表示绘制一个通用的形状。

派生类 Circle 

class Circle : public Shape {
public:Circle* clone() const override {return new Circle(*this);}void draw() const override {cout << "Drawing a circle" << endl;}
};

Circle 继承 Shape,并重写了两个虚函数:

  • clone返回一个指向 Circle 的指针,用于创建 Circle 对象的副本。通过 new Circle(*this) 创建当前 Circle 对象的拷贝。
  • draw输出 "Drawing a circle",表示绘制一个圆形。

主函数 main 

int main() {Shape* shape = new Circle();Shape* clonedShape = shape->clone();shape->draw();          // 输出 "Drawing a circle"clonedShape->draw();    // 输出 "Drawing a circle"delete shape;delete clonedShape;return 0;
}
  • Shape* shape = new Circle();创建一个指向 Circle 对象的基类指针 shape。通过基类指针指向派生类对象,这里体现了多态性
  • Shape* clonedShape = shape->clone();调用 shape 指向的对象的 clone 方法,返回一个指向新创建的 Circle 对象的指针 clonedShape。由于 Circle 重写了 clone 方法并返回 Circle* 类型,因此实际调用的是 Circle::clone,返回的是 Circle* 类型,但赋值给了 Shape* 指针 clonedShape这就是协变返回类型的作用。
  • shape->draw();调用 shape 指向对象的 draw 方法,实际调用的是 Circle::draw,输出 "Drawing a circle"。
  • clonedShape->draw();调用 clonedShape 指向对象的 draw 方法,实际调用的是 Circle::draw,输出 "Drawing a circle"。
  • delete shape; delete clonedShape;:删除 shapeclonedShape 对象,释放内存。

关键点解析 

虚函数与多态性:

  • 基类 Shapeclonedraw 方法被声明为虚函数,使得派生类 Circle 可以重写它们。
  • 通过基类指针 shape 调用虚函数 clone draw 时,实际调用的是 Circle 类中的实现。这是多态性在起作用。

协变返回类型: 

  • Circle 类的 clone 方法返回 Circle* 类型,而不是基类的 Shape* 类型。这是合法的,因为 Circle* Shape* 的派生类指针。
  • 通过返回派生类类型的指针,可以避免外部进行类型转换,提高代码的类型安全性和可读性。

深拷贝: 

  • clone 方法的实现通过 new Circle(*this) 创建当前对象的副本,保证了深拷贝的实现。
  • main 函数中,shape->clone() 返回的是一个新的 Circle 对象的指针 clonedShape,确保了深拷贝。

总结

协变返回类型的主要作用包括:

  • 提高代码的类型安全性
  • 提高代码的可读性和可维护性
  • 支持更好的面向对象设计和深拷贝
  • 提供更灵活的接口设计

🥝 析构函数的重写

在C++中,析构函数是一个特殊的成员函数,用于在对象被销毁时执行清理操作。关于析构函数的重写(override),实际上并不是一个准确的术语,因为析构函数在派生类中并不能像普通的虚函数那样被重写。然而,我们可以通过让基类的析构函数成为虚函数,从而实现多态性和正确的资源释放。 

为什么析构函数需要重写? 

当使用基类指针或引用指向派生类对象时,如果基类的析构函数不是虚函数,那么在删除对象时,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能会导致资源泄漏,因为派生类的清理工作未被执行。 


例子 

没有重写析构函数的情况 

#include <iostream>
using namespace std;class Base {
public:~Base() {cout << "Base destructor called" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "Derived destructor called" << endl;}
};int main() {Base* b = new Derived();delete b; // 只会调用 Base 的析构函数return 0;
}

输出: 

Base destructor called

在这个例子中,由于 Base 的析构函数不是虚函数,删除 b 时只调用了 Base 的析构函数,而没有调用 Derived 的析构函数。 

使用重写析构函数 

#include <iostream>
using namespace std;class Base {
public:virtual ~Base() {cout << "Base destructor called" << endl;}
};class Derived : public Base {
public:~Derived() {cout << "Derived destructor called" << endl;}
};int main() {Base* b = new Derived();delete b; // 会调用 Derived 和 Base 的析构函数return 0;
}

输出: 

Derived destructor called
Base destructor called

在这个例子中,由于 Base 的析构函数是虚函数,删除 b 时先调用了 Derived 的析构函数,然后调用了 Base 的析构函数。 


重写析构函数的作用 

虚析构函数确保了在使用基类指针或引用时,派生类的析构函数会被正确调用,从而防止资源泄漏和其他潜在问题。 


总结 

  • 重写析构函数的必要性:当使用 基类指针或引用指向 派生类对象时,必须将基类的析构函数声明为虚函数,以确保正确调用派生类的析构函数。
  • 实现方式:在基类中使用 virtual 关键字声明析构函数。
  • 作用:确保在销毁对象时,调用正确的析构函数,防止资源泄漏和其他问题。

🔥C++11 的 override 和 final 🔥

 🍑override

 override 是C++11引入的一种功能,用来明确表示派生类中的函数是覆盖基类中的虚函数。它有以下几个好处:

  • 增加代码可读性:明确表明这个函数是用来覆盖基类的虚函数的,方便代码阅读。
  • 编译器检查:编译器会检查这个函数是否确实覆盖了基类中的虚函数。如果没有(例如函数签名不匹配),编译器会报错。这可以帮助我们捕捉错误。

代码案例:

class Pet {
public:virtual void makeSound() {cout << "Some generic pet sound" << endl;}
};class Cat : public Pet {
public:void makeSound() override {cout << "Meow" << endl;}
};
  • Cat 类中,makeSound() 函数后面的 override 关键字告诉编译器这是对基类 PetmakeSound() 函数的覆盖。如果我们不小心拼写错误或参数列表不同,编译器会报错。 

 为什么只需要在基类中声明虚函数 ?

 在C++中,只有基类需要声明虚函数,因为这是多态性工作的基础。虚函数表(Virtual Table)是由基类维护的,当派生类重写这个虚函数时,它会自动被记录在虚函数表中。

  • 基类声明虚函数:当你在基类中声明一个函数为虚函数时,编译器会在对象的虚函数表(VTable)中为这个函数创建一个入口。虚函数表是一个指针数组,每个对象都有一个指向它所属类的虚函数表的指针。

  • 派生类覆盖虚函数:当派生类覆盖这个虚函数时,它会在虚函数表中替换基类的函数指针。因此,通过基类指针调用虚函数时,实际调用的是派生类的函数。

  • 调用虚函数:当你通过基类指针调用虚函数时,程序会查找虚函数表,并调用实际指向的函数。这就是多态性的工作原理。

派生类不需要显式声明函数为虚函数,因为它继承了基类的虚函数机制。 


代码案例

#include <iostream>
using namespace std;class Pet {
public:virtual void makeSound() {cout << "Some generic pet sound" << endl;}
};class Cat : public Pet {
public:void makeSound() override {cout << "Meow" << endl;}
};class Dog : public Pet {
public:void makeSound() override {cout << "Woof" << endl;}
};int main() {Pet* myPet = new Dog();myPet->makeSound();  // 输出 "Woof"myPet = new Cat();myPet->makeSound();  // 输出 "Meow"delete myPet;return 0;
}

在这个例子中:

  • Pet 是基类,其中 makeSound 被声明为虚函数。
  • Cat Dog 是派生类,覆盖了 makeSound 函数,并使用 override 关键字来确保正确的覆盖。
  • main 函数中,通过 Pet 类型的指针调用 makeSound 函数,根据指针实际指向的对象类型,调用相应的派生类函数。

 🍉final

 final 关键字在C++11引入,用于防止继承和重写,确保类或虚函数不会被进一步派生或重写。这在设计某些类层次结构时非常有用,可以增加代码的安全性和明确性。

final 关键字的作用 

  • 防止类被继承:将 final 关键字放在类声明之后,表示该类不能被继承。
  • 防止虚函数被重写:将 final 关键字放在虚函数声明之后,表示该虚函数不能在派生类中被重写。

示例解析 

防止类被继承 

class Base final {
public:void display() const {cout << "Base display" << endl;}
};// 下面的代码将导致编译错误,因为 Base 类被标记为 final,不能被继承
class Derived : public Base {
};int main() {Base b;b.display();return 0;
}

 在这个例子中,Base 类被标记为 final,因此 Derived 类无法从 Base 类继承。如果尝试继承,会导致编译错误。

防止虚函数被重写 

class Base {
public:virtual void display() const {cout << "Base display" << endl;}
};class Derived : public Base {
public:void display() const final {cout << "Derived display" << endl;}
};// 下面的代码将导致编译错误,因为 Derived::display 被标记为 final,不能被进一步重写
class MoreDerived : public Derived {
public:void display() const override {cout << "MoreDerived display" << endl;}
};int main() {Base b;b.display();Derived d;d.display();return 0;
}

 在这个例子中,Derived 类中的 display 函数被标记为 final,因此 MoreDerived 类无法重写该函数。如果尝试重写,会导致编译错误。


综合示例 

结合之前的多态示例,这里是一个使用 final 关键字的综合示例: 

#include <iostream>
using namespace std;class Shape {
public:virtual Shape* clone() const {return new Shape(*this);}virtual void draw() const {cout << "Drawing a shape" << endl;}
};class Circle final : public Shape {
public:Circle* clone() const override {return new Circle(*this);}void draw() const override final {cout << "Drawing a circle" << endl;}
};// 下面的代码将导致编译错误,因为 Circle 被标记为 final,不能被继承
class DerivedCircle : public Circle {
public:void draw() const override {cout << "Drawing a derived circle" << endl;}
};int main() {Shape* shape = new Circle();Shape* clonedShape = shape->clone();shape->draw();          // 输出 "Drawing a circle"clonedShape->draw();    // 输出 "Drawing a circle"delete shape;delete clonedShape;return 0;
}

在这个示例中:

  • Circle 类被标记为 final,因此不能被继承。
  • Circle 类中的 draw 方法被标记为 final,因此不能被重写。

总结 

  • final 关键字用于防止类被继承或虚函数被重写,增加代码的安全性和明确性。
  • 防止类被继承:将 final 放在类声明之后。
  • 防止虚函数被重写:将 final 放在虚函数声明之后。
  • 适用于设计稳定接口、提高代码安全性、优化性能等场景。

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

重载(Overloading)、覆盖(Overriding)、重定义(隐藏)(Hiding)在C++中是三个不同的概念,它们各自有不同的语法和应用场景。下面我将详细解释这些概念,并举例说明它们的区别。 

💢重载(Overloading) 

重载是指在同一个作用域中定义多个同名函数但这些函数的参数列表(参数的数量、类型或顺序)不同。重载主要用于提高代码的灵活性和可读性。 

#include <iostream>
using namespace std;class Printer {
public:void print(int i) {cout << "Printing int: " << i << endl;}void print(double d) {cout << "Printing double: " << d << endl;}void print(const string& s) {cout << "Printing string: " << s << endl;}
};int main() {Printer p;p.print(42);         // 调用 print(int)p.print(3.14);       // 调用 print(double)p.print("Hello");    // 调用 print(const string&)return 0;
}

 在这个例子中,print 函数被重载了三次,每个重载版本的参数列表不同。


💢覆盖(重写)(Overriding) 

覆盖是指派生类重新定义基类中已经存在的虚函数,以实现特定的行为。覆盖是多态性的重要机制,允许派生类根据需要修改基类的行为。覆盖函数必须与被覆盖函数具有相同的参数列表和返回类型。 

#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base show" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived show" << endl;}
};int main() {Base* b = new Derived();b->show();  // 调用 Derived 的 show()delete b;return 0;
}

在这个例子中,Derived 类中的 show 函数覆盖了 Base 类中的虚函数 show。 


💢重定义(隐藏)(Hiding) 

重定义是指在派生类中定义一个与基类中同名但参数列表不同的函数。重定义不会影响基类中的函数。重定义通常是无意的,并且可以通过使用 using 关键字在派生类中引入基类的同名函数来避免。

#include <iostream>
using namespace std;class Base {
public:void display() {cout << "Base display" << endl;}void display(int i) {cout << "Base display with int: " << i << endl;}
};class Derived : public Base {
public:void display() {cout << "Derived display" << endl;}
};int main() {Derived d;d.display();       // 调用 Derived 的 display()// d.display(42);  // 错误:Derived 中没有匹配的函数d.Base::display(42);  // 调用 Base 的 display(int)return 0;
}

在这个例子中,Derived 类中的 display 函数隐藏了 Base 类中的所有同名函数。如果在 Derived 类中想要使用基类的其他 display 函数,可以通过 using 关键字引入基类函数: 

class Derived : public Base {
public:using Base::display;  // 引入基类的 display 函数void display() {cout << "Derived display" << endl;}
};

这样可以在 Derived 类中使用 Base 类中的 display(int) 函数。 


总结 

  • 重载(Overloading):同一个作用域内同名函数的参数列表不同,构成重载。
  • 覆盖(重写)(Overriding):派生类重新定义基类中的虚函数,函数签名必须相同,构成覆盖。
  • 重定义(隐藏)(Hiding):派生类中定义一个与基类中同名但参数列表不同的函数,构成重定义或隐藏。


四、抽象类 

        抽象类(Abstract Class)是面向对象编程中的一个重要概念,尤其在实现多态性方面非常有用。抽象类通常作为基类定义接口供派生类实现,而不能实例化

抽象类的特点 

  • 包含纯虚函数:一个类至少包含一个纯虚函数(pure virtual function)就是抽象类。纯虚函数在声明时使用 = 0 来标识。
  • 不能实例化:抽象类不能创建对象。只能通过派生类来实例化对象。
  • 提供接口:抽象类通常用来定义接口,而具体的实现由派生类提供。

纯虚函数 

纯虚函数是在基类中声明没有定义的函数,需要派生类提供具体实现。纯虚函数的语法如下: 

virtual void functionName() = 0;

 示例

以下是一个关于抽象类和纯虚函数的示例:

定义抽象类 Animal 

#include <iostream>
using namespace std;class Animal {
public:// 纯虚函数,表示动物发出的声音virtual void makeSound() const = 0;// 虚析构函数,保证正确删除派生类对象virtual ~Animal() {}
};

派生类 DogCat 

class Dog : public Animal {
public:void makeSound() const override {cout << "Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() const override {cout << "Meow!" << endl;}
};

 使用抽象类和派生类

int main() {Animal* animals[2];animals[0] = new Dog();animals[1] = new Cat();for (int i = 0; i < 2; ++i) {animals[i]->makeSound();  // 调用派生类的 makeSound 方法}for (int i = 0; i < 2; ++i) {delete animals[i];  // 删除对象,调用虚析构函数}return 0;
}

 解析

抽象类 Animal 

  • Animal 类是一个抽象类,因为它包含一个纯虚函数 makeSound
  • 纯虚函数 makeSound 定义了一个接口,要求所有派生类必须实现这个函数。
  • 虚析构函数确保通过基类指针删除派生类对象时,正确调用派生类的析构函数。

派生类 DogCat 

  • Dog 类和 Cat 类从 Animal 类继承,并实现了纯虚函数 makeSound
  • Dog 类的 makeSound 方法输出 "Woof!",表示狗叫的声音。
  • Cat 类的 makeSound 方法输出 "Meow!",表示猫叫的声音。

 main 函数

  • main 函数中,创建了一个 Animal 类指针数组,其中包含 DogCat 对象。
  • 通过基类指针调用了派生类的 makeSound 方法,展示了多态性。
  • 最后,通过基类指针删除了派生类对象,确保正确调用派生类的析构函数。

运行结果 

Woof!
Meow!

 这表明:

  • animals[0] 指向一个 Dog 对象,并正确调用了 Dog::makeSound 方法,输出 "Woof!"。
  • animals[1] 指向一个 Cat 对象,并正确调用了 Cat::makeSound 方法,输出 "Meow!"。

 关键点

  • 抽象类定义接口:抽象类 Animal 定义了动物发出声音的方法接口。
  • 派生类实现接口DogCat 类实现了这些接口,并提供了具体的行为。
  • 多态性:通过基类指针调用派生类的方法,实现了多态性。
  • 虚析构函数:保证了通过基类指针删除派生类对象时,正确调用派生类的析构函数。

抽象类的用途 

抽象类适合用于描述无法拥有实体的类,比如 人、动物、植物,毕竟这些都是不能直接使用的,需要经过 继承 赋予特殊属性后,才能作为一个独立存在的个体(对象) 


五、多态的原理 

💢多态性的基本原理 

在C++中,多态性是通过虚函数来实现的,而虚函数的机制依赖于虚函数表(vtable)虚指针(vptr)。以下是这两个概念及其相互关系的详细解释。

  • 虚函数(Virtual Function):在基类中声明为 virtual 的函数,允许派生类重写这些函数。
  • 虚函数表(Virtual Table, vtable):编译器为每个含有虚函数的类生成一个虚函数表,其中存储了该类的虚函数指针

实现机制 

  • 虚函数表(vtable)虚函数表是编译器为每个包含虚函数的类生成的一个隐藏的表。这个表中存储了该类的所有虚函数的指针。

    • 每个含有虚函数的类都有一个虚函数表,表中存储了该类的所有虚函数的指针
    • 虚函数表存储了类的虚函数的地址。对于基类中的虚函数,如果在派生类中被重写,那么虚函数表中相应的条目会指向派生类的实现。
  • 虚指针(vptr)虚指针是编译器在每个对象实例中添加的一个隐藏指针。这个指针指向该对象所属类的虚函数表。

    • 每个对象实例都有一个隐藏的指针,指向该类的虚函数表    ------  vptr
    • 虚指针在对象实例化时被初始化,指向正确的虚函数表。
    • 当通过基类指针或引用调用虚函数时,程序通过 vptr 找到对应的 vtable,然后从 vtable 中找到实际调用的函数。

虚函数表和虚指针的关系 

  • 虚指针指向虚函数表。
  • 当通过基类指针或引用调用虚函数时,程序会通过虚指针找到虚函数表,然后从虚函数表中找到实际需要调用的函数。

💧虚函数表指针💧

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

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;char ch = 'A';
};int main()
{Base bb;cout << sizeof(Base) << endl;return 0;
}
  • 可能刚看到这个题目的时候,都会觉得答案是 8 个字节,但是我们在打印后却发现是 16 个字节;这是为什么呢?
  • 因为有了虚函数,这个对象里面就多了一个成员,虚函数表指针__vfptr。

为什么会有这个东西? ---------  _vfptr

  • 是因为我们的 Base类里面有虚函数,含 有虚函数的类里面就会有虚函数表指针
  • 那这里的 虚函数表指针 指向 一张表,即虚函数表(简称虚表)虚函数表里面存的是虚函数的地址。 

  • 因为Base里面现在只有一个虚函数,所以我们看到它里面现在只有一个元素,就是虚函数Func1的地址。 
  • 另外我也能知道虚函数表其实就是一个虚函数指针数组存放虚函数的地址,所以虚函数指针_vfptr其实就是一个数组指针(函数指针数组的指针) 

💧虚函数表💧

 虚函数表是编译器为每个包含虚函数的类生成的一个隐藏的表。这个表中存储了该类的所有虚函数的指针。

针对上面的代码我们做出以下改造:

  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;
}

通过监视窗口观察: 

大概模型应为下图所示: 

通过观察和测试,我们发现了以下几点问题: 

  • 派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
  • 基类b 对象和派生类 d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d 的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  • 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  • 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一nullptr


总结一下派生类的虚表生成:

  1. 先将基类中的虚表内容拷贝一份到派生类虚表中;
  2. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
  3. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

💧虚函数表和虚指针相互关联构成 --- 多态💧

为什么父类指针指向不同的对象就能实现多态呢? 

#include <iostream>
using namespace std;
//父类
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
private:int _b;
};
//子类
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
private:int _d;
};
int main()
{Person Mike;Student Johnson;Person* p1 = &Mike;Person* p2 = &Johnson;p1->BuyTicket(); //买票-全价p2->BuyTicket(); //买票-半价return 0;
}

通过监视窗口我们得到以下关系: 

两个父类指针分别指向对应的 Mike Johnson 对象,找到对应的虚表,调用对应的函数,即: 

  • 父类指针p1指向Mike对象,p1->BuyTicket 在 Mike 的虚表中找到的虚函数就是Person::BuyTicket。
  • 父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

总结

  • 虚函数表(vtable)存储了类的虚函数的指针,每个含有虚函数的类都有自己的虚函数表。
  • 虚指针(vptr)是每个对象实例中的隐藏指针,指向对象所属类的虚函数表。
  • 当通过基类指针或引用调用虚函数时,程序通过虚指针找到虚函数表,然后从虚函数表中找到实际需要调用的函数,实现多态性。

💢 动态绑定和静态绑定

多态(Polymorphism)是面向对象编程(OOP)中的一个重要概念,它使得对象能够以多种形式出现。多态性主要分为两种类型: 

  • 编译时多态性(静态多态性)

    • 函数重载(Function Overloading)
    • 运算符重载(Operator Overloading)
  • 运行时多态性(动态多态性)

    • 虚函数(Virtual Functions)和覆盖(Override)
    • 抽象类(Abstract Classes)和纯虚函数(Pure Virtual Functions)
    • 接口(Interfaces)

六、多态的常考面试题 

1. 什么是多态?

解答思路

  • 多态性是面向对象编程(OOP)的一个核心概念。
  • 在C++中,多态性允许同一个接口调用不同的实现。
  • 多态性分为编译时多态性(如函数重载和模板)和运行时多态性(通过虚函数实现)。

2. 解释虚函数和纯虚函数的区别。 

 解答思路:

  • 虚函数(virtual function):在基类中使用 virtual 关键字声明,可以在派生类中重写。通过基类指针或引用调用时,会调用实际对象的函数实现。
  • 纯虚函数(pure virtual function):在基类中声明但不提供实现,语法为 virtual void func() = 0;。包含纯虚函数的类是抽象类,不能实例化,必须在派生类中实现纯虚函数。

3. 什么是抽象类?如何使用抽象类? 

解答思路:

  • 抽象类是包含至少一个纯虚函数的类。
  • 抽象类不能实例化,但可以用作基类。
  • 派生类必须实现抽象类中的纯虚函数才能实例化。

4. 解释什么是虚函数表(vtable)和虚指针(vptr)。

解答思路:

  • 虚函数表是编译器为每个包含虚函数的类生成的表,存储类的虚函数的指针。
  • 虚指针是每个对象实例中的隐藏指针,指向该对象所属类的虚函数表。
  • 通过虚指针找到虚函数表,从而调用实际对象的函数实现。

5. 虚函数表是在什么阶段生成的,存在哪的? 

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。 


6. 为什么需要虚析构函数? 

  • 如果基类指针指向派生类对象,通过基类指针删除对象时,需要虚析构函数来确保调用派生类的析构函数,防止资源泄漏。
  • 基类中的析构函数应声明为虚函数。

7. 什么是协变返回类型? 

  解答思路

  • 协变返回类型允许派生类中的重写函数返回基类函数返回类型的派生类。
  • 使得重写函数返回更具体的类型,提高灵活性和类型安全性。

8. 解释重载、重写和隐藏的区别。

解答思路

  • 重载(Overloading):同一作用域内,函数名相同但参数列表不同(参数类型、数量或顺序不同)。
  • 重写(Overriding):派生类重新定义基类的虚函数,函数签名必须相同。
  • 隐藏(Hiding):派生类定义了与基类同名但参数列表不同的函数或属性,基类成员被隐藏。

9. 讲解构造函数和析构函数在多态中的作用。 

解答思路

  • 构造函数不能是虚函数,因为对象的类型在构造时已确定。
  • 析构函数应声明为虚函数,以确保通过基类指针删除对象时调用正确的析构函数。

10. 你能解释一下 final 关键字在多态中的作用吗? 

解答思路

  • final 关键字用于防止类被继承或防止虚函数被重写。
  • 类名后使用 final 禁止类被继承;虚函数声明后使用 final 禁止重写。

七、共勉 

  以下就是我对 【C++】多态 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对 【C++/STL】set 的理解,请持续关注我哦!!!    

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

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

相关文章

使用APEXSQL LOG解析sql server事务日志,进行审计与数据恢复

一 下载 https://download.csdn.net/download/sunke861/11449739 二 使用 解压安装包后&#xff0c;点击&#xff1a;ApexSQLLog.exe 2.1 连接数据库 连接要审计的数据库&#xff1a; 假如报错&#xff1a; 则点击ok关闭该窗口&#xff0c;然后点击左上方的New按钮&#xf…

Codeforces Round 958 (Div. 2)(A~C)题

A. Split the Multiset 思路: 最优的策略是每次操作分出 k−1&#x1d458;−1 个 1&#xff0c;然后考虑最后是否会剩下一个单独的 1。 代码: #include<bits/stdc.h> using namespace std; #define N 1000005 typedef long long ll; typedef unsigned long long ull;…

【找不到视图问题解决】@RestController 与 @Controller注解的使用区别

一、问题描述 苍穹外卖在菜品分页查询功能实现的过程中&#xff0c;出现了找不到视图的情况 2024-07-12 21:54:20.860 ERROR 22488 --- [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with p…

Transformer注意力机制

Transformer注意力机制 &#x1f42c; 目录: 一、Transformer简介二、理解注意力机制自注意力机制多头注意力机制 一、Transformer简介 Transformer是一种用于自然语言处理(NLP)和其他序列到序列(Seq2Seq)任务的深度学习模型框架&#xff0c;它在2017年由Vaswani等人首次提出…

手机m4a怎么转换成mp3,手机端即可完成格式转换

M4A&#xff08;MPEG-4 Audio&#xff09;是一种无损压缩的音频格式&#xff0c;通常用于苹果设备和 iTunes 上&#xff0c;因为它能提供较高的音质同时占用较小的存储空间。 然而&#xff0c;MP3 作为最普及的音频格式之一&#xff0c;兼容性更强&#xff0c;几乎所有的播放设…

【单元测试】SpringBoot

【单元测试】SpringBoot 1. 为什么单元测试很重要&#xff1f;‼️ 从前&#xff0c;有一个名叫小明的程序员&#xff0c;他非常聪明&#xff0c;但有一个致命的缺点&#xff1a;懒惰。小明的代码写得又快又好&#xff0c;但他总觉得单元测试是一件麻烦事&#xff0c;觉得代码…

ENSP中NAT的相关实验(两个私网,一个公网)

题目 实验需求 1.按照图示配置IP地址&#xff0c;公网地址100.1.1.1/24 2.私网A通过NAPT&#xff0c;使R1接入到互联网&#xff0c;私网B通过EASY IP&#xff0c;使R3接入到互联网 3.私网A配置NAT SERVER把Telnet的Telnet服务发布到公网&#xff0c;使PC2可以访问 三、实验…

el-table和 el-image图片预览使用插槽后层叠样式错乱问题

问题&#xff1a; 解决办法&#xff1a;在el-image组件中添加preview-teleported 属性 最终效果

MongoDB自学笔记(一)

一、MongoDB简介 MongoDB是一款基于C开发的文档型数据库。与传统的关系型数据库有所不同&#xff0c;MongoDB面向的是文档&#xff0c;所谓的文档是一种名为BSON &#xff08;Binary JSON&#xff1a;二进制JSON格式&#xff09;是非关系数据库当中功能最丰富&#xff0c;最像…

AV1 编码标准帧间预测技术概述

AV1 编码标准帧间预测 AV1&#xff08;AOMedia Video1&#xff09;是一种开源的视频编码格式&#xff0c;它在帧间预测技术上做出了显著的改进和扩展&#xff0c;以提供比现有标准更高的压缩效率和更好的视频质量。以下是AV1帧间预测技术的几个关键点&#xff1a; 参考帧扩展&a…

You are running Vue in development mode.和undefined is not iterable白屏问题

遇到的报错信息如下&#xff0c; 你正在开发模式下运行 Vue。 确保在部署生产环境时打开生产模式 但是我是关闭了的Vue.config.productionTip false 最后发现是服务器问题

Ubuntu安装 Nginx

前置条件&#xff1a; 把apt包更新到最新&#xff08;如果更新过就跳过这步&#xff09; 先检查 sudo apt update 后更新 sudo apt upgrade &#xff08;期间要选择确认&#xff0c;输入 y 即可&#xff09; 如果不行可以&#xff1a;sudo apt upgrade --fix-missing 先卸…

IIS的安装及Web服务器深度配置:打造高效稳定的网络门户

在构建现代网络环境的过程中&#xff0c;IIS&#xff08;Internet Information Services&#xff09;作为微软提供的强大Web服务器软件&#xff0c;扮演着至关重要的角色。无论是企业级的网站部署&#xff0c;还是个人开发者的小型项目测试&#xff0c;IIS都能提供稳定、高效的…

无人机使能的边缘计算优化问题

Joint Deployment and Task Scheduling Optimization for Large-Scale Mobile Users in Multi-UAV-Enabled Mobile Edge Computing论文阅读笔记 BackgroundContributionsSystem Model and Problem FormulationLocal Execution ModelMEC Execution ModelUAV Hover Model Propose…

kubernetes概念及基本介绍(一)

部署方式的演进过程 传统部署 直接将应用程序部署在物理机器上&#xff0c;很难合理分配计算机资源&#xff0c;而且程序之间会产生影响 虚拟化部署 可以在一台物理机上运行多个虚拟机&#xff0c;没个虚拟机都是独立的一个环境&#xff0c;程序环境不会产生影响&#xff0c;…

【Linux】权限管理与相关指令

文章目录 1.权限、文件权限、用户文件权限的理解以及对应八进制数值表示、设置目录为粘滞位文件类型 2.权限相关的常用指令susudochmodchownchgrpumaskwhoamifile 1.权限、文件权限、用户 通过一定条件&#xff0c;拦住一部分人&#xff0c;给另一部分权利来访问资源&#xff0…

Windows 2012安装之实现远程连接

新建虚拟机 点击稍后安装操作系统 点击Microsoft Windows(W) 选择Windows Server 2012 设置虚拟机名称、安装位置 选择你的电脑核数 点击编辑虚拟机设置 点击CD/DVD(SATA) 使用ISO映像文件(M) 配置完之后点击确定 然后开启虚拟机 下一步&#xff1a; 点击现在安装&#xff1a…

【学习笔记】无人机(UAV)在3GPP系统中的增强支持(十二)-无人机群在物流中的应用

引言 本文是3GPP TR 22.829 V17.1.0技术报告&#xff0c;专注于无人机&#xff08;UAV&#xff09;在3GPP系统中的增强支持。文章提出了多个无人机应用场景&#xff0c;分析了相应的能力要求&#xff0c;并建议了新的服务级别要求和关键性能指标&#xff08;KPIs&#xff09;。…

常用的点云预处理算法

点云预处理是处理点云数据时的重要部分&#xff0c;其目的是提高点云数据的质量和处理效率。通过去除离群点、减少点云密度和增强特征&#xff0c;可以消除噪声、减少计算量、提高算法的准确性和鲁棒性&#xff0c;从而为后续的点云处理和分析步骤&#xff08;如配准、分割和重…

三大知名向量化模型比较分析——m3e,bge,bce

先聊聊出处。 M3E 是 Moka Massive Mixed Embedding 的缩写&#xff0c; Moka&#xff0c;此模型由 MokaAI 训练&#xff0c;开源和评测&#xff0c;训练脚本使用 uniem &#xff0c;评测 BenchMark 使用 MTEB-zhMassive&#xff0c;此模型通过千万级 (2200w) 的中文句对数据…