C++中的多态机制

简介: C++中的多态机制

多态

牢记有虚函数的类就有虚表指针,**虚表的本质是函数指针数组。**静态是指编译时,动态是指运行时。


1. 多态的基本知识

1. 基本概念

==多态就是对同一件事,当不同的对象去执行时会有不同的结果,造成不同的状态。==比如买票,不同的人买票的结果是不同的—学生半价,军人优先…再比如一些社交软件,不同的会员有不同的待遇等等。


2. 构成多态的必要条件

  1. 必须是父类的指针或引用调用虚函数。
  2. 子类必须重写父类的虚函数。

3. 虚函数

虚函数就是类中用 virtual修饰的函数。

// 虚函数示例
class A
{
public:
  // 虚函数
  virtual void Print()
  {
    // ...
  }
  // ...
};


虚函数覆盖

也叫虚函数重写,是指**派生类中的虚函数和基类中对应的虚函数完全一样:包括返回值,函数名和参数列表都一样!!**但是具体的实现一般不一样,这样就构成了多态的原型—不同的对象调用同一个函数有不同的结果。


派生类重写的虚函数可以不用 virtual修饰,但是基类的虚函数必须用 virtual修饰。建议都加上!!


虚函数覆盖的两个例外

1.协变:是指基类虚函数返回某个基类对象的指针或者引用,派生类虚函数返回对应的派生类对象的指针或者引用。这样虽然返回值不同,但是也构成虚函数覆盖。


// 协变示例
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;}
}


2.析构函数的覆盖:只要基类的析构函数用virtual修饰(也建议这么做),那么派生类的析构函数就会自动构成覆盖!!这里可以认为编译器将析构函数名做了处理,统一将析构函数名命名为 destructor。


为什么基类的析构函数要定义成虚函数(构成多态)??

考虑这样一种情况:父类的指针指向子类的对象,现在 delete掉这个指针,如果不是多态,那就只能调用父类的析构,这样的话子类的资源就无法被清理了!!!而构成多态以后再去delete这个指针,就会去调用子类的析构(因为这个指针指向子类的对象),而子类的析构函数会自动调用父类的析构函数,所有的资源都会被清理。

class A
{
public:
  // 父类的析构必须声明成虚函数构成多态
  virtual ~A()
  {
    cout << "~A()" << endl;
  }
  // ...
};
class B : public A
{
public:
  // 子类不用virtual修饰也可以,但是不建议这样做
  ~B()
  {
    cout << "~B" << endl;
  }
  // ...
};
int main()
{
  // 父类的指针指向父类的对象,delete时调用父类的析构,没问题
  A* ptr1 = new A;
  delete ptr1;
  // 子类的指针指向子类的对象,delete时调用子类的析构,同时自动调用父类的析构
  // 没问题
  B* ptr2 = new B;
  delete ptr2;
  // 父类的指针指向子类的对象,delete时由于析构是虚函数,构成多态
  // 所以会调用到子类的析构,子类的析构有会自动调用父类的析构
  // 这样就可以把所有的资源都清理掉!!
  // 如果析构不是虚函数,那就只会调用父类的析构,子类的资源就无法清理!!
  A* ptr3 = new B;
  delete ptr3;
  return 0;
}


4. 重载,隐藏与覆盖

函数重载是指在同一个作用域中可以定义同名函数,只要参数列表不同即可。


隐藏也叫重定义,是指父类和子类(两个作用域)有相同的成员函数/成员变量名(只要名称一样就构成隐藏)。


覆盖也叫重写,是指父类和子类(两个作用域)有相同的虚函数(返回值,函数名,参数列表都一样)!



5. override 和 final

C++11的两个关键字,用于强制检查。


final关键字修饰基类虚函数,表示该虚函数不能被派生类覆盖,否则报错!


override关键字修饰派生类虚函数,表示该虚函数必须是覆盖了基类的虚函数,否则报错!

class A
{
public:
  virtual void Print() final  // 该虚函数不能被重写
  {
    // ...
  }
  virtual void Add()
  { 
    // ...
  }
  // ...
};
class B : public A
{
public:
  virtual void Add() override  // 这个虚函数必须是重写的
  {
    // ...
  }
  // ...
};


另外 **final修饰一个类表示这个类不能被继承!!**或者将类的构造函数私有,这个类也不能被继承。

// 不能被继承的类
class A final
{
  // ...
};


2. 抽象类

抽象类是指包含了纯虚函数的类,也叫做接口类。抽象类不能实例化出对象,而且派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。


这样抽象类就规范了派生类必须重写纯虚函数!这是一种强制接口继承!

// 抽象类示例
class A
{
public:
  virtual void Print() = 0;  // 纯虚函数  后面加 =0
}


1. 接口继承与实现继承

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


接口继承—派生类虚函数返回值,参数也都是继承的基类的,包括函数参数的缺省值也是基类的!!!


3. 多态的底层原理—虚表指针

1. 虚表指针(虚函数表指针)

类中只要有虚函数,其成员里就有一个虚函数表指针,简称虚表指针。指向虚函数表,虚函数表又是一个函数指针数组,数组里每一个成员是一个函数指针,指向虚函数的实现(虚函数表里没有一般成员函数的指针)。数组的最后一个元素是 nullptr。


虚表指针在类对象中存储(sizeof(类对象)时别忘了这个指针),而虚函数表在VS下存在代码段,虚函数和普通成员函数一样,也在代码段。因为虚表和虚函数都是所有这个类对象共享的!!


基类的虚表直接生成。而派生类的虚表则是先将基类中的虚表内容拷贝一份到派生类虚表中,如果派生类重写了基类中某个虚函数,就用派生类自己的虚函数覆盖虚表中基类的虚函数(覆盖的是指针,虚表本质是函数指针数组),然后将派生类自己新增加的虚函数(如果有的话)按其在派生类中的声明次序增加到派生类虚表的最后(增加的是指针)。


也就是说,类的虚表中有该类的所有虚函数指针,包括继承自父类的虚函数,重写父类的虚函数和自己类内声明的虚函数。

// 观察虚表
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;
};


在VS下观察上述代码的Base类和Derive类对象:



这样父类的指针或引用指向子类的对象时 其虚表里指针指向的虚函数 都是经过覆盖后的,自然就能调到不同的虚函数了!!


那么,为什么父类的指针和引用可以形成多态,但是父类的对象就不能实现多态呢??


==父类的对象就应该调用父类的方法!!==如果一个类对象 调用该类的方法 却调到了其他类的方法,比如 父类的对象中可能是指向父类的虚表指针,也可能是指向子类的虚表指针,那就乱套了!!! 所以多态这里都是通过指针和引用玩的,不能用对象玩多态!

2. 编译时决议(静态)与运行时决议(动态)

继承中对于普通成员函数的调用是编译时决议,编译时就已经确定好了要调用函数的地址。而对于多态中虚函数的调用时运行时决议,在程序运行时去对应的虚表中查找虚函数的地址然后找到要调用的函数。


4. 多继承中的虚表

这里和单继承的情况差不多,就是子类继承多个父类时 子类对象里有多个 父类的对象模型,每一个父类对象模型中都有一个虚函数指针!!



简单总结:只要类中有虚函数,就会在编译时形成虚函数表,实例化的时候形成虚函数指针指向虚函数表;在继承时父类的虚函数表也会拷贝给子类,然后对于子类重写的虚函数进行覆盖,子类新增的虚函数也要添加到虚函数表。多继承中子类就继承多个父类的虚函数表而已。


菱形继承和菱形虚拟继承中的虚表

多态又和菱形虚拟继承相结合,非常复杂了!!涉及到菱形虚拟继承中的虚基表指针和多态中的虚表指针。(虚基表中存偏移量,虚表是一个函数指针数组)


5. 其他多态问题

1. 虚函数与内联

多态机制中虚函数是可以写成内联函数,但是由于虚表中要存虚函数指针,而内联函数是直接展开的,没有函数地址!!所以当涉及到虚表时,编译器会忽略虚函数的内联属性!!当作普通函数。


2. 虚函数与静态成员函数

静态成员函数没有this指针,且所有类对象共享,不能放到虚函数表中!所以虚函数不能是静态成员函数。


!!涉及到菱形虚拟继承中的虚基表指针和多态中的虚表指针。(虚基表中存偏移量,虚表是一个函数指针数组)


5. 其他多态问题

1. 虚函数与内联

多态机制中虚函数是可以写成内联函数,但是由于虚表中要存虚函数指针,而内联函数是直接展开的,没有函数地址!!所以当涉及到虚表时,编译器会忽略虚函数的内联属性!!当作普通函数。


2. 虚函数与静态成员函数

静态成员函数没有this指针,且所有类对象共享,不能放到虚函数表中!所以虚函数不能是静态成员函数。


相关文章
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
32 1
|
1月前
|
存储 安全 编译器
【c++】深入理解别名机制--引用
本文介绍了C++中的引用概念及其定义、特性、实用性和与指针的区别。引用是C++中的一种别名机制,通过引用可以实现类似于指针的功能,但更安全、简洁。文章详细解释了引用的定义方式、引用传参和返回值的应用场景,以及常引用的使用方法。最后,对比了引用和指针的异同,强调了引用在编程中的重要性和优势。
39 1
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
2月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化2
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
77 6
|
2月前
|
安全 测试技术 C++
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化1
【C++篇】从零实现 C++ Vector:深度剖析 STL 的核心机制与优化
93 7
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
89 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
57 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)