【C++】—— 多态(1)

简介: 【C++】—— 多态(1)

一、多态的概念

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

       举个简单的例子:抢红包,我们每个人都只需要点击一下红包,就会抢到金额。有些人能抢到几十元,而有些人只能抢到几元甚至几毛。也正说明了不同的人做相同的事,结果却不同,这就是多态。


       在C++中有两种多态性,一种是静态的多态、一种是动态的多态;


静态的多态:函数重载,看起来调用同一个函数却有不同的行为。静态:原理是编译时实现。


动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。动态:原理是运行时实现。

1ecd1b2606ed46e9956a89f231c9802c.png

二、多态的定义及实现

1.多态的构成条件

在继承中要构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数。

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

2.虚函数

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


       一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。


       虚函数只能是类中的一个成员函数,不能是静态成员或普通函数。


注意:我们在继承中为了解决数据冗余和二义性的问题,需要用到虚拟继承,关键字也是virtual,和多态中的virtual是没有关系的。

3.虚函数的重写

       虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。  
//买票
class Person 
{
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
//学生买票
class Student : public Person 
{
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//军人买票
class Soldier : public Person
{
public:
  void BuyTicket() { cout << "优先-买票-半价" << endl; }
};

通过对虚函数的重写,就能够实现多态:

//买票
class Person 
{
public:
  virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
//学生买票
class Student : public Person 
{
public:
  virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
//军人买票
class Soldier : public Person
{
public:
  void BuyTicket() { cout << "优先-买票-半价" << endl; }
};
//构成多态,传的哪个类型的对象,调用的就是这个类型的虚函数 --- 跟对象有关
//不构成多态,调用就是P的类型 --- 跟类型有关
void Func(Person& p) //或void Func(Person* p)
{
  p.BuyTicket();   //p->BuyTicket(); 
}
int main()
{
  Person ps;
  Func(ps);   //没有任何身份去买票,一定是全价
  Student st;
  Func(st);   //以学生的身份去买票,是半价
  Soldier so;
  Func(so);   //以军人的身份去买票,是优先并且半价
  return 0;
}

4.虚函数重写的两个例外

1. 协变

协变(基类与派生类虚函数返回值类型不同)

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

class Person 
{
public:
  virtual Person* BuyTicket() { cout << "买票-全价" << endl; return new Person; }
};
class Student : public Person 
{
public:
  virtual Student* BuyTicket() { cout << "买票-半价" << endl; return new Student; }
};
class Soldier : public Person
{
public:
  virtual Soldier* BuyTicket() { cout << "优先-买票-半价" << endl; return new Soldier; }
};
void Func(Person& p)
{
  p.BuyTicket(); 
}
int main()
{
  Person ps;
  Func(ps);
  Student st;
  Func(st);
  Soldier so;
  Func(so);
  return 0;
}

2.析构函数的重写

基类与派生类析构函数的名字不同


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

class Person 
{
public:
  // ~Person() { cout << "~Person()" << endl; }//1
  virtual ~Person() { cout << "~Person()" << endl; }//2
};
class Student : public Person 
{
public:
  // ~Student() { cout << "~Student()" << endl; }//1
  virtual ~Student() { cout << "~Student()" << endl; }//2
};
int main()
{
  //普通对象,析构函数是否是虚函数,是否完成重写,都能正确调用
  Person p;
  Student s;
  //动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数
  Person* p1 = new Person;//operator new + 构造函数
  Person* p2 = new Student;
  delete p1;//析构函数 operator + delete
  delete p2;
  return 0;
}

普通对象去调用析构时:无论是否是多态都能正确调用

1ecd1b2606ed46e9956a89f231c9802c.png

动态申请的父子对象,如果给了父类指针管理,那么需要析构函数是虚函数:


       由下图1的运行结果可以看到,如果析构函数不是虚函数的话,他析构了子类中父类的那一部分。但不能析构子类的部分,从而造成内存泄露。如果想达到析构的目的,就必须是多态,但此时的析构函数之差两个条件就满足了:虚函数和函数名相同;函数名编译器已经做了特殊处理,及都改为了destructor,所以我们就需要加上virtual 来实现多态。

1ecd1b2606ed46e9956a89f231c9802c.png

5.C++11 override和final

       从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和fifinal两个关键字,可以帮助用户检测是否重写。

1. fifinal:修饰虚函数,表示该虚函数不能再被重写。

class A final //直接限制不能被继承(也称最终类)
{
private:
  A(int a = 0)
    :_a(a)
  {}
protected:
  int _a;
};
class B :public A //不能被继承
{
};
/****************************************************/
class c 
{
public:
  virtual void f() final//限制它不能被子类中的虚函数重写
  {
    cout << "c::f()" << endl;
  }
};
class d :public c
{
public:
  virtual void f() //不能被重写
  {
    cout << "d::f()" << endl;
  }
};

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car 
{
public:
  virtual void Drive()
  {}
};
class Benz :public Car 
{
public:
  virtual void Drive() override //检查是否完成重写
  { cout << "Benz-舒适" << endl; }
};

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

1ecd1b2606ed46e9956a89f231c9802c.png

三、抽象类

1.概念

       在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

//抽象类
class Car
{
public:
  virtual void Drive() = 0;//纯虚函数 
};
int main()
{
  Car c;//抽象类不能实例化出对象
    return 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;
  }
};
int main()
{
    //派生类只有重写了虚函数才能实例化出对象
    Benz b1;
  BMV b2;
    //通过基类的指针去调用不同对象的函数
  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 = 'A';
};
int main()
{
  Base bb;
  cout << sizeof(Base) << endl;
  return 0;
}

可能刚看到这个题目的时候,都会觉得答案是8个字节,但是我们在打印后却发现是12个字节;这是为什么呢?

      因为有了虚函数,这个对象里面就多了一个成员,虚函数表指针__vfptr。

1ecd1b2606ed46e9956a89f231c9802c.png

2.虚函数表

  对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。那么这个虚表中到底是什么呢?

我们通过下面的程序来进行分析:


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

1ecd1b2606ed46e9956a89f231c9802c.png

     


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

派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。

基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一nullptr。

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

3.多态的原理

未实现多态时:

class Base
{
public:
  virtual void Func1() { cout << "Base::Func1()" << endl; }
private:
  int _b = 1;
};
class Derive : public Base
{
private:
  int _d = 2;
};
int main()
{
  Base b;
  Derive d;
  Base* p1 = &b;
  p1->Func1();
  p1 = &d;
  p1->Func1();
  return 0;
}

1ecd1b2606ed46e9956a89f231c9802c.png

实现了多态:

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;
  Base* p1 = &b;
  p1->Func1();
  p1 = &d;
  p1->Func1();
  return 0;
}

1ecd1b2606ed46e9956a89f231c9802c.png

4.再次理解多态构成的条件

为什么必须要是父类的指针或引用来调用虚函数,为什么不能是对象调用?


       指针和引用调用、对象调用本质上是子类切片完成的,指针和引用的切片只针对子类中父类的那一部分,其他都不管,只管与父类一样的内容(虚表中)切片过去;如果是对象切片,就是将虚表里的内容全部切片过去(因为都是要调用构造函数的),那么就有可能父类也指向了子类的虚表,那就乱套了;


       可以理解为:指针和引用是将虚表的指针拷贝过去了,对象是将虚表中的内容拷贝过去了;



目录
相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
40 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
155 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
81 1
|
4月前
|
存储 编译器 C++
|
5月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
55 1
【C++】深度解剖多态(下)
|
5月前
|
存储 编译器 C++
|
5月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
5月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
56 2
|
5月前
|
程序员 C++
【C++】揭开C++多态的神秘面纱
【C++】揭开C++多态的神秘面纱
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型