【C++】多态(万字详解) —— 条件 | 虚函数重写 | 抽象类 | 多态的原理(上)

简介: 【C++】多态(万字详解) —— 条件 | 虚函数重写 | 抽象类 | 多态的原理(上)

前言


多态分为两类 ——


静态的多态:函数重载。传入不同参数,看起来调用一个函数,但是有不同的行为,最典型的比如流插入流提取的“自动识别类型”

int i = 10;
  double d = 1.1;
  cout << i; //cout.operator<<(int)
  cout << d; //cout.operator<<(double)


动态的多态:一个父类的引用或指针调用同一个函数,传递不同的对象,会调用不同的函数

怎么样区分呢?


静态:在编译时决议,(编译时决定调用谁)

动态:在运行时决议,(运行时决定调用谁)

不过本文主要围绕的是动态的多态进行展开


一. 多态的概念


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


比如买票,我们想让不同身份的人,买票的价格不同,就可以借助多态实现


0a2653c851af460fa595bd959398a8f1.png


class Person
{
public:
  virtual void BuyTicket() { cout << "买票——全价" << endl; }
};
class Student : public Person
{
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person
{
public:
  virtual void BuyTicket() { cout << "优先买票" << endl; }
};
void Func(Person& p)//父类的指针/引用
{
  p.BuyTicket();//虚函数重写
}
int main()
{
  Person ps;
  Student st;
  Soldier sd;
  Func(ps);//传父类对象 —— 调父类的
  Func(st);//传子类对象 —— 调子类的
  Func(sd);//传子类对象 —— 调子类的
  return 0;
}


其中子类的函数满足 三同(返回值类型、函数名、参数列表完全相同)的虚函数这两个条件,叫做重写(覆盖)


ps:此时的函数名相同,但是不构成隐藏,不满足三同的才叫做隐藏

这样就可以什么人对应什么政策


0a2653c851af460fa595bd959398a8f1.png


二. 多态的定义及实现


🌈多态的条件


🥑多态有两个条件,缺一不可:


必须通过父类的 指针或者引用调用虚函数

被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

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

重写要求 :虚函数 + 三同(父类和子类的返回值类型、函数名字、参数列表完全相同)(形参名和缺省参数名不一样不影响)

构成多态,传的哪个类型的对象,调用的就是哪个类型的虚函数 - 跟对象有关

不构成多态,调用的就是p类型函数 - 跟类型有关

下面进行验证:如果用对象来调用,够不够构成多态


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


0a2653c851af460fa595bd959398a8f1.png


我们思考为什么一定要是父类的指针或引用呢?为什么是父类?为什么是指针和引用?


因为只有指针和引用访问才能实现晚绑定,如果使用的是对象的话,在编译期间就已经绑定完毕了(也就是已经确定call好了地址了),也就不能实现多态

我们知道一个子类的第一个成员是父类成员,父类下的第一个就是虚表指针,我们是要通过父类指针来找到这个虚表的!如果是通过子类去访问就是静态绑定,不能达到动态调节

没有重写的话,是编译时决定还是运行时决定地址?


2d65d23f6d4748949b924e4057485923.png


调试打开反汇编可以看见是运行时决定的


4cebaac233b3433da32a72337a77fc60.png


此处的编译器并没有完全检查你是否重写,只是初略的检查是否是虚函数以及父类指针调用,但是调用的还是同一个虚函数,因为没有完成覆盖


🌈虚函数重写的两个特例


🥑协变

协变,返回值可以不同,但要求必须是父子关系的指针或者引用


0a2653c851af460fa595bd959398a8f1.png


实际上用的不多


🥑析构函数的重写

如果析构函数构是虚函数,这里构成重写吗?yes!但是他们的函数名不相同啊,因为析构函数名被特殊处理了,都处理成了destructor(),至于为什么要特殊处理,就是源于多态


//建议在继承中析构函数定义成虚函数
class Person {
public:
  virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
  //析构函数名会被处理成destructor,所以完成了重写
  virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Person p;
  Student s;
  return 0;
}


2d65d23f6d4748949b924e4057485923.png


普通的场景下是没有出现问题的,但是有特殊的场景!要记住,面试高频考点


✨ 那什么场景下,析构函数要是虚函数呢?


Person* ptr1 = new Person;
  delete ptr1;
  Person* ptr2 = new Student;
  delete ptr1;


如果不是虚函数,那也就不构成多态,那与类型有关,都会去调用父类的析构函数,但是这样会导致子类对象可能有资源未被清理,我们希望的是父类调用父类,指向子类调用子类的(完了再调用父类),这样是不是就符合我们多态的理念


0a2653c851af460fa595bd959398a8f1.png


析构函数的重写很简单,因为函数名“相同”,没有参数,加一个virtual就可以


2d65d23f6d4748949b924e4057485923.png


在其他场景,析构函数是不是虚函数都可以


🌈只有父类带 virtual 的情况


虚函数,允许父子类两个都是虚函数 或 只有父类是虚函数也行。这其实是C++不是很规范的地方,建议两个都写上virtual


这是因为虽然子类没带virtual,但是它 继承了父类的虚函数属性,重写是实现


0a2653c851af460fa595bd959398a8f1.png


🌈C++11 final & override


🥑final

final有两个功能


修饰一个类,这个类不能被继承

修饰虚函数,限制它不能被子类中的虚函数重写

C++11中final还可以限制重写

修饰虚函数,限制它不能被子类中的虚函数重写


2d65d23f6d4748949b924e4057485923.png


🥑override

override放在子类重写的虚函数后面,帮助检查是否完成重写,没有重写会报错


类似于核酸检测,,没有做就报错(做核酸魔怔了)


0a2653c851af460fa595bd959398a8f1.png


三. 重载 vs 重写 vs 隐藏


2d65d23f6d4748949b924e4057485923.png


四. 抽象类


💛 包含纯虚函数的类叫做抽象类(接口类)。在虚函数的后面写上=0 ,则这个函数为纯虚函数


纯虚函数一般只声明,不实现,抽象类不能实例化出对象;相当于间接强制你重写!


0a2653c851af460fa595bd959398a8f1.png


即使我们创造了一个子类对象,其派生类继承后也不能实例化出对象,因为继承了抽象类后,这个派生类就继承了纯虚函数,那它同样也是一个抽象类!


只有重写纯虚函数,派生类才能实例化出对象。所以呀,抽象类本质上强制继承它的子类完成虚函数重写


class Car
{
public:
  virtual void Drive() = 0;
};
class BMW: public Car
{
public:
  virtual void Drive()
  {
  cout << "操控——好开" << endl;
  }
};
class Benz :public Car
{
public:
  virtual void Drive()
  {
  cout << "Benz-舒适" << endl;
  }
};
int main()
{
  //BMW b;
  Car* ptr = new BMW;
  ptr->Drive();
  Car* ptr = new Benz;
  ptr->Drive();
  return 0;
}


0a2653c851af460fa595bd959398a8f1.png


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


五. 多态的原理


🔥虚函数表

⚡引入


// 其中sizeof(Base)是多少?
class Base
{
public:
  virtual void Func1()
  {
  cout << "Func1()" << endl;
  }
private:
  int _b = 1;
  char _ch = 'A';
};
int main()
{
  cout << sizeof(Base) << endl;
}


如果我只考虑到了内存对齐的话,答案就是8


2d65d23f6d4748949b924e4057485923.png


但此处的考点不仅仅只有内存对齐,真正考察的是多态,那究竟是什么东西的存在多了4个字节

通过监视窗口,发现这个对象多了一个成员,虚函数表指针_vfptr(virtual function table)(简称虚表指针) ,所谓的虚函数表就是一个指针数组,里面存放的是函数指针(放的是虚函数地址),一般这个数组的最后面放了一个nullptr——


虚函数等等的函数都是放在代码段的!


0a2653c851af460fa595bd959398a8f1.png


记住对象里面没有虚表,只有指向虚表的指针;


🔥多态的原理

虚函数表是理解多态原理的关键,下面将在底层剖析


class Person {
public:
  virtual void BuyTicket() 
  { cout << "买票-全价" << endl; }
protected:
  int _a = 0;
};
class Student : public Person 
{
public:
  virtual void BuyTicket() 
  { cout << "买票-半价" << endl; }
protected:
  int _b = 0;
};
void Func(Person& p) {
  p.BuyTicket();
}
int main()
{
  Person Scort;
  Func(Scort);
  Student Durant;
  Func(Durant);
  return 0;
}


虚函数的“重写”也叫“覆盖”,重写是语法上的概念,覆盖是原理层的概念;子类继承父类的虚函数,可以认为深拷贝了一份虚函数表,没有重写时,子类与父类虚表完全相同;若重写了,便会用新地址覆盖。


🍂转到反汇编可以发现:


对于普通成员函数的调用,是在编译后就已经确定了调用地址(橙色的);

给父类/子类对象,调用虚函数p.BuyTichet()的汇编代码却是相同的,那就说明此时调用函数时,不再是直接确定地址,而是借助了eax这个寄存器,这是多态原理的关键

2d65d23f6d4748949b924e4057485923.png

(汇编不要求全部看懂,懂大概意思就可)


🟢多态的本质原理,基类的指针/引用指向谁,就去谁的虚函数表中找到对应位置的虚函数进行调用,这是在运行中确定的,所以叫动态的多态


4cebaac233b3433da32a72337a77fc60.png


而普通函数,在编译链接的时候已经确定了函数运行地址,直接调用即可


6de278e6d6694ce5bb08e7e842b7e74b.png


🔥小细节

p1和p2是共用一个虚表吗?


class Person 
{
public:
  virtual void BuyTicket() { cout << "买票——全价" << endl; }
};
int main()
{
  Person p1;
  Person p2;
  return 0;
}

0a2653c851af460fa595bd959398a8f1.png

结论是:同一类型的对象共用一个虚表


class Person 
{
public:
  virtual void BuyTicket() { cout << "买票——全价" << endl; }
};
class Student :public Person {
public:
  //virtual void BuyTicket() { cout << "买票——半价" << endl; }
};
int main()
{
  Person p1;
  Person p2;
  Student s1;
  Student s2;
  return 0;
}

0a2653c851af460fa595bd959398a8f1.png

结论:vs下,不管是否完成重写,子类虚表和父类的虚表不是同一个


相关文章
|
1月前
|
缓存 算法 程序员
C++STL底层原理:探秘标准模板库的内部机制
🌟蒋星熠Jaxonic带你深入STL底层:从容器内存管理到红黑树、哈希表,剖析迭代器、算法与分配器核心机制,揭秘C++标准库的高效设计哲学与性能优化实践。
C++STL底层原理:探秘标准模板库的内部机制
|
5月前
|
存储 人工智能 编译器
c++--多态
上一篇文章已经介绍了c++的继承,那么这篇文章将会介绍多态。看完多态的概念,你一定会感觉脑子雾蒙蒙的,那么我们先以举一个例子,来给这朦胧大致勾勒出一个画面,在此之前,先介绍一个名词虚函数,(要注意与虚拟继承区分)重定义: 重定义(隐藏)只要求函数名相同(但要符合重载的要求,其实两者实际上就是重载);重定义下:在这种情况下,如果通过父类指针或引用调用函数,会调用父类的函数而不是子类。重定义(或称为隐藏)发生的原因是因为函数名相同但参数列表不同,导致编译器无法确定调用哪一个版本的函数。
112 0
|
9月前
|
编译器 C++
c++中的多态
c++中的多态
|
8月前
|
存储 编译器 C++
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
1100 0
|
9月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
441 0
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
143 1
|
9月前
|
编译器 C++ 开发者
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
|
5月前
|
人工智能 机器人 编译器
c++模板初阶----函数模板与类模板
class 类模板名private://类内成员声明class Apublic:A(T val):a(val){}private:T a;return 0;运行结果:注意:类模板中的成员函数若是放在类外定义时,需要加模板参数列表。return 0;
158 0
|
5月前
|
存储 编译器 程序员
c++的类(附含explicit关键字,友元,内部类)
本文介绍了C++中类的核心概念与用法,涵盖封装、继承、多态三大特性。重点讲解了类的定义(`class`与`struct`)、访问限定符(`private`、`public`、`protected`)、类的作用域及成员函数的声明与定义分离。同时深入探讨了类的大小计算、`this`指针、默认成员函数(构造函数、析构函数、拷贝构造、赋值重载)以及运算符重载等内容。 文章还详细分析了`explicit`关键字的作用、静态成员(变量与函数)、友元(友元函数与友元类)的概念及其使用场景,并简要介绍了内部类的特性。
249 0
|
7月前
|
编译器 C++ 容器
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
288 12