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.rhkb.cn/news/172292.html

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

相关文章

【计算机网络笔记】DNS报文格式

DNS 提供域名到主机IP地址的映射  域名服务的三大要素&#xff1a;  域&#xff08;Domain&#xff09;和域名(Domain name)&#xff1a; 域指由地 理位置或业务类型而联系在一起的一组计算机构 成。  主机&#xff1a;由域名来标识。域名是由字符和&#xff08;或&a…

如何在linux服务器上安装Anaconda与pytorch,以及pytorch卸载

如何在linux服务器上安装Anaconda与pytorch&#xff0c;以及pytorch卸载 1&#xff0c;安装anaconda1.1 下载anaconda安装包1.2 安装anaconda1.3 设计环境变量1.4 安装完成验证 2 Anaconda安装pytorch2.1 创建虚拟环境2.2 查看现存环境2.3 激活环境2.4 选择合适的pytorch版本下…

Python:实现日历到excel文档

背景 日历是一种常见的工具,用于记录事件和显示日期。在编程中,可以使用Python编码来制作日历。 Python提供了一些内置的模块和函数,使得制作日历变得更加简单。 在本文,我们将探讨如何使用Python制作日历,并将日历输出到excel文档中。 效果展示 实现 在代码中会用到cale…

FFmpeg5.1.3编译动态库踩坑之旅(基于Linux虚拟机)

准备工作 环境准备 1.Windows安装Oracle VM VirtualBox 7.0.10&#xff0c;安装ubuntu-22.04.3。 坑一&#xff1a;无法往虚拟机里拖放复制文件&#xff0c;解决办法&#xff1a;登录Ubuntu虚拟机时切换到xorg方式登录&#xff0c;参考地址&#xff1a;Ubuntu Desktop 22.04…

软考系统架构之案例篇(架构设计相关概念)

案例篇-架构设计相关概念 1. 架构风格的概念2. 五大架构风格有哪些3. MVC架构含义4. 云计算架构5. 云原生架构设计原则6. ESB的主要功能包括7. 质量属性的含义及其设计策略8. EJB中的 Bean 分三种类型9. 风险点、敏感点、权衡点的含义10. REST 的5个原则 其它相关推荐&#xff…

Generative AI 新世界 | Falcon 40B 开源大模型的部署方式分析

在上期文章&#xff0c;我们探讨了如何在自定义数据集上来微调&#xff08;fine-tuned&#xff09;模型。本期文章&#xff0c;我们将重新回到文本生成的大模型部署场景&#xff0c;探讨如何在 Amazon SageMaker 上部署具有 400 亿参数的 Falcon 40B 开源大模型。 亚马逊云科技…

ICLR 2023丨3DSQA:3D 场景中的情景问答

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 论文链接&#xff1a;https://arxiv.org/pdf/2210.07474.pdf 主页链接&#xff1a;http://sqa3d.github.io 图 1&#xff1a;3D 场景中情景问答 (SQA3D) 的任务图示。给定场景上下文 S&#xff08;例如&#…

并发编程 - 并发可见性,原子性,有序性 与 JMM内存模型

1. 并发三大特性 并发编程Bug的源头&#xff1a; 原子性 、 可见性 和 有序性 问题 1.1 原子性 一个或多个操作&#xff0c;要么全部执行且在执行过程中不被任何因素打断&#xff0c;要么全部不执行。 在 Java 中&#xff0c;对基本数据类型的变量的读取和赋值操作是原子性操…

常用字符串函数拓展

文章目录 字符串拓展函数strncpystrncatstrncmpstrstrstrtokstrerrormemcpymemmovememcmpmemset 库函数模拟实现memmoveqsort 我们在学习C语言时已经学习了一些常见的字符串函数&#xff0c;但这还不能满足我们的需求&#xff0c;为此我们拓展了几个常用的字符串函数。 字符串拓…

Maven项目转为SpringBoot项目

Maven项目转为SpringBoot项目 前言创建一个maven项目前的软件的一些通用设置Maven仓库的设置其他的设置字符编码编译器注解支持 创建的Maven项目修改为Spring Boot项目修改pom.xml文件修改启动类-Main新建WAR包所需的类 添加核心配置文件 测试的控制器最后整个项目的目录结构![…

ce从初阶到大牛(两台主机免密登录)

一、配置ssh远程连接 实现两台linux主机之间通过公钥验证能够互相实现免密登陆 1.确认服务程序是否安装 rpm -qa | grep ssh 2.是否启动 ps -aux | grep ssh 3.生成非对称公钥 ssh-keygen -t rsa 4.公钥发送到客户端 cd /root/.ssh/ ssh-copy-id root192.168.170.134 因为…

OpenCV学习(五)——图像基本操作(访问图像像素值、图像属性、感兴趣区域ROI和图像边框)

图像基本操作 5. 图像基本操作5.1 访问像素值并修改5.2 访问图像属性5.2 图像感兴趣区域ROI5.3 拆分和合并图像通道5.4 为图像设置边框&#xff08;填充&#xff09; 5. 图像基本操作 访问像素值并修改访问图像属性设置感兴趣区域&#xff08;ROI&#xff09;分割和合并图像 …

本来打算做功能测试的,但是发现playwright太好玩了,玩了一天,功能测试进度为空

本文是作者的自言自语&#xff1a;//todo 未完待续 https://blog.csdn.net/lineuman 微软果然有大牛啊&#xff01;有能人的公司总是令人敬佩。 playwright这种级别的工具简直就是核弹级别的。 当我开始使用playwright的时候&#xff0c;嘭的一下&#xff0c;我的世界炸了&…

javaEE -10(11000字详解5层重要协议)

一&#xff1a;应用层重点协议 1.1&#xff1a; DNS DNS&#xff0c;即Domain Name System&#xff0c;域名系统。DNS是一整套从域名映射到IP的系统。 TCP/IP中使用IP地址来确定网络上的一台主机&#xff0c;但是IP地址不方便记忆&#xff0c;且不能表达地址组织信息&#x…

2023年正版win10/win11系统安装教学(纯净版)

第一步&#xff1a;准备一个8G容量以上的U盘。 注意&#xff0c;在制作系统盘时会格式化U盘&#xff0c;所以最好准备个空U盘&#xff0c;防止资料丢失。 第二步&#xff1a;制作系统盘。 安装win10 进入windows官网 官网win10下载地址&#xff1a;https://www.microsoft.c…

Oracle数据库设置归档模式(超级简单)

1、打开监听 查看监听的状态&#xff0c;如果没打开监听需要打开监听&#xff0c;如果打开直接下一步 lsnrctl status 打开监听 lsnrctl start 2、启动数据库 首先进入数据库 sqlplus /nolog 然后连接管理员 conn / as sysdba 3、查看当前模式 archive log list 可以…

图像特征Vol.1:计算机视觉特征度量|第一弹:【纹理区域特征】

目录 一、前言二、纹理区域度量2.1&#xff1a;边缘特征度量2.2&#xff1a;互相关和自相关特征2.3&#xff1a;频谱方法—傅里叶谱2.4&#xff1a;灰度共生矩阵(GLCM)2.5&#xff1a;Laws纹理特征2.6&#xff1a;局部二值模式&#xff08;LBP&#xff09; 一、前言 &#x1f…

【C++】C++入门(下)--内联函数 auto关键字 nullptr

目录 一 内联函数 1 内联函数概念和定义 2 内联函数特性 二 auto关键字 1 auto概念 2 auto 的使用细则 (1) auto与指针和引用结合起来使用 (2) 在同一行定义多个变量 3 auto不能推导的场景 (1) auto不能作为函数的参数 (2) auto不能直接用来声明数组 4 基于范围的fo…

使用 excel 快速拼接省市区镇街村居五级区划完整名称

你知道的越多&#xff0c;你不知道的越多 点赞再看&#xff0c;养成习惯 如果您有疑问或者见解&#xff0c;欢迎指教&#xff1a; 企鹅&#xff1a;869192208 文章目录 前言数据准备excel 函数附件 前言 之前做了国家区划的映射关系&#xff0c;在其过程中&#xff0c;使用代码…