【C++】从零开始认识多态(一)

简介: 面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。

image.png

送给大家一句话:

一个犹豫不决的灵魂,奋起抗击无穷的忧患,而内心又矛盾重重,真实生活就是如此。  – 詹姆斯・乔伊斯 《尤利西斯》


_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ

_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ

_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ_φ(* ̄ω ̄)ノ

从零开始认识多态

1 前言

面向对象技术(oop)的核心思想就是封装,继承和多态。通过之前的学习,我们了解了什么是封装,什么是继承。

封装就是对将一些属性装载到一个类对象中,不受外界的影响,比如:洗衣机就是对洗衣服功能,甩干功能,漂洗功能等的封装,其功能不会受到外界的微波炉影响。


继承就是可以将类对象进行继承,派生类会继承基类的功能与属性,类似父与子的关系。比如水果和苹果,苹果就有水果的特性。


接下来我们就来了解学习多态!

2 什么是多态

多态是面向对象技术(OOP)的核心思想,我们把具有继承关系的多个类型称为多态类型,通俗来讲:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个例子:就拿刚刚结束的五一假期买票热为例,买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。同样一个行为在不同的对象上就有不同的显现。

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。


#include<iostream>

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 p;
  Student s;
  //同一个函数对不同对象有不同效果
  Func(p);
  Func(s);

  return 0;
}

比如Student继承了Person。Person对象买票全价,Student对象买票半价。我们运行看看:

  • 多态调用:运行时,到指定对象的虚表中找虚函数来调用(指向基类调用基类的虚函数,指向子类调用子类的虚函数)
  • 普通调用:编译时,调用对象是哪个类型,就调用它的函数。

乍一看还挺复杂,接下来我们就来了解多态的构成。

3 多态的构成

继承的情况下才有虚函数,才有多态!!!

多态构成的条件

  1. 必须通过基类的指针或者引用调用虚函数(virtual修饰的类成员函数)
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(父子虚函数要求三同)

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数

看起来很是简单,当时其实有很多的坑!!!一不小心就会掉进去。

3.1 协变

上面我们说了多态的条件:父子虚函数要求三同。但是却有这样一个特殊情况:协变!

协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同

  1. 基类虚函数返回基类对象的指针或者引用
  2. 派生类虚函数返回派生类对象的指针或者引用

这样的情况称为协变。

#include<iostream>

using namespace std;

class A {};
class B : public A {};
//这里明显返回类型不同但是结构仍然正常
class Person 
{
public:
  virtual A* BuyTicket() { cout << "买票->全价" << endl; return nullptr; }
};

class Student : public Person
{
public:
  virtual B* BuyTicket() { cout << "买票->半价" << endl; return nullptr; }
};

很明显派生类与基类的返回值不同(注意一定是:基类返回“基类”,派生类返回“派生类”):

但是结果确实正常的,依然构成多态,这样的情况就称为协变!!!

3.2 析构函数的重写

析构函数在编译阶段都会转换成:destructor(),所以表面析构函数名字不同,但是实质上是一致的。这样就会构成多态。

来看正常情况下的析构:

 
#include<iostream>
using namespace std;

class Person
{
public:
  ~Person() { cout << "~Person()" << endl; }
};

class Student : public Person
{
public:
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  Person p;
  Student s;

  return 0;
}

这样会正常的调用析构函数(子类析构会自动调用父类析构->先子后父):

再来看:

int main()
{
  //Person p;
  //Student s;
  //基类可以指向基类 也可以指向派生类的基类部分
  Person* p1 = new Person ;
  //通过切片来指向对应内容
  Person* p2 = new Student;

  delete p1;
  delete p2;

  return 0;
}

如果是这样呢?

这样调用的析构不对啊!Student对象没有调用自身的析构函数,而是调用Person的,为什么会出现这样的现象呢???

这样就可能会引起一个十分严重的问题:内存泄漏

#include<iostream>

using namespace std;

class Person
{
public:
  ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
  Student() { int* a = new int[100000000]; }
  ~Student() { cout << "~Student()" << endl; }
};
int main()
{
  for(int i = 0; i< 100000 ; i++)
  {
    Person* p2 = new Student;
    delete p2;
  }
  return 0;
}

如果我们在Student中申请一个空间,而析构的时候却不能调用其析构函数俩把申请的空间free这样就导致了内存泄漏!!!

这就十分危险了!!!

而我们希望的是指向谁就调用谁的析构:指向基类调用基类析构,指向派生类调用派生类析构。

那我们怎么做到呢 ----> 当然就是多态了!!!

那我们来看看现在满不满足多态的条件:

  1. 必须通过基类的指针或者引用调用虚函数(virtual修饰的类成员函数)
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(父子虚函数要求三同)

在编译的时候,析构函数都会变成destructor,这样满足三同!构成重写

那么我们就只需要将析构函数变为虚函数就可以了:

class Person
{
public:
  virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
  virtual ~Student() { cout << "~Student()" << endl; }
};

来运行看看:

老铁 OK了!!!应该释放的空间全都释放了!!!

所以建议析构函数设置为虚函数,避免出现上述的情况。

3.3 语法细节

  1. 派生类(基类必须写)的对应函数可以不写virtual(这个语法点非常奇怪!建议写上virtual
  2. “重写”的本质是重新写函数的实现,函数声明(包括缺省参数的值)与基类一致

来看一道面试题:

以下程序输出结果是什么()

#include<iostream>

using namespace std;

class A
{
public:
  virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
  virtual void test() { func(); }
};
class B : public A
{
public:
  void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
  B* p = new B;
  p->test();
  return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

答案是:B 为什么呢?

  1. 首先:
  • A类与B类构成继承关系
  • func函数是虚函数(B类是派生类,可以不写virtual),并且AB 中满足三同。构成多态。
  • test函数的参数是基类指针(A* this 成员函数的默认参数),满足多态条件

2.然后:

  • 主函数中调用test函数,因为B是子类,没有test函数,所以会在父类A中寻找。
  • test函数调用 func函数,参数this指向的是B类(指向谁调用谁),所以就会调用B类的func函数B->
  • 重写的本质是对函数的实现进行重写,函数的结构部分(包括参数,缺省值,函数名,返回值等)与基类一致。所以是 1

所以就可以判断是B选项。

当然实际中不能这么写代码奥!!!会有生命危险(Doge)

3.4 C++11 override 和 final

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

  1. final:
  • 修饰类(最终类),表示该类不能被继承。(C++98直接粗暴使用private来做到不能继承)
    class car final { };
  • 修饰虚函数,表示该虚函数不能再被继承
    virtual void func() final { }
  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
  virtual void Drive() {}
};

class Benz :public Car {
public:
  virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

3.5 重写(覆盖) - 重载 - 重定义(隐藏)

我们来区分一下这三个类似的概念:

1.重载 :

  • 两个函数作用在同一作用域
  • 函数名相同,参数不同

2.重写(覆盖):

  • 两个函数分别在基类作用域好派生类作用域
  • 函数名、参数、返回值都一样(协变例外)
  • 两个函数必须是虚函数

3.重定义:

  • 两个函数分别在基类作用域好派生类作用域
  • 仅仅函数名相同
  • 两个基类和派生类的同名函数不是重写就是重定义

重定义包含重写!!!

相关文章
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
42 2
C++入门12——详解多态1
|
7月前
|
C++
C++中的封装、继承与多态:深入理解与应用
C++中的封装、继承与多态:深入理解与应用
163 1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
83 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中,如何定义一个简单的线性回归模型