C++——类和对象[上]

news2024/11/27 6:33:22

目录

1.初识面向对象

2.类的引入

3.类的定义

4.成员变量的命名规则

5.类的实例化

6.类对象模型

7.this指针

1.初识面向对象

C语言是一门面向过程的语言,它关注的是完成任务所需要的过程;C++是一门面向对象的语言,将一个任务分为多个对象,每个对象具有不同的行为,将这些对象组合起来、互相交互从而达到完成任务的目的。

例如洗衣服这件事,我们的惯性思维是:将衣服放进洗衣机、撒上一些洗衣粉、放水、启动洗衣机、洗衣机工作完成将衣服拿出来晒......这就是一种典型的面向过程的思想,所以说C语言更加符合人的思维逻辑。而面向对象的思维逻辑则不同,它是一种宏观的、通用的思维逻辑,还拿洗衣服这件事来说,面向对象会将洗衣服这个任务拆分出两个对象,即人和洗衣机,人的行为有把衣服放进洗衣机、撒上洗衣粉、启动洗衣机、晒衣服等等,洗衣机的行为有洗衣服、脱水等等,那么这些对象之间通过互相交互就能完成洗衣服的任务。那么面向过程的思维导图可能是下面这样的:

而面向对象的思维导图可能是这样的:

2.类的引入

凭借C语言的经验我们可以知道结构体是某一类事务的属性集合。就比如用结构体描述一个学生,我们可以发现学生当中的很多共性:学生都有姓名、学号、年纪、班级......我们把这些属性集合起来构成一个结构体,再用这个结构体去定义一个变量,这个变量就可以说是一个具体的"人"。那么C语言的局限性非常明显,结构体不能定义行为(函数),虽然可以定义函数指针,但是这种做法好像没必要。所以C++便出手了,在原有的"struct"关键字上进行了扩展,其中之一便是struct结构体中不仅可以定义变量,还可以定义函数,并且C++的"struct"关键字保留了C语言的所有用法。所以我们再对学生的例子进行扩展,我们把学生的共有属性集合到一起之后,我们还可以发掘他们之间的共有行为:学生都会上课、吃饭、睡觉......那么用C++描述学生就非常合适了:

struct Student
{
	char* name;
	char* id;
	int age;

	void eat(){} // 吃饭行为
	void sleep(){}// 睡觉行为
	void study(){}// 学习行为
};

在C++中就没有结构体的概念了,我们上面代码的"Student"称为"Student类",这个"Student"是一个自定义类型,与内置类型不同的是,内置类型是C++帮我们定义好的类型,我们开箱即用;而自定义类型是我们自己规定的一个类型,如何管理这个类型是程序员自己要做的事。举个例子,内置类型int可以支持"+"、"-"、"*"、"/"等运算,如果我们想要"Student"类也支持这些运算,是我们程序员自己要去规定的(在后面的章节会介绍有关这部分的内容)。

那么类型有了,我们该如何定义"变量"?注意此时我将"变量"这两个字用双引号括起来,目的就是为了告诉读者,在C++中,几乎不会出现"变量"这种说法,而是统称对象。我们重新问一遍,有了"Student"类,如何定义"Student"类型的对象?

int main()
{
	// 哪种定义方式是正确的?
	struct Student s1;// 写法1
	Student s2;// 写法2
	return 0;
}

事实上,写法1在C++的角度看来是"错误"的写法,C++更倾向于写法2,也就是说,在C++中使用struct关键字实现的类,在实例化对象时,不需要再增加struct关键字!注意,这里又有一个新名词——实例化,实际上实例化和定义没有区别,这只是C++的习惯。因为C++要兼容C语言,所以保留了看似"累赘"的写法。

3.类的定义

定义类的时候,我们将在类中定义的变量称为成员变量,定义的函数称为成员函数或者成员方法,这些成员函数都被编译器视为内联函数(即使我们不加inline关键字)。同时,类的定义构建了一个全新的作用域——类域

既然类构成了类域,在类中使用成员时,是否也需要遵循编译器的"向上搜索"呢?

struct Student
{
	char* name;

	// 函数当中这样使用成员,是否正确?
	void Init()
	{
		name = "";
		id = 0;
		age = 0;
	}

	char* id;
	int age;
};

这种使用方法是没有问题的,也就是说类域与常规的作用域不一样。编译器将类域当成一个整体,当某个成员函数需要使用某个成员变量时,会在整个类域当中搜索

如果我们在类中声明函数,而在类外定义函数,就需要使用类域来配合完成工作:

#include <stdlib.h>
struct Stack
{
	void Init();

	int* a;
	int top;
	int capacity;
};

struct Queue
{
	void Init();

	int* a;
	int front;
	int tail;
	int capacity;
};

void Init()
{
	a = (int*)malloc(4);
	top = 0;
	capacity = 4;
}

void Init()
{
	a = (int*)malloc(4);
	front = 0;
	tail = 0;
	capacity = 4;
}

这种写法是错误的。一是函数重定义,二是在类外定义的函数是普通函数,这些函数的作用域使用了当前函数作用域和全局域都没有定义的对象。我们的本意是定义Stack类和Queue类当中声明的Init函数,所以我们需要使用类域,指明到底是哪个类的成员函数。当我们指明了成员函数后,编译器就知道了该函数不是全局函数,而是某一个类当中的成员函数,所以自然而然能够使用类中其他的成员:

void Stack::Init()
{
	a = (int*)malloc(4);
	top = 0;
	capacity = 4;
}

void Queue::Init()
{
	a = (int*)malloc(4);
	front = 0;
	tail = 0;
	capacity = 4;
}

那么要在类外使用成员的时候,需要通过对象去使用,不能直接使用类域指定成员使用,这样的语法是错误的:

struct Stack
{
	void Init()
	{
		a = (int*)malloc(4);
		top = capacity = 0;
	}

	int* a;
	int top;
	int capacity;
};

int main()
{
	// 正确的用法
	Stack st;
	st.Init();
	cout << st.top << endl;

	// 错误的用法
	Stack::Init();
	cout << Stack::top << endl;
	return 0;
}

其实在C++当中,并不喜欢用struct定义类,而是用一个新的关键字——class来定义类。我们可以用class替换struct,但我们很可能会因为不熟悉而"触发"bug:

using namespace std;
#include <stdlib.h>

class Stack
{
	void Init()
	{
		a = (int*)malloc(4);
		top = capacity = 0;
	}

	int* a;
	int top;
	int capacity;
};

int main()
{
	Stack st;
	st.Init();
	cout << st.top << endl;
	return 0;
}

如果大家勤快的话,将这段代码粘贴到你的编译器当中,会发现你的编译器报错了。

这就不得不提到面向对象的三大特性:封装、继承、多态。三大特性并不是只面向对象只有这三个特性,而是这三个特性在面向对象中占据主要地位。那么C++为了考虑封装性,引入了访问限定符:public(公有)、protected(保护)、private(私有),而现在,我们主要使用两个访问限定符,即public和private。

访问限定符说明:

  1.public修饰的成员可以在类外直接访问(通过对象)

  2.private修饰的成员在类外不能直接访问

  3.访问权限的作用域从当前访问限定符的位置直到类域结束或者直到下一个访问限定符

  4.class定义的类的访问权限默认为private,struct默认为public

我们修改上面的代码,我们的本意是让Stack类的成员变量不能在类外访问,在类外能访问的只能是成员函数。这种屏蔽底层实现而只暴露接口的做法是封装的常用手段

class Stack
{
public:
	void Init()
	{
		a = (int*)malloc(4);
		top = capacity = 0;
	}
private:
	int* a;
	int top;
	int capacity;
};

我们能够推导出,class和struct的区别就在于默认的访问权限不同

4.成员变量的命名规则

上面的成员变量的命名都是"不规范"的,我们观察一下代码:

class Date// 日期类
{
public:
	void Init(int year = 0, int month = 0, int day = 0)
	{
		year = year;
		month = month;
		day = day;
	}
private:
	int year;
	int month;
	int day;
};

有的读者可能就会钻牛角尖了,修改一下Init函数的参数不就可以了吗?道理是这么个道理,实际上真正的工程项目可能有几十几百个类,如果把成员函数的参数都修改成"a"、"b"、"c"......那还得了?所以在设计类的时候就需要考虑这个问题,C++的习惯(实际上不是C++规定的,而是公司、企业里面规定的)是在定义成员变量时,在其变量名之前或者之后加一个或多个"_",从而区分成员变量和非成员变量:

class Date// 日期类
{
public:
	void Init(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

5.类的实例化

使用类类型创建对象的过程,就称为类的实例化

对于类,我们应当有一下两点认识:

  1.类是对对象进行描述的,是一个抽象的类型。定义类的时候并不会给它分配实际的内存空间来存储它

  2.一个类可以实例化出多个对象,实例化出的对象才实际占用物理空间,存储类的成员(暂且这么理解)

以一个具体的例子来方便大家理解:我们可以把类看成建筑物的设计图纸,而对象便是通过这张图纸实例化出的建筑物。

我们在上面说过不能直接通过类域去访问类当中的成员,其中一个原因便是因为不能去操作"设计图纸":

#include <iostream>
using namespace std;
class Date// 日期类
{
public:
	void Init(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void print()
	{
		cout << _year << ":" << _month << ":" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date::_year = 2023;// 错误的用法
	Date::print();// 也是错误的
	return 0;
}

对于成员变量来说,它们在实例化对象之前都没有实际的存储空间,没有实际空间意味着它们只是声明。而我们要将整数2023赋值给一个没有空间的成员变量本身就是一种错误的做法。拿具体的例子来说,售楼中心有会有一个沙盘模型,它可以详细的看到每一栋楼、每一间房的具体设计,我们就可以把这个沙盘模型看作一个类,某天我们购买了一个冰箱,能直接放进沙盘模型里面去吗?当然,也有读者会产生一个疑问,那么成员函数print已经是定义好的函数,为什么不能指定类域直接访问?确实,print函数已经被定义好了,但是它缺失了一个调用条件,这个调用条件便是后面要介绍的this指针。

6.类对象模型

先计算下面这段程序中A类对象的大小(代码是在x86环境下跑的,计算过程与计算结构体大小相同):

#include <iostream>
using namespace std;
class A
{
public:
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A a;
	cout << sizeof(a) << endl;
	return 0;
}

很多读者觉得输出的结果应该是8,因为类当中有一个函数,可以当成函数指针来看待。实际上输出的是4,也就是说并没有计算函数的大小,换句话说,成员函数好像并不在对象中存储。我们可以猜测三种类对象的存储方式

  1.对象中存储类中声明的每个成员:

这种做法的缺陷是很明显的,会浪费很多空间。我们确实需要保证对象与对象之间的成员变量是独立的,但是成员函数并不需要各自私有一份,因为每个对象的行为都是一样的,每个对象调用的成员函数都是同一个函数

  2.成员函数放在内存的某个区域,对象模型只存储一个指向该区域的指针:

很显然,这种方案即使再合理也不被C++采用(已经证明过了)。

  3.只保存成员变量,成员函数存放在代码段当中:

很明显,C++采用第三种对象存储模型。我们可以这么推理:这些成员函数都属于特定的类,那么编译器在维护这些函数时一定有方法可以分辨,对象在调用成员函数时一定只能调用属于该类域的函数,编译器就能够通过对象调用的函数去找到属于该类域的函数。

我们再研究一个不寻常的问题,请读者猜一猜该段程序当中A、B、C类的大小各是多少?

#include <iostream>
using namespace std;
class A
{};

class B
{
public:
	void print(){}
};

class C
{
public:
	void print(){}
private:
	int _c;
};

int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;
	return 0;
}

那么对于类A和类B,我们都可以把它看作空类(对象模型不存储成员函数),其大小为1;类C的大小毋庸置疑为4。那么为什么空类的大小为1?这是一个占位大小,目的是告诉编译器存在这个类,这个类还能实例化出对象,如果空类的大小为0,那么对象的地址是取不到的:

int main()
{
	A a;
	B b;
	cout << &a << endl;
	cout << &b << endl;
	return 0;
}

空类的大小设置为1,表明该类是一个有效类、合法类,可以实例化出对象,并且该对象在内存当中是持有内存空间的,否则无法对其进行取地址操作。

7.this指针

以日期类为例:

#include <iostream>
using namespace std;
class Date// 日期类
{
public:
	void Init(int year = 0, int month = 0, int day = 0)
	{
		_year = year;
		_month = month;
		_day = day;
	}

	void print()
	{
		cout << _year << ":" << _month << ":" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1, d2;
	d1.Init(2023, 4, 22);
	d2.Init(2024, 12, 11);
	d1.print();
	d2.print();
	return 0;
}

为什么d1和d2对象调用一个函数却能够打印出不同的结果?原因在于C++为类当中的每个普通成员函数增加了一个隐藏的this指针,注意我说的普通成员函数,后面还会介绍静态成员函数。这个this指针会被当做普通成员函数的第一个参数,this指针是指向调用该普通成员函数的对象,在普通成员函数的内部,需要使用成员变量的场景,编译器都会隐式地发生解引用。this指针的原型为[类名* const this],其中this以关键字的形式存在,不可被修改

那么上面的Date类当中的print成员函数实际上的形式这样的:

void print(Date* const this)// 实际的代码当中不能这么写
{
    // 这种写法是可以的
    cout << this->_year << ":" << this->_month << ":" << this->_day << endl;
}

虽然说上面这段才是print函数全貌,但我们自己在写的时候不能显式定义this指针,但我们可以显式地使用this指针。

那么this指针从何而来?实际上在外部使用对象调用这些普通成员函数的时候编译器就会自动地、隐式地将调用普通成员函数的对象的地址(指针)作为实参传递过去:

int main()
{
	Date d1, d2;
	d1.Init(2023, 4, 22);// 隐式传递d1对象的地址
	d2.Init(2024, 12, 11);// 隐式传递d2对象的地址
	d1.print();// 隐式传递d1对象的地址
	d2.print();// 隐式传递d2对象的地址
	return 0;
}

那么现在又有一个关键问题,this指针能否为空指针(一道面试题)?答案是可以,我们看下面的两段代码:

#include <iostream>
using namespace std;

class A
{
public:
	void print()
	{
		cout << "print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* pa = nullptr;
	pa->print();
	return 0;
}

这段代码是编译错误,还是运行时崩溃或者是正常运行?答案是正常运行,其原因在于pa指针即使是一个空的对象指针,即使使用了"->",但是它并不会发生解引用,而是指明类域,之后编译器就能找到正确的函数调用,因为对象的成员函数并不存储在对象当中,而是存储在了所有同类对象可见的代码段。调用print函数时,编译器会隐式的传递一个对象指针,现在已经有了一个现成的对象指针,就是pa,pa传递给print后,在其内部并没有发生任何有关空指针的解引用问题,所以该程序不会发生错误,反而是正常运行。

#include <iostream>
using namespace std;

class A
{
public:
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* pa = nullptr;
	pa->print();
	return 0;
}

这段程序非常执行结果非常明显,运行时崩溃,原因是发生了空指针的解引用。

最后,再解答一个遗留的问题,为什么不能在类外通过使用类域指定调用普通成员函数?

#include <iostream>
using namespace std;

class A
{
public:
	void print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A::print();
	return 0;
}

事实上答案已经非常简单了。print作为A类的普通成员函数,表面上看起来是一个无参的函数,但实际上它有一个隐藏的this指针,所以print是一个单参数的函数。而在外部使用类域指定调用print时,指定的是无参的print,而A类当中并没有无参的prnit函数,所以不能调用。在不改变A类内部结构的情况下,我们无法修改代码使得通过编译,因为C++不允许我们显式传递this指针。

那么this指针变量存储在哪里?很多读者会认为this指针作为成员函数的参数应该存储在代码段中,实际上this指针作为成员函数的形参,只有在函数被调用时才会创建函数栈,创建了函数栈才能存放this指针,所以this指针存储在栈中

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

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

相关文章

思维链Prompting促进大型语言模型的推理能力

论文标题&#xff1a;Chain-of-Thought Prompting Elicits Reasoning in Large Language Models 论文链接&#xff1a;https://arxiv.org/abs/2201.11903 论文来源&#xff1a;NeurIPS 2022 一、概述 近来NLP领域由于语言模型的发展取得了颠覆性的进展&#xff0c;扩大语言模型…

【接口自动化测试】selenium旗舰版Web测试理论篇

【接口自动化测试】selenium旗舰版Web测试理论篇 一、Web自动化测试 1.1 简介 1.2 优势 1.3 E2E测试 二、主流的Web自动化测试工具 三、SeleniumWeb自动化测试 3.1 conda终端一键搭建命令 3.2 测试浏览器 3.2.1 步骤①&#xff1a;自动打开浏览器 3.2.2 步骤②&#…

基于pytorch的车牌识别(一)

目录 CCPD2019数据集CCPD数据集标注信息单例再现 CCPD2019数据集 本项目所有的数据都是基于CCPD2019数据集&#xff0c;CCPD2019数据集包含了25万多幅中国城市车牌图像和车牌检测与识别信息的标注&#xff0c;主要采集于合肥市&#xff0c;采集人员手持Android POS机对停车场的…

轻量级网络EfficientNetB0,利用迁移学习中的微调技术进行小样本轴承故障诊断(Python代码,带有数据集,训练集集的每类只需10个样本)

1. 训练集数量&#xff08;正常类别&#xff1a;10个&#xff0c;内圈故障&#xff1a;10个&#xff0c;外圈故障&#xff1a;10个&#xff0c;滚动体故障&#xff1a;10个&#xff09; 测试集数量&#xff08;正常类别&#xff1a;90个&#xff0c;内圈故障&#xff1a;90个&…

LeetCode第160题——相交链表(Java)

题目描述&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点&#xff0c;返回 null 。 图示两个链表在节点 c1 开始相交**&#xff1a;** 题目数据 保证 整个链式结构中不存在环。 注意&…

非线性扰动观测器的基本设计

非线性扰动观测器的基本设计 目录 非线性扰动观测器的基本设计前言问题描述扰动观测器设计小结参考资料 前言 因为看到论文中有扰动观测器的设计&#xff0c;但只是大概地明白设计的原理&#xff0c;实际上不明白扰动观测器是如何设计出来的。因此在查阅了相关文献和文章后&am…

树的刷题,嗝

今天忘记带本子了,就没有学习java了,于是一心刷题,好烦遇到了两个奇怪的题目,我没跟题解写的,但是我是没想到奇怪的样例. no.1 617. 合并二叉树 难度简单1221收藏分享切换为英文接收动态反馈 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中…

【数据结构】第十三站:排序(中)快速排序

本文目录 一、快速排序递归法1.快速排序思想2.hoare版本实现快速排序3.hoare版本的优化1>使用随机值rand()函数2>三数取中3>三路划分 4.证明hoare版本的key在左边&#xff0c;必须让右边先走5.挖坑法实现快速排序6.将前面快排的一趟排序给提取出来7.双指针法实现快速排…

java小技能:使用FeignClient

文章目录 引言I FeignClient1.1 定义FeignClient1.2 启用FeignClient1.3 使用FeignClient引言 一款不错的SpringCloud 脚手架项目:注册中心(nacos)+RPC(feign) https://blog.csdn.net/z929118967/article/details/127966912 RPC(feign):使用Feign的方式,进行微服务调…

AlgoC++第九课:手写AutoGrad

目录 手写AutoGrad前言1. 基本介绍1.1 计算图1.2 智能指针的引出 2. 示例代码2.1 Scale2.2 Multiply2.3 Pow 总结 手写AutoGrad 前言 手写AI推出的全新面向AI算法的C课程 Algo C&#xff0c;链接。记录下个人学习笔记&#xff0c;仅供自己参考。 本次课程主要是手写 AutoGrad …

逆向动态调试工具简介

常用逆向工具简介&#xff1a; 二进制尽管属于底层知识&#xff0c;但是还是离不开一些相应工具的使用&#xff0c;今天简单介绍一下常用的逆向工具OD以及他的替代品x96dbg&#xff0c;这种工具网上很多&#xff0c;也可以加群找老满&#xff08;184979281&#xff09;&#x…

java实现乘法的方法

我们都知道&#xff0c;乘法运算的核心思想就是两个数相乘&#xff0c;如果能将乘法运算转化成一个加数的运算&#xff0c;那么这个问题就很容易解决。比如我们要实现23的乘法&#xff0c;首先需要定义两个变量&#xff1a;2和3。我们将这两个变量定义为一个变量&#xff1a;2x…

如何利用Mybatis-Plus自动生成代码(超详细注解)

1、简介 MyBatis-Plus (opens new window)&#xff08;简称 MP&#xff09;是一个 MyBatis (opens new window)的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 特性 无侵入&#xff1a;只做增强不做改变&#xff0c;引入…

一例感染型病毒样本的分析

这个样本是会释放两个dll和一个驱动模块&#xff0c;通过感染USB设备中exe文件传播&#xff0c;会向C&C下载PE执行&#xff0c;通过rookit关闭常用的杀软&#xff0c;是一例典型的感染型病毒&#xff0c;有一定的学习价值。 原始样本 样本的基本信息 Verified: Unsigned …

免费无需魔法会语音聊天的ChatGPT

今天发现了一个很好的ChatGPT&#xff0c;可以语音聊天&#xff0c;而且免费无需魔法 角色目前包括夏洛克、雷电影等等&#xff0c;对话的声调完全模拟了原角色&#xff01; 目前只有英文和日语两种对话&#xff0c;我们可以文字输入或者语音输入&#xff0c;中文即可&#xff…

泰克Tektronix DPO5204B混合信号示波器

特征 带宽&#xff1a;2 GHz输入通道&#xff1a;4采样率&#xff1a;1 或 2 个通道上为 5 GS/s、10 GS/s记录长度&#xff1a;所有 4 个通道 25M&#xff0c;50M&#xff1a;1 或 2 个通道上升时间&#xff1a;175 皮秒MultiView zoom™ 记录长度高达 250 兆点>250,000 wf…

M序列测量幅频特性

M序列 M 序列是一种伪随机序列&#xff0c;具有很好的伪噪声特性&#xff0c;常用于信道噪声测试和保密通信。不过 M 序列还有一个用途&#xff0c;也就是本文所介绍的——通过 M 序列测量频率响应。在讨论这个问题之前&#xff0c;我们先介绍 M 序列的特征与生成方法。 M 序列…

活力二八:CRM助力销售管理再现“浓缩”新活力

活力28、沙市日化&#xff01; 央视段子手朱广权再次喊出这句口号时&#xff0c;迅速激活了人们心中对于曾经“日化一哥”的记忆。 作为市场占率曾超 70% 的家清品牌&#xff0c;活力二八业务始于1950年&#xff0c;前身为沙市油脂化工厂&#xff0c;伴随中国改革开放大潮&…

第十一章_SpringBoot集成Redis

总体概述 redisTemplate-jedis-lettuce-redission之间的的联系 1、redisTemplate是基于某个具体实现的再封装&#xff0c;比如说springBoot1.x时&#xff0c;具体实现是jedis&#xff1b;而到了springBoot2.x时&#xff0c;具体实现变成了lettuce。封装的好处就是隐藏了具体的…

大家都在用的视频音频提取器,免费用!

随着互联网的日益普及&#xff0c;人们可以通过多种方式获取和分享媒体内容&#xff0c;例如通过社交媒体、视频共享网站等。但是&#xff0c;在处理媒体文件时&#xff0c;提取其中的音频或视频仍然是一个挑战。这就是为什么越来越多的人都在使用免费的视频音频提取器。 这些…