第七层:多态(上)

简介: 第七层:多态(上)

前情回顾


在第六层中,我遇到了继承,它是面向对象三大特性之一,它也是我遇到的第二个面向对象的特性,因为继承,C++中的类被分成子类和父类,还有虚继承等强大的力量,但是,我还是掌握,走向了第七层…


🚄上章地址:第六层:继承


多态


“你来了啊,这层有着面向对象的最后一种特性——多态,它是每一个C++程序员都必须掌握的核心技术,希望你也可以掌握…”“面向对象的最后一中特性了吗?看起来是一场恶战。”


多态的基本概念


多态是C++面向对象的三大特性之一,多态分为两类:


静态多态:两种重载(函数重载和运算符重载)属于静态多态,复用函数名

动态多态:子类和虚函数实现运行时发生的多态

那这两种多态有什么区别呢?


静态多态的函数地址早绑定(在编译阶段确定函数地址)

动态多态的函数地址晚绑定(运行阶段确定函数地址)

那什么是晚绑定,什么是早绑定?下面就是早绑定的案例:


现在有一个函数,它的参数是父类引用,里面调用父类和子类当中都有的函数,现在传过去一个子类,可以调用吗?可以的话,是调用子类还是父类?

#include<iostream>
using namespace std;
class A
{
public:
  void a1()
  {
  cout << "A在调用" << endl;
  }
};
class A1 :public A
{
public:
  void a1()
  {
  cout << "A1在调用" << endl;
  }
};
void polym(A& a)
{
  a.a1();
}
void test1()
{
  A1 a;
  polym(a);
}
int main()
{
  test1();
  return 0;
}

0a2653c851af460fa595bd959398a8f1.png

是可以调用的,因为在C++中,允许父子之间的类型转换,不需要做强制类型转换,父类的引用可以直接指向子类,但是反省调用的是父类中的函数,那为什么传过去一个子类却调用的是父类呢?因为在底层,是调用父类,这个时候就是因为地址的早绑定,在编译阶段就确定了函数地址,所以不管传子还是父,都会指向父类中的函数地址,调用父类中的函数地址,如果要执行子类,那便不能让编译器就编译阶段就确定函数地址,不能进行早绑定,需要在运行时在进行绑定,这个时候就叫做地址晚绑定,需要在父类成员函数前加:


virtual

这个时候成员函数就变成了虚函数,也就实现晚绑定了:


#include<iostream>
using namespace std;
class A
{
public:
  virtual void a1()
  {
    cout << "A在调用" << endl;
  }
};
class A1 :public A
{
public:
  void a1()
  {
    cout << "A1在调用" << endl;
  }
};
void polym(A& a)
{
  a.a1();
}
void test1()
{
  A1 a;
  polym(a);
}
int main()
{
  test1();
  return 0;
}

0eacb84100b54626af849e6b562bf92a.png


动态多态的满足条件


在上面所提到的地址晚绑定,就是动态多态,那要实现动态多态,有一些需要满足的条件:


有继承关系

子类中要重写父类中虚函数(返回类型、函数名、参数列表都要一致),对于子类中的函数重写,virtual可加可不加


动态多态的使用


使用父类指针或者引用去执行子类对象

那具体为什么会这样呢?就是因为虚函数的作用。


虚函数


在第三层:C++中的对象和this指针中提到了,在类内没有非静态成员时的大小为一,那类内只有有虚函数的时候,大小是多少?


#include<iostream>
using namespace std;
class A
{
public:
  virtual void a1()
  {
  cout << "A在调用" << endl;
  }
};
class A1 :public A
{
public:
  void a1()
  {
  cout << "A1在调用" << endl;
  }
};
void test1()
{
  cout << sizeof(A) << endl;
}
int main()
{
  test1();
  return 0;
}


0a2653c851af460fa595bd959398a8f1.png

0a2653c851af460fa595bd959398a8f1.png

是8,那它的本质其实是指针,因为我编译器的环境是x64,所以是8,那虚函数的内部指向的其实和虚继承一样,内部也只一个指针,但是是vfptr,叫做虚函数指针,会指向vftable,叫做虚函数表,这个虚函数表内部是记录的是虚函数地址,当父类的指针指向的是指针或者引用的时候,发生多态,当子类通过父类引用调用函数的时候,就会去子类的虚函数表内调用子类当中的函数,因为重写,子类的函数就会覆盖掉自己虚函数表中的父类函数,这个时候就会调用子类函数。

0eacb84100b54626af849e6b562bf92a.png


多态的优点


代码组织结构清晰

可读性强

利于前期和后期的扩展以及维护

可以用代码实现一个不用多态的和一个使用多态的来对比验证:


现在处于一个麦饮料的地方,需要你选择饮料

普通方法:


#include<string>
#include<iostream>
using namespace std;
class drink
{
public:
  void dele(string s)
  {
  if (s == "mike")
  {
    cout << "牛奶来咯" << endl;
  }
  else if (s == "orange")
  {
    cout << "橙汁来咯" << endl;
  }
  else if (s == "coke")
  {
    cout << "可乐来咯" << endl;
  }
  } 
  string _d;
};
void test1()
{
  drink d;
  cin >> d._d;
  d.dele(d._d);
}
int main()
{
  test1();
  return 0;
}


0a2653c851af460fa595bd959398a8f1.png0a2653c851af460fa595bd959398a8f1.png

0a2653c851af460fa595bd959398a8f1.png

多态实现:


#include<string>
#include<iostream>
using namespace std;
class drink
{
public:
  virtual void dele()
  {
  } 
};
class mike :public drink
{
public:
  void dele()
  {
  cout << "牛奶来咯" << endl;
  }
};
class orange :public drink
{
public:
  void dele()
  {
  cout << "橙汁来咯" << endl;
  }
};
class coke :public drink
{
public:
  void dele()
  {
  cout << "可乐来咯" << endl;
  }
};
void test1()
{
  drink *d = new coke;
  d->dele();
  delete d;
  d=NULL;
}
int main()
{
  test1();
  return 0;
}

0eacb84100b54626af849e6b562bf92a.png

可以发现,多态的代码量要远远超于普通的写法,那为什么还要使用多态?就是因为多态的三个优点,并且,对于第三个优点来说,对于普通写法,要加入什么饮品,需要修改原码,而在真实的开发中,是不建议去修改的,提倡开闭原则1。


纯虚函数和抽象类


上面例子中,可以看到对于父类中的虚函数是基本不会调用的,用的多的是子类中的同名函数,这个时候,可以将父类中的虚函数变成纯虚函数,语法:


virtual 返回类型 函数名 (参数) =0;(大括号不用写)

这个时候,当类内有了纯虚函数,这个类也就被称为抽象类。


抽象类特点


抽象类无法实例化对象

子类必须重写抽象类函数中的纯虚函数,否则子类也会成为抽象类

验证抽象类无法实例化对象:


#include<string>
#include<iostream>
using namespace std;
class drink
{
public:
  virtual void dele() = 0;
};
class mike :public drink
{
public:
  void dele()
  {
  cout << "牛奶来咯" << endl;
  }
};
class orange :public drink
{
public:
  void dele()
  {
  cout << "橙汁来咯" << endl;
  }
};
class coke :public drink
{
public:
  void dele()
  {
  cout << "可乐来咯" << endl;
  }
};
void test1()
{
  drink d;
}
int main()
{
  test1();
  return 0;
}


0a2653c851af460fa595bd959398a8f1.png

0a2653c851af460fa595bd959398a8f1.png

目录
打赏
0
0
0
0
2
分享
相关文章
【C++11保姆级教程】移动构造函数(move constructor)和移动赋值操作符(move assignment operator)
【C++11保姆级教程】移动构造函数(move constructor)和移动赋值操作符(move assignment operator)
1233 0
vue 不同环境的baseurl配置
1.不管讲多少遍,对这个不同的环境中 baseURL的配置还是很懵 2.今天在单独开篇文章写下吧 3.vue-cli 模式环境变量 官方说明 4.process 说明 5.最新脚手架 vue/clie 4x
861 0
vue 不同环境的baseurl配置
Python创意爱心代码大全:从入门到高级的7种实现方式
本文分享了7种用Python实现爱心效果的方法,从简单的字符画到复杂的3D动画,涵盖多种技术和库。内容包括:基础字符爱心(一行代码实现)、Turtle动态绘图、Matplotlib数学函数绘图、3D旋转爱心、Pygame跳动动画、ASCII艺术终端显示以及Tkinter交互式GUI应用。每种方法各具特色,适合不同技术水平的读者学习和实践,是表达创意与心意的绝佳工具。
1222 0
深入理解Apache HBase:构建大数据时代的基石
在大数据时代,数据的存储和管理成为了企业面临的一大挑战。随着数据量的急剧增长和数据结构的多样化,传统的关系型数据库(如RDBMS)逐渐显现出局限性。
1341 12
现代前端开发中的动态组件加载与性能优化
传统的前端应用加载所有组件可能会导致性能问题和用户体验下降。本文讨论了现代前端开发中采用动态组件加载的策略,通过异步加载和按需渲染优化页面加载速度和资源利用效率。
Redis缓存应用与最佳实践:优化性能与处理挑战
本篇深入探讨了Redis在缓存应用中的最佳实践,旨在优化性能并处理常见的缓存挑战。我们首先介绍了设计高效缓存架构的基本原则,展示了如何使用Redis作为缓存存储来提升应用性能。进一步地,我们讨论了缓存更新策略,演示了如何在源数据更新时同时更新缓存,以确保数据的一致性。
1085 0
vue 头像修改-裁剪图片 vue-cropper
vue 头像修改-裁剪图片 vue-cropper
223 0
区块链交易所系统开发(稳定版)/开发案例/详细逻辑/规则方案丨区块链链交易所源码项目
The source code parsing of blockchain exchanges involves a large amount of technical details and complexity. The following is an overview and explanation of the common components and functions of blockchain exchange source code
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问