C++ 多态和虚函数详解

news2024/9/25 11:14:58

本文章内容来源于C++课堂上的听课笔记

多态基础

多态(Polymorphism)是面向对象编程中的一个重要概念,它允许使用统一的接口来表示不同的对象和操作。多态性有两种主要形式:静态多态性(编译时多态性)和动态多态性(运行时多态性)

多态性分为两类: 静态多态性和动态多态性

静态多态性(编译时多态性):
定义: 在编译时确定方法的调用,通常与函数重载(overloading)相关。
例子: 方法重载是一种静态多态性的体现,编译器在编译时能够根据方法的参数类型或个数来选择正确的方法

class StaticPolymorphism {
public:
    void display(int value) {
        // ...
    }

    void display(double value) {
        // ...
    }
};

注意:重载(overload)和重写(override)的区别,下面是个重写的例子:

#include <iostream>
using namespace std;
class CA 
{  public:
 void f(int)
   { cout << "CA::f(int)"<< endl; }}; 
class CB : public CA 
{   public:
	void f(int) {cout << "CB::f(int) "
                                 << endl;} 
	void f(int,int) {cout << "CB::f(int,int)" << endl; } 
	int f(int,int,int) {
                  cout << "CB::f(int,int,int)"
                          << endl; }
	  void test() {
                f(1);  f(1,1); f(1,1,1); }};
 int main() 
 {  CB B;
    B.test();} 

如果把上面的void f(int) {cout << "CB::f(int) "<< endl;} 注释掉,会发生什么错误?

原因:在调用一个类的成员函数时,编译器会沿着类的继承链逐级的向上查找函数的定义,如果找到了则停止查找 如果派生类CB和基类CA都有同一个同名函数f(不论参数是否相同),编译器最终将选择派生类中的函数,即派生类的成员函数“隐藏”了基类的成员函数, 它阻止了编译器继续向上查找函数的定义

所以如果修改成下面这样就可以运行了:

#include <iostream>
using namespace std;
class CA 
{  public:
 void f(int)
   { cout << "CA::f(int)"<< endl; }}; 
class CB : public CA 
{   public:
	  void test() {
                f(1);  }};
 int main() 
 {  CB B;
    B.test();} 

动态多态性(运行时多态性):
定义: 在运行时确定方法的调用,通常与虚函数(virtual function)和继承相关。
例子: 通过虚函数和基类指针实现动态多态性,可以在运行时选择调用合适的函数。 

#include<iostream>
using namespace std;

class Base {
public:
    virtual void display() {
        // ...
        cout<<1<<endl;
    }
};

class Derived : public Base {
public:
    void display()  override{
        // ...
        cout<<2<<endl;
    }
};

int main() {
    Base* ptr = new Derived();
    ptr->display(); // 在运行时调用 Derived 类的 display 方法
    delete ptr;
    return 0;
}

静态多态性和动态多态性的区别:
时机不同: 静态多态性在编译时确定,动态多态性在运行时确定。

实现机制不同: 静态多态性通常与函数重载等相关,而动态多态性通常与虚函数和继承相关。

使用场景不同: 静态多态性适用于编译时能够确定的情况,而动态多态性适用于在运行时确定的情况。

总的来说,多态性是面向对象编程的一个强大特性,它允许代码更加灵活、可扩展和易维护。

虚函数

在面向对象编程中,我们有时候会有一系列的类,它们可能会有一些相同的函数名,但是具体的实现可能会因为类的不同而有所不同。虚函数就是为了解决这个问题而设计的。

想象一下,你有一群动物,比如猫、狗、鸟等,它们都能发出声音。你可能会定义一个名为 makeSound 的函数来表示这个动作。但是,猫“喵喵”叫,狗“汪汪”叫,鸟“啾啾”叫,它们的叫声不同。这时候,你就可以使用虚函数了

#include<iostream>
using namespace std;
class Animal {
public:
    virtual void makeSound() {
        // 这里可以是一个默认的实现,也可以是空的
    }
};
class Cat : public Animal {
public:
    void makeSound() override {
        std::cout << "喵喵" << std::endl;
    }
};

class Dog : public Animal {
public:
    void makeSound() override {
        std::cout << "汪汪" << std::endl;
    }
};

class Bird : public Animal {
public:
    void makeSound() override {
        std::cout << "啾啾" << std::endl;
    }
};
int main()
{
	Animal* myPet = new Dog();
	myPet->makeSound();  // 输出:汪汪
	Cat cat;
	Bird bird;
	myPet = &cat;
	myPet->makeSound();
	myPet = &bird;
	myPet->makeSound();
	return 0;
}

 如果基类的函数不加关键字virtual会发生什么?

1.所有派生类的相关函数不能添加override关键字(以为实际上没有重写)

2.函数最终调用的是基类的函数,但可能使用对应派生类对象的数据

举个例子展示不用virtual时的情况

#include <iostream>
#include <string>
using namespace std;
class Student
{public:
   Student(int, string,float);
   void display( );                                            
  protected:
   int num;
   string name;
   float score;
 };
Student::Student(int n, string nam,float s) 
 {num=n;name=nam;score=s;}

void Student::display( ) 
{cout<<"num:"<<num<<"\nname:"<<
name<<"\nscore:"<<score<<"\n\n";}
class Graduate: public Student
{public:
   Graduate(int, string, float, float);                          
   void display( );                                             
private:
  	float pay;
};

void Graduate::display( ) {cout<<"num:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\npay="<<pay<<endl;}

Graduate::Graduate(int n, string nam,float s,float p):Student(n,nam,s),pay(p){ }
int main()
 { 
  Student stud1(1001,"Li",87.5);                   
  Graduate grad1(2001,"Wang",98.5,563.5); 
  Student *pt=&stud1;                            
  pt->display( );
  pt=&grad1;
  pt->display( );
  return 0;
 }

如果修改成虚函数,即Student类中函数声明变为:

virtual void display( );  

运行结果

在派生类中重新定义此函数,要求函数名、函数类型、函数参数个数和类型全部与基类的虚函数相同,当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数。派生类重新声明该虚函数时,可以加virtual,也可以不加

下面我们通过修改上述代码来证明这一点 

添加类Test

class Test: public Graduate
{
	public:
		Test(int ,string,float,float,int);
		void display();
	private:
		int others;
};
void Test::display( ) {cout<<"\n\nnum:"<<num<<"\nname:"<<name<<"\nscore:"<<score<<"\nothers="<<others<<endl;}

Test::Test(int n, string nam,float s,float p,int o):Graduate(n,nam,s,p),others(o){ }

main中添加:

Test t(9999,"s",11,22,33);
  pt=&t;
  pt->display();

结果:

说明Graduate派生类Test中的同名函数display已经成为虚函数,如果不是虚函数,应该输出下面的结果:
num:1001
name:Li
score:87.5

num:2001
name:Wang
score:98.5
pay=563.5


num:9999
name:s
score:11
pay=22

静态关联和动态关联

在C++中,静态关联和动态关联是面向对象编程中两个关键的概念,通常与继承和多态性相关

静态关联(Static Binding):
概念: 静态关联发生在编译时,编译器在编译阶段就能够确定程序中各个函数或方法的调用关系。
实现: 在静态关联中,编译器根据函数或方法的声明类型来确定调用哪个函数或方法,这种绑定在编译时期就已经确定,因此称为静态关联。
优点: 效率高,因为在编译时已经确定了函数调用关系,不需要在运行时进行额外的查找。
例子: C++中的函数重载是一种静态关联的例子,编译器在编译时根据参数的类型和数量确定调用哪个重载版本。

动态关联(Dynamic Binding):
概念: 动态关联发生在运行时,程序在执行过程中才能够确定调用哪个函数或方法。
实现: 在动态关联中,通常通过使用虚函数和指针(或引用)来实现。这种绑定在运行时根据实际对象的类型确定,因此称为动态关联。
优点: 提供了更高的灵活性和可扩展性,允许在运行时根据实际情况改变调用关系。
例子: C++中的虚函数和纯虚函数是动态关联的例子。当基类指针或引用指向派生类对象,并调用虚函数时,根据实际对象的类型来确定调用哪个版本的函数

什么情况下适合声明虚函数?
1.基类预期被继承: 如果你设计一个基类,并且希望它能够作为其他类的基础,支持多态性,那么你应该在基类中声明虚函数。这样,派生类就有机会覆盖这些虚函数,实现自己的版本
2.需要运行时动态绑定: 如果你希望在运行时根据对象的实际类型来调用函数,而不是根据指针或引用的静态类型,那么你应该使用虚函数。这种动态绑定提供了多态性的特性
3.需要覆盖基类函数: 如果你希望派生类能够覆盖基类中的同名函数,以提供特定于派生类的实现,那么这个函数应该声明为虚函数
4.实现抽象类和接口: 如果你想要创建一个抽象类(包含至少一个纯虚函数)或者接口,那么你应该声明虚函数。这样的类不能被实例化,但可以作为基类供其他类继承并实现虚函数

虚析构函数

在C++中,析构函数是用来在对象生命周期结束时进行清理工作的函数。而当我们在面向对象的程序设计中使用继承时,有时候我们会遇到这样的情况:基类指针指向派生类对象。

如果你使用了继承,并且在基类和派生类中都定义了析构函数,那么就有可能发生一个问题。当你使用基类指针指向派生类对象时,如果你删除这个指针,只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致一些资源(比如内存)没有得到正确释放,从而产生问题。

这时候,虚析构函数就发挥作用了。在基类的析构函数前面加上virtual关键字,就可以将它声明为虚析构函数。这样,当你通过基类指针删除派生类对象时,会正确地调用派生类的析构函数。

#include<iostream>
using namespace std;
class Base {
public:
    virtual ~Base() {
        // 基类的清理工作
        cout<<"Base Destruction"<<endl;
    }
};

class Derived : public Base {
public:
    ~Derived()  {
        // 派生类的清理工作
        cout<<"Derived Destruction"<<endl;
    }
};
int main()
{
	Base *pt=new Derived;
	delete pt;
	return 0;
}

这里,~Base()是虚析构函数,而~Derived()覆盖了基类的虚析构函数。当你使用基类指针指向派生类对象时,通过这个基类指针删除对象时,会正确地调用~Derived()来完成清理工作,确保所有相关资源都被正确释放。

总的来说,虚析构函数是为了在继承层次结构中正确地释放资源而引入的。通过使用虚析构函数,可以确保在删除基类指针时正确调用派生类的析构函数,从而实现正确的资源清理。

纯虚函数和抽象类

抽象类:
首先,想象一下你要画一只动物的图,但是你不知道具体是什么动物,只知道它是动物。你可能会画一些共性的特征,比如四条腿、有尾巴等。这个画得很模糊的图就好比一个抽象类。
在编程中,抽象类也是一种类,但是它是一种不完整的类,不能被实例化(也就是不能创建对象)。抽象类里面可能包含了一些方法(函数),但是这些方法没有具体的实现,只是一个声明。抽象类的存在是为了给其他类提供一种共同的接口,让这些类去继承它并实现这些方法。
纯虚函数:
再想象一下,你画的那只动物,有一些特征是必须由具体的动物来定义的,比如各种动物的叫声。这时,你可以把叫声这个特征标记为一个“待定”项,告诉其他人,这个特征必须由实际的动物类来具体实现。
在编程中,这个“待定”项就是纯虚函数。一个纯虚函数是在抽象类中声明的虚函数,但是没有具体的实现。它的目的是让派生类强制实现这个方法,以使抽象类变得更加具体。
综合起来,抽象类是一种不完整的类,包含一些没有具体实现的方法,其中可能包含了纯虚函数。而纯虚函数是为了强制派生类实现某些方法,使得抽象类可以更具体、更有实际意义。在C++中,含有纯虚函数的类就被称为抽象类

注意!
1.凡是包含纯虚函数的类都是抽象类,包含纯虚函数的类是无法建立对象的
2.如果在抽象类所派生出的新类中对基类的所有纯虚函数进行了定义,那么这些函数就被赋予了功能,可以被调用。这个派生类就不是抽象类,而是可以用来定义对象的具体类。如果在派生类中没有对所有纯虚函数进行定义,则此派生类仍然是抽象类,不能用来定义对象
3.虽然抽象类不能定义对象(或者说抽象类不能实例化),但是可以定义指向抽象类数据的指针变量
下面举一个综合性例子体现上面所说的一切:

#include <iostream>

// 抽象类 Animal
class Animal {
public:
    // 纯虚函数,表示动物的叫声
    virtual void makeSound() const = 0;

    // 普通方法,表示动物的一般特征
    void sleep() const {
        std::cout << "Zzz..." << std::endl;
    }
};

// 具体的动物类 Lion(狮子)
class Lion : public Animal {
public:
    // 实现了纯虚函数,给出了狮子的叫声
    void makeSound() const override {
        std::cout << "Roar!" << std::endl;
    }
};

// 具体的动物类 Elephant(大象)
class Elephant : public Animal {
public:
    // 实现了纯虚函数,给出了大象的叫声
    void makeSound() const override {
        std::cout << "Trumpet!" << std::endl;
    }
};

// 具体的动物类 Monkey(猴子)
class Monkey : public Animal {
public:
    // 猴子并没有实现纯虚函数 makeSound
    // 因此 Monkey 仍然是抽象类
};

int main() {
    // 1. 尝试创建抽象类的对象,编译错误
    // Animal animal;  // 编译错误,抽象类不能实例化

    // 2. 使用抽象类指针,指向派生类对象
    Animal* lion = new Lion();
    Animal* elephant = new Elephant();
    // Animal* monkey = new Monkey();  // 编译错误,抽象类不能实例化

    // 调用纯虚函数,实际上会调用相应派生类的实现
    lion->makeSound();     // 输出:Roar!
    elephant->makeSound(); // 输出:Trumpet!

    // 调用抽象类的普通方法
    lion->sleep();
    elephant->sleep();

    // 3. 定义指向抽象类的指针变量
    Animal* abstractAnimal = nullptr;  // 可以定义指针变量

    // 4. 尝试使用派生类 Monkey,编译错误
    // Monkey monkeyObj;  // 编译错误,抽象类不能实例化

    delete lion;
    delete elephant;

    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1236651.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【设备树添加节点】

节点结束位置都需要加分号 of_iomap 完成映射 of_property_read_u32_array of_property_read_string of_fine_node_by_path

子虔科技出席2023WAIC“智能制造融合创新论坛”

7月7日&#xff0c;2023世界人工智能大会&#xff08;WAIC&#xff09;闵行会场在大零号湾举办。子虔科技联合创始人周洋作为专家嘉宾受邀参与智能制造融合创新论坛大会。会上探讨了工业和制造业数字化转型的机遇、挑战和对策。其中&#xff0c;周洋提到&#xff0c;工业制造业…

【电路笔记】-电源电压

电源电压 文章目录 电源电压1、概述1.1 交流发电机1.2 电池1.3 理想电压源1.4 实际电压源1.5 连接规则 2、相关源2.1 压控电压源 (VCVS)2.2 电流控制电压源 (CCVS) 3、总结 在本文中&#xff0c;我们详细介绍了称为电源电压的重要电子元件的架构、功能和使用。 我们首先提出理想…

leetcode刷题详解——粉刷房子

1. 题目链接&#xff1a;LCR 091. 粉刷房子 2. 题目描述&#xff1a; 假如有一排房子&#xff0c;共 n 个&#xff0c;每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种&#xff0c;你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。 当然&#xff0c;因为…

https和http的区别和优势

大家好&#xff0c;我是咕噜-凯撒&#xff0c;HTTP&#xff08;超文本传输协议&#xff09;和HTTPS&#xff08;安全超文本传输协议&#xff09;是用于在网络上传输数据的协议&#xff0c;HTTPS相比HTTP在数据传输过程中更加安全可靠&#xff0c;适合对数据安全性要求较高的场景…

力扣第463题 岛屿的周长 C++ 深度优先搜索 + 思维判断的边界

题目 463. 岛屿的周长 简单 相关标签 深度优先搜索 广度优先搜索 数组 矩阵 给定一个 row x col 的二维网格地图 grid &#xff0c;其中&#xff1a;grid[i][j] 1 表示陆地&#xff0c; grid[i][j] 0 表示水域。 网格中的格子 水平和垂直 方向相连&#xff08;对角线…

Altium Designer学习笔记6

原理图库的制作&#xff0c;SMA元件的制作&#xff1a; 图形不是很重要&#xff0c;重要的是管脚的功能。 Design Item ID和Designator两个值是要注意的。 进行Place放置&#xff0c;切换到原理图工作区&#xff0c;测试下功能。 AD9851元件库制作&#xff1a; 不需要再新建原…

在银行外包如何自我提升

作者&#xff1a;苍何&#xff0c;前大厂高级 Java 工程师&#xff0c;阿里云专家博主&#xff0c;CSDN 2023 年 实力新星&#xff0c;土木转码&#xff0c;现任部门技术 leader&#xff0c;专注于互联网技术分享&#xff0c;职场经验分享。 &#x1f525;热门文章推荐&#xf…

Linux调度域与调度组

引入调度域的讨论可以参考这篇文章。这篇笔记重点分析了内核调度域相关的数据结构以及内核用于构建调度域的代码实现&#xff0c;以此来加深对调度域的理解。调度域是调度器进行负载均衡的基础。 调度域拓扑层级 整个系统的调度域组成一个层级结构&#xff0c;内核设计了stru…

解决VSCode运行时自动保存问题【图文解析】

用VSCode写前端时老是自动保存&#xff0c;代码还没写完就开始 刷新页面 调用接口 出现报错之类的&#xff0c;很烦人&#xff0c;所以就写一篇修改VSCode自动保存文件的文章&#xff0c;以免自己忘记在哪设置。 同事总是用不自动保存&#xff0c;每次写完都要ctrls一下&#x…

求二叉树的最大密度(可运行)

最大密度&#xff1a;二叉树节点数值的最大值 如果没有输出结果&#xff0c;一定是建树错误&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 我设置输入的是字符型数据&#xff0c;比较的ASCII值。 输入&#xff1a;FBE###CE### 输…

ubuntu安装nvm

需求 在 virtualbox 虚拟机上运行的 ubuntu &#xff08;22.04.3&#xff09;里安装 nvm &#xff08;Node Version Manager&#xff09; 简述 官网文档 &#xff08;github地址&#xff09;上有提到两种安装方式&#xff0c;一种是直接 curl | wget 命令安装&#xff0c;一…

【C++】泛型编程 ⑪ ( 类模板的运算符重载 - 函数实现 写在类外部的不同的 .h 头文件和 .cpp 代码中 )

文章目录 一、类模板的运算符重载 - 函数实现 写在类外部的不同的 .h 头文件和 .cpp 代码中1、分离代码 后的 友元函数报错信息 - 错误示例Student.h 头文件内容Student.cpp 代码文件内容Test.cpp 代码文件内容执行报错信息 2、问题分析 二、代码示例 - 函数实现 写在类外部的不…

Linux安装ErLang(亲测可用)

注&#xff08;我这里安装完成后显示的是中文&#xff0c;有的是显示的英文&#xff09; 1.下载er wget https://packages.erlang-solutions.com/erlang-solutions-1.0-1.noarch.rpm2.安装er yum -y install epel-release截图截不全&#xff0c;就只截安装完成的部分了 rp…

USART的标准库编程

使用USART与计算机通信 电脑上只有usb端口 没有TX 和RX需要一个USB转TTL电平模块来实现通信 芯片C8T6中只有三个UASRT 选其中一个UASRT来通信即可 那么如何定位那个USART的TX 和RX引脚呢&#xff1f; 方式1 查找最小系统板引脚分布图 查找USART1的引脚 RTS CTS是硬件流控 CK…

科技赋能,创新发展!英码科技受邀参加2023中国创新创业成果交易会

11月17日至19日&#xff0c;2023中国创新创业成果交易会&#xff08;简称&#xff1a;创交会&#xff09;在广州市广交会展馆圆满举行。英码科技受邀参加本届创交会&#xff0c;并在会场展示了创新性的AIoT产品、深元AI引擎和行业热门解决方案。 据介绍&#xff0c;本届创交会由…

RT-Thread Hoist_Motor PID

本节介绍的是一个举升电机&#xff0c;顾名思义&#xff0c;通过转轴控制物体升降&#xff0c;为双通道磁性译码器&#xff0c;利用电调进行操控&#xff0c;具体驱动类似于大学期间最大众的SG180舵机&#xff0c;在一定的频率下&#xff0c;通过调制脉宽进行控制。 设备介绍…

V100 GPU服务器安装CUDNN教程

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

Startdrive中上传参数设置的具体方法和注意事项

Startdrive中上传参数设置的具体方法和注意事项 适用于配 SINAMICS S120、G130、G150、S150和MV(基于CU3x0-2的驱动器)和所有启动驱动器版本INAMICS G115D/G120/G120D/G120C/G120P/G110M(基于CU2x0-2的驱动器) 根据SINAMICS类型的不同,Startdrive中的Upload参数有所不同。…

机器人制作开源方案 | 莲花灯

1. 功能描述 莲花灯是一款基于莲花形象设计的机器人&#xff0c;本文示例将用两种模式来实现莲花灯的亮灭功能。 自主模式&#xff1a;用 光强传感器 控制莲花灯的灯叶开合。暗光情况下灯叶打开&#xff0c;灯亮&#xff1b;强光情况下灯叶闭合&#xff0c;灯灭。 …