《C++多态》

news2024/12/26 11:28:43

文章目录

  • 思维导图
  • 一、多态的概念
  • 二、多态的定义及其实现
    • 1.多态的构成条件
    • 2.虚函数
    • 3.虚函数的重写
    • 不构成多态的情况展示
    • 4.虚函数重写的两个例外
      • 4.1 协变
      • 4.2析构函数的重写
    • 5.C++11 override和final
      • 5.1. final
      • 5.2.override
  • 三、抽象类
    • 1.概念
    • 2、对比纯虚函数与override
    • 3.接口继承和实现继承
  • 四、多态的原理
    • 1.虚函数表指针
    • 2.虚函数表
    • 3.多态的原理
    • 4.再次理解多态构成的条件
    • 5.多态的易错知识点
    • 6.虚函数,虚表,虚表指针存储的位置
    • 7.动态绑定与静态绑定
  • 五、单继承和多继承关系的虚函数表
    • 1、 单继承中的虚函数表
    • 2、 多继承中的虚函数表
  • 六、继承和多态习题练习
    • 1.概念考察
    • 2.问答题


思维导图


一、多态的概念

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

在C++中有两种多态性,一种是静态的多态、一种是动态的多态

  • 静态的多态:函数重载,看起来调用同一个函数却有不同的行为。
    静态:原理是编译时实现

  • 动态的多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数。
    动态:原理是运行时实现

二、多态的定义及其实现

1.多态的构成条件

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

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写

2.虚函数

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

一旦定义了虚函数,该基类的派生类中同名函数也自动成为了虚函数。也就是说在派生类中有一个和基类同名的函数,只要基类加了virtual修饰,派生类不加virtual修饰也是虚函数。

虚函数只能是类中的一个成员函数,不能是静态成员或普通函数
注意:我们在继承中为了解决数据冗余和二义性的问题,需要用到虚拟继承,关键字也是virtual,和多态中的virtual是没有关系的。

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

3.虚函数的重写

虚函数的重写也叫做覆盖子类中有一个与父类完全相同的虚函数,他们两个的虚函数满足三同:返回值,函数名,参数列表完全相同,则称子类的虚函数重写了父类的虚函数。

通过对虚函数的重写,就能够实现多态:

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;
}

这里就完成了我们的多态,如果传参传的是父类对象,就会调用父类的函数如果传参传的是子类对象,就会调用子类的函数。
在这里插入图片描述
本质:不同的人做相同的事情,结果不同!

注意:

    1. 虚函数的重写允许,两个都是虚函数或者父类是虚函数,再满足三同,就构成重写。
    1. 在重写父类虚函数时,子类的虚函数在不加virtual关键字时,虽然也可以构成重写,因为继承后,父类的虚函数被继承下来了,在子类中依旧保持虚函数的属性,其实这个是C++不是很规范的地方,当然我们建议两个都写上virtual,虽然子类没写virtual,但是他是先继承了父类的虚函数的属性,再完成重写,那么他也算是虚函数。
      在这里插入图片描述

不构成多态的情况展示

1️⃣构成多态,跟p类型没有关系,传参传的是哪个类型的对象,调用的就是哪个类型对象中的虚函数——(跟对象有关)
2️⃣不构成多态,调用的就是p类型函数——(跟类型有关)

1、使用父类的对象调用,不构成多态,只会调用父类对象的函数
在这里插入图片描述

2、不加virtual也不会构成多态
在这里插入图片描述

3、虚函数没有重写,因为两个虚函数的参数列表不同,不构成多态
在这里插入图片描述

4.虚函数重写的两个例外

4.1 协变

父类与子类的虚函数构成重写,返回值有个例外(返回值可以不同)即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用,称为协变

class Person {
public:
	virtual Person* BuyTicket() { cout << "买票-全价" << endl; return  nullptr; }
};
class Student : public Person {
public:
	virtual Student* BuyTicket() { cout << "买票-半价" << endl; return nullptr; }
};

4.2析构函数的重写

析构函数是虚函数,是否构成重写?——构成
因为析构函数名被编译器特殊处理成了destructor

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person p;
	Student s;
	
	return 0;
}

在这里插入图片描述
重点:
下面再来看看这个代码:

class Person {
public:
	 ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	 ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* p1=new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
	
	return 0;
}

解释:这两个函数并没有构成多态(因为没有用virtual修饰),我们析构的时候,想要父类调用父类的析构,而子类调用子类的析构,而并没有构成多态,就会跟类型有关,p1 p2都是person类型的,都会去调用父类的析构函数。万一子类对象中有些动态开辟的资源,没有被释放,就会很危险!
在这里插入图片描述

解决:构成多态即可解决这个问题!重写里面的虚函数,加上virtual关键字:
在这里插入图片描述

5.C++11 override和final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了overridefinal两个关键字,可以帮助用户检测是否重写。

5.1. final

修饰类:限制类的继承
修饰虚函数,表示该虚函数不能被重写

例如:如何设计一个类无法被继承?
可以父类的构造函数私有(private)

class A
{
private:
	A(int a)
		:_a(a)
	{}
public:
	static A CreateObj(int a=10)
	{
		return A(20);//创建一个对象出来
	}
protected:
	int _a = 10;
};
// 间接限制,子类的构造函数无法调用父类的构造函数初始化,没办法实例化出对象,因为是private私有权限
class B :public A
{

};
int main()
{
	A aa=A::CreateObj(100);
	// A类的构造函数是私有的,类内能用,但是在类外却不能实例化
	// 所以调用一个公共的函数接口,来实例化
	return 0;
}

以上的方法太过复杂,C++11新增加了一个语法,final关键字

// 直接限制,该类无法被继承
class A final
{
protected:
	int _a = 10;
};
class B :public A
{

};

限制虚函数的重写

class C 
{
public:
	virtual void f() final
	{
		cout << "C::f()" << endl;
	}
};

class D :public C
{
public:
	virtual void f()
	{
		cout << "D::f()" << endl;
	}// 无法重写final函数
};

5.2.override

放在子类重写的虚函数的后面,检查是否完成重写,没有重写就报错

class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

在这里插入图片描述

三、抽象类

1.概念

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

//抽象类
class Car
{
public:
	virtual void Drive() = 0;//纯虚函数 
};
 
int main()
{
	Car c;//抽象类不能实例化出对象
    return 0;
}

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	//纯虚函数一般只声明,不实现(可以实现,但没有价值,因为不能实例化出对象,可以定义指针或引用)
	virtual void Drive() = 0;
};
 
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
 
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
 
int main()
{
    //派生类只有重写了虚函数才能实例化出对象
    Benz b1;
	BMV b2;
    //通过基类的指针去调用不同对象的函数
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

2、对比纯虚函数与override

  1. 纯虚函数的类,本质上强制子类去完成虚函数的重写
  2. override只是语法上检查是否完成重写

3.接口继承和实现继承

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

四、多态的原理

1.虚函数表指针

这里常考一道笔试题:sizeof(Base)是多少?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
	char c = 'A';
};
int main()
{
	cout << sizeof(Base) << endl;// 12
	return 0;
}

可能刚看到这个题目的时候,都会觉得答案是8个字节,但是我们在打印后却发现是12个字节;这是为什么呢?
因为有了虚函数,这个对象里面就多了一个成员,虚函数表指针__vfptr。简称虚表指针——virtual function pointer

在这里插入图片描述

2.虚函数表

虚函数表指针指向的这个表就是虚函数表,本质是一个函数指针数组,表中存储的是虚函数的地址

注:一个含有虚函数的类中,都至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中。
如下:含有多个虚函数
在这里插入图片描述

我们通过下面的程序来进行分析虚表到底是什么:

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:
	virtual void Func1() { cout << "Derive::Func1()" << endl; }
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的(如果没重写且没有新增就一样了),这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一nullptr。

总结一下派生类的虚表生成:

  • a.先将基类中的虚表内容拷贝一份到派生类虚表中;
  • b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;
  • c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

3.多态的原理

未实现多态时:

class Base
{
public:
	virtual void Func1() { cout << "Base::Func1()" << endl; }
 
private:
	int _b = 1;
};
 
class Derive : public Base
{
private:
	int _d = 2;
};
 
int main()
{
 
	Base b;
	Derive d;
 
	Base* p1 = &b;
	p1->Func1();
	p1 = &d;
	p1->Func1();
 
	return 0;
}

在这里插入图片描述

实现了多态:

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:
	virtual void Func1() 
	{ cout << "Derive::Func1()" << endl; }
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	Base* p1 = &b;
	p1->Func1();
	p1 = &d;
	p1->Func1();
	return 0;
}

在这里插入图片描述

注意:虚表指针实在构造函数的初始化列表由系统自动进行创建的。

多态原理总结

基类的指针或者引用,调用谁,就去谁的虚表中找到对应的位置的虚函数,就实现了对应的多态的功能
如上述所示:
传递的是父类对象,就直接去父类虚表中寻找该虚函数的地址
传递的是子类对象,先会完成切片操作,相当于现在是子类中父类那部分对象的别名,再去子类中继承自父类中去寻找该类中重写的虚函数的地址,进而完成调用,实现多态。

4.再次理解多态构成的条件

为什么必须要是父类的指针或引用来调用虚函数,为什么不能是对象调用?
传入的是对象:就是传入的值,是值拷贝
我们再来看看对象的引用r1和对象本身p,切片过后内存布局,如下图所示:
我们注意到,引用的r1其实就是Johnson中继承自分类那部分对象的别名,里面虚表是一样的
对象切片的时候,我们会把值(注意_a变量)给拷贝过去,不会把子类的虚表指针给赋值过去,如果赋值成功,父类的虚表指针会指向子类的虚表,那么多态就不可能实现了。
在这里插入图片描述

5.多态的易错知识点

1、同类型的对象,虚表指针是相同的,指向同一张虚表
如下所示,三个同类型的person对象都指向同一张虚表。
在这里插入图片描述

2、普通函数和虚函数的存储位置是否一样?
答案:一样的!都是在代码段
只不过虚函数要把地址存一份到虚表中,方便实现多态

注意:如果子类的虚函数是私有的,那么也是能够实现多态的。
从语法上来看,重写是一种接口继承,继承父类的接口,重写函数体内容,编译器检查不出来,因为是运行时,去p指向对象的虚表中,找到虚函数的地址,所以私有的限制不起作用。

6.虚函数,虚表,虚表指针存储的位置

  • 虚函数:和普通函数一样在代码段,只不过虚函数要把地址存一份到虚表中,方便实现多态

  • 虚表:在编译阶段生成,存放在代码段(常量区)

  • 虚表指针:存放在对象的前四个字节(32位平台下),有些平台可能会放到对象的最后面,这个跟平台有关

7.动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

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

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;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("vft[%d]:%p\n",i,table[i]);
	}
}
int main()
{
	Base b;
	PrintVTable((VF_PTR*)(*(int*)&b));

	Derive d;
	return 0;
}

思路:取出b对象的头4bytes,就是虚表的指针

  1. 先取b的地址,强制类型转换为int*的指针
  2. 在解引用取值,就取到了b对象头4bytes,这个值就是指向虚表的指针
  3. 再强转成VF_PTR*,因为虚表就是一个存放VF_PTR虚函数指针类型的数组
    在这里插入图片描述

2、 多继承中的虚函数表

// 多继承中的虚表
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;
};
typedef void(*VF_PTR)(); // 等价于 typedef void(*)() VF_PTR;函数指针比较特别
void PrintVTable(VF_PTR* table)// void PrintVTable(VF_PTR _table[])
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("vft[%d]:%p\n",i,table[i]);
	}
}
int main()
{
	Base1 b1;
	Base2 b2;
	Derive d;
	PrintVTable((VF_PTR*)(*(void**)&d));
	PrintVTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
	return 0;
}

在这里插入图片描述

六、继承和多态习题练习

1.概念考察

  1. 下面程序输出结果是什么? ( A )
    A:class A class B class C class D
    B:class D class B class C class A
    C:class D class C class B class A
    D:class A class C class B class D
#include<iostream>
using namespace std;
class A {
public:
	A(char* s) { cout << s << endl; }
	~A() {}
};
 
class B :virtual public A {
public:
	B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
 
class C :virtual public A 
{
public:
	C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
 
class D :public B, public C
{
public:
	D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
	{
		cout << s4 << endl;
	}
};
 
int main() 
{
	D* p = new D("class A", "class B", "class C", "class D");
	delete p;
	return 0;
}

解释:上述代码首先会构造A(s1),因为如果A(s1)最后再构造就会产生歧义了。然后的构造顺序就是继承的顺序,自己的D最后再构造


  1. 多继承中指针偏移问题?下面说法正确的是( C )
    A:p1 == p2 == p3 B:p1 < p2 < p3
    C:p1 == p3 != p2 D:p1 != p2 != p3
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;
	return 0;
}

图解:
在这里插入图片描述


  1. 以下程序输出结果是什么( B )
    A: A->0 B: B->1 C: A->1
    D: B->0 E: 编译出错 F: 以上都不正确
class A 
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
 
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
 
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

图解:在这里插入图片描述

2.问答题

1. 什么是多态?
a、通俗的讲,多态就是多种不同的对象做同一件事会有不同的状态。多态有两种静态多态和动态多态。
b、静态多态例如函数重载,在编译阶段编译器根据函数名的修饰规则就确定下来要执行的的函数。
c、动态多态是在运行时才确定下来的状态。是以继承为基础的,当子类继承了父类的虚函数,并且完成了重写就构成了多态,当我们使用父类对象指针或者引用去调用虚函数,根据调用对象的不同,,调用的虚函数就不同,就会产生不同的状态

  1. 什么是重载、重写(覆盖)、重定义(隐藏)?
    a、重载:两个函数在同一个作用域,且函数名相同,函数的参数个数,参数类型,类型的顺序不同
    b、重写(覆盖):两个函数在不同的作用域,一个在父类,一个在子类。两个函数的返回值,函数名,参数列表相同(协变除外)。且两个函数都是虚函数
    c、重定义(隐藏):在继承中,在子类中当出现和父类同名的变量和同名函数时候,子类对象会将父类的成员屏蔽,优先访问子类的成员,这就叫做隐藏。
    在这里插入图片描述
  1. 多态的实现原理?
    当子类继承了父类的虚函数并完成了重写就构成了多态。底层是有一个虚函数表指针,指向一张虚表(本质是函数指针数组),虚表中存 的是各自类中虚函数的地址(不是真正的地址,可以理解为间接地址)。当一个父类的指针或者引用来调用虚函数的时候,看调用的对象是谁,是父类就去父类的虚表中找对应的虚函数,调用的是子类就去子类的虚表中去找对用的虚函数的
  1. inline函数可以是虚函数吗?
    可以,不过编译器就忽略inline属性,因为内敛函数没有地址,忽略后这个函数就不再是inline,因为虚函数要放到虚表中去。
  1. 静态成员可以是虚函数吗?
    不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

    注意:静态成员是属于整个类的,不属于某个对象,调用方式也不是通过this指针,虽然可以使用对象调用,但是不是通过this指针。并且这种调用不常用。一般都是使用类型::成员函数的方式。
  1. 构造函数可以是虚函数吗?
    不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
    可以,并且最好把基类的析构函数定义成虚函数。当我们在子类中也有动态开辟的内存需要释放时,就需要将虚构函数写为虚函数。
  1. 对象访问普通函数快还是虚函数更快?
    首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
  1. 虚函数表是在什么阶段生成的,存在哪的?
    虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  1. C++菱形继承的问题?虚继承的原理?
    菱形继承的问题是子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
    采用虚继承即可解决。假设B和C继承A,D继承了B和C,在B和C继承处使用virtual修饰。在D对象中只会存储一份A类,这个A类的成员同时属于B和C。如果B和C需要访问A的成员,会到自己的虚基表中查找偏移量,通过自身地址+偏移量即可找到A类的成员。
  1. 什么是抽象类?抽象类的作用?
    包含纯虚函数的类就是抽象类,抽象类不能定义对象,且抽象类要求子类在继承后必须重写虚函数,否则子类对象也无法定义对象,体现了接口继承

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

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

相关文章

suse linux安装介质下载

在suse官网注册一个账号&#xff0c;就可以免费在上面下载软件的安装介质。 SUSE HAE介质下载和安装说明&#xff1a;

工作十年还不知道数字化转型工具?别等老板问你时才去查资料!

在职场中&#xff0c;到底有什么比较好上手又能轻易提升数字化的工具&#xff1f; 应粉丝邀请&#xff0c;我来给出一个回答。 对于企业来说&#xff0c;你可以选择大屏。对于个人来说&#xff0c;你可以选择仪表板。 工作汇报已经越来越卷&#xff0c;对于个人来说&#xff0c…

Windows远程桌面(mstsc)不能复制粘贴的解决办法

最近突然发现Windows远程桌面(mstsc)不能在远程端和本地端之间自由的复制和粘贴了&#xff0c;这还是非常影响使用体验的&#xff1b;因此记录一下解决方法&#xff0c;以便后续再遇到此类问题时查看如何解决&#xff1b; 文章目录 一、背景二、解决办法2.1 方法1 重启rdpclip.…

ChatGPT或致全球3亿人失业,人工智能时代下教育会发生什么样的变革?

不久前&#xff0c;谷歌教育发布了一份关于未来教育的研究报告。该报告由谷歌公司和 Canvas8合作&#xff0c;对来自世界24个国家的94位教育专家进行了历时长达2年的调研&#xff0c;探讨了未来教育形态、教育在未来的作用、教育公平、全球人才需求、教学方式、学习生态、工作技…

Attentive Moment Retrieval in Videos论文笔记

Attentive Moment Retrieval in Videos论文笔记 0.论文地址1.摘要2.引言3.模型结构3.1Memory Attention Network3.2Cross-Modal Fusion Network 4.训练4.1对齐损失4.2定位回归损失4.3合并 5.实验5.1数据集5.2效果5.3ACRN的研究 6未来工作 0.论文地址 2018 Attentive Moment Re…

验证码客户端回显测试-业务安全测试实操(15)

验证码客户端回显测试,验证码绕过测试,验证码自动识别测试 往期文章: 验证码暴力破解测试-业务安全测试实操(13)_luozhonghua2000的博客-CSDN博客 验证码客户端回显测试 测试原理和方法 当验证码在客户端生成而非服务器端生成时,就会造成此类问题。当客户端需要和服务器进行…

【正点原子STM32连载】第三十五章 IIC实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第三…

2023年NPDP产品经理认证线上班,到这里

产品经理国际资格认证NPDP是新产品开发方面的认证&#xff0c;集理论、方法与实践为一体的全方位的知识体系&#xff0c;为公司组织层级进行规划、决策、执行提供良好的方法体系支撑。 【认证机构】 产品开发与管理协会&#xff08;PDMA&#xff09;成立于1979年&#xff0c;是…

docker 镜像制作 与 CI/CD

目录 镜像到底是什么&#xff1f; 使用docker创建镜像 步骤&#xff1a; 1、编辑Dockerfile&#xff08;Dockerfile是docker制作镜像的配方文件&#xff09; 2、编辑requirements.txt文件 3、编辑app.py文件&#xff0c;我们的程序文件 4、生成镜像文件 5、查看生成的镜…

这些软件,你知道几个呢?

软件分享一&#xff1a;情绪指压 情绪指压(MoodPress)是一款记录心情的应用&#xff0c;也是一款非常简单的减压游戏。可以根据自己现在的心情来决定指压的力度和时间&#xff08;压力越大或者时间越长越生气&#xff09;&#xff0c;适时释放&#xff0c;来判断和记录自己的心…

Oracle19c安装和远程访问设置

Oracle Database&#xff0c;又名Oracle RDBMS&#xff0c;或简称Oracle。是甲骨文公司的一款关系数据库管理系统。它是在数据库领域一直处于领先地位的产品。可以说Oracle数据库系统是世界上流行的关系数据库管理系统&#xff0c;系统可移植性好、使用方便、功能强&#xff0c…

cron表达式 字符含义 详细解释

cron表达式的组成 cron表达式是一个字符串&#xff0c;由6到7个字段组成&#xff0c;用空格分隔。 其中前6个字段是必须的&#xff0c;最后一个年是可选填的。 cron表达式的字段含义 字段允许值通用字符秒0-59* , - /分0-59时0-23日期1-31 ? L C W月1-12 JAN-DEC星期1-7 SU…

单片机原理及接口技术 - 第三版 张毅刚 习题答案

第 1 章 思考题及习题 1 参考答案 一、填空 1.除了单片机这一名称之外&#xff0c;单片机还可称为 微控制器 或 嵌入式控制器 2.单片机与普通微型计算机的不同之处在于其将 CPU、存储器、I/O 口三部分,通过内部总线连接一起&#xff0c;集成芯片上。 3. AT89S52 单片机工作频率…

高并发缓存实战RedisSon、性能优化

高并发缓存实战RedisSon、性能优化 分布式锁性能提升 1.数据冷热分离 对于经常访问的数据保留在redis缓存当中&#xff0c;不用带数据设置超时时间定期删除控制redis的大小 String productStr redisUtil.get(productCacheKey);if (!StringUtils.isEmpty(productStr)) {prod…

docker搭建nginx

一、安装Docker 1、安装&#xff1a; yum install docker 2、启动/停止/重启docker服务 service docker start service docker stop service docker restart 3、查看docker版本信息 docker version 4、查看所有docker镜像 docker images 二、安装Nginx 1、拉取Nginx镜像…

关于POL网络中的ODN部署方案,这些你都了解吗?

近年来&#xff0c;行业的智能化和信息化呈现加速发展趋势&#xff0c;高清视频会议、云服务、移动办公等应用几乎成为企业标配。与此同时&#xff0c;带宽的接入、升级和物联网融合等网络新要求也变得越来越迫切&#xff0c;网络架构升级成为企业解决网络难题的一个新选择。 …

Python基础(10)——Python条件语句

Python基础&#xff08;10&#xff09;——Python条件语句 文章目录 Python基础&#xff08;10&#xff09;——Python条件语句目标一. 了解条件语句二. if 语法2.1 语法2.2 快速体验 三. 实例&#xff1a;上网3.1 简单版3.2 进阶版 四. if...else...4.1 语法4.2 实用版&#x…

KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW

​ KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW KSM01.2B-061C-35N-M1-HP2-SE-NN –D7-NN-FW 集散控制的基本思想是集中管理&#xff0c;分散控制。即&#xff1a;将流程工业的自动控制过程与操作管理人员对自动控制过程的管理过程相对分离&#xff1b;流程工业的自动控制过程…

Dump寄存器使用、解析

前人种树&#xff0c;后人乘凉&#xff1b;创造不易&#xff0c;请勿迁移~ author daisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主 daisy.skye的博客_CSDN博客-嵌入式,Qt,Linux领域博主daisy.skye擅长嵌入式,Qt,Linux,等方面的知识https://blog.csdn.net/qq_40715266?t…

git的安装与配置教程-超详细版

一、git的安装 1、下载git git官网地址&#xff1a;https://git-scm.com/download/win/ 选择所需要的版本&#xff0c;进行下载。 2、下载完成之后&#xff0c;双击下载好的exe文件进行安装。 3、默认是C盘&#xff0c;推荐修改一下路径&#xff08;非中文并且没有空格&…