探索多态的本质【C++】

文章目录

  • 多态的构成条件
    • 虚函数
    • 虚函数的重写(覆盖)
  • 虚函数重写的两个例外
  • C++11 override和final
  • 区分重载、覆盖(重写)、隐藏(重定义)
  • 抽象类
  • 接口继承和实现继承
  • 多态的原理
    • 虚函数表
  • 动态绑定和静态绑定
    • 动态绑定
    • 静态绑定
  • 单继承中的虚函数表
  • 多继承中的虚函数表

多态的构成条件

在继承中要构成多态还有两个条件:
1、必须通过基类的指针或者引用调用虚函数。

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

//父类
class Person
{
public://父类的虚函数virtual void BuyTicket() const{cout << "买票-全价" << endl;}
};//子类
class Student : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket() const{cout << "买票-半价" << endl;}
};引用
//void func(const Person &  p)
//{
//	p.BuyTicket();
//}//指针
void func(const Person *  p)
{p->BuyTicket();
}int main()
{    //多态条件// 1、调用函数必须是重写的虚函数//基类必须是指针或者引用//多态,不同对象传递过去,调用不同参数//多态调用看指向的对象//普通对象,看当前类型//引用/*func(Person());func(Student());*///指针Person pp;func(&pp);Student st;func(&st);return 0;
}

虚函数

被virtual修饰的类成员函数被称为虚函数。

class A
{
public:virtual void func(){cout << "virtual void func() " << endl;}
};

注意:
只有类的非静态成员函数前可以加virtual,普通函数前不能加virtual

虚函数这里的virtual和虚继承中的virtual虽然是同一个关键字,但是它们之间没有任何关系。

虚函数的virtual是为了实现多态
虚继承的virtual是为了解决菱形继承的数据冗余和二义性。

虚函数的重写(覆盖)

如果派生类中有一个和基类完全相同的虚函数(返回值类型相同、函数名相同以及参数列表完全相同,这里所说的参数列表是指参数类型要相同)
此时我们称该派生类的虚函数重写了基类的虚函数。

//父类
class Person
{
public://父类的虚函数virtual void BuyTicket(){cout << "买票-全价" << endl;}
};
//子类
class Student : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout << "买票-半价" << endl;}
};
//子类
class Soldier : public Person
{
public://派生类的虚函数重写了父类的虚函数virtual void BuyTicket(){cout << "优先-买票" << endl;}
};

通过父类Person的指针或者引用调用虚函数BuyTicket,此时不同类型的对象,调用的就是不同的函数,产生的也是不同的结果,进而实现了函数调用的多种形态。

void Func(Person& p)
{//通过父类的引用调用虚函数p.BuyTicket();
}
void Func(Person* p)
{//通过父类的指针调用虚函数p->BuyTicket();
}
int main()
{Person p;   //普通人Student st; //学生Soldier sd; //军人Func(p);  //买票-全价Func(st); //买票-半价Func(sd); //优先买票Func(&p);  //买票-全价Func(&st); //买票-半价Func(&sd); //优先买票return 0;
}

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

虚函数重写的两个例外

协变(基类与派生类虚函数的返回值类型不同)

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

//基类
class A
{};
//派生类
class B : public A
{};//基类
class Person
{
public://虚函数virtual  A* fun(){cout << "A* Person::f()" << endl;return new A;}
};
//派生类
class Student : public Person
{
public:// 虚函数virtual  B * fun(){cout << "B* Person::f()" << endl;return new B;}};
int main()
{Person p;Student st;//基类指针指向基类对象Person* ptr1 = &p;//基类指针指向子类对象Person* ptr2 = &st; //切片//父类指针ptr1指向的p是父类对象,调用父类的虚函数ptr1->fun();//父类指针ptr2指向的st是子类对象,调用子类的虚函数ptr2->fun();return 0; 
}

析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

//析构函数加上virtual ,是虚函数重写,为什么?
// 因为析构函数都被处理成了destructor这个统一的名字,为什么统一处理成destructor?
//因为统一处理成destructor,是要将派生类和基类的析构函数构成重写,而重写是构成多态的一个重要条件
class Person
{
public:virtual  ~Person(){cout << "~Person()" << endl;}
};
//子类
class Student : public Person
{
public:virtual  ~Student(){cout << "~Student()" << endl;delete[]ptr;}
protected :	int* ptr = new int[10];
};int main()
{//Person p;//Student s;//析构顺序:先子后父Person* p = new Person;delete p;p = new Student;delete p;//p->destuctor() +  operator delete (p)//这里我们希望p->destuctor()是一个多态调用 ,而不是普通调用return 0;
}

在继承当中,子类和的析构函数和父类的析构函数构成隐藏的原因就在这里,这里表面上看子类的析构函数和父类的析构函数的函数名不同,但是为了构成重写,编译后析构函数的名字会被统一处理成destructor();

C++11 override和final

C++11提供了override和final两个关键字,可以帮
助用户检测是否重写。

final:修饰虚函数,表示该虚函数不能再被重写。

//基类
class Person
{
public://虚函数//final:修饰虚函数,表示该虚函数不能再被重写virtual void BuyTicket() final{cout << "买票-全价" << endl;}
};//派生类
class Student : public Person
{
public:virtual void BuyTicket()//err{cout << "买票-半价" << endl;}
};

override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错

//基类
class Person
{
public://虚函数virtual void BuyTicket() {cout << "买票-全价" << endl;}
};//派生类
class Student : public Person
{
public://override,派生类完成基类的重写,就不报错virtual  void BuyTicket() override{cout << "买票-半价" << endl;}
};

区分重载、覆盖(重写)、隐藏(重定义)

在这里插入图片描述

抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。

class Car
{
public://纯虚函数virtual void Drive() = 0;
};
int main()
{Car c; //errreturn 0; 
}

派生类继承抽象类后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

//抽象类class Car
{
public://纯虚函数virtual void Drive() = 0;
};//派生类继承抽象类class Benz : public Car
{
public://重写纯虚函数virtual void Drive(){cout << "Benz-舒适" << endl;}
};int main()
{//派生类重写了纯虚函数,可以实例化对象Benz b1;Car* p1 = &b1;p1->Drive();return 0;
}

抽象类不能实例化出对象,那抽象类存在的意义是什么?

抽象类体现了虚函数的继承是一种接口继承,强制子类去重写纯虚函数,因为子类若是不重写从父类继承下来的纯虚函数,那么子类也是抽象类也不能实例化出对象。

接口继承和实现继承

实现继承: 普通函数的继承是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。

接口继承: 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。

建议: 所以如果不实现多态,就不要把函数定义成虚函数。

多态的原理

虚函数表

看下面的代码

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{Base b;cout << sizeof(b) << endl;return 0; 
}

b对象当中除了_b成员外,实际上还有一个_vfptr(虚函数表指针简称虚表指针)放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。虚函数
的地址要被放到虚函数表中,虚函数表也简称虚表
在这里插入图片描述

#include <iostream>
using namespace std;
//父类
class Base
{
public://虚函数virtual void Func1(){cout << "Base::Func1()" << endl;}//虚函数virtual void Func2(){cout << "Base::Func2()" << endl;}//普通成员函数void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
//子类
class Derive : public Base
{
public://重写虚函数Func1virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

通过观察,我们发现:
基类对象b和基类对象d当中除了自己的成员变量之外,基类和派生类对象都有一个虚表指针,分别指向属于自己的虚表。
在这里插入图片描述
实际上虚表当中存储的就是虚函数的地址

派生类虽然继承了基类的虚函数Func1和Func2,但是派生类对基类的虚函数Func1进行了重写。所以,派生类对象d的虚表当中存储的是基类的虚函数Func2的地址和重写的Func1的地址。这就是为什么虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数地址的覆盖,重写是语法的叫法,覆盖是原理层的叫法。

注意:Func2是虚函数,所以继承下来后放进了子类的虚表,而Func3是普通成员函数,继承下来后不会放进子类的虚表。并且,虚函数表本质是一个存虚函数指针的指针数组,一般情况下会在这个数组最后放一个nullptr。

总结:派生类的虚表生成步骤如下
1、先将基类中的虚表内容拷贝一份到派生类的虚表

2、如果派生类重写了基类中的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址

3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

虚表是什么阶段初始化的?虚函数存在哪里?
虚表实际上是在构造函数初始化列表阶段进行初始化的,注意虚表当中存的是虚函数的地址不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是他的地址又存到了虚表当中。另外,对象中存的不是虚表而是指向虚表的指针。

那虚表是存在哪里的?

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}virtual void Func1() { }virtual void Func2() { }
//protected:int _a = 0;
};
class Student : public Person
{
public://派生类重写基类虚函数virtual void BuyTicket(){cout << "买票-半价" << endl;}virtual void Func3() { }
protected:int _b = 1;
};
int main()
{Person ps;Student st; //栈int a = 0;printf("栈:%p\n", &a);//静态区static  int b = 0;printf("静态区:%p\n", &b);//堆int* p = new int;printf("堆:%p\n", p);const char* str = "hello world";printf("常量区(代码段):%p\n", str);printf("虚表1:%p\n",*(   (int*)&ps ) );printf("虚表2:%p\n", *( (int*) &st ) );return  0; 
}

从上述代码可以发现虚表地址与代码段的地址非常接近,由此可以得出虚表是存在代码段的

详细分析下面的代码
为什么当父类Person指针指向的是父类对象Mike时,调用的就是父类的BuyTicket?
当父类Person指针指向的是子类对象Johnson时,调用的就是子类的BuyTicket?

class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}int _a = 1;
};class Student : public Person 
{
public://派生类重写基类虚函数virtual void BuyTicket() { cout << "买票-半价" << endl;}int _b = 1;
};int main()
{Person Mike;Student Johnson;Johnson._b = 3; //以便观察是否完成切片Person* p1 = &Mike;Person* p2 = &Johnson;p1->BuyTicket(); //买票-全价p2->BuyTicket(); //买票-半价return 0;
}

对象Mike中包含一个成员变量_a和一个虚表指针,对象Johnson中包含两个成员变量_a和_b以及一个虚表指针,这两个对象当中的虚表指针分别指向自己的虚表。

在这里插入图片描述

通过上图可分析:
1、父类指针p1指向Mike对象,p1->BuyTicket在Mike的虚表中找到的虚函数就是Person::BuyTicket。

2、父类指针p2指向Johnson对象,p2>BuyTicket在Johnson的虚表中找到的虚函数就是Student::BuyTicket。

这样就实现出了不同对象去完成同一行为时,展现出不同的形态,即多态

多态构成的两个条件,
1、完成虚函数的重写,
2、必须使用父类的指针或者引用去调用虚函数。
完成虚函数的重写是因为需要完成子类虚表当中虚函数地址的覆盖,这样才能做到指针指向父类,调用父类对象,指针指向子类,调用子类对象

为什么多态的设计必须使用父类的指针或者引用,不使用父类的对象?
指针和引用的切片不存在拷贝问题 ,但是对象的切片需要拷贝
子类赋值给父类对象切片,不会拷贝虚表,如果拷贝虚表,那么父类对象虚表中是父类虚函数还是子类虚函数就不确定了

在这里插入图片描述

使用父类的指针或者引用时,实际上是一种切片行为,切片时只会让父类指针或者引用得到父类对象或子类对象中切出来的那一部分。

在这里插入图片描述
用p1和p2调用虚函数时,p1和p2通过虚表指针找到的虚表是不一样的,最终调用的函数也是不一样的。

Person p1 = Mike;
Person p2 = Johnson;

使用父类对象时,切片得到部分成员变量后,会调用父类的拷贝构造函数对那部分成员变量进行拷贝构造,而拷贝构造出来的父类对象p1和p2当中的虚表指针指向的都是父类对象的虚表。因为同类型的对象共享一张虚表,他们的虚表指针指向的虚表是一样的。

总结:

对象的调用:
如果是普通对象的调用(不符合多态),看调用者的类型,普通对象的调用在编译时就确定好了地址
如果调用符合多态,看指向的对象,在运行时到指向对象的虚函数表中找调用函数的地址从而完成调用

动态绑定和静态绑定

动态绑定

动态绑定又称为后期绑定(晚绑定),在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也成为静态多态,比如:函数重载

普通对象的调用

//基类
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};//派生类
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
};int main()
{Student st;Person p = st;//不构成多态,函数的调用是在编译时就确定的p.BuyTicket();return 0;
}

在这里插入图片描述
将调用函数的那句代码翻译成汇编就只有以上两条汇编指令,也就是直接调用的函数。

使用多态调用

//基类
class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
protected:int _a = 0;
};//派生类
class Student : public Person
{
public:virtual void BuyTicket(){cout << "买票-半价" << endl;}
protected:int _b = 1;
};int main()
{Student st;Person & p = st;//构成多态,看指向的对象p.BuyTicket();return 0;
}

在这里插入图片描述
构成多态时调用函数的那句代码翻译成汇编后就变成了八条汇编指令,原因就是我们需要在运行时,先到指定对象的虚表中找到要调用的虚函数,然后才能进行函数的调用。

体现了静态绑定是在编译时确定的,而动态绑定是在运行时确定的。

单继承中的虚函数表

//基类
class Base
{
public:virtual void func1() { cout << "Base::func1()" << endl;}virtual void func2() {cout << "Base::func2()" << endl; }
private:int _a = 0 ;
};
//派生类
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 =1 ;
};int main()
{Base b;Derive d;return 0;
}

派生类和基类的内存分布

在这里插入图片描述
单继承关系当中,派生类的虚表生成过程如下:

1、继承基类的虚表内容到派生类的虚表。
2、对派生类重写了的虚函数地址进行覆盖,比如func1。
3、虚表当中新增派生类当中新的虚函数地址,比如func3和func4。

在调试过程中,某些编译器的监视窗口当中看不到虚表当中的func3和func4,可能是编译器的监视窗口故意隐藏了这两个函数,也可以认为这是一个小bug,此时想要看到派生类对象完整的虚表有两个方法。

使用内存监视窗口
在这里插入图片描述
使用代码打印虚表内容

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{printf("虚表地址:%p\n", ptr);for (int i = 0; ptr[i] != nullptr; i++){printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址ptr[i](); //使用虚函数地址调用虚函数}printf("\n");
}
int main()
{Base b;PrintVFT((VFPTR*)(*(int*)&b)); //打印基类对象b的虚表地址及其内容Derive d;PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的虚表地址及其内容return 0;
}

多继承中的虚函数表

//基类1
class Base1
{
public:virtual void func1() { cout << "Base1::func1()" << endl; }virtual void func2() {cout << "Base1::func2()" << endl;}
private:int _b1;
};//基类2
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()
{Base1 b1;Base2 b2;Derive d;return 0;
}

在这里插入图片描述
多继承中,派生类的虚表生成过程如下:

1、分别继承各个基类的虚表内容到派生类的各个虚表当中。
2、对派生类重写了的虚函数地址进行覆盖(派生类中的各个虚表中存有该被重写虚函数地址的都需要进行覆盖),比如func1。
3、在派生类第一个继承基类部分的虚表当中新增派生类当中新的虚函数地址,比如func3。

看到派生类对象完整的虚表有两种方法。
一、使用内存监视窗口

在这里插入图片描述

二、使用代码打印虚表内容
在派生类第一个虚表地址的基础上,向后移sizeof(Base1)个字节即可得到第二个虚表的地址。

typedef void(*VFPTR)(); //虚函数指针类型重命名
//打印虚表地址及其内容
void PrintVFT(VFPTR* ptr)
{printf("虚表地址:%p\n", ptr);for (int i = 0; ptr[i] != nullptr; i++){printf("ptr[%d]:%p-->", i, ptr[i]); //打印虚表当中的虚函数地址ptr[i](); //使用虚函数地址调用虚函数}printf("\n");
}
int main()
{Base1 b1;Base2 b2;PrintVFT((VFPTR*)(*(int*)&b1)); //打印基类对象b1的虚表地址及其内容PrintVFT((VFPTR*)(*(int*)&b2)); //打印基类对象b2的虚表地址及其内容Derive d;PrintVFT((VFPTR*)(*(int*)&d)); //打印派生类对象d的第一个虚表地址及其内容PrintVFT((VFPTR*)(*(int*)((char*)&d + sizeof(Base1)))); //打印派生类对象d的第二个虚表地址及其内容return 0;
}

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

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

相关文章

Java——》synchronized锁升级

推荐链接&#xff1a; 总结——》【Java】 总结——》【Mysql】 总结——》【Redis】 总结——》【Kafka】 总结——》【Spring】 总结——》【SpringBoot】 总结——》【MyBatis、MyBatis-Plus】 总结——》【Linux】 总结——》【MongoD…

mysql中GROUP_CONCAT函数详解

GROUP_CONCAT是MySQL中的一个聚合函数&#xff0c;它用于将多行数据按照指定的顺序连接成一个字符串&#xff0c;并返回结果。下面是对GROUP_CONCAT函数的详解&#xff1a; 语法&#xff1a; GROUP_CONCAT([DISTINCT] expr [,expr …] [ORDER BY {unsigned_integer | col_name…

Vue错误记录

文章目录 1. 项目build的时候报错Warning: Accessing non-existent property cat of module exports inside circular dependency2. WebpackOptionsValidationError: Invalid configuration object. Webpack has been initialised using a configuration object that does not …

数据结构与算法之(赫夫曼树,哈夫曼树,压缩软件)

一&#xff1a;思考 1.电报发送&#xff1a;二战的时候大家都知道那时候普遍会应用电报&#xff0c;如果让你来设计一个电报的发送编码你该如何设计呢&#xff1f; 2.压缩算法&#xff1a;给你10000个字符&#xff08;每个字符1btye&#xff0c;也就是8bit&#xff09;的文件&a…

GitHub星标超70K,阿里大佬的架构总结“分布式全解”笔记霸榜

分布式架构与微服务平台是当今IT界的关键技术&#xff0c;也是资深软件工程师和系统架构师必须掌握的核心技术。 因此小编为各位粉丝朋友带来这份阿里大佬的分布式笔记&#xff1a;从传统分布式架构迁移到基于容器技术的微服务架构为主线&#xff0c;全面、透彻地介绍了与分布…

华为回击:制裁无法阻挡中国科技创新 | 百能云芯

华为最新推出的Mate 60 Pro手机引发了中国市场的抢购热潮&#xff0c;这一成功的举措为华为带来了信心。华为在这个背景下再度推出两款新机&#xff0c;其中包括高阶版的Mate 60 Pro和折叠式手机Mate X5。这两款手机在首批预购开始后迅速售罄&#xff0c;不仅取得了市场的热烈欢…

JavaScript基础

1 JavaScript概述 1.1 什么是JavaScript 1.1.1 概念 JavaScript 是脚本语言&#xff0c;是一种解释性脚本语言&#xff08;代码不进行预编译&#xff09; JavaScript 是一种轻量级的编程语言。 JavaScript 是可插入 HTML 页面的编程代码。 JavaScript 插入 HTML 页面后&a…

CANoe中的工作模式之争:由一段简单的代码引出的问题

1、引子 有网友问我一个CAPL中timer定时器的代码问题。他在CANoe工程中写了一段代码:每5秒循环触发一次定时器事件程序,输出一句文本信息到Write窗口。但是执行后发现并不是每5秒触发一次定时器事件程序,而是非常快的触发定时器事件程序。当他把这段代码复制到一个新的CANo…

【开发】安防监控/视频汇聚/云存储/AI智能视频融合平台页面新增地图模式

AI智能分析网关包含有20多种算法&#xff0c;包括人脸、人体、车辆、车牌、行为分析、烟火、入侵、聚集、安全帽、反光衣等等&#xff0c;可应用在安全生产、通用园区、智慧食安、智慧城管、智慧煤矿等场景中。将网关硬件结合我们的视频汇聚/安防监控/视频融合平台EasyCVR一起使…

2023-简单点-怎么知道树莓派是什么cpu架构?

树莓派是几位&#xff1f; getconf LONG_BIT https://qengineering.eu/install-ncnn-on-raspberry-pi-4.html

【C#】C#调用进程打开一个exe程序

文章目录 一、过程二、效果总结 一、过程 新建WinForm程序&#xff0c;并写入代码&#xff0c;明确要调用的程序的绝对路径&#xff08;或相对路径&#xff09;下的exe文件。 调用代码&#xff1a; 这里我调用的另一个程序的路径是&#xff1a; F:\WindowsFormsApplication2…

uni-app--》基于小程序开发的电商平台项目实战(一)

&#x1f3cd;️作者简介&#xff1a;大家好&#xff0c;我是亦世凡华、渴望知识储备自己的一名在校大学生 &#x1f6f5;个人主页&#xff1a;亦世凡华、 &#x1f6fa;系列专栏&#xff1a;uni-app &#x1f6b2;座右铭&#xff1a;人生亦可燃烧&#xff0c;亦可腐败&#xf…

ES6之 变量的解构赋值 ➕ 扩展运算符(…)

ES6之 变量的解构赋值 ➕ 扩展运算符 1. 变量的解构赋值1.1 直接解构赋值1.2 解构赋值——修改属性名1.3 连续解构赋值 2. 扩展运算符2.1 简介&#xff08;官网&#xff09;2.2 应用例子2.2.1 简单例子12.2.2 数组拷贝2.2.3 连接多个数组2.2.4 拷贝对象&#xff08;多层和多维数…

可以用Python实现RPC协议的接口自动化测试

基于RPC协议的接口自动化测试可以用Python语言实现。下面是实现步骤&#xff1a; 1、安装依赖库&#xff0c;如protobuf、grpc。 2、编写.proto文件定义接口参数和返回值。 3、使用protoc编译.proto文件生成Python代码。 4、编写客户端代码调用远程接口进行测试。 具体实现…

数据结构-堆的实现及应用(堆排序和TOP-K问题)

数据结构-堆的实现及应用[堆排序和TOP-K问题] 一.堆的基本知识点1.知识点 二.堆的实现1.堆的结构2.向上调整算法与堆的插入2.向下调整算法与堆的删除 三.整体代码四.利用回调函数避免对向上和向下调整算法的修改1.向上调整算法的修改2.向下调整算法的修改3.插入元素和删除元素函…

操作系统——复习笔记

主要是按照王道考研408的ppt进行复习。 概述 操作系统概览 操作系统&#xff08;Operating System&#xff0c; OS&#xff09;是指控制和管理整个计算机系统的硬件和软件资源&#xff0c;并合理地组织调度计算机的工作和资源的分配&#xff1b;以提供给用户和其他软件方便的…

Unity Shader 溶解效果

一、效果图 二、原理分析 实现原理就是在片元着色器中&#xff0c;对像素点进行丢弃不显示。借助美术做的噪点图(利用噪点图中rgb中r值来做计算)。比如噪点图r值从0-1。我们从小到大让r值逐渐丢弃&#xff0c;比如刚开始r < 0.1丢弃&#xff0c;然后t < 0.2丢弃...知道t…

OpenCV(二十二):均值滤波、方框滤波和高斯滤波

目录 1.均值滤波 2.方框滤波 3.高斯滤波 1.均值滤波 OpenCV中的均值滤波&#xff08;Mean Filter&#xff09;是一种简单的滤波技术&#xff0c;用于平滑图像并减少噪声。它的原理非常简单&#xff1a;对于每个像素&#xff0c;将其与其周围邻域内像素的平均值作为新的像素值…

数据库基础——数据库、数据表和SQL语句

数据库、数据表和SQL语句 数据库、数据表和SQL语句是什么&#xff1f;数据库安装数据库登录及退出创建、查看数据库及修改名字查看及修改数据库编码删除数据库使用或查看当前正在使用的数据库创建、查看数据表及修改名字查看及修改数据表编码查看及修改数据表结构增加约束删除约…

Postman接口测试流程

一、工具安装 ● 安装Postman有中文版和英文版&#xff0c;可以选择自己喜欢的版本即可。安装时重新选择一下安装路径&#xff08;也可以默认路径&#xff09;&#xff0c;一直下一步安装完成即可。&#xff08;本文档采用英文版本&#xff09;安装文件网盘路径链接&#xff1…