C++ - 多态的实现原理

前言

本博客主要介绍C++ 当中 多态语法的实现原理,如果有对 多态语法 有疑问的,请看下面这篇博客:

C++ - 多态语法 - 虚函数使用介绍-CSDN博客

 探究,为什么多态的条件是那样的(虚函数表)

 首先,调用虚函数必须是 父类的 指针或 引用,不能是子类的。这是因为,子类当中有父类部分,而父类当中只有自己。我们使用父类指针,当指向对象是父类的时候,指针类型也是相匹配的,就会调用父类的函数;如果指向那个对象是子类的,但是子类当中构造了父类,就会发生切片,因为指针类型是父类的,指针只会访问父类的那一部分。

但是如果指针类型是子类的,就不行了,因为子类虚函数表当中只有自己虚函数的地址。父类指针可以指向子类和父类,但是子类指针只能指向子类。


为什么不能是父类的对象,而必须是指针或引用呢
因为 ,指针的切片和 对象的切片做造成的结果是不一样的

如果是指针切片,在创建派生类的时候,子类当中父类的虚函数表,是先从父类当中拷贝一份到在子类当中父类的虚函数表,然后,如果子类当中重写了虚函数,再把子类当中重写虚函数地址直接覆盖在虚函数表中之前父类中对应的虚函数地址位置

 拷贝之后如下所示:

 所以,才会有指向父类调用父类的虚函数;指向子类调用子类的虚函数;只不过在指针看来,看到的都是对象对象。一个是之间看到父类对象,一个是切片之后看到了子类当中父类对象。所以说,指针的切片不考虑拷贝的问题,就可以理解为他只是把原本就有的部分切片出来给指针看到。

 而对象的切片,像上述的例子, ps = st,相当于是把子类对象拷贝到 ps 当中;因为ps 是父类的指针, 而 st 是子类的指针,这时候就会发生切片。把切片出来的子类当中的父类拷贝到 ps 当中。

在子类的父类当中有两个部分,首先 _a 肯定是会拷贝到 ps  当中,但是虚函数表会不会拷贝呢

我们先来看,拷贝之后会发生什么。如果我们把子类当中的虚函数表拷贝到父类当中的虚函数表当中,那么当指针指向父类的时候,此时应该调用父类的函数,但是此时父类当中的虚函数表存储的是子类的虚函数地址(因为刚刚假设是直接拷贝),那么此时就会去调用子类的虚函数,这部乱套了吗?这肯定不会是我们所期望的,我们肯定期望父类指针指向父类对象 ,就去调用父类的虚函数。

所以,此时肯定不能 把 子类当中的 虚函数表拷贝到 父类当中的虚函数表当中。在实际当中,子类拷贝给父类,编译器也没有拷贝虚函数表,和我们刚刚所想是一样的。

所以说,上述就是我们不能使用父类对象调用虚函数的原因(如果使用对象调用,就要进行赋值拷贝开空间,而新开出来的父类就需要重新构建虚函数表,而又不能直接拷贝原本的虚函数表,原本子类和父类构建的重写关系可能会乱套

 这里提一嘴,我们普通继承(比如 A类 继承 B类),这种称之为 实现继承。而 上述的多态继承称之为 接口继承

至于为什么需要虚函数的重写,上述给出的过程也可以证明了,因为只有是虚函数重写,在子类当中的虚函数表当中才会 有 子类新重写的虚函数地址。而调用虚函数的 父类指针 或 引用,只需要“无脑的” 从 父类当中的虚函数表 ,找到这个虚函数地址,调用这个虚函数就行了。 


 关于虚函数表的一些问题

  • 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  •  基类b对象和派生类d对象虚表是不一样的,这里我们发现BuyTicket完成了重写,所以Student的虚表中存的是重写的Student::BuyTicket,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法.
  • 虚函数表当中只放虚函数的地址,如果不是虚函数,该函数的地址是不会放到虚函数表当中的;
  • 派生类对于父类当中的虚函数表是先拷贝,然后在把派生类重写的虚函数地址进行直接覆盖,如果在派生类当中自己有虚函数,就按照派生类的声明次序写在虚函数表当中(注:VS的调试窗口下不会显示派生类当中的虚函数(不是重写的),但是我们打开内存窗口可以看到派生类虚函数的地址)。也就是说,在派生类中父类的虚函数表当中有三个部分:从父类拷贝下来父类的虚函数;子类重写之后覆盖的虚函数;子类当中的虚函数;
  • 虚函数表的本质是一个 存放 函数指针的指针数组,一般情况下,这个数组在最后放了一个 nullptr(0),但是这个看不同的编译器,在VS下就给了,但是在 g++ 当中没给。
  • 虚函数表是存储在 代码段,也就是常量区当中的。

虚函数表存储在代码段的验证:

  • C++当中数据存储位置大概有以下几个地方:栈  堆   数据段(静态区)   代码段(常量区)。首先排除的是 堆 ,因为堆是拿给我们动态开辟空间的,而虚函数表是由编译器生成的,所以不可能是编译器 什么 malloc new出来的。
  • 栈也不可能,因为同类型的对象共用一个虚函数表(一表多用)(比如 Person ps1 和 Person ps2这两个对象是共用一个虚函数表的,不管这两个对象分别构造在任意位置),而栈上的空间一般是跟着栈帧走的,不能单独开空间。而且如果存储在栈上也有一个问题,就是函数执行结束,战争销毁,存储在这个栈帧当中的虚函数表也要进行销毁,那么当下一个同类型的对象构造的时候,虚函数表难道要重新进行构造吗?肯定是不行的。(除非是 main 函数栈帧)
  • 我们可以来验证一下,我们用 分别存储在上述四个存储位置的  四个数据,分别打印他们的地址,这样我们可以大概的看出这个四个存储位置的地址区间,在打印对象当中虚函数表的地址(用强转类型,int* ,这样解引用的话只会访问 4 个字节的内容),因为这个例子的数据量很小,地址最接近的我们可以认为虚函数表就存储在那个 存储位置:

 我们发现,虚函数表 和 常量区 存储位置地址最接近。

VS当中 虚函数表最后的 nullptr(有时候编译器在你调试的时候修改一些代码,编译器可能不会给nullptr,但是清理一下,重新生成解决方案之后就会有了);

 验证,派生类的虚函数表当中,VS调试窗口看不到的,派生类的虚函数地址:
 

class Person
{
public:virtual void Func1(){cout << "Person::Func1()" << endl;}virtual void Func2(){cout << "Person::Func2()" << endl;}};class Student : public Person
{
public:virtual void Func3(){cout << "Student::Func3()" << endl;}
};

如上例子所示,在子类 Student 中的虚函数表,除了存储父类 Person 的两个虚函数之外,还要存储 子类 当中的 虚函数--func3()的地址,但是 这个 func3 ()函数的地址,在VS的调试窗口上是不会显示的,但是在内存当中,除了 有父类 当中两个虚函数的地址,还多出来一个地址,我们怀疑这个地址的空间存储的就是 func3()函数的地址:
 

调试窗口子类虚函数表没有func3()地址:

 内存窗口当中多出一个地址:

 上述说过,虚函数表其实就是函数指针数组,这个数组当中存储的是每一个虚函数的指针,所以我们可以利用C 当中的函数指针来帮助我们验证这个地址是不是 func3()函数的指针。

我们可以在虚函数表数组当中找到这个地址,然后用这个地址调用这个地址的函数看是不是func3()。

函数指针语法(转自博客:c++ 函数指针_c++指针函数_Alpha205的博客-CSDN博客):

double (*pf)(int);   // 指针pf指向的函数, 输入参数为int,返回值为double

这样不太好看,我们可以typedef一下:

typedef void(*FUNC_PTR) ();

在数组当中找到 这个地址,然后调用这个地址上的 函数:

typedef void(*FUNC_PTR) ();void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p\n", i, table[i]);}printf("\n");
}int main()
{Person ps;Student st;int vft1 = *((int*)&ps);PrintVFT((FUNC_PTR*)vft1);int vft2 = *((int*)&st);PrintVFT((FUNC_PTR*)vft2);return 0;
}

我们拿输出结果看和内存对比,地址是否相同,来验证我们当前取出来的地址是否正确:
 

 我们发现是完全吻合的。

然后我们在把 疑似 func3()函数的函数指针拿出来调用,看这个地址是不是 func3()函数的地址:
 

//打印虚函数表当中 虚函数地址的函数
void PrintVFT(FUNC_PTR* table)
{for (size_t i = 0; table[i] != nullptr; i++){printf("[%d]:%p\n", i, table[i]);FUNC_PTR func = table[i];func();}printf("\n");
}

输出:

[0]:00FC1357
Person::Func1()
[1]:00FC12AD
Person::Func2()[0]:00FC1357
Person::Func1()
[1]:00FC12AD
Person::Func2()
[2]:00FC12D0
Student::Func3()

此时我们就验证了,那个多出来的地址就是 Func3()函数的地址。

 以上验证方式需要注意的点:

  •  首先,这个程序是在 X86 也就是在 32位环境下执行的,也就是说,我们在寻找虚函数表的首地址的时候,是寻找对象前4字节的存储的数据,这个数据就是虚函数表的地址。如果是 64 位环境的话,应该是取 对象的前8个字节。
  • 上述写的PrintVFT()这个函数中的循环,是以 nullptr(0)作为循环的终止条件的,因为在VS当中的虚函数表,在最后会以 nullptr 来结尾。但是有时候我们不注意在调试时候修改代码,可能就不会以 nullptr 来结尾了,这时候我们需要重新生成解决方案。
  • 在Linux中,也就是在g++环境下,虚函数表不是以 nullptr 结尾的,这时候的循环只能写死了。

 看到上述的验证,我们应该注意了,VS当中的监视窗口有时候可能不靠谱,而内存当中是绝对靠谱的。

函数指针(函数地址)在使用的时候需要注意,如果你知道函数的地址,不管这个函数受哪一个权限修饰符修饰,就算是使用 private 修饰,照样可以访问。

因为,此时你都已经知道了这函数的地址,使用函数指针来调用函数,是直接在代码段当中找到这个函数,然后调用。

还有一个原因是,权限的限定只是在语法层次来限定,不是在运行层当中进行限制的。这里的函数指针直接跳过了语法层次,直接在语法层次来进行寻找函数调用。

 动态绑定和静态绑定(动态多态和静态多态)

 其实多态这一现象不止发生在对象当中,在函数的当中时常发生。如下例子:
 

int a = 1;
double b = 1;cout << a << endl;
cout << b << endl;

 库函数当中的 cout 流插入之所以实现自动判别类型,其实底层实现就是使用 函数重载。当我们传入不同类型的参数的时候,编译器就会自动的去寻找参数列表对应的函数来调用。

上述这种用函数重载来实现的多态,就叫做静态多态

而我们上述的多态,也就是使用继承,虚函数来实现的多态,就是动态多态

 多继承当中的虚函数表

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

我们先来计算 Deribve d ,这个对象的大小是多少,sizeof(d)= 20;Base1的虚表指针 + b1 + Base2的虚表指针 + b2 + d1 = 20。

派生类是没有自己的虚表的,因为派生类在构造之前需要先构造其父类,它的父类当中就有虚表,而且这个虚表还是子类修改过的(重写就覆盖地址)。

多继承当中,派生类当中创建的多个父类当中都有虚表,我们可以认为,这些虚表都是属于这个派生类的,因为这些虚表当中,如果派生类对其中的虚函数进行了重写,那么都是对这个虚表进行了修改的,就算这个表当中没有,也不影响子类调用函数。 

那么在子类当中func3()这个函数,没有重写,但是是虚函数,那么也要放进虚表当中,但是紫烈继承了两个父类,此时有两个虚表,究竟是放到哪一个虚表当中的呢?

要得到上述问题的答案,我们还是要进行虚表当中虚函数的打印,打印过程和上述一样,唯一不一样的是,base2父类不在 d 对象当中的第一位置,Base1当中的虚表好弄,因为是Base1是在第一位置。所以,此时我们要像去Base1一样先取出第一位置的地址,然后加上 sizeof(base2),因为 所用的指针是 d 类型指针,所以此时还需要把 d 指针强转为 char* ,使得我们加上 sizeof(base2)是一个字节一个字节加的。具体代码如下所示:
 

Derive d;
int vft2 = *((int*)( (char*)&d + sizeof(Base1)));

这样就可以取出d对象当中 Base2 父类当中的虚函数表的地址了。

还有一个更好的方法:使用 Base2 类型的指针,指向子类对象(d),这样就会发生切片,Base2类型的指针直接指向 d 对象当中的 Base2 首地址。虚函数就在首地址处,直接按照4个字节大小取出就好:

Derive d;
Base2* ptr = &d;
int vft2 = *((int*)ptr);
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}int main()
{Derive d;int vTableb1 = (*(int*)&d);PrintVTable((VFPTR*)vTableb1);int vTableb2 = (*(int*)((char*)&d + sizeof(Base1)));PrintVTable((VFPTR*)vTableb2);return 0;
}

输出:

 虚表地址>00A69B94第0个虚函数地址 :0Xa61244,->Derive::func1第1个虚函数地址 :0Xa612e9,->Base1::func2第2个虚函数地址 :0Xa61230,->Derive::func3虚表地址>00A69BA8第0个虚函数地址 :0Xa61357,->Derive::func1第1个虚函数地址 :0Xa610b9,->Base2::func2

通过上述输出结果发现,func3()函数的地址是存在 第一个 父类对象的虚函数表当中的。


我们还看到,上述在 Base1 当中的 func1()函数的地址 和 在 Base1 当中的 func1()函数的地址是不一样的

我们先来模拟,用两个分别指向 Base1 和 Base2 的指针来调用func1()函数,来看看:
 

int main()
{Derive d;Base1* pb1 = &d;pb1->func1();Base2* pb2 = &d;pb2->func1();return 0;
}

输出结果是一样的:

Derive::func1
Derive::func1

虽然两次函数调用的结果是一样的,但是两次调用函数的地址是不一样的。

我们来查看反汇编,来看看编译器在这里究竟干了些什么,为什么要这样做?

 上述就是我们在main函数的当中写的代码所转化的反汇编截图。

过程描述:

首先是 Base1 指针调用 func1(),call指令调用函数,call指令当中 eax寄存器 存储的地址是 jmp的地址,因为在VS当中,调用函数之前要先走一趟jmp,而jmp跳跃到的地址才是调用函数真真的地址,我们发现,Base1 指针调用 func1()函数就是直接跳到子类重写的 func1()函数地址来进行调用的

 然后我们来看 Base2 指针调用func1()的过程,同样在call指令开始查看,发现此时 eax寄存器当中存储的地址和 Base1当中 eax存储的地址不一样了

 也就是说,此时call之后执行的 jmp 指令也不会是之前的那个指令了:

 此时的jmp 跳到了另一个指令位置当中,此时就只有 sub 和 jmp这两个指令,首先执行 sub 这个指令,这个指令是 减 的意思,意思是 寄存器 ecx 当中的值 减 8。而 ecx 当中存储的值是 this 指针的值,也就是说,sub 指令是让 this 指针 减8。

然后 接下来执行的 jmp指令 就和 Base1 当中的 jmp 指令地址一样的了,也就是执行的是一个指令,此时就跳到了 func1()子类重写的地址处进行执行。我们发现,Base2也是跳到 子类重写的函数当中进行调用 func1()函数的。

 既然,两处最后都是跳到 子类当中对 func1()函数重写位置进行 调用的,那么 Base2 指针调用的 func1()函数为什么要多执行这几步绕一圈在执行呢?

其实不难发现,Base2 指针调用的 func1()函数过程,多执行的几步当中,jmp指令都不重要,重要的是执行的 那个 sub 指令。这个指令对当时的 this 指针进行了修改,那么为什么要对当中的 this 指针进行修改呢?

 首先我们要知道,此时的this指针指向的是什么。此时的this指针,谁调用的,谁就是 this 指针,很明显,此时的this指针是 pb2。而此时的pb2 指向的是 d 子类对象当中的 Base2 这个父类对象,也就是说,此时的this 指针指向的是 Base2 这个对象。

但是,我们此时调用的 func1()函数进行了重写,所以 func1 ()函数的实现是在 子类当中,而不在Base2 当中,当 func1()当中调用了 子类当中的成员函数或成员变量,我们知道,调用成员是需要 this 指针来调用的,如果此时this 指针还是指向 Base2,就出大问题了!! 

 所以,此时编译器就对this指针进行了修改:

 ecx 存储的是 this指针,在最开始是从 ptr2 拷贝过来的this指针的值,ptr2 存储的是 Base2 对象的指针,这肯定是不对的,所以此时编译器才饶了一圈来修改this指针指向的位置。

 那为什么 Base1 类型的指针(ptr1)调用 func1()函数就没有这样饶圈,而是直接跳到 func1()函数实现位置调用呢?

其实是也 ptr1 指针指向位置很特殊,他就是 子类对象 d 的首地址,就是 d 对象的 this指针应该指向的地方,所以此时编译器不需要对this 指针进行修改。

 当然,上述是基于 VS 当中的编译器做到事情,其他编译器不好说。

除了上述方法,我们还可以让两个虚表当中的 func1()函数的地址是一样的,而且同样的可以修改this 指针。

就是不要 那 ptr2 来赋值 ecx ,就算要拿 ptr2 来赋值给 ecx ,在下一行指令就把 ecx 当中的值向上述一样 减8 就行了。

 菱形继承、菱形虚拟继承 当中的虚函数表

 实际中我们不建议设计出菱形继承及菱形虚拟继承,因为菱形继承很复杂容易出现问题,而且菱形继承当中进行了一些 bug 的修复,有一定的性能损耗。如果想了解的可以看下面这两篇文章:
C++ 虚函数表解析 | 酷 壳 - CoolShell

C++ 对象的内存布局 | 酷 壳 - CoolShell

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

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

相关文章

qt day6 人脸识别

在C和C中static关键字的用法 static修饰局部变量、全局变量&#xff08;不能被外部引用extern|未初始化的值为0&#xff09;、函数&#xff08;不能被外部引用extern&#xff09;&#xff0c;不能修饰auto类型的指针&#xff08;因为计算机先为静态变量分配空间&#xff0c;后再…

重磅|Falcon 180B 正式在 Hugging Face Hub 上发布!

引言 我们很高兴地宣布由 Technology Innovation Institute (TII) 训练的开源大模型 Falcon 180B 登陆 Hugging Face&#xff01;Falcon 180B 为开源大模型树立了全新的标杆。作为当前最大的开源大模型&#xff0c;有180B 参数并且是在在 3.5 万亿 token 的 TII RefinedWeb 数据…

打造基于终端命令行的IDE,Termux配置Vim C++开发环境

Termux配置Vim C开发环境&#xff0c;打造基于终端命令行的IDE 主要利用VimCoc插件&#xff0c;配置C的代码提示等功能。 Termux换源 打开termux&#xff0c;输入termux-change-repo 找到mirrors.tuna.tsinghua.edu.cn&#xff0c;清华源&#xff0c;空格选中&#xff0c;回…

Hadoop Hive入门

0目录 1.linux 安装hive 2.hive入门 3.hive高级语法1 1.linux 安装hive 先确保linux虚拟机中已经安装jdk&#xff1b;mysql和hadoop 并可以成功启动hadoop和mysql 下载hive对应版本到opt/install目录下并解压到opt/soft目录下 重命名 hive312 配置profile 文件&#xff…

Qt+C++桌面计算器源码

程序示例精选 QtC桌面计算器源码 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对<<QtC桌面计算器源码>>编写代码&#xff0c;代码整洁&#xff0c;规则&#xff0c;易读。 学习与…

Hadoop的第二个核心组件:MapReduce框架第二节

Hadoop的第二个核心组件&#xff1a;MapReduce框架第二节 六、MapReduce的工作流程原理&#xff08;简单版本&#xff09;七、MapReduce中的序列化机制问题八、流量统计案例实现&#xff08;序列化机制的实现&#xff09; 六、MapReduce的工作流程原理&#xff08;简单版本&…

Flutter实用工具Indexer列表索引和Search搜索帮助。

1.列表索引 效果图&#xff1a; indexer.dart import package:json_annotation/json_annotation.dart;abstract class Indexer {///用于排序的字母JsonKey(includeFromJson: false, includeToJson: false)String? sortLetter;///用于排序的拼音JsonKey(includeFromJson: fal…

如何有效防止服务器被攻击?

随着互联网的快速发展&#xff0c;服务器安全问题日益引起人们的关注。近期&#xff0c;全球范围内频繁发生的服务器攻击事件引发了广泛关注。为了保护企业和个人的数据安全&#xff0c;有效防止服务器被攻击已成为迫在眉睫的任务。 首先&#xff0c;及时更新服务器的操作系统和…

基于SpringBoot的医院挂号系统

基于SpringBootVue的医院挂号、预约、问诊管理系统&#xff0c;前后端分离 开发语言&#xff1a;Java数据库&#xff1a;MySQL技术&#xff1a;SpringBoot、Vue、Mybaits Plus、ELementUI工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 角色&#xff1a;管理员、用户、医生 管…

系统架构技能之设计模式-组合模式

一、上篇回顾 我们上篇主要讲述了结构型模式中的外观模式&#xff0c;外观模式作为结构型模式中的一个简单又实用的模式&#xff0c;外观模式通过封装细节来提供大粒度的调用&#xff0c; 直接的好处就是&#xff0c;封装细节&#xff0c;提供了应用写程序的可维护性和易用性。…

十二、集合(4)

本章概要 集合 Set映射 Map队列 Queue 优先级队列 PriorityQueue 集合与迭代器 集合Set Set 不保存重复的元素。 如果试图将相同对象的多个实例添加到 Set 中&#xff0c;那么它会阻止这种重复行为。 Set 最常见的用途是测试归属性&#xff0c;可以很轻松地询问某个对象是否…

命名空间的详讲

本篇文章旨在讲解C中命名空间的概念以及其相关注意事项&#xff01; C的介绍 C作为C语言的衍生&#xff0c;其对C语言中的一些缺陷进行了一些的补充和优化。但是C也对C语言具有兼容性&#xff01; 本文旨在讲解C对C语言中当声明的变量与库函数的一些标识符&#xff0c;关键字…

【数论】容斥问题教程+符号含义+公式(c++)

容斥问题是什么 比如我们平常考试&#xff0c;我们会统计有几个语文及格&#xff0c;有几个数学及格&#xff0c;比如5个语文及格&#xff0c;2个数学及格&#xff0c;当然了&#xff0c;也会有大学霸两科都及格&#xff0c;比如1个人语文数学都及格&#xff0c;那我们班上一共…

Hadoop的第二个核心组件:MapReduce框架第四节

Hadoop的第二个核心组件&#xff1a;MapReduce框架 十、MapReduce的特殊应用场景1、使用MapReduce进行join操作2、使用MapReduce的计数器3、MapReduce做数据清洗 十一、MapReduce的工作流程&#xff1a;详细的工作流程第一步&#xff1a;提交MR作业资源第二步&#xff1a;运行M…

vnc与windows之间的复制粘贴

【原创】VNC怎么和宿主机共享粘贴板 假设目标主机是linux&#xff0c;终端主机是windows&#xff08;就是在windows上使用VNC登陆linux&#xff09; 在linux中执行 vncconfig -nowin& 在linux选中文字后&#xff0c;无需其他按键&#xff0c;直接在windows中可以黏贴。 …

【C++基础】5. 变量作用域

文章目录 【 1. 局部变量 】【 2. 全局变量 】【 3. 局部变量和全局变量的初始化 】 作用域是程序的一个区域&#xff0c;一般来说有三个地方可以定义变量&#xff1a; 在函数或一个代码块内部声明的变量&#xff0c;称为局部变量。 在函数参数的定义中声明的变量&#xff0c;称…

【前端demo】圣诞节灯泡 CSS动画实现轮流闪灯

文章目录 效果过程灯泡闪亮实现&#xff08;animation和box-shadow&#xff09;控制灯泡闪亮时间和顺序&#xff08;animation-delay&#xff09;按钮开关 代码htmlcssjs 参考代码1代码2 前端demo目录 效果 效果预览&#xff1a;https://codepen.io/karshey/pen/zYyBRWZ 参考…

Vue + Element UI 前端篇(十二):用户管理模块

Vue Element UI 实现权限管理系统 前端篇&#xff08;十二&#xff09;&#xff1a;用户管理模块 用户管理模块 添加接口 在 http/moduls/user.js 中添加用户管理相关接口。 import axios from ../axios/* * 用户管理模块*/// 保存 export const save (params) > {ret…

React中消息订阅与发布(PubSubJS)——两个组件之间通信

结合案例&#xff1a;github搜索案例 结果如下图 1.父容器代码 import React, { Component } from react import Search from ./components/Search import List from ./components/List export default class App extends Component {render() {return (<div className&…

【面试题】如何实现数组去重的?有几种方式?

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 【国庆头像】- 国庆爱国 程序员头像&#xff01;总有一款适合你&#xff01; 1. 方法一&#xff1a;利用两层循环数组的splice方法 通过两层循环对数组…