【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这个父类对象给取出它的地址以后,使用这个指针去调用它里面的函数的话,就会反而调用了派生类中的函数。这个明显有点奇怪。因为指向父类的居然调用了子类的函数。这明显不符合多态的要求。毕竟多态是指向什么类就调用什么类的。这样做显然就会乱套的。
所以我们得到一个结论:子类赋值给父类对象切片,不会拷贝虚表。如果拷贝虚表,那么父类对象虚表中的究竟是父类虚函数还是子类虚函数就不知道了,因为我们并不知道究竟这个对象有没有被赋值切片过。总之,就乱套了
上面这个结论,也就回答了我们前面的问题,为什么多态的条件不能是父类的对象。
四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?
在前面我们也已经提到过,虚函数的重写/覆盖本质就是是什么?
在语法层面称之为重写,重写的是它的实现。所以有时候我们也会提出一个概念,普通的函数的继承称为实现继承,而多态,虚函数的重写,其实就是一个接口继承,然后重写它的实现
在原理上就是说将父类的虚函数表给拷贝下来,然后将子类中重写的部分给覆盖。
其次,因为只有完成了虚函数的重写,那派生类的虚表里面才能是派生类的虚函数。这样的话,这个基类指针才能做到指向父类调用父类,指向子类调用子类。
五、虚函数表的一些总结
-
派生类对象st中也有一个虚表指针,st对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在这一部分的,另一部分是自己的成员。
-
基类b对象和派生类d对象虚表是不一样的,这里我们发现BuyTicker完成了重写,所以d的虚表中存的是重写的Student::Buyticker,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
-
另外Func1和Func2继承下来后是虚函数,所以放进了虚表,如果Func2不是虚函数,那么它也继承下来了,但是因为不是虚函数,所以不会放进虚表
-
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr 。(注意不是所有的编译器都会给的,g++编译器就没有给,而且有时候vs的编译器有一些问题,不会给这个nullptr,这时候我们可以自己清理一下解决方案,然后重新编译一下就有了,这里算是一个编译器的bug)
下面的就是给了nullptr的
-
总结一下派生类的虚表生成:
a. 先将基类中的虚表内容拷贝一份到派生类虚表中
b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
注意c这个小点中,虽然它会将这个添加到派生类虚表的最后,但是我们的监视窗口有时候是看不见的,如下所示,我们并没有看到Func3的虚函数表中的地址。
但是我们是可以从内存窗口看到有一个地址的,这个地址就是Func3的虚表中的地址。这里算是一个监视窗口的一个bug
所以说监视窗口和内存窗口有时候还是有一些不一样的。准确的来说,这里我们也不能断言说这里一定是Func3函数的地址。因为我们并没有给出证明,所以后面我们会给出一个证明。
- 还有一点是虚表是存储在哪里的呢?是栈区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到王者】第二十四站:多态的底层原理
文章目录 前言一、虚函数表二、一道经典的例题三、深度剖析多态的条件之一:为什么必须是父类的指针或引用四、深度剖析多态的条件之二:为什么是虚函数的重写/覆盖?五、虚函数表的一些总结六、关于Func3的验证七、动态绑定与静态绑定八、总结 …...
Java从入门到精通24==》数据库、SQL基本语句、DDL语句
Java从入门到精通24》数据库、SQL基本语句、DDL语句 2023.8.27 文章目录 <center>Java从入门到精通24》数据库、SQL基本语句、DDL语句一、什么是数据库二、数据库的优缺点1、使用数据库的优点:2、使用数据库的缺点: 三、MySQL基本语句四、DDL语句 …...
学习ts(十)装饰器
定义 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法,访问符,属性或参数上,是一种在不改变原类和使用继承的情况下,动态的扩展对象功能。 装饰器使用expression形式,其中expression必须…...
如何在 Opera 中启用DNS over HTTPS
DNS over HTTPS(基于HTTPS的DNS)是一种更安全的浏览方式,但大多数 Web 浏览器默认情况下不启用它。了解如何在 Opera 浏览器中启用该功能。 您可能不知道这一点,但您的网络浏览器并不像您希望的那样私密或安全。您会看到ÿ…...
STM32 F103C8T6学习笔记13:IIC通信—AHT10温湿度传感器模块
今日学习一下这款AHT10 温湿度传感器模块,给我的OLED手环添加上测温湿度的功能。 文章提供源码、测试工程下载、测试效果图。 目录 AHT10温湿度传感器: 特性: 连接方式: 适用场所范围: 程序设计: 设…...
QT基础使用:组件和代码关联(信号和槽)
自动关联 ui文件在设计环境下,能看到的组件可以使用鼠标右键选择“转到槽”就是开始组件和动作关联。 在自动关联这个过程中软件自动动作的部分 需要对前面头文件进行保存,才能使得声明的函数能够使用。为了方便,自动关联时先对所有文件…...
TCP最大连接数问题总结
最大TCP连接数量限制有:可用端口号数量、文件描述符数量、线程、内存、CPU等。每个TCP连接都需要以下资源,如图所示: 1、可用端口号限制 Q:一台主机可以有多少端口号?端口号与TCP连接?是否能修改&#x…...
【Docker】云原生利用Docker确保环境安全、部署的安全性、安全问题的主要表现和新兴技术产生
前言 Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux或Windows操作系统的机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。 云原生利用Docker确保环境安全、部署的…...
explain各个字段代表的意思
id:联表查询是每个表的读取顺序,数字越大越先被读取。相同就需要通过table字段判断select_type:查询类型或者是其他操作类型(PRIMARY、UNION、UNION RESULT等)table:正在访问哪个表partitions:匹…...
【已解决】Windows10 pip安装报错:UnicodeDecodeError: ‘gbk‘ codec can‘t decode byte 0x98
环境:win10, Python3.9 在Pycharm安装YoloV5的依赖包时出现报错:UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x98 出现 ‘gbk’ codec can’t decode… 的报错一般是因为读取文件出现编码问题导致没法读取文件,因此可以在报错…...
goland 中的调试器 -- Evaluate
今天一个好朋友 找到我,问我关于goland中Evaluate 小计算器的使用方式,说实话,我在此之前也没用过这个东西,然后我就找一些相关文档,但是这类文档少的可怜,所以我就稍微研究一下,找找材料&#…...
你知道公司内部维基到底有哪些功能吗
维基指的是一种协作工作的平台,也就是开源的编辑系统。员工可以在企业维基里面进行存储、共享和协作之类的操作,将企业内部员工的知识共享聚集在一起。今天looklook将会详细讲讲公司内部维基具体到底有哪些功能,供大家参考。 公司内部维基的功…...
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.创建一个数据库以及一个用户表,并插入三条数据用来测试 2.创建一个空的maven项目 在pom.xml中导入本次测试用到的三个依赖,mysql驱动、mybatis依赖、以及单元测试junit依赖 将这个 空的maven项目当成一个父项目,再创建一个空的maven子项目用…...
计算机网络MTU和MSS的区别
在计算机网络中,MTU代表最大传输单元(Maximum Transmission Unit),而MSS代表最大分节大小(Maximum Segment Size)。 1.MTU(最大传输单元): MTU是指在网络通信中&#x…...
redis学习笔记 - 进阶部分
文章目录 redis单线程如何处理并发的客户端,以及为何单线程快?redis的发展历程:redis单线程和多线程的体现:redis3.x单线程时代但性能很快的主要原因:redis4.x开始引入多线程:redis6/redis7引入多线程IO&am…...
SE5 - BM1684 人工智能边缘开发板入门指南 -- 模型转换、交叉编译、yolov5、目标追踪
介绍 我们属于SoC模式,即我们在x86主机上基于tpu-nntc和libsophon完成模型的编译量化与程序的交叉编译,部署时将编译好的程序拷贝至SoC平台(1684开发板/SE微服务器/SM模组)中执行。 注:以下都是在Ubuntu20.04系统上操…...
基于Java+SpringBoot+vue前后端分离英语知识应用网站设计实现
博主介绍:✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专…...
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,并推送其到私有仓库。
环境: CentOS 7 Linux 3.10.0-1160.el7.x86_64 具体要求如下: (1)基于centos基础镜像; (2)指定作者信息; (3)安装nginx服务,将提供的dest目录…...
js 类、原型及class
js 一直允许定义类。ES6新增了相关语法(包括class关键字)让创建类更容易。新语法创建的类和老式的类原理相同。js 的类和基于原型的继承机制与Java等语言中的类和继承机制有着本质区别。 1 类和原型 类意味着一组对象从同一个原型对象继承属性。因此,原型对象是…...
day-30 代码随想录算法训练营 回溯part06
332.重新安排行程 思路:使用unordered_map记录起点机场对应到达机场,内部使用map记录到达机场的次数(因为map会进行排序,可以求出最小路径) class Solution { public:vector<string>res;unordered_map<stri…...
txt、pcd、las、ply 格式点云基本的读写和显示 (附 python c++ 代码)
一、文本(txt) 1.1、存储结构 使用文本格式存储的点云数据文件结构比较简单,每个点是一行记录,点的信息存储格式为 x y z或者 x y z r g b。 1.2、读取 读取文本格式的点云数据时,可以按照一般的文本读取方法,这里记录一下如何使用open3d读取txt格式的点云数据 impo…...
k8s节点pod驱逐、污点标记
一、设置污点,禁止pod被调度到节点上 kubectl cordon k8s-node-145 设置完成后,可以看到该节点附带了 SchedulingDisabled 的标记 二、驱逐节点上运行的pod到其他节点 kubectl drain --ignore-daemonsets --delete-emptydir-data k8s-node-145 显示被驱逐…...
【项目 计网6】 4.17 TCP三次握手 4.18滑动窗口 4.19TCP四次挥手
文章目录 4.17 TCP三次握手4.18滑动窗口4.19TCP四次挥手 4.17 TCP三次握手 TCP 是一种面向连接的单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“连接”,其实是客户端和服务器的内存里保存的一份关于对方的信息ÿ…...
茶叶小笔记
文章目录 茶叶的作用茶叶的主要成分茶多酚氨基酸(蛋白质)生物碱 茶叶的分类乌龙茶铁观音(安溪) 绿茶龙井(西湖)龙井43 绿茶(日照)毛尖(信阳毛尖)太平猴魁六安瓜片 红茶金骏眉大红袍 白茶云南白茶 黄茶黑茶花草茶 茶叶的形状过期茶的利用茶叶蛋大排档泡澡泡脚除湿除臭 茶渣的利用…...
安全开发-JS应用NodeJS指南原型链污染Express框架功能实现审计WebPack打包器第三方库JQuery安装使用安全检测
文章内容 环境搭建-NodeJS-解析安装&库安装安全问题-NodeJS-注入&RCE&原型链案例分析-NodeJS-CTF题目&源码审计打包器-WebPack-使用&安全第三方库-JQuery-使用&安全 环境搭建-NodeJS-解析安装&库安装 Node.js是运行在服务端的JavaScript 文档参考…...
Android JNI系列详解之CMake编译工具的使用
一、CMake工具的介绍 如图所示,CMake工具的主要作用是,将C/C编写的native源文件编译打包生成库文件(包含动态库或者静态库文件),集成到Android中使用。 二、CMake编译工具的使用 使用主要是配置两个文件:CM…...
springboot中关于继承WebMvcConfigurationSupport后自定义的全局Jackson失效解决方法,localdate返回数组问题
一般情况下我们在config里增加jackson的全局配置文件就能满足基本的序列化需求,比如前后端传参的问题。 Configuration public class JacksonConfig {public static final String LOCAL_TIME_PATTERN "HH:mm:ss";public static final String LOCAL_DATE…...
LeetCode 面试题 02.03. 删除中间节点
文章目录 一、题目二、C# 题解 一、题目 若链表中的某个节点,既不是链表头节点,也不是链表尾节点,则称其为该链表的「中间节点」。 假定已知链表的某一个中间节点,请实现一种算法,将该节点从链表中删除。 例如&#x…...
网站微信开发/潍坊网站模板建站
http://sh.house.ifeng.com/detail/2015_10_20/50592267_0.shtml...
有关网站建设的公众号/软文营销经典案例200字
A. timestampdiff() 传三个参数,第一个时间类型如年,月,日,第二个开始时间,第三个结束时间select test_name, timestampdiff(YEAR,create_time,end_time) y_date from test_table; --计算时间-------------------| tes…...
如何做网站的逻辑结构图/哪个模板建站好
本文实例为大家分享了python实现烟花小程序的具体代码,供大家参考,具体内容如下FIREWORKS SIMULATION WITH TKINTER *self-containing code *to run: simply type python simple.py in your console *compatible with both Python 2 and Python 3 *Depen…...
做网站的的价位/排名优化价格
《北航计算机软件技术基础实验报告实验报告4-2——数据库应用系统的开发》由会员分享,可在线阅读,更多相关《北航计算机软件技术基础实验报告实验报告4-2——数据库应用系统的开发(10页珍藏版)》请在人人文库网上搜索。1、实验报告实验名称 数据库应用系…...
php网站后台页面/新闻头条最新消息国家大事
又大一岁了,先祝自己生日快乐,今年是我快乐的一年,最期待的事就是下半年小宝贝的出生,也希望自己今年能在C#上有更高的成就,虽然我已经不做软件开发了, 但是编程做为一种爱好又何尝不可呢...
wordpress wpuf/中文域名交易网站
先获取div img 先清空img 给刚加载时设定初始图片 img.empty(); img.append("<img srcimg/1.jpg />");设置定时器 每4秒给i6自增1 当超过图片数量时将i6重新设定为0 开始新的自增 下面是js $(document).ready(function(){ var img$("#img");var …...