C++多态(超级详细版)

目录

 

一、什么是多态

二、多态的定义及实现

1.多态构成条件

2.虚函数的重写和协变

虚函数重写的两个例外:

2.1协变

2.2析构函数的重写  (析构函数名统一处理成destructor)

3.重载、覆盖(重写)、隐藏(重定义)的对比 

4.final 和 override

三、抽象类 

四.多态的原理

1.虚函数表

2.多态的原理 

2.1虚表指针里的内容

2.2引用和指针如何实现多态 

2.3普通类接收为什么实现不了多态

 3.虚函数表存放位置

五.单继承和多继承关系的虚函数表

1.单继承中的虚函数表

2.多继承中的虚函数表

3.菱形继承和菱形虚拟继承

做一道题吧


一、什么是多态

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态

举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。 

二、多态的定义及实现

1.多态构成条件

在继承中要构成多态还有两个条件:

1. 必须通过基类的指针或者引用调用虚函数

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{ p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

注意:接受对象为父类的指针或者引用,你传递的是父类就调用父类的函数,传递的是子类就调用子类的函数

在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.虚函数的重写和协变

上面例子中,我们实现了虚函数的重写(覆盖):

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

虚函数重写的两个例外:

2.1协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变。

这里不仅仅可以返回当前基类和子类的类型,还可以返回其他有继承关系的类和类型。

2.2析构函数的重写  (析构函数名统一处理成destructor)

首先,我们来看看析构函数不处理成virtual的情况

我们本义是想让p1调用Person的析构,p2先调用Person的析构在调用Student的析构,但是这里并没有调用Student的析构,只析构了父类,就可能发生内存泄漏。

这是为什么呢? 

因为这里发生了隐藏,~Person()变为 this->destructor()  ~Student()为this->destructor() 

编译器将他们两个的函数名都统一处理成了destructor,因此调用的时候只看自身的类型,是Person就调用Person的函数,是Student就调用Student的函数,根本不构成多态,这并不是我们期望的哪样。

 我们给析构函数添加上virtual

发现子类对象,Student对象就能正常析构了

注意::析构函数加virtual是在new场景下才需要, 其他环境下可以不用

3.重载、覆盖(重写)、隐藏(重定义)的对比 

4.final 和 override

在添加父类虚函数后面添加final代表不能再被重写

 

 final修饰类,代表不能被继承

override代表必须要重写虚函数,如果没有重写便会报错

三、抽象类 

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

注意这里的包含,只要类里面有一个有纯虚函数,就是抽象类,就无法实例化对象,间接强制派生类重写。

 

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

四.多态的原理

1.虚函数表

以下代码环境在X86中,涉及到的指针是4个字节

我们定义一个Base类,里面有虚函数,还有一个变量int,按照我们之前学习到了,这里Base类的大小应该是4个字节,图中确是8个字节

为什么会发生这种现象呢?

用监视窗口看一下

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。

其实应该叫__vftptr(多个t代表table)

我们多添加几个虚函数,看看这个表里面的内容是怎么样的 

可以发现虚函数会放到虚函数表中,普通函数不会,并且表里面的内容是一个数组,是函数指针数组

2.多态的原理 

 有了虚函数表的概念,我们可以尝试通过虚函数表,去找到多态的原理

下面是测试代码

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void fun(){}
private:int a;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
private:int b;
};
void Func(Person* p)
{p->BuyTicket();
}
int main()
{Person p;Student s;Func(&p);Func(&s);return 0;
}

2.1虚表指针里的内容

从图中我们可以看到,在内存1里面输入&p可以找到p的地址, 因为p的第一个内容就是__vfptr,因此p的地址也是__vfptr的地址,那么我们通过__vfptr的地址就可以找到虚函数表里面的内容,因此我们在内存2里面输入__vfptr的地址,我们便找到了两个虚函数的地址。 

去找s的虚表虚函数也同理 

为什么我们要这么麻烦的去找呢?监视窗口不是可以看到吗?

这是因为VS2022的监视窗口可能会骗人(不一定百分百准确),使用内存是一定准确的。

通过上面的图片,我们可以提炼出如下内容

注意这里Student类和Teacher的类表里的第二个虚函数地址是一样的,因为B类没有重写第二个虚函数,因此继承下来了。

为什么第一个虚函数不一样呢?

因为子类重写后覆盖掉了(这也是为什么重写被称作覆盖的由来)

2.2引用和指针如何实现多态 

可以分析,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?

传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数

传递子类,首先会进行切割

将子类的内容切割掉,父类再去接受这个数据了,一样会有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。

这样就完成了多态。

附加一句,对于下面指出的代码(他其实并不清楚自己所存放的虚函数表指针是父类的还是子类的,他只是蠢蠢的去调用这个虚函数而已)

2.3普通类接收为什么实现不了多态

依然是之前的代码,参数部分不再是指针和引用,而是用普通类,我们发现这里没有实现多态。

我们将代码做一个小改动,方便观看区别 

 给Person类添加上一个构造函数

class Person {
public:Person(int x = 0):a(x){}virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void fun(){}
private:int a;
};

 我们给Person类构建出的p对象传10,他会调用构造函数将10赋值给成员a。

当执行Func(p)函数时,注意观察此时的a的值为10,虚函数表地址为0x004f9bfc

当执行Func(s)函数时,注意观察此时的a的值为0,虚函数表地址也为0x004f9bfc。

从上面的分析可以看出,Func(s)传递时,切割出子类中父类的那一份,成员会拷贝给父类,但是没有拷贝虚函数表指针

为什么只拷贝成员,不拷贝虚函数表指针呢?C++祖师爷为何这么设计?

我们可以用反证法

假设 拷贝构造赋值重载 会拷贝虚函数表指针

那么我们写出如下代码,运行后输出结果就应该为 两个 买票-半价  了(因为不管指向的累人,只管你所存储的数据)

这样就不能保证多态调用时,指向父类,父类调用的是父类的虚函数。因为还有可能经过一些操作,变成子类的虚函数

也许上面的问题并不那么致命,你说你自己控制好一点不就行了。

那么析构呢?要知道虚函数表中还可能有析构函数,如果我写出如下代码,阁下又该如何应对?

	Person* p = new Person;Student s;*p = s;delete p;

这个时候,你会发现Person父类的对象delete会去调用子类Student类的析构函数,这样会引发很多不可控制的事情。因此祖师爷帮我们处理了

这里会有点绕,不理解也没关系, 只要知道只有引用和指针才能触发多态就行!!!

最后再补充两点:

同类对象的虚表一样。

如果子类没有重写父类的虚函数,那么他们的虚函数表指针不同,但里面的内容相同

 3.虚函数表存放位置

 我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};void func()
{}int main()
{Base b1;Base b2;static int a = 0;int b = 0;int* p1 = new int;const char* p2 = "hello world";printf("静态区:%p\n", &a);printf("栈:%p\n", &b);printf("堆:%p\n", p1);printf("代码段:%p\n", p2);printf("虚表:%p\n", *((int*)&b1));printf("虚函数地址:%p\n", & Base::func1);printf("普通函数:%p\n", func);
}

注意打印虚表这里,vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节

b1是类对象,取地址取出类对象的地址,强转为(int*)代表我们只取4个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

从图中可以发现代码段和虚表地址非常接近,存在代码段的常量区。

虚函数和普通函数地址非常接近,存在代码段。

五.单继承和多继承关系的虚函数表

1.单继承中的虚函数表

我们使用如下代码测试一下单继承的虚函数表。

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
class X : Derive
{
public:virtual void fun3(){ cout << "X::func3" << endl; }
};int main()
{Base b;Derive d;X x;return 0;
}

明明Base类和X类应该有4个虚函数,监视窗口发现表里面竟然只有2个,这真的很奇怪。

VS下的监视窗口不一定准确,我们用之前的办法打开内存来看看 

通过输入__vfptr的地址,我们成功找到了里面虚函数的地址。并且我们还发现似乎下面那两个地址跟上面两个非常接近,我们可以合理的设想,下面两个地址也是虚函数指针。

在vs环境下,虚函数表里面的虚函数以0结尾, 也很符合之前我们观察到的。

我们可以通过这一点,来打印虚表。

下面我们typedef了虚函数表指针  typedef void(*VFTPTR)(); 可以通过这个函数指针数组来打印里面的虚函数,这个打印函数终止条件就是 !=0 ,传递的参数内容跟前面我们分析的差不多,只是躲了一个强转,PrintVFPtr((VFTPTR*)*(int*)&b)  ; 因为后面的  *(int*)&b 虽然内容是地址,但是表现形式是一个整形,需要强为  (VFTPTR*) 

 

在*((int*)&d) 就会取到vTableAddress指向的地址,就得到虚函数的地址了。

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
class X : Derive
{
public:virtual void func3(){ cout << "X::func3" << endl; }
};typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++){printf("a[%d]:%p\n", i, a[i]);}cout << endl;
}int main()
{Base b;Derive d;X x;PrintVFPtr((VFTPTR*)*(int*)&b);PrintVFPtr((VFTPTR*)*(int*)&d);PrintVFPtr((VFTPTR*)*(int*)&x);return 0;
}

我们运行一下如上代码,便可以打印出虚函数表里面的内容 

但是目前我们还是可以质疑这个地址到底是不是虚函数地址,我们可以打印虚函数的内容看一下。下面给打印代码略作修改,因为a[i]里面存放的就是函数指针,因此我们可以选择直接调用。 

void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++){printf("a[%d]:%p->", i, a[i]);VFTPTR p = a[i];p();}cout << endl;
}

这下真的可以看到结果了可以确认虚函数都会放到虚表里面。并且监视窗口可能是个大骗子,要小心他!!!

2.多继承中的虚函数表

这里选择多继承,如图

这里我们代码

Base1有虚函数func1和func2,

Base2也有虚函数func1和func2。

derive继承了Base1和Base2,并重写了虚函数func1,还有虚函数func3

typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i = 0; a[i] != 0; i++){printf("a[%d]:%p->", i, a[i]);VFTPTR p = a[i];p();}cout << endl;
}
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};
int main()
{Derive d;PrintVFPtr((VFTPTR*)(*(int*)&d));return 0;
}

使用监视窗口观察到Dervie类对象d有两个虚函数表,那么问题就来了,他自身的虚函数func3会放在哪一张表里面呢? 

我们还是选择打印来观看,发现这里只能打印出第一张虚表的内容,并且func3在第一张虚表里,第二张虚表有没有func3呢?好像也需要打印出来观看,而好像我们对于第二张虚表不好打印,我们有没有什么方法可以打印第二张虚表里面的内容呢?

答案是有的,有很多种方法,这里我们介绍两种

第一种:将&d强转为Base1*,这样+1就会跳过整个Base1,就刚好到达了Base2类的开始,再进行之前的强转便可以打印了。

第二种方法:直接将&d赋值给Base2* ptr;这样Base2会进行切片操作,于是ptr就直接指向了Base2的虚函数表,依然就行之前的强转操作便可以打印了。

主函数代码如下

int main()
{Derive d;PrintVFPtr((VFTPTR*)(*(int*)&d));//写法1//PrintVFPtr((VFTPTR*)(*(int*)((Base1*)&d +1)));//写法2Base2* ptr = &d;PrintVFPtr((VFTPTR*)(*(int*)ptr));return 0;
}

 我们打印出来看一看这两个表是什么情况

这里可以得出结论了,Derive类对象的虚函数会放在多继承中继承的第一个类的虚函数表里(即Base1类虚函数表) 

问题又来了,为什么多继承要搞多个虚表呢?

还是之前的继承关系,请看如下图和代码,如果不搞多个虚表,那么p1去调用func1(),p2也去调用func1(),如若d没有重写func1(),那么这个多态就会紊乱,调用的都是那一个func1()了,而不是p1调用Base1的func1(),p2调用Base2的func1()了。因此我们多继承就搞多个虚表才不会出现紊乱的问题

int main()
{Derive d;Base1* p1 = &d;PrintVFPtr((VFTPTR*)*(int*)p1);p1->func1();Base2* p2 = &d;PrintVFPtr((VFTPTR*)*(int*)p1);p2->func1();return 0;
}

运行一下代码 

 调用的func1()函数确实没问题,实现了多态,但是我们发现两张虚表里func1()函数的地址竟然不同,这是为什么,我们重写的func1()两个都实现了啊,调用的也肯定是同一个函数,按道理来说应该地址是一样的,为什么地址不一样呢?我们尝试用反汇编来看一下

p1调用func1的反汇编  call了eax,走到了eax里面的jmp指令,再走一步,就到了func1()函数

p2调用反汇编,首先也是call了eax,再jmp,但是这个jmp竟然没有走到func1()函数,而是先执行了 sub   ecx,8 指令,后面再jmp了两下,才走到了func1()函数。why???

 sub   ecx,8 指令代表了什么?为什么调用的同一个函数,汇编代码却不相同?

我们尝试破解一下这个指令到底是为什么,首先Derive类重写了func1()函数,既然是Derive类的func1()函数,那么他所存放的*this指针类型肯定也是Derive。那么在这个函数里,我是不是可以调用父类的非私有成员或者函数?

 

那如果我执行Base2* p2 = &d;   这就会对d对象进行切片,p2对象按道理来说只能看到Base2的虚函数表和自己存放的数据b2,但是又有可能在func1()函数访问Base1或者Base2的非私有成员和函数,这样好像就不太好调用了,如果我将p2的地址放在Derive类对象地址的地方,那是不是就会很方便访问了。

sub   ecx,8  此时,我们在俩看这句指令,从下面图片可以看出来exc存放的是p2的地址,sub 是减去的意思, 这局指令是 exc - 8,也就是p2的地址-8,那么我现在的地址是什么???没错啦,现在的地址就回到的&d的地址,这样一来是不是就可以调用整个Derive类还有继承下来的数据啦!!!!

这局代码的本质就是修正this指针,指向derive对象

那么为何 Base1* p1 = &d;  p1却没有减去值,回到&d的地方了呢?笨蛋,因为p1的地址就是d的地址,Derive首先继承的就是Base1,而d的首元素就是Base1的虚函数表指针啊。因此我自己就在这里,我还减去什么,我传过去直接开用就完事了!!!!

我们成功的管中窥豹,明白了多继承虚函数表的情况,还有为什么重写多个父类的同名虚函数,地址为何不一样了(如果地址一样,就不可能一起回到Derive类对象的地址,因为类里的元素总会有先后顺序)!!

3.菱形继承和菱形虚拟继承

我的建议是别碰,饶了我吧,投降了。

做一道题吧

 

 这道题选B,很难相信

首先,B类型的对象p去调用test(); test()是B类继承下来的,但是里面默认存放的this指针依然是A*,将一个B类型的指针传给A类型的指针,会发生多态,B类里面的func()是重写了A类的func()  (A类func()为虚函数,B类重写了可以不写virtual)。

注意重写的关键点,仅仅是重写了A类的实现,而前面的那些声明,依然是调用的A类的声明,因此给到的val默认值是1,调用了B类的函数实现!!! 所以输出B->1

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

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

相关文章

大模型在数据分析场景下的能力评测

“你们能对接国产大模型吗&#xff1f;” “开源的 LLaMA 能用吗&#xff0c;中文支持怎么样&#xff1f;” “私有化部署和在线服务哪个更合适&#xff1f;” 自 7 月 14 日发布 AI 数智助理 Kyligence Copilot 后&#xff0c;我们收到了很多类似上面的咨询&#xff0c;尤其…

编程实例:洗车店会员管理系统软件一卡多项目管理编程

编程实例&#xff1a;洗车店会员管理系统软件一卡多项目管理编程 编程系统化课程总目录及明细&#xff0c;点击进入了解详情。 https://blog.csdn.net/qq_29129627/article/details/134073098?spm1001.2014.3001.5502 1、会员可以直接用手机号&#xff0c;并可以绑定车牌号 2…

软考系统架构之案例篇(软件工程相关概念)

案例篇-软件工程相关概念 1. 流程图和数据流图之间的区别与联系2. 状态图和活动图的含义及其区别3. 活动图和流程图的区别4. 数据流图中所包含的基本元素及其作用5. 数据流图的平衡原则:6. 用例之间的关系7. 类之间的关系以及基本含义8. 对象模型、动态模型和功能模型的含义以及…

虚拟化 vs. 裸金属:K8s 部署环境架构与特性对比

伴随着 IT 云化转型的逐步推进&#xff0c;越来越多的用户加入应用容器化改造的行列&#xff0c;并使用 Kubernetes&#xff08;K8s&#xff09;进行容器部署管理。然而&#xff0c;令不少用户感到困惑的是&#xff0c;由于大部分应用此前都部署在虚拟化或超融合环境&#xff0…

JVM相关面试题(每日一练)

1. 什么是垃圾回收机制&#xff1f; 垃圾收集 Garbage Collection 通常被称为“GC”&#xff0c;它诞生于1960年 MIT 的 Lisp 语言&#xff0c;经过半个多世纪&#xff0c;目前已经十分成熟了。 jvm 中&#xff0c;程序计数器、虚拟机栈、本地方法栈都是随线程而生随线程而灭&a…

在本地模拟C/S,Socket套接字的使用

public class SocketTCP01Server {public static void main(String[] args) throws IOException {/**1.在本机的 9999 端口监听 &#xff0c;等待连接细节&#xff1a; 要求在本机没有其他服务在监听999细节&#xff1a;这个ServerSocket 可以通过accept()返回多个Socket[多个客…

使用Jenkins触发gitlab的webhook

满足条件&#xff1a; 首先手动构建可以完成构建 例如&#xff1a; 打开项目点击配置 在“Build Triggers”栏勾选&#xff0c;Build when a change is pushed to GitLab. GitLab webhook &#xff1b;如下 复制URL链接&#xff0c;我的链接是&#xff1a;http://192.168.44…

设计模式之门面模式

前言 什么是门面模式 门面模式是一种结构型设计模式&#xff0c;它提供了一个统一的接口&#xff0c;用来访问子系统中的一群接口。它定义了一个高层接口&#xff0c;让子系统更容易使用。这种模式常用于将一个复杂的子系统封装成一个简单的接口&#xff0c;使得客户端可以方…

Java 枚举类型与泛型-第13章

Java 枚举类型与泛型-第13章 1.枚举类型 枚举类型是一种特殊的数据类型&#xff0c;用于表示一组有限的命名常量。枚举类型可以帮助您更清晰地定义和管理相关常量&#xff0c;并提供类型安全性。 1.1使用枚举类型设置常量 枚举类型是一种非常方便的方式来设置常量。我们可以…

基于51单片机的温度测量报警系统的设计与制作

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、实习目的二、实习任务2.1 设计温度测量报警系统硬件电路2.2 温度测量报警系统软件编程、仿真与调试&#xff1b;2.3 完成温度测量报警系统的实物制作与调试…

从一线到联合,克唑替尼在ALK阳性NSCLC治疗新旅程【医游记】

&#xff08;图片来源于网络&#xff09; 一、克唑替尼简介 克唑替尼(Crizotinib),商品名赛可瑞,是一款口服服用的小分子酪氨酸激酶抑制剂。克唑替尼最早于2011年被美国FDA批准用于ALK阳性晚期NSCLC的治疗。其主要靶点为间变淋巴瘤激酶(ALK)和ROS1(ROS proto-oncogene 1)融合…

3.1、Linux的vim编辑器

个人主页&#xff1a;Lei宝啊 愿所有美好如期而遇 目录 前言&#xff1a; 插入模式 底行模式 命令模式 前言&#xff1a; 没有进行配置的vim编辑器在写代码时和记事本没什么两样&#xff0c;所以最开始我们可以先下载一个插件&#xff0c;在Linux下两行指令的问题&…

视频格式高效转换:MP4视频批量转MKV格式的方法

随着数字媒体技术的不断发展&#xff0c;视频格式转换已经成为了我们日常工作中不可或缺的一部分。不同的视频格式适用于不同的场景和设备&#xff0c;因此将视频从一种格式转换为另一种格式往往是我们必须完成的任务。在本文中&#xff0c;我们将重点介绍如何运用云炫AI智剪高…

Apollo安装全攻略

安装方式 概述快速安装安装基础软件安装 Ubuntu Linux安装 Docker Engine 安装 Apollo 环境管理工具获取 GPU 支持&#xff08;可选&#xff09;创建和进入 Apollo 环境容器 源码安装安装 Linux 系统&#xff08;可选&#xff09;安装 NVIDIA GPU 驱动安装 docker下载并编译 Ap…

【Linux】安装配置解决CentosMobaXterm的使用及Linux常用命令命令模式

目录 一、介绍 1. 背景 2. 讲述&功能 二、Centos安装配置&MobaXterm 1. 创建 2. 安装 3. 配置 4. MobaXterm使用 三、Linux常用命令&模式 1. 常用命令 2. 三种模式 3. 命令使用&换源 4. 拍照备份 一、介绍 1. 背景 CentOS的背景可以追溯到200…

STM32F4X SDIO(二) SDIO协议

上一节简单介绍了SD卡的分类&#xff0c;本节将会介绍SD卡的通信协议&#xff0c;也就是SDIO协议。 STM32F4X SDIO&#xff08;二&#xff09;SDIO协议 SD 卡管脚和寄存器SD卡管脚分布SD卡通信协议SD卡寄存器SD卡内部结构 SDIO总线SDIO总线拓扑SDIO总线协议SDIO协议的基本结构…

java将list转为逗号隔开字符串,将逗号连接的字符串转成字符数组,​将逗号分隔的字符串转换为List​(Java逗号分隔-字符串与数组相互转换)

一、通过testList.stream().collect(Collectors.joining(",")) &#xff0c;通过流转换&#xff0c;将list转为逗号隔开字符串 List<String> testList new ArrayList<>(); testList.add("test1"); testList.add("test2"); testList…

ArcGIS笔记12_ArcGIS搜索工具没法用?ArcGIS运行很慢很卡?

本文目录 前言Step 1 ArcGIS搜索工具没法用Step 2 ArcGIS运行很慢很卡 前言 这是笔者最近遇到的两个小问题&#xff0c;新换了台式机&#xff0c;安装上ArcGIS后发现搜索工具没法用&#xff0c;而且感觉还不如原来笔记本运行的流畅&#xff0c;加载图层很慢&#xff0c;编辑要…

web - 前段三剑客

目录 前言 一. HTML 常用标签演示 图片标签 ​编辑 表格标签(重点) ​编辑 表单标签 (重点) 布局标签 其余标签 二. CSS 2.1 . css的三种引入方式 2.2 . 三大选择器 2.3 . css样式 - 浮动 2.4 . css样式 - 定位 1.static 2.absolute(绝对位置) 3.relavite(相…

【PythonRS】Pyrsgis库安装+基础函数使用教程

pyrsgis库是一个用于处理地理信息系统(GIS)数据的Python库。它提供了一组功能强大的工具&#xff0c;可以帮助开发人员使用Python语言创建、处理、分析和可视化GIS数据。通过使用pyrsgis库&#xff0c;开发人员可以更轻松地理解和利用地理信息。 pyrsgis库包含了许多常见的GIS操…