C++进阶 多态讲解

简介: C++进阶 多态讲解

多态的概念


多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果



例如 我们去吃海底捞的时候 普通人去就是原价 学生去就会有学生优惠 这就叫做多态


多态的定义及实现


多态的构成条件


多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。语法上 我们这里要满足两个条件


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

我们会在文章的后面解释 为什么只能用指针或者是引用 不能使用对象


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

还是一样 我们下面会解释 为什么是虚函数 为什么必须要重写


虚函数


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


例如下面的这段代码


class Person
{
  // 虚函数
  virtual void Print();
};
int main()
{
  return 0;
}


我们的Print就是虚函数


这里有两点需要注意的:


只有类的非静态成员函数前可以加virtual

关于这个问题 因为静态成员函数是没有this指针的


虚函数这里的virtual和虚继承中的virtual是同一个关键字,但是它们之间没有任何关系。虚函数这里的virtual是为了实现多态,而虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

这个是关于virtual的用法 就不用过多解释了


虚函数的重写

虚函数的重写在语法层面上叫做重写


在原理层面上叫做覆盖 后面的例子会让大家明白这一点


它有两个必要条件


必须是虚函数


三同 即 函数名相同 参数相同 返回值相同


还是一样 我们来看代码


class Person
{
public:
  virtual void buy_ticket()
  {
  cout << "买票 - 原价" << endl;
  }
private:
};
class child : public Person
{
public:
  // 这里的virtual也可以不写 因为语法规定 只要三同 实际上这里的函数就继承了父类的虚函数属性
  // 但是不管我们平时敲代码 或者写项目的时候都要加上去 保证代码的可读性
  virtual void buy_ticket()
  {
  cout << "买票 - 半价" << endl;
  }
private:
};
class soldier : public Person
{
public:
  // 为了证明上面说可以省略 virtual 的正确性 这里省略之 
  void buy_ticket()
  {
  cout << "买票 - 优先" << endl;
  }
private:
};


现在我们通过父类的对象指针还有引用调用看看能不能完成多态


void func1(Person& p)
{
  p.buy_ticket(); 
}
void func2(Person* p)
{
  p->buy_ticket();
}
void test_vritual()
{
  Person p;
  child c;
  soldier s;
  func1(p);
  func1(c);
  func1(s);
  cout << "test ------ ptr" << endl;
  func2(&p);
  func2(&c);
  func2(&s);
}
int main()
{
  test_vritual();
  return 0;
}

显示效果如下


f27ae746ae1e4416b8c6f770a4ea7f02.png


虚函数重写的两个例外


协变


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


比如说我们改写下之前写的代码

ea43dbac64924e43946eb4757386ca06.png


我们可以看到这里它们的返回值并不相同 但是依然满足多态 可以运行 这就是协变机制


要记住的一点是 协变的返回值必须是基类或者派生类的指针或引用 不然会报错 类似这样


844517b8a9144b5091797249839c4644.png


析构函数的重写


如果父类的析构函数为虚函数 那么只要子类的析构函数定义了 那么它就与父类中的析构函数构成重写


比如说我们看下面的代码


class a
{
public:
  virtual ~a()
  {
  cout << "~a" << endl;
  }
};
class b : public a
{
public:
  virtual ~b()
  {
  cout << "~b" << endl;
  }
};


其中 a和b的析构函数就构成多态


怎么证明呢? 我们再来看下面的一段代码


void func(a& p)
{
  p.~a();
}
int main()
{
  a a1;
  b b1;
  cout << "start test" << endl;
  func(a1);
  func(b1);
  cout << "test end" << endl;
  return 0;
}

运行结果如下

89810d84dc1c49efa1e21e23ac763110.png


我们可以发现 我们输入不同的对象引用确实触发了不同的析构函数


至于为什么出现了三次析构函数 可以参考下我上一篇继承的博客


派生类对象在析构时,会先调用派生类的析构函数再调用基类的析构函数。


至于后面的三次析构则是 a1 和 b1的生命周期结束了 自动调用的


那么这里的问题就来了


父类和子类的析构函数构成重写的意义何在呢?


我们试想下面的场景


我们创建一个父类对象和一个子类对象 并且使用父类的指针指向它们


然后全部delete掉


a* a1 = new a;
a* b1 = new b;
delete a1;
delete b1;


此时如果没有重写析构函数的话 两次析构其实都是析构的父类的


这样子就会造成一个内存泄漏的情况


而我们期望的是 delete a1 就是析构父类


delete b1 就是析构父类加子类


本质上是一种多态 所以我们要重写


记不记得我们上面继承提过一个知识点


析构函数的名字会被统一处理成destructor();


现在应该能充分理解为什么这么做的原因了吧 为了多态开路


C++11 override和final


我们从上面的博文中就可以看出 C++对于函数重写比较严格 但是我们有可能由于自身的疏忽 导致字符写反 或者返回值写错等原因无法构成重写


而这种错误要在程序运行之后才能被编译器发现 我们觉得有点太慢了


为了解决这个问题 C++中给出了两个关键字 这里我们来一个个学习下它们


final

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


我们来看下面的代码


class Person
{
public:
  virtual void print() final;
private:
};
class child : public Person
{
public:
  void print()
  {
  ;
  }
private:
};


运行下我们可以发现


953afe86c5cd4a7c8d9b50dc1fbfed55.png


编译的时候会报错 不能够重写


override


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


我们来看下面的两组对比

8dc7f3fb628e4b5fa0281828e290f03c.png


0ae8e630ba6947e8a5e38bb874ba90d8.png


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

b9f36105c37f4ee888f34bbcd61caefc.png


具体的内容看上面这张图就好


抽象类


抽象类的概念


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


抽象类不能实例化出对象


为了证明这个概念 我们写出下面的代码


class person
{
public:
  virtual void print() = 0;
private:
};
int main()
{
  person p;
  return 0;

f13cdd3cacb2477d9c3fc34954a86a54.png

我们可以发现 符合我们上面的结论


派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象


比如说这样子

class child : public person
{
public:
private:
};

eeecd6b92c2c44dbbb759abad01dec93.png


接着我们重写下虚函数试试


class child : public person
{
public:
  virtual void print() 
  {
  cout << "child" << endl;
  }
private:
};

e670a9e907964788a741bf15da5e64b8.png


我们发现 这样子就可以运行了


抽象类既然不能实例化出对象,那抽象类存在的意义是什么?


我们说 意义有二


抽象类可以更好的去表示现实世界中,没有实例对象对应的抽象类型,比如:植物、人、动物等。


抽象类很好的体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。


接口继承和实现继承


实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。


接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态


总结


本文主要讲解了C++中多态的一些使用

相关文章
|
1月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
36 2
C++入门12——详解多态1
|
1月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
74 1
|
3月前
|
存储 编译器 C++
|
4月前
|
存储 编译器 C++
【C++】深度解剖多态(下)
【C++】深度解剖多态(下)
53 1
【C++】深度解剖多态(下)
|
3月前
|
存储 编译器 C++
C++多态实现的原理:深入探索与实战应用
【8月更文挑战第21天】在C++的浩瀚宇宙中,多态性(Polymorphism)无疑是一颗璀璨的星辰,它赋予了程序高度的灵活性和可扩展性。多态允许我们通过基类指针或引用来调用派生类的成员函数,而具体调用哪个函数则取决于指针或引用所指向的对象的实际类型。本文将深入探讨C++多态实现的原理,并结合工作学习中的实际案例,分享其技术干货。
71 0
|
4月前
|
机器学习/深度学习 算法 C++
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
C++多态崩溃问题之为什么在计算梯度下降时需要除以批次大小(batch size)
|
4月前
|
Java 编译器 C++
【C++】深度解剖多态(上)
【C++】深度解剖多态(上)
52 2
|
4月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
4月前
|
编译器 程序员 C++
【C++高阶】掌握C++多态:探索代码的动态之美
【C++高阶】掌握C++多态:探索代码的动态之美
43 0
|
4月前
|
存储 编译器 C++
C++基础知识(七:多态)
多态是面向对象编程的四大基本原则之一,它让程序能够以统一的接口处理不同的对象类型,从而实现了接口与实现分离,提高了代码的灵活性和复用性。多态主要体现在两个层面:静态多态(编译时多态,如函数重载)和动态多态(运行时多态,主要通过虚函数实现)。