【FFmpeg实战】视频解码流程

news2024/9/30 19:27:18

原文链接:https://blog.csdn.net/weekend_y45/article/details/125168344

一、解码流程使用到的FFmpeg结构体说明

1、AVFormatContext结构体

该结构体描述了一个媒体文件或媒体流的构成和基本信息。它是一个贯穿始终的数据结构,很多函数调用需要使用到它。它也是FFMPEG解封装(flv,avi,mp4)功能的结构体。

其主要的几个变量(主要考虑解码情况说明):

struct AVInputFormat *iformat;//输入数据的封装格式。仅解封装用,由avformat_open_input()设置。
struct AVOutputFormat *oformat;//输出数据的封装格式。仅封装用,调用者在avformat_write_header()之前设置。
AVIOContext *pb;// I/O上下文。
解封装:由用户在avformat_open_input()之前设置(然后用户必须手动关闭它)或通过avformat_open_input()设置。
封装:由用户在avformat_write_header()之前设置。 调用者必须注意关闭/释放IO上下文。
 
unsigned int nb_streams;//AVFormatContext.streams中元素的个数。
AVStream **streams;//文件中所有流的列表。char filename[1024];//输入输出文件名。
 
int64_t start_time;//第一帧的位置。
int64_t duration;//流的持续时间
int64_t bit_rate;//总流比特率(bit / s),如果不可用则为0。 
int64_t probesize;
//从输入读取的用于确定输入容器格式的数据的最大大小。
仅封装用,由调用者在avformat_open_input()之前设置。
AVDictionary *metadata;//元数据
AVCodec *video_codec;//视频编解码器
AVCodec *audio_codec;//音频编解码器
AVCodec *subtitle_codec;//字母编解码器
AVCodec *data_codec;//数据编解码器
int (*io_open)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options);
//打开IO stream的回调函数。
void (*io_close)(struct AVFormatContext *s, AVIOContext *pb);
//关闭使用AVFormatContext.io_open()打开的流的回调函数。

使用时可以通过avformat_alloc_context分配后使用,也可以直接avformat_open_input。

//1、方法一
AVFormatContext *fmt_ctx = NULL;
string filename = "test.avi" ;
fmt_ctx = avformat_alloc_context();
avformat_open_input(&fmt_ctx, ilename.c_str(), NULL, NULL);
avformat_close_input(&fmt_ctx);
 
//2、方法二
AVFormatContext *fmt_ctx = NULL;
string filename = "test.avi" ;
int ret = avformat_open_input(&fmt_ctx, filename.c_str(), NULL, NULL);
avformat_close_input(&fmt_ctx);

推荐使用方法2,因为若传进avformat_open_input的fmt_ctx为NULL,该函数内部会调用avformat_alloc_context函数。相应的avformat_close_input内部会调用avformat_free_context。

2、AVCodec

ffmpeg中的解码器及编码器都用AVCodec结构体保存一些编解码的配置信息。

对解码来说可以按照下面方式使用。

   //解码H264流  
  AVCodec*   Vcodec = NULL;
  Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
  
  //或者直接通过解码器名字找到解码器 
  Vcodec = avcodec_find_decoder_by_name("h264_mediacodec");

3、AVCodecContext

该结构体用于存储编解码器上下文的数据结构,包含了众多编解码需要的参数信息。这些信息参数需要进行初始化,使用avcodec_parameters_to_context进行初始化。不初始化解析一些格式的封装视频会导致编解码失败。该结构体内很多参数是编码时使用的,解码用不上。

几个主要的成员:

enum AVMediaType codec_type:编解码器的类型(视频,音频...struct AVCodec  *codec:采用的解码器AVCodec(H.264,MPEG2...int bit_rate:平均比特率
 
uint8_t *extradata; int extradata_size:针对特定编码器包含的附加信息(例如对于H.264解码器来说,存储SPS,PPS等)
 
AVRational time_base:根据该参数,可以把PTS转化为实际的时间(单位为秒s)
 
int width, height:如果是视频的话,代表宽和高
 
int refs:运动估计参考帧的个数(H.264的话会有多帧,MPEG2这类的一般就没有了)
 
int sample_rate:采样率(音频)
 
int channels:声道数(音频)
 
enum AVSampleFormat sample_fmt:采样格式
 
int profile:型(H.264里面就有,其他编码标准应该也有)
 
int level:级(和profile差不太多)

使用:

AVCodec*   Vcodec = NULL;
Vcodec = avcodec_find_decoder(AV_CODEC_ID_H264);
AVCodecContext*     AvContext = NULL;
AvContext = avcodec_alloc_context3(mVcodec);
avcodec_parameters_to_context(mAvContext, 
fmt_ctx->streams[mVideoStreamIdx]->codecpar);

4、AVStream

该结构体用于描述一个流媒体,该结构体中大部分值域可以由avformat_open_input函数根据文件头的信息确定,缺少的信息需要通过调用av_find_stream_info进一步获得。

av_find_stream_info函数读取一部分音视频来获取有关视频文件的一些信息,如编码宽高、视频时长等。对于一些没有头部信息的视频文件(如mpeg编码的文件)调用该函数是必须的。调用该函数可能会带了很大的延迟。

主要的成员:

index/id:index对应流的索引,这个数字是自动生成的,根据index可以从AVFormatContext::streams表中索引到该流;而id则是流的标识,依赖于具体的容器格式。比如对于MPEG TS格式,id就是pid。
 
time_base:流的时间基准,是一个实数,该流中媒体数据的pts和dts都将以这个时间基准为粒度。通常,使用av_rescale/av_rescale_q可以实现不同时间基准的转换。
 
start_time:流的起始时间,以流的时间基准为单位,通常是该流中第一个帧的pts。
 
duration:流的总时间,以流的时间基准为单位。
 
need_parsing:对该流parsing过程的控制域。
 
nb_frames:流内的帧数目。
 
avg_frame_rate:帧率相关。
 
codec:指向该流对应的AVCodecContext结构,调用avformat_open_input时生成。
 
parser:指向该流对应的AVCodecParserContext结构,调用av_find_stream_info时生成。

5、AVIOContext

用于管理FFMPEG输入输出数据的结构体。

主要成员:

unsigned char *buffer:缓存开始位置
 
int buffer_size:缓存大小(默认32768unsigned char *buf_ptr:当前指针读取到的位置
 
unsigned char *buf_end:缓存结束的位置
 
void *opaque:URLContext结构体

在解码的情况下,buffer用于存储ffmpeg读入的数据。如打开一个视频文件时,先把数据从硬盘读入buffer,然后在送给解码器解码。

URLContext结构体中有一个URLProtocol。每种协议(rtp,rtmp,file,udp等)都有一个对应的URLProtocol。

6、AVPacket

该结构体是ffmpeg中很重要的一个结构体,它保存了解码后或编码前的数据(仍然是压缩数据)和这些数据的一些附加信息,如显示时间戳(pts)、数据时长、所在媒体的索引等。

对于视频来说,一个AVPacket通常包含一帧压缩数据,而音频则有可能包含多个压缩的Frame。

重要的成员变量:

uint8_t *data:压缩编码的数据。
//例如对于H.264来说。1个AVPacket的data通常对应一个NAL。
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流.因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
 
 
int size:data的大小
int64_t pts:显示时间戳
int64_t dts:解码时间戳
int stream_index:标识该AVPacket所属的视频/音频流。

avpacket.h内有API说明,常用的几个API

​ av_packet_ref,av_packet_unref

​ av_new_packet, av_packet_alloc, av_init_packet, av_packet_unref,av_packet_free(free这个API为旧接口)

​ av_packet_clone:拷贝packet

7、AVFrame

AVFrame结构体一般用于存储原始数据(非压缩的YUV,RGB数据等),此外还包含一些相关信息,比如解码的时候存储宏块类型表,QP表,运动矢量等数据。

AVFrame必须用av_frame_alloc分配,用av_frame_free释放。注意av_frame_alloc函数只创建实例但是该实例存储数据的buffer则需要通过另外的操作进行分配,如av_image_fill_arrays。

几个常用变量:

uint8_t *data[AV_NUM_DATA_POINTERS]:解码后原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
 
int linesize[AV_NUM_DATA_POINTERS]:data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽。
 
int width, height:视频帧宽和高(1920x1080,1280x720…)
 
int nb_samples:音频的一个AVFrame中可能包含多个音频帧,在此标记包含了几个
 
int format:解码后原始数据类型(YUV420,YUV422,RGB24…)
 
int key_frame:是否是关键帧
 
enum AVPictureType pict_type:帧类型(I,B,P…)
 
AVRational sample_aspect_ratio:宽高比(16:94:3…)
 
int64_t pts:显示时间戳
 
int coded_picture_number:编码帧序号
 
int display_picture_number:显示帧序号
 
int interlaced_frame:是否是隔行扫描
 
uint8_t motion_subsample_log2:一个宏块中的运动矢量采样个数,取log的

二、解码过程的API调用流程

1、API调用流程图:

这里主要针对解码新接口:

在这里插入图片描述

2、解码并生成YUV文件过程API调用说明

(1)、注册各大组件

    //注册各大组件
    av_register_all();

(2)、打开视频文件并获取相关上下文

在解码之前我们得获取里面的内容,这一步就是打开地址并且获取里面的内容。其中avFormatContext是内容的一个上下文。

并使用avformat_open_input打开播放源,inputPath为输入的地址,可以是视频文件,也可以是网络视频流。然后使用avformat_find_stream_info从获取的内容中寻找相关流。

    AVFormatContext *avFormatContext = avformat_alloc_context();    //获取上下文
    //打开视频地址并获取里面的内容(解封装)
    if (avformat_open_input(&avFormatContext, inputPath, NULL, NULL) < 0) {
        LOGE("打开视频失败")
        return;
    }
    if (avformat_find_stream_info(avFormatContext, NULL) < 0) {
        LOGE("获取内容失败")
        return;
    }

(3)、寻找视频流

我们在上面已经获取了内容,但是在一个音视频中包括了音频流,视频流和字幕流,所以在所有的内容当中,我们应当找出相对应的视频流。

    //获取视频的编码信息
    AVCodecParameters *origin_par = NULL;
    int mVideoStreamIdx = -1;
    mVideoStreamIdx = av_find_best_stream(avFormatContext, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
    if (mVideoStreamIdx < 0) {
        av_log(NULL, AV_LOG_ERROR, "Can't find video stream in input file\n");
        return;
    }
    LOGE("成功找到视频流")

(4)、获取并打开解码器

如果要进行解码,那么得有解码器并打开解码器。avcodec_parameters_to_context去初始化解码器否则解析avi封装的mpeg4视频没问题但是解析MP4封装的mpeg4视频会报错。如下:

    // 寻找解码器 {start
    AVCodec *mVcodec = NULL;
    AVCodecContext *mAvContext = NULL;
    mVcodec = avcodec_find_decoder(origin_par->codec_id);
    mAvContext = avcodec_alloc_context3(mVcodec);
    if (!mVcodec || !mAvContext) {
        return;
    }
 
 
    //不初始化解码器context会导致MP4封装的mpeg4码流解码失败
    int ret = avcodec_parameters_to_context(mAvContext, origin_par);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error initializing the decoder context.\n");
    }
 
    // 打开解码器
    if (avcodec_open2(mAvContext, mVcodec, NULL) != 0){
        LOGE("打开失败")
        return;
    }
    LOGE("解码器打开成功")
    // 寻找解码器 end}

(5)、申请AVPacket和AVFrame以及相关设置

申请AVPacket和AVFrame,其中AVPacket的作用是:保存解码之前的数据和一些附加信息,如显示时间戳(pts)、解码时间戳(dts)、数据时长,所在媒体流的索引等;AVFrame的作用是:存放解码过后的数据。

  //申请AVPacket
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
    av_init_packet(packet);
    //申请AVFrame
    AVFrame *frame = av_frame_alloc();//分配一个AVFrame结构体,AVFrame结构体一般用于存储原始数据,指向解码后的原始帧

(6)、申请用于存放解码后YUV格式数据的相关buf

解码后的数据,按照YUV相关格式保存为yuv文件,先申请buf,用于存放解码后的数据,并按照yuv格式排列保存:

     uint8_t *byte_buffer = NULL;
 
    int byte_buffer_size = av_image_get_buffer_size(mAvContext->pix_fmt, mAvContext->width, mAvContext->height, 32);
    LOGE("width = %d , height = %d ",mAvContext->width, mAvContext->height);
    byte_buffer = (uint8_t*)av_malloc(byte_buffer_size);
    if (!byte_buffer) {
        av_log(NULL, AV_LOG_ERROR, "Can't allocate buffer\n");
        return AVERROR(ENOMEM);
    }

(7)、开始解码

接下来就可以开始解码,如下是解码的核心段代码:

    // 发送待解码包
    int result = avcodec_send_packet(mAvContext, packet);
    av_packet_unref(packet);
    if (result < 0) {
        av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
        continue;
    }
 
    // 接收解码数据
    while (result >= 0) {
        result = avcodec_receive_frame(mAvContext, frame);
        if (result == AVERROR_EOF)
             break;
        else if (result == AVERROR(EAGAIN)) {
             result = 0;
             break;
        } else if (result < 0) {
             av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
             av_frame_unref(frame);
             break;
        }
        av_frame_unref(frame);
    }

(8)、解码并保存为yuv格式文件

YUV是解码后的纯视频原数据格式,我们将其保存为yuv文件,用于分析解码后的数据是否正确。完整的解码,赋值,然后保存文件代码如下:

    while(1)
    {
        int ret = av_read_frame(avFormatContext, packet);
        if (ret != 0){
            av_strerror(ret,buf,sizeof(buf));
            LOGE("--%s--\n",buf);
            av_packet_unref(packet);
            break;
        }
 
        if (ret >= 0 && packet->stream_index != mVideoStreamIdx) {
            av_packet_unref(packet);
            continue;
        }
 
        {
            // 发送待解码包
            int result = avcodec_send_packet(mAvContext, packet);
            av_packet_unref(packet);
            if (result < 0) {
                av_log(NULL, AV_LOG_ERROR, "Error submitting a packet for decoding\n");
                continue;
            }
 
            // 接收解码数据
            while (result >= 0){
                result = avcodec_receive_frame(mAvContext, frame);
                if (result == AVERROR_EOF)
                    break;
                else if (result == AVERROR(EAGAIN)) {
                    result = 0;
                    break;
                } else if (result < 0) {
                    av_log(NULL, AV_LOG_ERROR, "Error decoding frame\n");
                    av_frame_unref(frame);
                    break;
                }
 
                int number_of_written_bytes = av_image_copy_to_buffer(byte_buffer, byte_buffer_size,
                                                                      (const uint8_t* const *)frame->data, (const int*) frame->linesize,
                                                                      mAvContext->pix_fmt, mAvContext->width, mAvContext->height, 1);
                if (number_of_written_bytes < 0) {
                    av_log(NULL, AV_LOG_ERROR, "Can't copy image to buffer\n");
                    av_frame_unref(frame);
                    continue;
                }
 
                // 写文件保存视频数据
                fwrite(byte_buffer, number_of_written_bytes, 1, fp_YUV);
                fflush(fp_YUV);
 
                av_frame_unref(frame);
            }
        }
 
    }

(9)、收尾释放资源

完成过后记得释放资源。

    //释放
    fclose(fp_YUV);
    av_frame_free(&frame);
    avcodec_close(mAvContext);
    avformat_free_context(avFormatContext);
  >>> 音视频开发 视频教程: https://ke.qq.com/course/3202131?flowToken=1031864 
  >>> 音视频开发学习资料、教学视频,免费分享有需要的可以自行添加学习交流群: 739729163  领取

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

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

相关文章

【云原生丶Docker】理解Docker的核心概念

Docker的核心优势是什么&#xff1f; 在 什么是虚拟化技术&#xff1f; 一文中&#xff0c;我们对虚拟机和 Docker 都有了简单的了解&#xff0c;需要的小伙伴可以自行跳转回顾~ Docker与传统虚拟化技术有什么区别&#xff1f; Docker 相比传统的虚拟机技术更轻量级&#xff0…

TikTok上的中国商品,老外买上瘾了!

在TikTok的全球影响力下&#xff0c;一些中国商品正实现销量的创纪录增长 2022年底&#xff0c;TikTok发了其第五份“Year on TikTok”年度报告&#xff0c;概述了过去12个月TikTok平台上热度最高的创作者、视频内容&#xff0c;以及#TikTokMadeMeBuyIt标签下最热门的5大产品&…

c语言printf输出格式(%d %f %e %x)

一、%d第一组&#xff1a;%d %nd %-nd第二组&#xff1a;%d %0nd %-0nd %.nd第三组&#xff1a;%d %m.nd %-m.nd 二、%f%f %.nf %m.nf %-m.nf 三、%e%e %.ne %me %-me %m.ne %-m.ed 四、%x%x %0nx %nx %-nx 一、%d 第一组&#xff1a;%d %nd %-nd ①n表示输出宽度&#xff08…

深度学习中使用的16位浮点数格式

深度学习中使用的16位浮点数格式 flyfish 文章目录 深度学习中使用的16位浮点数格式浮点数是一个近似值浮点数的表示float单精度类型 和 double双精度类型 的比较查看所占的字节数大小端的比较计算方法16位 float 半精度格式bflat16格式bflat16格式 与 float32单精度格式 比较…

手敲MyBatis(十二章)-ResultMap解析映射使用

1.前言 这一章节主要解决好几章之前留下的坑&#xff0c;需要根据XML配置的ReultMap进行解析映射成具体的PO供用户使用。 我们本章就来解决下在xml中配置了如下标红框的内容&#xff0c;怎么解析映射到具体的实体类中&#xff0c;如下就是将id为activityMap的resultMap怎么解…

发送图文并茂的html格式的邮件

本文介绍如何生成和发送包含图表和表格的邮件&#xff0c;涉及echarts图表转换为图片、图片内嵌到html邮件内容中、html邮件内容生成、邮件发送方法等 一、图表处理 因为html格式的邮件不支持echarts,也不支持js执行&#xff0c;所以图表需要转换为图片内嵌在邮件内容中 因为平…

Sui Builder House京都站倒计时!

Sui Builder House京都站将于6月29日-30日举行&#xff0c;为世界各地的开发者提供身临其境地学习和交流的机会。查看Sui Builder House京都站的五个亮点 活动信息 &#x1f4c6; Builder House时间&#xff1a;2023年6月29日 — 6月30日 &#x1f4cd; 活动地址&#xff1a;…

NetSuite OAuth1.0中InvalidSignature问题

本周闭关写代码&#xff0c;用Java通过TBA方式访问NetSuite REST Webservices。由于是手生&#xff0c;卡在InvalidSignature报错上&#xff0c;在这个问题上被卡了一整天。 直到终于到来的Aha时刻。 在NetSuite中的样例代码是PHP的&#xff0c; 我平移到Java后&#xff0c;代码…

【云原生丶Docker】虚拟化技术简介

什么是虚拟化技术&#xff1f; Docker 是一款基于容器虚拟化技术构建的软件&#xff0c;那到底什么虚拟化技术呢&#xff1f;在学习 Docker 之前&#xff0c;先简单了解下虚拟化技术。 虚拟化是云原生的实现基础&#xff0c;它能够帮助我们更加有效地利用物理计算机硬件。 虚…

clip-interrogator本地部署

clip-interrogator本地部署 1. 克隆源码2. 创建虚拟环境及配置3. 下载远程相关文件3.1 下载Salesforce相关文件失败3.2 下载cache相关文件失败3.3 解决库不完整问题 4. 测试代码4.1 脚本一4.2 脚本二4.3 测试run_cli.py文件4.4 测试run_gradio.py文件 源码链接&#xff1a;http…

d3dcompiler_47.dll缺失怎么办?d3dcompiler_47.dll缺失修复方法(详解)

d3dcompiler_47.dll在Windows操作系统和 DirectX应用程序中使用此 DLL 文件,包括游戏&#xff0c;图形处理软件&#xff0c;CAD软件和其他3D应用程序中。如果电脑提示“由于找不到d3dcompiler_47.dll&#xff0c;无法继续执行此代码”&#xff0c;“d3dcompiler_47.dll缺失”&a…

Docker卷与持久化数据

数据主要分为两类&#xff0c;持久化的与非持久化的。 两者都很重要&#xff0c;并且Docker均有对应的支持方式。每个Docker容器都有自己的非持久化存储。非持久化存储自动创建&#xff0c;从属于容器&#xff0c;生命周期与容器相同。这意味着删除容器也会删除全部非持久化数…

网络维护岗位个人求职简历

网络维护岗位个人求职简历1 姓名&#xff1a; 国籍&#xff1a;中国 目前所在地&#xff1a;广州民族&#xff1a;汉族 户口所在地&#xff1a;湖南身材&#xff1a;170cm64kg 婚姻状况&#xff1a;未婚年龄&#xff1a;28 求职意向及工作经历 应聘职位&#xff1a; 工作年限&a…

day14_面向对象的三大特征之一(继承)

继承的概述 Java是对现实社会的模拟&#xff0c;现实中有儿子可以继承父亲的财产&#xff0c;因此有了富二代的。 java中多个类中存在相同属性和行为时&#xff0c;将这些内容抽取到单独一个类中&#xff0c;那么多个类中无需再定义这些属性和行为&#xff0c;只需要和抽取出来…

【Java-SpringBoot+Vue+MySql】项目开发杂记

目录 1、关闭eslint 2、 新建路由 3、安装jQuery依赖-在Vue使用JQuery语法 4、MySQL———数据全部清除&#xff0c;自增归零 5、前后端数据传递——增加功能 6、使用element-ui获取当前表格中的数据——删除功能 1、关闭eslint 2、 新建路由 {path: /user,component: Lay…

LLM - Hugging Face 工程 BERT base model (uncased) 配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://blog.csdn.net/caroline_wendy/article/details/131400428 BERT是一个在大量英文数据上以自监督的方式预训练的变换器模型。这意味着它只是在原始文本上进行预训练&#xff0c;没有人以…

RabbitMQ实现延迟消息,RabbitMQ使用死信队列实现延迟消息,RabbitMQ延时队列插件

文章目录 一、什么是延迟消息1、队列的属性2、消息的属性3、什么是死信4、使用死信队列的缺点5、延时消息插件 二、JavaAPI利用死信队列实现RabbitMQ延迟消息1、代码实现2、基本流程 三、JavaAPI利用插件实现RabbitMQ延迟消息1、代码实现2、基本原理 四、Springboot利用死信队列…

5分钟自建可视化平台,在线拖拽组件也太方便了!

一、前言 我们在完成 C 端用户的业务需求时&#xff0c;发现大量电商推广页面的逻辑复杂度和交互程度都比较低&#xff0c;却要消耗大量的开发资源。同时在后台管理系统的开发过程中&#xff0c;我们也发现一些相同套路的页面在不同场景下需要重复开发&#xff0c;即使我们封装…

如何提升企业采购绩效?提高采购绩效的方法

采购是任何企业的一个重要方面&#xff0c;它在确保企业平稳高效运行方面发挥着重要作用。作为一名采购专业人员&#xff0c;你有责任确保你的企业以正确的质量、数量和价格获得所需的货物和服务。 本文将讨论一些最佳做法&#xff0c;这些做法可以帮助你提高采购专业人员的绩…

【032】C++高级开发之多态技术详解(虚函数最全讲解)

C的多态技术&#xff08;虚函数&#xff09;详解 引言一、多态的概念二、虚函数2.1、父类指针保存子类空间地址 带来的问题2.2、虚函数的定义2.3、虚函数的动态绑定机制2.4、重载、重定义、重写的区别 三、纯虚函数3.1、纯虚函数的定义方式3.2、纯虚函数的案例&#xff1a;饮品…