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

【C++从0到王者】第二十四站:多态的底层原理

文章目录

  • 前言
  • 一、虚函数表
  • 二、一道经典的例题
  • 三、深度剖析多态的条件之一:为什么必须是父类的指针或引用
  • 四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?
  • 五、虚函数表的一些总结
  • 六、关于Func3的验证
  • 七、动态绑定与静态绑定
  • 八、总结

前言

在前面,我们也了解了多态的定义、概念、实现。对于多态的使用,有很多需要注意的细节,可谓到处都是坑!了解了多态的使用,那么现在我们来了解一下多态的原理吧。


一、虚函数表

我们先来猜猜下面程序的运行结果是多少?

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

我们可能会以为是1,实际上运行结果是8
在这里插入图片描述

那么为什么是8呢?

我们可以进入调试观察一下,我们会发现它里面似乎多了一个指针
在这里插入图片描述

这个指针是四字节的话,那么内存对齐一下,刚好是8个字节。

那么这个指针究竟是何方神圣呢?实际上这个指针是虚函数指针(v代表虚拟,f代表函数,ptr是指针)。从后面的vftable也可以看出来,它是一个虚函数表

从这里我们也可以知道,我们一般不使用多态的话,最好还是不要加上virtual,因为是有开销的。

这个虚表里面存储的就是虚函数的地址。而虚函数是存放在代码段的。

如果我们有两个虚函数的话,那么这个虚表里面就有两个虚函数的地址
在这里插入图片描述

以上是由于虚函数导致的对象中的一些变化,虚函数是应用于重写的。那么重写的时候会发生什么呢?

我们接下来使用如下代码

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

我们对其进行分析:下面是监视窗口里面的样子
在这里插入图片描述

我们不妨将他们用下图代替,即用下面的图更能清晰的表达他们的关系

在这里插入图片描述

根据上面的图中,我们可以注意到,所谓的重写,其实从原理层的角度来看,其实就是将虚表里面的地址给覆盖了。才导致调用不同的函数。所以重写是语法层的概念,覆盖是原理层的概念。

对于子类的虚表,我们也可以认为是将父类的虚表给拷贝下来了,然后在将重写的给覆盖上去。

这个时候我们就知道了如何实现的指向父类调父类,指向子类调子类了。

所以现在,我们知道了多态是如何实现的。如果是父类的对象,进行调用的时候那么自然就是调用它的虚表里面的函数,如果是将子类对象,使用指针或者引用进行切片的话,本质上还是指向子类,只不过是指向子类中的父类的那一部分罢了,而我们这里的虚表中的地址已经被替换了。所以当然可以实现调用不同的函数了。

其实如果是普通的调用的话,那么它在编译的时候地址已经被确定了。

如下就是普通调用的时候,在编译的时候地址早已被确定好了,所以它恒定的调用一个函数。这里也就解释了为什么必须是基类的指针和引用。如果是对象的切片的话,这里的虚表中的内容是不会被切片过去的,p调用函数的时候地址在编译时候早已被确定了。
在这里插入图片描述

如果符合多态的话,运行时到指向对象的虚函数表中找调用对象的地址。不是在编译时候就确定了地址了

我们也不管他是子类还是父类,即便是子类,经过切片后,也是一个父类。我们只需要找到对应的虚表中的地址就可以了。我们可以看到,同一个函数,多态调用的时候指令都变多了
在这里插入图片描述

二、一道经典的例题

我们来看下面这道题,猜猜它选什么呢?

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()
{B* p = new B;p->test();return 0;
}

这道题的运行结果为
在这里插入图片描述

看到这里,我们肯定已经蒙了,这是为什么呢?为什么是这么一个出乎意料的结果呢?

我们现在来分析一下代码

首先我们定义了一个派生类的指针,指向了B对象。然后我们现在想用这个派生类的指针去调用test函数,这里是可以去调用的,因为子类继承了父类

在test函数里面,只有一个功能就是调用func函数,注意,这里的func函数是由this指针来进行调用的,只不过是this指针隐藏了。

现在我们来思考一下,这里调用func是不是多态调用呢?

我们知道,多态调用有两大条件:父类的指针或引用去调用虚函数,这个虚函数必须是重写的虚函数。

那么这里的this指针是父类的指针吗?答案是这里的this指针确实是父类的指针,而不是派生类的指针!

为什么是A*父类的指针呢?因为这里的func函数是继承下来的。这里的继承并不是单纯的将test函数在派生类生成了一份,编译器不会那样做的。

继承的对象模型是这样做的,它的对象模型分为两部分,一部分是将父类的整体当成一个成员给拿下来,这里父类会自己内存对其等操作,然后另一部分就是自己的本来的成员,经过与父类对象进行内存对齐以后,整个进行建模。然后这些成员函数它都是在代码段的,它并不会生成多份的。编译的时候是检查语法的,先去派生类里面去找,找不到再去父类里面去找。 所以test不会有两份,所以这里只能是A*指针了。这样就满足了多态的第一个条件了。或者说这里发生一个切片,B*指针切片给了A*类型的指针。

第二个条件是虚函数的重写,那么这个func满足虚函数的重写吗?其实是满足的,首先有基类和派生类里面都有func函数,这两个函数满足虚函数加三同的条件,注意形参的类型相同指的是类型的相同,有没有缺省参数,缺省参数是多少跟他们没有任何关系,即便是形参名字不同也是无所谓的。

所以现在满足了多条的条件,已经是多态的调用了。我们知道多态的调用看的是指向的对象是哪里。而这里我们的A*的指针是由B*的指针切片得到的,所以这里实际指向的是一个派生类,那么自然就调用的是派生类的func了

此时我们以为得到了正确答案B->0,实则不然,我们又调入了一个大坑里面,我们要注意,多态改变的是函数的实现,虚函数加上三同只是可以告诉我们说这个构成了多态。换言之,多态在调用的时候,前面的部分,即返回值形参函数名这些看的是基类的部分,而实现的部分看的是多态的调用,即指向的对象的那一部分。而在这里,形参使用基类中的1,实现打印B。

所以最终结果为B->1

甚至于我们还可以将派生类中的缺省参数给去掉也是没有任何问题的
在这里插入图片描述

甚至于我们可以直接换名
在这里插入图片描述

但是我们不可以连形参名字都不写了,因为我们在里面毕竟用到了val了,如果连val名字都不写的话是不行的
在这里插入图片描述

所以说,虚函数的重写,重写的只是实现,那一个壳用的还是基类的。这里也印证了为什么派生类可以不加virtual。因为只是重写的实现。

如果我们将上面的题稍作修改,如下所示:那么结果又会如何变化呢?

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};
class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }virtual void test() { func(); }
};
int main()
{B* p = new B;p->test();return 0;
}

这里比较有意思的是,我们只是将test这个函数换在了B类里面,即派生类里面,这样的话我们p调用test的时候,this指针和p一样了都是派生类的指针类型,这已经不构成多态的条件了,所以是一个普通的调用,就直接看的是派生类中的这个函数了所以结果为B->0

在这里插入图片描述

三、深度剖析多态的条件之一:为什么必须是父类的指针或引用

我们在回过头来看一下多态的条件为什么是那两条:1.基类的指针或引用去调用虚函数2.被调用的函数必须是重写的虚函数

为什么必须是父类的指针或引用,子类的指针或引用为什么不可以呢?为什么不能是父类的对象呢?

先回答第一个问题:因为只有父类的指针才可以指向子类和父类,如果是子类的指针的话就只能指向子类了,不能指向多种形态了。这个问题还算比较容易理解

再来回答第二个问题:我们知道对象的切片和指针与引用的切片是有一些不同的,我们先要知道对象切片和指针切片的差异是什么。为了演示这个差异,我们使用如下代码

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; }
protected:int _b = 1;
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;return 0;
}

如下是监视窗口中的样子
在这里插入图片描述

我们使用如下方式进行展现,这样方便我们进行观察
在这里插入图片描述

此时我们还是比较容易理解的,这里两个对象,分别有他们自己的虚函数表。这里的虚函数表严格来说应该是一个指针,指向着虚函数表。

我们知道下面的三种方式都是切片,那么他们的差异究竟在哪里呢?

ps = st;
Person* ptr = &st;
Person& ref = st;

首先毋庸置疑的是,如果是指向父类的指针,那么它指向的是一个父类的对象,看到的自然就是父类的虚表了。

如果是指向子类的指针或引用的话,那么它指向的是一个子类中的父类的那一部分,看到的其实是子类中的虚表,这个虚表是经过虚函数重写覆盖过的。
在这里插入图片描述

所以说指针和引用的切片他们是不存在任何的拷贝的问题的。

而对象的切片就存在拷贝的问题了。

当我们使用对象的切片的时候,子类中的父类部分的成员变量肯定是都会被拷贝过去的,但是虚表会被拷贝过去吗?我们可以测试一下

为了方便我们观察,我们可以提前先修改一下派生类中_a的值
在这里插入图片描述

然后我们在使用对象的切片,如下图所示,是未切片的时候

在这里插入图片描述

如下所示,是切片发生之后
在这里插入图片描述

我们已经发现,对象的切片,并不会改变虚表,所以虚表是不会进行拷贝的

那么为什么不拷贝虚表呢?拷贝虚表会带来什么问题呢?

我们可以这样思考一下,假如我们将派生类中的虚表给拷贝过去了,那么我们使用ps这个父类对象给取出它的地址以后,使用这个指针去调用它里面的函数的话,就会反而调用了派生类中的函数。这个明显有点奇怪。因为指向父类的居然调用了子类的函数。这明显不符合多态的要求。毕竟多态是指向什么类就调用什么类的。这样做显然就会乱套的。

所以我们得到一个结论:子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中的究竟是父类虚函数还是子类虚函数就不知道了,因为我们并不知道究竟这个对象有没有被赋值切片过。总之,就乱套了

上面这个结论,也就回答了我们前面的问题,为什么多态的条件不能是父类的对象。

四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?

在前面我们也已经提到过,虚函数的重写/覆盖本质就是是什么?

在语法层面称之为重写,重写的是它的实现。所以有时候我们也会提出一个概念,普通的函数的继承称为实现继承,而多态,虚函数的重写,其实就是一个接口继承,然后重写它的实现

在原理上就是说将父类的虚函数表给拷贝下来,然后将子类中重写的部分给覆盖。

其次,因为只有完成了虚函数的重写,那派生类的虚表里面才能是派生类的虚函数。这样的话,这个基类指针才能做到指向父类调用父类,指向子类调用子类。

五、虚函数表的一些总结

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

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

  3. 另外Func1和Func2继承下来后是虚函数,所以放进了虚表,如果Func2不是虚函数,那么它也继承下来了,但是因为不是虚函数,所以不会放进虚表

  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr 。(注意不是所有的编译器都会给的,g++编译器就没有给,而且有时候vs的编译器有一些问题,不会给这个nullptr,这时候我们可以自己清理一下解决方案,然后重新编译一下就有了,这里算是一个编译器的bug)

    下面的就是给了nullptr的
    在这里插入图片描述

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

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

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

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

    注意c这个小点中,虽然它会将这个添加到派生类虚表的最后,但是我们的监视窗口有时候是看不见的,如下所示,我们并没有看到Func3的虚函数表中的地址。
    在这里插入图片描述

但是我们是可以从内存窗口看到有一个地址的,这个地址就是Func3的虚表中的地址。这里算是一个监视窗口的一个bug
在这里插入图片描述

所以说监视窗口和内存窗口有时候还是有一些不一样的。准确的来说,这里我们也不能断言说这里一定是Func3函数的地址。因为我们并没有给出证明,所以后面我们会给出一个证明。

  1. 还有一点是虚表是存储在哪里的呢?是栈区or堆区or数据段(静态区)or代码段(常量区)这四个中的哪一个呢?

首先我们就可以排除的是堆区,因为堆区还需要new,delete一下,编译器大概率是不会这样做的

然后我们还可以排除的是栈区,因为如果是存在栈区的话,那如果是两个栈帧的话,里面的虚表的地址肯定是不一样的,而我们经过下面的测试,发现地址是一样的,也就是说他们共用虚表,所以可以排除栈区。当然其实也不能百分之百排除掉栈区,因为万一存储在main函数的栈帧中呢?但是大概率还是不会存储在main中的。

在这里插入图片描述

同时上面的情形还说明了一件事,同类型的对象共用虚表

然后我们就可能会去猜测是静态区中存储着虚表,实际上不是的,虽然说网上的很多答案都是静态区,不过这个答案其实是错误的。

我们可以使用如下代码去验证:

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

我们先来解释一下这段代码,前面都很简单,最后两个打印的时候,由于对象里面是没有虚表的,但是有一个虚表指针,并且这个指针就是第一个成员变量,所以我们ps的地址就是虚表指针的地址,然后我们为了可以直接用这个虚表指针的地址去打印出来虚表所在的地址,于是我们就对其进行强制类型转换为int*,因为我们的指针是四字节的。然后我们直接解引用,就可以拿到这个虚表指针所指向的值了。由于这个虚表指针本身就是一个二级指针,里面存储的就是一个地址,这个地址所指向的就是虚函数所存储的地址了。

或许你已经被绕晕了,不要紧,我们来画个图来直观的感受一下:

在这里插入图片描述

而我们上面所进行的操作,正好取出来的就是绿色方块里面的值,也就是一个地址,这个地址就是虚函数表的地址。相信大家这会儿已经听懂了吧

而我们最终的运行结果是这样的
在这里插入图片描述

我们对比后发现,与常量区,即代码段的数值最为接近。所以虚表应该存储在常量区/代码段

那么虚函数存储在哪里呢?

如果直接打印地址的话,恐怕并不好打印,有点繁琐,我们不如直接在监视窗口里面观察

在这里插入图片描述

可以注意到,虚函数显然距离常量区更近一些。所以也是存储在常量区的

六、关于Func3的验证

我们在前面中提到了,监视窗口中的虚表少了一个func3的地址,但是当我们进入内存查看的时候,存在一个指针。那么这个指针究竟是不是func3我们还需要进行验证。

我们想要验证这个东西,我们得先将虚表里面的地址看能否给拿出来。只要能拿到虚表里面的函数地址,我们就可以去调用这些函数从而判断是不是该函数

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void Func1() {cout << "Person::Func1()" << endl;};virtual void Func2() {cout << "Person::Func2()" << endl;};int _a = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }virtual void Func3(){cout << "Student::Func3()" << endl;};
protected:int _b = 1;
};
typedef void (*Func_Ptr)();
void PrintVFT(Func_Ptr* table)
{for (int i = 0; table[i] != nullptr; i++){printf("[%d]:%p\n", i, table[i]);}cout << endl;
}
int main()
{Person ps;Student st;int vft1 = *(int*)&ps;PrintVFT((Func_Ptr*)vft1);int vft2 = *(int*)&st;PrintVFT((Func_Ptr*)vft2);return 0;
}

如上所示的代码就是可以打印出虚表,上面代码的原理是这样的,由于虚表是一个函数指针数组,每一个函数指针都是void(*)()类型的指针。所以我们直接使用typedef一下方便我们使用这种类型的指针,然后我们在想办法取出虚表的地址。这个取法在前文中已经提及了。然后我们就可以直接去打印这个虚表了。注意:我们这里使用的vs2022 ,x86环境的,我们的指针都是4字节的,其次vs在虚表结束的时候是会添加一个nullptr的,如果是Linux环境的话,首先默认是x64环境的,所以指针是八字节的,在取地址的时候就要小心了。我们不能用int类型了,可以使用long long类型的。其次Linux环境下最后是不会在虚表的结尾补一个nullptr的,所以就不能像我们上面那样使用了。必须得写死了才能打印出虚表。

如下就是我们此时打印出来的虚表

在这里插入图片描述

现在我们已经有了虚表中的每一个函数的地址了,那么有了函数的地址了,再去调用这个函数就非常之简单了,我们对前面的代码稍作修改,得到如下代码,可以去正常访问每一个函数

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void Func1() {cout << "Person::Func1()" << endl;};virtual void Func2() {cout << "Person::Func2()" << endl;};int _a = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }virtual void Func3(){cout << "Student::Func3()" << endl;};
protected:int _b = 1;
};typedef void (*Func_Ptr)();
void PrintVFT(Func_Ptr* table)
{for (int i = 0; table[i] != nullptr; i++){printf("[%d]:%p->", i, table[i]);table[i]();}cout << endl;
}int main()
{Person ps;Student st;int vft1 = *(int*)&ps;PrintVFT((Func_Ptr*)vft1);int vft2 = *(int*)&st;PrintVFT((Func_Ptr*)vft2);return 0;
}

对于上面的代码,我们简要的分析一下,我们的目的是为了打印虚表中的每一个函数,虚表本质是一个函数指针数组,注意它与虚基表是不一样的,虚基表是菱形虚拟继承中用来存储偏移量的。虚表是虚函数表,简称虚表,它本质是就是一个函数指针数组。我们可以给每个虚函数都加上打印。因为我们在前面已经取出来了每一个函数的地址,这里就有点类似于回调函数中的做法。我们有了函数地址,它就可以当作函数名直接调用这个函数,然后观察打印结果就可以验证。

在这里插入图片描述

注意在这里我们有时候可能会遇到程序崩溃的情况,这是因为vs的一个bug,本来在虚表后面是要补一个nullptr的,但是我们有时候生成完解决方案以后去修改了代码,可能就不会添加这个空指针了。从而导致程序调用野指针,程序崩溃。这时候我们只需要重新生成一下解决方案即可解决这个问题。

从上面的运行结果来看,是由Func3的,那么这里就已经验证了Func3的存在是在虚表中的,也就说明了那个指针确实是Func3。至于监视窗口没有显示Func3函数的地址,可能是由于编译器的bug

在这里插入图片描述

这里其实也说明了一件事,我们不要太过于相信监视窗口,只有内存窗口里面的才是最真实的

不过需要注意的是,上面的代码其实是被精心设计过的,它并不是正常的访问方式,首先我们的虚表中每一个函数我们的类型都设置成了一模一样的,否则的话在调用函数的时候必然因为指针的类型不同而出现问题。

其次我们的函数都是没有访问成员变量的,一旦函数里面存在访问成员变量的话,可能会出现很多问题。毕竟我们的是非正常访问,是没有this指针的。这里的非正常访问方式是无视类域的限制的,即便是私有的照样可以访问。因为他们都只是语法层面的限制,我们这里直接从内存中去找到对应的地址去调用的。

七、动态绑定与静态绑定

我们有时候又将多态分为静态的多态与动态的多态

所谓静态的多态,一般是指编译时的多态,也就是函数重载

比如下面的例子:

int main()
{int i = 1;double d = 1.1;cout << i << endl;cout << d << endl;return 0;
}

即不同的对象调用不同的函数,这些是在编译时候就确定好了的。通过函数名修饰规则等,来匹配不同的函数。我们也将之称为静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态 。它与普通的调用是一样的,在编译时就确定了地址

如下所示,现在所处的就是一个普通的调用。它在编译时就确定好了地址。
在这里插入图片描述

而下面这个则是多态的调用,编译器也不知道调用的到底是谁,反正就是通过一系列方法将这里面的函数给取出来去调用

在这里插入图片描述

这里也就是动态的多态,即运行时的多态,他是通过继承,虚函数重写实现的多态。


八、总结

本次主要讲解了多态的底层原理。深入浅出的讲解了虚函数表,深度剖析了多态的条件,以及虚表的很多细节。希望能对大家带来帮助

相关文章:

【C++从0到王者】第二十四站:多态的底层原理

文章目录 前言一、虚函数表二、一道经典的例题三、深度剖析多态的条件之一&#xff1a;为什么必须是父类的指针或引用四、深度剖析多态的条件之二&#xff1a;为什么是虚函数的重写/覆盖&#xff1f;五、虚函数表的一些总结六、关于Func3的验证七、动态绑定与静态绑定八、总结 …...

Java从入门到精通24==》数据库、SQL基本语句、DDL语句

Java从入门到精通24》数据库、SQL基本语句、DDL语句 2023.8.27 文章目录 <center>Java从入门到精通24》数据库、SQL基本语句、DDL语句一、什么是数据库二、数据库的优缺点1、使用数据库的优点&#xff1a;2、使用数据库的缺点&#xff1a; 三、MySQL基本语句四、DDL语句 …...

学习ts(十)装饰器

定义 装饰器是一种特殊类型的声明&#xff0c;它能够被附加到类声明&#xff0c;方法&#xff0c;访问符&#xff0c;属性或参数上&#xff0c;是一种在不改变原类和使用继承的情况下&#xff0c;动态的扩展对象功能。 装饰器使用expression形式&#xff0c;其中expression必须…...

如何在 Opera 中启用DNS over HTTPS

DNS over HTTPS&#xff08;基于HTTPS的DNS&#xff09;是一种更安全的浏览方式&#xff0c;但大多数 Web 浏览器默认情况下不启用它。了解如何在 Opera 浏览器中启用该功能。 您可能不知道这一点&#xff0c;但您的网络浏览器并不像您希望的那样私密或安全。您会看到&#xff…...

STM32 F103C8T6学习笔记13:IIC通信—AHT10温湿度传感器模块

今日学习一下这款AHT10 温湿度传感器模块&#xff0c;给我的OLED手环添加上测温湿度的功能。 文章提供源码、测试工程下载、测试效果图。 目录 AHT10温湿度传感器&#xff1a; 特性&#xff1a; 连接方式&#xff1a; 适用场所范围&#xff1a; 程序设计&#xff1a; 设…...

QT基础使用:组件和代码关联(信号和槽)

自动关联 ui文件在设计环境下&#xff0c;能看到的组件可以使用鼠标右键选择“转到槽”就是开始组件和动作关联。 在自动关联这个过程中软件自动动作的部分 需要对前面头文件进行保存&#xff0c;才能使得声明的函数能够使用。为了方便&#xff0c;自动关联时先对所有文件…...

TCP最大连接数问题总结

最大TCP连接数量限制有&#xff1a;可用端口号数量、文件描述符数量、线程、内存、CPU等。每个TCP连接都需要以下资源&#xff0c;如图所示&#xff1a; 1、可用端口号限制 Q&#xff1a;一台主机可以有多少端口号&#xff1f;端口号与TCP连接&#xff1f;是否能修改&#x…...

【Docker】云原生利用Docker确保环境安全、部署的安全性、安全问题的主要表现和新兴技术产生

前言 Docker 是一个开源的应用容器引擎&#xff0c;让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 云原生利用Docker确保环境安全、部署的…...

explain各个字段代表的意思

id&#xff1a;联表查询是每个表的读取顺序&#xff0c;数字越大越先被读取。相同就需要通过table字段判断select_type&#xff1a;查询类型或者是其他操作类型&#xff08;PRIMARY、UNION、UNION RESULT等&#xff09;table&#xff1a;正在访问哪个表partitions&#xff1a;匹…...

【已解决】Windows10 pip安装报错:UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0x98

环境&#xff1a;win10, Python3.9 在Pycharm安装YoloV5的依赖包时出现报错&#xff1a;UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x98 出现 ‘gbk’ codec can’t decode… 的报错一般是因为读取文件出现编码问题导致没法读取文件&#xff0c;因此可以在报错…...

goland 中的调试器 -- Evaluate

今天一个好朋友 找到我&#xff0c;问我关于goland中Evaluate 小计算器的使用方式&#xff0c;说实话&#xff0c;我在此之前也没用过这个东西&#xff0c;然后我就找一些相关文档&#xff0c;但是这类文档少的可怜&#xff0c;所以我就稍微研究一下&#xff0c;找找材料&#…...

你知道公司内部维基到底有哪些功能吗

维基指的是一种协作工作的平台&#xff0c;也就是开源的编辑系统。员工可以在企业维基里面进行存储、共享和协作之类的操作&#xff0c;将企业内部员工的知识共享聚集在一起。今天looklook将会详细讲讲公司内部维基具体到底有哪些功能&#xff0c;供大家参考。 公司内部维基的功…...

netdata监控服务器主机(包括Docker容器)

效果 Docker部署 创建挂载目录 mkdir -p /data/netdata/{netdatacache,netdatalib}docker运行 docker run -d --namenetdata \-p 19999:19999 \-v /data/netdata/netdatalib:/var/lib/netdata \-v /data/netdata/netdatacache:/var/cache/netdata \-v /etc/passwd:/host/etc…...

Mybatis学习|第一个Mybatis程序

1.创建一个数据库以及一个用户表&#xff0c;并插入三条数据用来测试 2.创建一个空的maven项目 在pom.xml中导入本次测试用到的三个依赖&#xff0c;mysql驱动、mybatis依赖、以及单元测试junit依赖 将这个 空的maven项目当成一个父项目&#xff0c;再创建一个空的maven子项目用…...

计算机网络MTU和MSS的区别

在计算机网络中&#xff0c;MTU代表最大传输单元&#xff08;Maximum Transmission Unit&#xff09;&#xff0c;而MSS代表最大分节大小&#xff08;Maximum Segment Size&#xff09;。 1.MTU&#xff08;最大传输单元&#xff09;&#xff1a; MTU是指在网络通信中&#x…...

redis学习笔记 - 进阶部分

文章目录 redis单线程如何处理并发的客户端&#xff0c;以及为何单线程快&#xff1f;redis的发展历程&#xff1a;redis单线程和多线程的体现&#xff1a;redis3.x单线程时代但性能很快的主要原因&#xff1a;redis4.x开始引入多线程&#xff1a;redis6/redis7引入多线程IO&am…...

SE5 - BM1684 人工智能边缘开发板入门指南 -- 模型转换、交叉编译、yolov5、目标追踪

介绍 我们属于SoC模式&#xff0c;即我们在x86主机上基于tpu-nntc和libsophon完成模型的编译量化与程序的交叉编译&#xff0c;部署时将编译好的程序拷贝至SoC平台&#xff08;1684开发板/SE微服务器/SM模组&#xff09;中执行。 注&#xff1a;以下都是在Ubuntu20.04系统上操…...

基于Java+SpringBoot+vue前后端分离英语知识应用网站设计实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…...

vue使用vue-router报错

报错1. app.js:172 Uncaught TypeError: vue_router__WEBPACK_IMPORTED_MODULE_0__.default is not a constructor at eval (index.js:4:1) at ./src/router/index.js (app.js:108:1) at webpack_require (app.js:169:33) at fn (app.js:442:21) at eval (main.js:7:71) at ./…...

编写Dockerfile制作Web应用系统nginx镜像,生成镜像nginx:v1.1,并推送其到私有仓库。

环境&#xff1a; CentOS 7 Linux 3.10.0-1160.el7.x86_64 具体要求如下&#xff1a; &#xff08;1&#xff09;基于centos基础镜像&#xff1b; &#xff08;2&#xff09;指定作者信息&#xff1b; &#xff08;3&#xff09;安装nginx服务&#xff0c;将提供的dest目录…...

RocketMQ延迟消息机制

两种延迟消息 RocketMQ中提供了两种延迟消息机制 指定固定的延迟级别 通过在Message中设定一个MessageDelayLevel参数&#xff0c;对应18个预设的延迟级别指定时间点的延迟级别 通过在Message中设定一个DeliverTimeMS指定一个Long类型表示的具体时间点。到了时间点后&#xf…...

Oracle查询表空间大小

1 查询数据库中所有的表空间以及表空间所占空间的大小 SELECTtablespace_name,sum( bytes ) / 1024 / 1024 FROMdba_data_files GROUP BYtablespace_name; 2 Oracle查询表空间大小及每个表所占空间的大小 SELECTtablespace_name,file_id,file_name,round( bytes / ( 1024 …...

QMC5883L的驱动

简介 本篇文章的代码已经上传到了github上面&#xff0c;开源代码 作为一个电子罗盘模块&#xff0c;我们可以通过I2C从中获取偏航角yaw&#xff0c;相对于六轴陀螺仪的yaw&#xff0c;qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...

Cloudflare 从 Nginx 到 Pingora:性能、效率与安全的全面升级

在互联网的快速发展中&#xff0c;高性能、高效率和高安全性的网络服务成为了各大互联网基础设施提供商的核心追求。Cloudflare 作为全球领先的互联网安全和基础设施公司&#xff0c;近期做出了一个重大技术决策&#xff1a;弃用长期使用的 Nginx&#xff0c;转而采用其内部开发…...

LLM基础1_语言模型如何处理文本

基于GitHub项目&#xff1a;https://github.com/datawhalechina/llms-from-scratch-cn 工具介绍 tiktoken&#xff1a;OpenAI开发的专业"分词器" torch&#xff1a;Facebook开发的强力计算引擎&#xff0c;相当于超级计算器 理解词嵌入&#xff1a;给词语画"…...

大模型多显卡多服务器并行计算方法与实践指南

一、分布式训练概述 大规模语言模型的训练通常需要分布式计算技术,以解决单机资源不足的问题。分布式训练主要分为两种模式: 数据并行:将数据分片到不同设备,每个设备拥有完整的模型副本 模型并行:将模型分割到不同设备,每个设备处理部分模型计算 现代大模型训练通常结合…...

20个超级好用的 CSS 动画库

分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码&#xff0c;而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库&#xff0c;可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画&#xff0c;可以包含在你的网页或应用项目中。 3.An…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化

缓存架构 代码结构 代码详情 功能点&#xff1a; 多级缓存&#xff0c;先查本地缓存&#xff0c;再查Redis&#xff0c;最后才查数据库热点数据重建逻辑使用分布式锁&#xff0c;二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...