当前位置: 首页 > news >正文

【C++】C++中的虚函数和多态的定义与使用

C++中的虚函数和多态

  • 1.引入多态
      • 示例代码:引入多态
  • 2.实现多态---虚函数实现
        • 示例代码:虚函数实现
    • 2.1 语法规则:
  • 3.多态的特点和要求
  • 4.多态(虚函数)的底层原理
        • 示例代码:一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址
        • 示例代码:证明子类重写了父类的虚函数,父类的虚函数会被替换
  • 5.小结
  • 6.多态的分类
  • 练习:
        • 示例代码:基类不是虚函数的情况
        • 示例代码:基类是虚函数的情况
  • 7.重载和重写(覆盖),隐藏的区别
    • 7.1 重写(覆盖,复写) -->是指子类函数覆盖基类函数,子类重新定义了父类的同名方法(虚函数)
    • 7.2 重载 -->发生在同一个类的里面
    • 7.3 隐藏 -->子类和父类的函数名相同
  • 8.父类的同名函数是虚函数和普通函数(非虚函数)的区别
    • 8.1 虚函数
    • 8.2 普通函数(非虚函数)
  • C++的函数分成两大类

1.引入多态

多态:同一个方法在父类和子类中具备不同的表现形式,传递不同子类可以使用不同子类的同名方法–》叫做多态
字面上理解就是多种表现形式
比如: Animal Cat Dog Sheep 都有eat(),但是eat方法具备了多种表现形式
C++允许父类的指针或者引用指向不同的子类对象,传递实参的时候,传递不同子类对象,到时候可以调用不同子类里面的同名方法 —》多态

示例代码:引入多态

#include <iostream>
#include <cstring>
using namespace std;/*引入多态:面向对象三大思想:类封装,继承和派生,多态封装一个函数,要求该函数可以展示各种动物吃什么?*/
class Animal
{
public:void eat()  {cout<<"动物吃"<<endl;}
};class Cat:public Animal
{
public:void eat(){cout<<"猫吃鱼"<<endl;}
};class Dog:public Animal
{
public:void eat(){cout<<"狗吃骨头"<<endl;}   
};class Sheep:public Animal
{
public:void eat(){cout<<"羊吃草"<<endl;}   
};// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*思考1:需要参数,参数要具有通用性(可以兼容所有的动物)答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)父类 *p=&子类父类 &b=子类思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other)   //只要是Animal的子类都可以兼容 
{//调用各个动物里面的eat方法other->eat();
}int main(int argc,char **argv)
{// 定义各种动物对象Cat c1;Dog d1;Sheep s1;Animal a1;Animal &p = c1;/* 当方法不是虚函数时,编译器根据 指针/引用的声明类型(而不是实际对象类型)决定调用哪个方法 这里 p 是 Animal& 类型(基类引用),因此 p.eat() 始终调用 Animal::eat()*/p.eat();    //showAnimalEat(c1);//showAnimalEat(d1);//showAnimalEat(s1);showAnimalEat(&c1);showAnimalEat(&d1);showAnimalEat(&s1);return 0;   
}/*
执行结果:动物吃动物吃动物吃动物吃*/

2.实现多态—虚函数实现

虚函数是为了多态而生的

示例代码:虚函数实现
#include <iostream>
#include <cstring>
using namespace std;/*引入多态:面向对象三大思想:类封装,继承和派生,多态封装一个函数,要求该函数可以展示各种动物吃什么?*/
class Animal
{
public:// 在基类函数中添加virtual关键字virtual void eat(){cout<<"动物吃"<<endl;}
};class Cat:public Animal
{
public:void eat(){cout<<"猫吃鱼"<<endl;}
};class Dog:public Animal
{
public:void eat(){cout<<"狗吃骨头"<<endl;}   
};class Sheep:public Animal
{
public:void eat(){cout<<"羊吃草"<<endl;}   
};// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*思考1:需要参数,参数要具有通用性(可以兼容所有的动物)答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)父类 *p=&子类父类 &b=子类思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other)   //只要是Animal的子类都可以兼容 
{//调用各个动物里面的eat方法other->eat();
}int main(int argc,char **argv)
{// 定义各种动物对象Cat c1;Dog d1;Sheep s1;Animal a1;Animal &p = c1;// 定义基类为虚函数后调用的是子类的方法p.eat();    //showAnimalEat(c1);//showAnimalEat(d1);//showAnimalEat(s1);showAnimalEat(&c1);showAnimalEat(&d1);showAnimalEat(&s1);return 0;   
}/*
执行结果:猫吃鱼猫吃鱼狗吃骨头羊吃草*/

2.1 语法规则:

virtual  返回值  函数名(参数)
{}

3.多态的特点和要求

  • 必须要有继承,没有继承,就没有多态(父类的指针/引用可以指向不同的子类对象)
  • 子类必须要重写父类的同名方法
  • 父类的同名方法必须定义成虚函数

注意:父类的同名方法定义成了虚函数,所有子类中同名的方法全部都默认是虚函数(子类加不加virtual都行)

4.多态(虚函数)的底层原理

  • 虚函数表(虚表):C++中专门用来存放虚函数地址的一种数据结构(本质是个存放函数地址的数组)
  • 一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址
  • 一个类中定义了虚函数,那么这个类以及它派生出来的子类都会有各自独立的虚函数表
  • 父类的指针或者父类的引用去调用方法的时候,其实就是去查询虚函数表
    在这里插入图片描述
示例代码:一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址
#include <iostream>
#include <cstring>
using namespace std;/*一个类只要定义了虚函数,该类所有的对象中会新增一个指针,该指针用来指向虚函数表(虚表)的首地址*/
class Animal
{
public:virtual void eat(){cout<<"动物吃"<<endl;}
};class Cat:public Animal
{
public:void eat(){cout<<"猫吃鱼"<<endl;}
};class Dog:public Animal
{
public:void eat(){cout<<"狗吃骨头"<<endl;}   
};class Sheep:public Animal
{
public:void eat(){cout<<"羊吃草"<<endl;}   
};int main(int argc,char **argv)
{cout<<"sizeof(Animal):"<<sizeof(Animal)<<endl;cout<<"sizeof(Cat):"<<sizeof(Cat)<<endl;cout<<"sizeof(Dog):"<<sizeof(Dog)<<endl;cout<<"sizeof(Sheep):"<<sizeof(Sheep)<<endl;return 0;   
}/*
执行结果:sizeof(Animal):8sizeof(Cat):8sizeof(Dog):8sizeof(Sheep):8*/
示例代码:证明子类重写了父类的虚函数,父类的虚函数会被替换
#include <iostream>
#include <cstring>
using namespace std;/*引入多态:面向对象三大思想:类封装,继承和派生,多态封装一个函数,要求该函数可以展示各种动物吃什么?*/
class Animal
{
public:virtual void eat(){cout<<"动物吃"<<endl;}
};class Cat:public Animal
{
public:// 如果子类重写了父类的虚函数,父类的虚函数会被替换void eat(){cout<<"===子类调用父类的虚函数方法====="<<endl;Animal::eat();  //调用父类的虚函数方法cout<<"===子类调用父类的虚函数方法====="<<endl;cout<<"子类-->猫吃鱼"<<endl;}
};class Dog:public Animal
{
public:// 如果子类不重写父类的虚函数,父类的虚函数会被保留// void eat()// {//  cout<<"狗吃骨头"<<endl;// }    
};class Sheep:public Animal
{
public:// 如果子类不重写父类的虚函数,父类的虚函数会被保留// void eat()// {//  cout<<"羊吃草"<<endl;// }    
};// 封装一个函数,要求该函数可以展示各种动物吃什么?
/*思考1:需要参数,参数要具有通用性(可以兼容所有的动物)答案:C++规定,有了继承之后,父类的指针或者父类的引用可以直接指向不同的子类对象(不需要使用任何强转类型转换)父类 *p=&子类父类 &b=子类思考2:我们希望传递不同的子类对象,可以调用不同子类里面的同名eat方法答案:必须使用虚函数才能解决,此时无法解决
*/
//void showAnimalEat(Animal &other) //只要是Animal的子类都可以兼容
void showAnimalEat(Animal *other)   //只要是Animal的子类都可以兼容 
{//调用各个动物里面的eat方法other->eat();
}int main(int argc,char **argv)
{// 定义各种动物对象Cat c1;Dog d1;Sheep s1;Animal a1;Animal &p = c1;p.eat();    // ----》Animal &p = c1;调用的是子类的eat方法//showAnimalEat(c1);//showAnimalEat(d1);//showAnimalEat(s1);showAnimalEat(&c1);		// 调用的是子类的eat方法showAnimalEat(&d1);   	// ----》调用的是父类的eat方法showAnimalEat(&s1);		// 调用的是子类的eat方法return 0;   
}/*
执行结果:===子类调用父类的虚函数方法=====动物吃   ===子类调用父类的虚函数方法=====子类-->猫吃鱼===子类调用父类的虚函数方法=====动物吃===子类调用父类的虚函数方法=====子类-->猫吃鱼动物吃动物吃*/

5.小结

C++三大"虚"

  • 第一:虚继承和虚基类
  • 第二:虚函数和多态
  • 第三:纯虚函数和抽象类

6.多态的分类

分为两大类:

  • 编译时多态:指的就是函数重载
  • 运行时多态(动态联编):指的就是目前学习的这种,父类的指针,引用指向不同的子类对象

练习:

示例代码:基类不是虚函数的情况
#include <iostream>
#include <cstring>
using namespace std;/*规律:如果vf不是虚函数此时四种情况究竟调用谁的vf,看赋值运算左边的指针类型即可如果vf是虚函数此时四种情况究竟调用谁的vf,看赋值运算右边的指针类型即可
*/
class B
{
public:void vf(){cout<<"父类B里面的vf函数"<<endl;}
};class D:public B
{
public:void vf(){cout<<"子类D里面的vf函数"<<endl;}
};int main(int argc,char **argv)
{//定义父类和子类的对象,指针B b;B *pb;D d;D *pd;//第一组pb=&b;      //父类指针指向父类对象pb->vf();  pb=&d;      //父类指针指向子类对象pb->vf();//第二组pd=&d;          //子类指针指向子类对象pd->vf();pd=(D *)&b;     //子类指针指向父类对象pd->vf();return 0;    
}/*  
执行结果:父类B里面的vf函数父类B里面的vf函数子类D里面的vf函数子类D里面的vf函数
*/
示例代码:基类是虚函数的情况
#include <iostream>
#include <cstring>
using namespace std;/*规律:如果vf不是虚函数此时四种情况究竟调用谁的vf,看赋值运算左边的指针类型即可如果vf是虚函数此时四种情况究竟调用谁的vf,看赋值运算右边的指针类型即可
*/
class B
{
public:virtual void vf(){cout<<"父类B里面的vf函数"<<endl;}
};class D:public B
{
public:void vf(){cout<<"子类D里面的vf函数"<<endl;}
};int main(int argc,char **argv)
{//定义父类和子类的对象,指针B b;B *pb;D d;D *pd;//第一组pb=&b;      //父类指针指向父类对象pb->vf();  pb=&d;      //父类指针指向子类对象pb->vf();//第二组pd=&d;          //子类指针指向子类对象pd->vf();pd=(D *)&b;     //子类指针指向父类对象pd->vf();return 0;    
}/*  
执行结果:父类B里面的vf函数子类D里面的vf函数子类D里面的vf函数父类B里面的vf函数
*/

在这里插入图片描述

7.重载和重写(覆盖),隐藏的区别

7.1 重写(覆盖,复写) -->是指子类函数覆盖基类函数,子类重新定义了父类的同名方法(虚函数)

要求:

  • 在不同的类中(分别位于子类和父类)
  • 同名同参,同返回值(此时返回值类型不可以不一样,返回值类型不一样编译报错)
  • 基类的函数名前必须有virtual关键字

7.2 重载 -->发生在同一个类的里面

7.3 隐藏 -->子类和父类的函数名相同

  • 如果派生类函数与基类函数同名,但参数不同,无论基类函数前是否有virtual修饰,基类函数被隐藏.
  • 如果派生类函数与基类函数同名,参数也相同(不关心返回值类型),但是基类函数前无virtual修饰,基类函数被隐藏。

隐藏的具体表现是:

假设父类是void eat();
子类是void eat(int n);
Cat c;
c.eat();   //编译语法错误,原因是父类同名方法被隐藏了
c.Animal::eat(); //编译正确,父类被隐藏的同名方法只能通过类作用域解析运算符调用
c.eat(666);//编译正确

重写(覆盖,复写)的例子

class Animal
{
public:virtual void eat();
};
class Dog:public Animal
{
public:void eat();   //Dog重写(覆盖,复写)了父类Animal的eat方法
};

重载的例子

class Dog
{
public:Dog();Dog(int age);  //同一个类中重载了构造了函数void setAge();void setAge(int age);
}

隐藏的例子1

class Animal
{
public:virtual void eat();
};
class Dog:public Animal
{
public:void eat(string);  //父类的eat被隐藏
};

隐藏的例子2

class Animal
{
public:void eat();
};
class Dog:public Animal
{
public:void eat(string);  //父类的eat被隐藏
};

隐藏的例子3

class Animal
{
public:void eat();
};
class Dog:public Animal
{
public:void eat();  //父类的eat被隐藏
};

8.父类的同名函数是虚函数和普通函数(非虚函数)的区别

8.1 虚函数

虚函数:编译器采用动态联编
动态联编:编译器会严格按照赋值运算右边的类型来调用对应的同名函数

8.2 普通函数(非虚函数)

普通函数(非虚函数):编译器采用静态联编
静态联编:编译器会严格按照赋值运算左边的类型来调用对应的同名函数

  • 父类eat()函数不是虚函数:
Animal a;
Cat c;
Animal &r=c; //父类的引用指向子类对象 
r.eat(); // 当父类eat()函数不是虚函数时此时调用父类的eat()

无论是指针还是引用,此时都以左边类型(Animal)为准

  • 父类eat()函数是虚函数:
Animal a;
Cat c;
Animal &r=c; 
r.eat(); // 父类eat()函数是虚函数,此时调用子类的eat()

无论是指针还是引用,此时都以右边类型(Cat )为准

C++的函数分成两大类

第一类:类的成员函数 —》通过对象或者类名调用
第二类:非成员函数,普通函数 —》直接调用

http://www.lqws.cn/news/489079.html

相关文章:

  • AI 领航设计模式学习:飞算 JavaAI 解锁单例模式实践新路径
  • PROFIBUS DP转ETHERNET/IP在热电项目中的创新应用
  • WinUI3入门9:自制SplitPanel
  • Java基础(三):逻辑运算符详解
  • 提高WordPress网站加载速度和用户体验
  • C# SolidWorks二次开发-实战2,解决SolidWorks2024转step文件名乱码问题
  • 【25】木材表面缺陷数据集(有v5/v8模型)/YOLO木材表面缺陷检测
  • 【开源工具】一键解决使用代理后无法访问浏览器网页问题 - 基于PyQt5的智能代理开关工具开发全攻略
  • 干货分享 如何做好数据可视化?
  • Qt联合Halcon开发四:【详细图解】海康相机配置并链接测试
  • Zynq + FreeRTOS + YAFFS2 + SQLite3 集成指南
  • Windows电脑数据恢复终极指南:从原理到实战
  • el-cascader 设置可以手动输入也可以下拉选择
  • 性能监控与智能诊断系统的全流程
  • (LeetCode 面试经典 150 题) 27.移除元素
  • Java 类加载机制详解
  • Spring AI 项目实战(十二):Spring Boot +AI + DeepSeek + 百度OCR 公司发票智能处理系统的技术实践(附完整源码)
  • C++11 <array>从入门到精通
  • Git新建分支并同步到远程
  • 终端创建虚拟环境
  • Blazor-内置输入组件
  • 华为云 Flexus+DeepSeek 征文|增值税发票智能提取小工具:基于大模型的自动化信息解析实践
  • 2025 年焊接相机十大品牌测评:抗光耐高温解决方案深度解析
  • Three.js入门第一步:两种方式搭建你的3D项目[特殊字符]️
  • CentOS 上安装snmp
  • mac隐藏文件现身快捷键
  • 从 0 到 1 实现 C++ string 类:深入理解动态字符串的底层机制--《Hello C++ Wrold!》(11)--(C/C++)
  • 编程实践:sigmastar330 调用IVE图像处理加速
  • Linux密码校验机制深度剖析:从shadow文件到crypt加密
  • PicHome结合容器化与内网穿透实现跨平台影像管理