当前位置: 首页 > news >正文

【C++】多态的语法与底层原理

1.多态的概念

1.1 概念

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

举个例子:在现实当中,我们去火车站买票,一般都分三种情况:当普通人买票时,是全价买票;学生买票时,可以销售半折优惠;军人买票时,可以享受优先购票

2.多态的定义与实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要构成多态的条件有两个:

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

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

被virtual修饰的成员函数称为:虚函数(只要成员函数才能变成虚函数)

class person
{
public:virtual void buyticket(){cout << "全价" << endl;}
};

2.2虚函数的重写

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

class person
{
public:virtual void buyticket(){cout << "全价" << endl;}
};class student : public person
{
public:virtual void buyticket(){cout << "半折" << endl;}
};void buyticket(person* p)
{p->buyticket();
}void buyticket(person& p)
{p->buyticket();
}int main()
{student s;person p;buyticket(s); //半折buyticket(P); //全价buyticket(&s); //半折buyticket(&p); //全价return 0;
}

以上派生类中就完成了重写(覆盖)了

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

虽然说虚函数的重写需要符合三同(返回类型,函数名,参数列表(主要是类型))

但是存在两个例外

1.协变(基函数与派生类虚函数放会值类型不同)

C++的语法允许派生类和基类中虚函数返回值类型不同,但是要求返回值必须是基类和派生类关系的指针或者引用。满足前面要求就是协变

class person
{
public:virtual person* buyticket(){cout << "全价" << endl;return 0;}
};class student : public person
{
public:virtual student* buyticket(){cout << "半折" << endl;return 0;}
};

注意:只要是父子关系的指针或者引用都可以,父是父,子是子指针或者引用要匹配

class A{};
class B : public A{};
class person
{
public:virtual A* buyticket(){cout << "全价" << endl;return 0;}
};class student : public person
{
public:virtual B* buyticket(){cout << "半折" << endl;return 0;}
};

🚀析构函数可以是虚函数吗?为什么是虚函数?

答:

析构函数加virtual,是不是虚函数重写?

是,因为在类中析构函数被特殊处理成了destructor这个统一的名字。

为什么要这么处理?

因为要让它们构成重写。

为什么要让他们构成重写?

因为以下的场景:

class person
{
public:~person(){cout << "~person" << endl;}
};class student : public person
{
public:~student(){cout << "~student" << endl;}
};int main()
{person* p = new person;delete p;p = new student;delete p;
}

运行之后我们来看看它调用析构情况

可以看到程序运行结束之后,只调用了两次析构函数,这里可以发现在派生类中,少调用了一次析构函数,析构派生类部分(派生类的析构需要调用基类和它自己的析构函数,来进行空间的释放),原因就是在类中,析构函数名被编译器特殊处理成了destructor,导致基类中的析构函数将派生类中的析构函数隐藏/重定义了,编译器找不到派生类中的析构函数,所以就会造成以上的结果

在delete释放派生类空间的时候,需要进行两个操作( 1.调用析构函数destructor进行资源的释放 2. 调用operator delete() 释放整个空间 ),我们这里所期望的是一个多态调用,而不是一个普通调用

解决方法:使用虚函数,进行多态调用

class person
{
public:virtual ~person(){cout << "~person" << endl;}
};class student : public person
{
public:virtual ~student(){cout << "~student" << endl;}
};int main()
{person* p = new person;delete p;p = new student;delete p;
}

2.3 C++11 override 和 final

final

对函数使用:不允许该函数进行重写操作

class person
{
public:virtual void buyticket() final{cout << "全价" << endl;}
};class student : public person
{
public:virtual void buyticket(){cout << "半折" << endl;}
};

对类使用:不允许该类被继承

class person final
{
public:virtual void buyticket(){cout << "全价" << endl;}
};class student : public person
{
public:virtual void buyticket(){cout << "半折" << endl;}
};

override

帮助派生类检查是否完成重写,如果没有就会报错

class person 
{
public:virtual void buyticket(){cout << "全价" << endl;}
};class student : public person
{
public:void buyticket(int) override{cout << "半折" << endl;}
};

设计题:设计一个类不想被继承应该怎么设计,不能使用final

方法一:将基类构造函数私有(C++98)

class A
{
private:A(){}
};class B : A
{
public:int _a;
};int main()
{B a;return 0;
}

基类中构造函数被封装之后,派生类就无法调用构造函数创建对象

如果要进行访问,可以使用静态成员函数

class A
{
public:static A createObj(){return A();}int _a = 1;
private:A(){}
};class B : A
{
public:int _a;
};int main()
{A p = A::createObj();p._a;return 0;
}

方法二:使用final,不允许基类被继承(C++11)

class A
{
private:A(){}
};class B : A
{
public:int _a;
};int main()
{B a;return 0;
}

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

3. 抽象类

3.1 概念

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

class car
{
public:virtual void func1() = 0; //纯虚函数
};int main()
{car a;return 0;
}

使用纯虚函数实例化对象,编译器会直接报错

继承car的派生类对象,也不能实例化对象

class car
{
public:virtual void func1() = 0; //纯虚函数
};class BMW : public car
{
public:};int main()
{car a;BMW b;return 0;
}

抽象类的使用:

class car
{
public:virtual void func1() = 0; //纯虚函数
};class BMW : public car
{
public:virtual void func1(){cout << "启动!" << endl;}
};class Benz : public car
{
public:virtual void func1(){cout << "刹车" << endl;}
};class BYD : public car
{
public:virtual void func1(){cout << "减速" << endl;}
};void func2(car* c)
{c->func1();
}int main()
{func2(new BMW);func2(new Benz);func2(new BYD);return 0;
}

抽象类也可以实现多态调用,抽象类就是用来间接强制要求你重写虚函数的

3.2 接口继承和实现继承

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

4.多态的原理

4.1单继承的虚函数表

大家看下面这一道题目:

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

我们知道算类的大小不算成员函数,我们只需要考虑成员变量,只有一个成员变量,但是最终的运行结果是8,我们打开监视窗口看一下base中的成员变量

可以看到在类中多出了_vfptr(v代表virtual,f代表function),它是一个指针,这里就可以知道为什么sizeof(b1)算出来的值是8

_vfptr在vs下是放在第一个位置,存放位置与编译器有关,别的编译器可能会放到最后一位

_vfptr是一个虚表指针,指向虚函数表,虚函数表是用来存放虚函数指针的

我们来分析虚表里面存放的什么?

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

观察b和d两个对象

可以看到在derive中重写了func1的实现之后d对象中虚函数表也将Base::Func1覆盖成了Derive::Func1,所以重写也叫覆盖

通过虚指针指向的地址我们可以找到虚函数表在内存中的储存情况,从图中我们可以清楚的看到虚函数表中的最后一个位置被处理为了nullptr,这是编译器个性处理

此时我在Derive中新加了一个虚函数func4(),看这个函数是否会放到虚函数表中

class Derive : public Base
{
public:virtual void Func1(){}virtual void func4(){}
private:int _d = 2;
};

在监视窗口中看不到函数func4的身影,我们看看内存视图

内存视图中确实三个函数的地址,所以我猜测这最后一个地址就是func4函数的地址

那如何验证这个猜测呢?

我们这里做了一个小设计,通过使用函数指针指向虚函数表,来访问虚函数

//重定义函数指针为FUNC_TEST
typedef void(*FUNC_TEST) ();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(){}virtual void func4(){}
private:int _d = 2;
};void _vfptr_Print(FUNC_TEST* _vfptr)
{for (int i = 0; _vfptr[i] != nullptr; ++i){cout << _vfptr[i] << ' ';}cout << endl;
}int main()
{Base b;Derive d;//使用int*访问_vfptr,然后通过解引用放问虚函数表第一个元素FUNC_TEST* ptr = (FUNC_TEST*)(*(int*)&b);_vfptr_Print(ptr);ptr = (FUNC_TEST*)(*(int*)&d);_vfptr_Print(ptr);return 0;
}

通过将地址打印出来,来验证func4的存在,可以看到func4确实是存在于虚函数表中的

那么虚表存在哪呢?

栈, 堆, 动态区, 数据段, 代码段

同理,我们可以通过代码实践得出结果

int a = 1;
printf("栈区:%p\n", &a);int* b = new int;
printf("堆区:%p\n", b);static int c = 1;
printf("静态区:%p\n", &c);const char* str = "nxbw";
printf("代码段:%p\n", str);Base q;
printf("虚表1:%p\n", *(int*)&q);

可以看到虚表距离常量区几十个字节,离常量区非常近,其他区域相对常量区而言距离虚表很远的距离,依此可以判断虚函数表可能是在常量区

多态调用的条件:

1.基类的指针或者引用

为什么不能是派生类的指针或者引用?

答:因为使用派生类的指针或者引用只能接收派生类的指针或引用,它不能多种形态

为什么不能是基类对象?

person p = s;      //切割赋值
person* ptr = &s;  //引用
person& ref = s;   //指针

有这样一个问题,如果我们使用对象,那就是将派生类切割拷贝赋值给基类,这时如果想访问派生类的虚函数的话,就需要将虚函数表也拷贝过去,通过虚函数表来访问这个函数,但是这样就会引发一个问题,如果下次使用指针或者引用调用的虚函数时候,就不知道调用的是父类还是子类的虚函数了,就乱套了,而且在vs下,切割赋值时,它不会拷贝虚函数表去基类中

2.在派生类中重写虚函数

答:在派生类中重写虚函数是必要的,在多态调用中我们需要传递不同的对象进行函数调用,我们需要重写虚函数,改变虚函数中指针的指向,以以此找到访问相对应的虚函数

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

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚 表指针也就是存在部分的另一部分是自己的成员。

2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。

4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

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

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

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

6. 虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的,

4.2动态绑定与静态绑定

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

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

5.多继承关系的虚函数表

需要注意的是在多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的

5.1 多继承中的虚函数表

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()
{cout << sizeof(Derive) << endl;Derive d;return 0;
}

可以看到在Derive的模型中它有两个虚表指针,在Derive中我添加了一个虚函数func4函数,大伙可以猜猜这个函数被放到了那个虚函数表中(1.Base1的函数表 2.Base2的虚函数表 3.放在Base1和Base2的函数表中)

开始测试:

//重定义函数指针为FUNC_TEST
typedef void(*FUNC_TEST) ();void _VFPTR_PRINT(FUNC_TEST* _vfptr)
{for (int i = 0; _vfptr[i] != nullptr; ++i){cout << i << " : "<<_vfptr[i] << ' ';}cout << endl;
}int main()
{Derive d;FUNC_TEST _vfptr1 = (FUNC_TEST)*(int*)&d;Base2* b2 = &d;FUNC_TEST _vfptr2 = (FUNC_TEST) * (int*)b2;_VFPTR_PRINT((FUNC_TEST*)_vfptr1);_VFPTR_PRINT((FUNC_TEST*)_vfptr2);return 0;
}

可以看到在Derive中加入的虚函数func4放在Base1的虚函数表中

typedef void(*FUNC_TEST) ();void _VFPTR_PRINT(FUNC_TEST* _vfptr)
{for (int i = 0; _vfptr[i] != nullptr; ++i){FUNC_TEST f = _vfptr[i];cout << _vfptr[i] << ' ';f();}cout << endl;
}int main()
{Derive d;FUNC_TEST _vfptr1 = (FUNC_TEST)*(int*)&d;Base2* b2 = &d;FUNC_TEST _vfptr2 = (FUNC_TEST) *(int*)b2;_VFPTR_PRINT((FUNC_TEST*)_vfptr1);_VFPTR_PRINT((FUNC_TEST*)_vfptr2);return 0;
}

在Derive中重写func1后,为什么Derive和Base1中调用的func1地址会不一样?

转到汇编,看看底层是怎么操作的

转到底层汇编之后发现ptr1调用func1时会直接跳到func1的地址处调用func1,而ptr经过不断的跳转之后也会去调用func1,在跳转期间我们可以看到最关键的一步操作就是:ecx减8(8就是Base1类的大小),ecx是this指针(ptr2)

因为ptr2指向的是Base2,这个指针并不能调用func1,所以编译器将this指针减8,修正ptr2所指向的位置,让它指向的Derive的开头,修正之后,它就可以调用func1函数了

为什么ptr1不需要修正指向?

在内存中,编译器不会去看数据是什么类型,类型只是代码层的叫法,它只会看你的地址指向,所以ptr1不会去做任何修改

int main()
{Derive d;Base1* ptr1 = &d;ptr1->func1();Base2* ptr2 = &d;ptr2->func1();Derive* ptr3 = &d;ptr3->func1();return 0;
}

ptr1和ptr2使用的是多态调用,它们满足重写和父子关系指针或引用

ptr3是普通调用,因为ptr3是一个基类的指针,它并不满足多态调用条件

5.2. 菱形继承、菱形虚拟继承

菱形继承:

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}int _a = 1;
};class B : public A
{
public:int _b = 1;
};class C : public A
{
public:int _c = 1;
};class D : public B, public C
{
public:int _d = 1;
};

在菱形继承中我们在A类中写下一个虚函数,C类,B类,D类中没有重写,并且都没有函数,观察D的对象模型是怎么样

由上图可以看到B和C类都继承了A类的虚表,并且带到了D类对象模型中,所以D类带有两个虚表

菱形虚拟继承:(在B和C类中重写A的虚函数)

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}int _b = 1;
};class C : virtual public A
{
public:int _c = 1;
};class D : public B, public C
{
public:int _d = 1;
};

报错了,A类中func1继承不明确,因为B和C都继承了A,可以理解为A现在是B和C的共享,又因为B和C中都重写了func1,编译器不知道是谁重写了func1,这样就会导致继承不明确而报错

解决方法:在D类中重写func1

class A
{
public:virtual void func1(){cout << "A::func1()" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1()" << endl;}int _b = 1;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1()" << endl;}int _c = 1;
};class D : public B, public C
{
public:int _d = 1;
};

我们来看看它的对象模型是怎么样的

使用了菱形虚拟继承并且重写函数之后,可以看到编译器将A类的虚表和成员变量单独放到了D类对象模型的最后,并且给B和C类中加入了虚基表,在D类中重写的func1放到了A类的虚表中

在B和C中重写func1有意义吗?

有,在你单独使用B类或C类中的func1的时候,我们就需要在它们中进行重写。

将它们看作有符号ffffffff是-1,fcffffff是-4

由上图可知B和C类中的虚函数并没有放进A类虚表中,而是在B和C类中新增了一个虚表储存它们的虚函数

最后的最后,在实际中,尽量不要使用菱形虚拟继承,太麻烦了,可能会把自己给绕进去

下面有一道很经典的面试题:

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

在main函数中,我们new了一个B的对象,然后使用该对象去调用基类的虚函数,使用派生类对象调用A中的成员函数时,派生类对象需要通过切割基类部分来调用test函数,这时候test当中它隐藏的this指针的类型为:A*(基类的指针),我们上面所说多态调用成立的条件:1.重写虚函数 2.子和父的指针或者引用,前面我们分析,this指针是基类指针,在这段代码中也可以看到func函数被重写,派生类中重写的虚函数符合三同,大家注意:函数表中只要参数类型一样就符合参数列表相同(上面提到过基类中的虚函数写过virtual后,派生类重写基类的虚函数可以不用带virtual),经过上一轮的分析,我们可以知道this->func()是一个多态调用,这个题目使用的是派生类的对象进行多态调用,所以这里调用的是派生类的虚函数,这道题的输出结果为:B->1

为什么会是这个结果呢?

因为我们在派生类中重写的是基类虚函数的实现而不是重写它的函数头(返回值,函数名,参数列表)所以这里虚函数的的调用其实是: 基类的函数头 + 派生类重写的实现 

所以这题的输出结果为:B->1

相关文章:

【C++】多态的语法与底层原理

1.多态的概念 1.1 概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会 产生出不同的状态。 举个例子&#xff1a;在现实当中&#xff0c;我们去火车站买票&#xff0c;一般都分三种情况&…...

RTP和RTCP的详细介绍及其C代码示例

RTP和RTCP的详细介绍及其C代码示例 RTP和RTCP简介RTP协议详解RTCP协议详解RTP和RTCP之间的关系C代码示例RTP和RTCP简介 RTP(Real-time Transport Protocol,实时传输协议)和RTCP(Real-time Transport Control Protocol,实时传输控制协议)是流媒体传输中常用的两个协议。R…...

深入浅出了解AI教育发展与落地应用情况

2023年,是生成式AI能力涌现的一年,通用大模型是其中的主旋律。经过一年的发展,通用大模型格局已初步形成,生成式AI也从能力展示走向应用落地。进入2024年,对生成式AI的讨论和实践也都转向如何赋能产业。相比于通用大模型,进入产业内的大模型需要的是对行业的Know-How,以…...

Hive数据库操作语法

数据类型 内部表和外部表 内部表 &#xff08;CREATE TABLE table_name ......&#xff09;未被external关键字修饰的即是内部表&#xff0c; 即普通表。 内部表又称管理表,内部表数据存储的位置由hive.metastore.warehouse.dir参数决定&#xff08;默认&#xff1a;/user/h…...

容器架构-Docker的成长之路

目录 1. 什么是容器 2. 容器 vs 虚拟机 3. Docker极速上手指南 环境准备 3.1 配置docker源 3.2 下载镜像加速的配置 3.3 下载自动补全工具 4. Docker C/S架构 5. Docker的镜像管理 5.1 下载nginx:alpine镜像并查看 5.2 sl大法 5.3 删除镜像 5.4 镜像清理用的命令 5…...

关于我、重生到500年前凭借C语言改变世界科技vlog.14——常见C语言算法

文章目录 1.冒泡排序2.二分查找3.转移表希望读者们多多三连支持小编会继续更新你们的鼓励就是我前进的动力&#xff01; 根据当前所学C语言知识&#xff0c;对前面知识进行及时的总结巩固&#xff0c;出了这么一篇 vlog 介绍当前所学知识能遇到的常见算法&#xff0c;这些算法是…...

简记Vue3(三)—— ref、props、生命周期、hooks

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…...

ARM cpu算力KDMIPS测试

一、引言 KDMIPS(KiloDhrystone Million Instructions Per Second)是一种衡量处理器性能的指标,它表示处理器每秒钟可以执行多少百万条Dhrystone指令。 二、测试说明 1、将cpu模式调整为perfermance 2、将cpu的频率和gpu的频率调大最大 3、将ddr和各core的电压和频率调大最…...

自杀一句话木马(访问后自动删除)

在做安全测试时&#xff0c;例如文件上传时就要上传可以解析的脚本文件解析证明存在漏洞&#xff0c;这个时候就需要(访问后自动删除文件的一句话木马) PHP <?php echo md5(1);unlink(__FILE__); ?> 访问后自动删除...

Nginx 反向代理(解决跨域)

文章目录 前言一、同源策略二、跨域是什么&#xff1f;三、Nginx解决跨域1.前端示例代码2.说明 四、nginx反向代理配置五、启动nginx六、最终效果总结 前言 Nginx反向代理解决跨域 一、同源策略 定义&#xff1a;同源策略&#xff08;Same-Origin Policy&#xff09;是指浏览…...

gRPC-4种通信模式

4种通信模式 1、简单模式&#xff08;Simple RPC&#xff09; 简单模式&#xff1a;也称简单 RPC&#xff0c;即客户端发起一次请求&#xff0c;服务端响应处理后返回一个结果给客户端。 在 proto 文件中可如下定义&#xff1a; rpc SayHello(HelloRequest) returns (Hello…...

第五项修炼—系统思考

感谢合作伙伴的推荐&#xff0c;圆满结束为期两天的马上消费《第五项修炼—系统思考》项目&#xff01;这不仅是一次培训&#xff0c;更是未来实践的起点。 两天的系统思考学习让我们看到&#xff0c;在技术管理的每个决策背后&#xff0c;都蕴含着深刻的系统关联。希望各位技…...

PYNQ 框架 - VDMA驱动 - 帧缓存

目录 1. 简介 2. 代码分析 2.1 _FrameCache 类定义 2.1.1 xlnk.cma_array() 2.1.2 pointerNone 2.1.3 PynqBuffer 2.2 _FrameCache 例化与调用 2.3 _FrameCache 测试 2.4 _FrameList 类定义 2.5 _FrameList 例化与调用 2.6 _FrameList 测试 3. 帧的使用 3.1 读取帧…...

Java导出Word文档的几种方法

文章目录 1. 使用 Apache POI2. 使用 Docx4j3. 使用 JODConverter4. 使用 FreeMarker 模板 在 Java 中导出 Word 文档可以通过多种库和方法实现。以下是几种常用的方法&#xff1a; 1. 使用 Apache POI Apache POI 是一个强大的库&#xff0c;可以用来读写 Microsoft Office 格…...

OceanBase V4.3.3,首个面向实时分析场景的GA版本发布

在10月23日举办的 OceanBase年度发布会 上&#xff0c;我们怀着激动之情&#xff0c;正式向大家宣布了 OceanBase 4.3.3 GA 版的正式发布&#xff0c;这也是OceanBase 为实时分析&#xff08;AP&#xff09;场景打造的首个GA版本。 2024 年初&#xff0c;我们推出了 4.3.0 版本…...

Maven随笔

文章目录 1、什么是MAVEN2、Maven模型3、Maven仓库4、项目集成1_Idea集成Maven设置2_创建Maven项目3_POM配置详解4_maven 坐标详情5_Maven工程类型6_导入Maven项目 5、依赖管理1_依赖配置2_依赖传递3_可选依赖4_排除依赖4_可选依赖和排除依赖的区别5_依赖范围6_继承与聚合7_版本…...

牛客题目解析

一.最长回文子串 1.题目&#xff1a;给定一个仅包含小写字母的字符串&#xff0c;求它的最长回文子串的长度。 最长回文子串__牛客网 2.算法原理&#xff1a; <1>动态规划算法:O(n^2),O(n^2) 具有通性&#xff0c;凡涉及回文子串的问题都可利用此法解决 知识储备&am…...

AG32的3个ADC可以并联使用吗

AG32的3个ADC可以并联使用吗&#xff1f; Customer: 需求&#xff1a; 在t1时间段&#xff0c;用5M的速度ch1通道采样得到结果1. 在t2时间段&#xff0c;用5M的速度ch2通道采样得到结果2. 在t3时间段&#xff0c;用5M的速度ch3通道采样得到结果3. 然后如此循环 。 考虑用3…...

什么是 OpenTelemetry?

OpenTelemetry 定义 OpenTelemetry (OTel) 是一个开源可观测性框架&#xff0c;允许开发团队以单一、统一的格式生成、处理和传输遥测数据&#xff08;telemetry data&#xff09;。它由云原生计算基金会 (CNCF) 开发&#xff0c;旨在提供标准化协议和工具&#xff0c;用于收集…...

[vulnhub]DC:7

https://www.vulnhub.com/entry/dc-7,356/ 端口扫描主机发现 探测存活主机&#xff0c;178是靶机 nmap -sP 192.168.75.0/24 Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-11-03 13:30 CST Nmap scan report for 192.168.75.1 Host is up (0.00037s l…...

Element Plus 表单(el-form)中关于正整数输入的校验规则

目录 1 单个正整数输入1.1 模板1.2 校验规则 2 两个正整数输入&#xff08;联动&#xff09;2.1 模板2.2 校验规则2.3 CSS 1 单个正整数输入 1.1 模板 <el-formref"formRef":model"formData":rules"formRules"label-width"150px"…...

服务器--宝塔命令

一、宝塔面板安装命令 ⚠️ 必须使用 root 用户 或 sudo 权限执行&#xff01; sudo su - 1. CentOS 系统&#xff1a; yum install -y wget && wget -O install.sh http://download.bt.cn/install/install_6.0.sh && sh install.sh2. Ubuntu / Debian 系统…...

GO协程(Goroutine)问题总结

在使用Go语言来编写代码时&#xff0c;遇到的一些问题总结一下 [参考文档]&#xff1a;https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现&#xff1a; 今天在看到这个教程的时候&#xff0c;在自己的电…...

逻辑回归暴力训练预测金融欺诈

简述 「使用逻辑回归暴力预测金融欺诈&#xff0c;并不断增加特征维度持续测试」的做法&#xff0c;体现了一种逐步建模与迭代验证的实验思路&#xff0c;在金融欺诈检测中非常有价值&#xff0c;本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...

【Linux】Linux安装并配置RabbitMQ

目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的&#xff0c;需要先安…...

ThreadLocal 源码

ThreadLocal 源码 此类提供线程局部变量。这些变量不同于它们的普通对应物&#xff0c;因为每个访问一个线程局部变量的线程&#xff08;通过其 get 或 set 方法&#xff09;都有自己独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段&#xff0c;这些类希望将…...

算法刷题-回溯

今天给大家分享的还是一道关于dfs回溯的问题&#xff0c;对于这类问题大家还是要多刷和总结&#xff0c;总体难度还是偏大。 对于回溯问题有几个关键点&#xff1a; 1.首先对于这类回溯可以节点可以随机选择的问题&#xff0c;要做mian函数中循环调用dfs&#xff08;i&#x…...

使用ch340继电器完成随机断电测试

前言 如图所示是市面上常见的OTA压测继电器&#xff0c;通过ch340串口模块完成对继电器的分路控制&#xff0c;这里我编写了一个脚本方便对4路继电器的控制&#xff0c;可以设置开启时间&#xff0c;关闭时间&#xff0c;复位等功能 软件界面 在设备管理器查看串口号后&…...

【SSM】SpringMVC学习笔记7:前后端数据传输协议和异常处理

这篇学习笔记是Spring系列笔记的第7篇&#xff0c;该笔记是笔者在学习黑马程序员SSM框架教程课程期间的笔记&#xff0c;供自己和他人参考。 Spring学习笔记目录 笔记1&#xff1a;【SSM】Spring基础&#xff1a; IoC配置学习笔记-CSDN博客 对应黑马课程P1~P20的内容。 笔记2…...

DOM(文档对象模型)深度解析

DOM(文档对象模型)深度解析 DOM 是 HTML/XML 文档的树形结构表示,提供了一套让 JavaScript 动态操作网页内容、结构和样式的接口。 一、DOM 核心概念 1. 节点(Node)类型 类型值说明示例ELEMENT_NODE1元素节点<div>, <p>TEXT_NODE3文本节点元素内的文字COMMEN…...