深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)

目录

成员函数种类

虚函数的实现

单一继承下的虚函数


接下来我将持续更新“深度解读《深度探索C++对象模型》”系列,敬请期待,欢迎关注!也可以关注公众号:iShare爱分享,或文章末尾扫描二维码,自动获得推文和全部的文章列表。

        假如有这样的一段代码,代码中定义了一个Object类,类中有一个成员函数print,通过以下的两种调用方式调用:

Object b;
Object* p = new Object;
b.print();
p->print();

        请问这两种方式有什么区别吗?在效率上一样吗?答案是不确定。因为得看成员函数print的声明方式,它可能是静态的,可能是非静态的,也可能是一个虚函数。还得看Object类的具体定义,它可能是独立的类,也有可能是经过多重继承来的类,或者继承的父类中有一个虚基类。

        静态成员函数和非虚成员函数比较简单,我们在下一小节简单介绍一下即可,本文重点讲解虚函数的实现及其效率。

成员函数种类

  • 非静态成员函数

        非静态成员函数和普通的非成员函数是一样的,它也是被编译器放置在代码段中,且可以像普通函数那样可以获取到它的地址。和普通非成员函数的区别是它的调用必须得经由一个对象或者对象的指针来调用,而且可以直接访问类中非公开的数据成员。下面的代码打印出函数的地址:

#include <cstdio>class Object {
public:void print() {printf("a=%d, b=%d\n", a, b);}int a = 1;int b = 2;
};void printObject(Object* obj) {printf("a=%d, b=%d\n", obj->a, obj->b);
}int main() {printf("Object::print = %p\n", &Object::print);printf("printObject = %p\n", &printObject);return 0;
}

        程序的输出结果如下,从打印结果来看,两者的地址比较相近,说明它们都是一起放在代码段中的,从生成的汇编代码也可以看出来。

Object::print = 0x1007b3f30
printObject = 0x1007b3e70

        非静态成员函数和普通非成员函数的运行效率上也是一样的,普通非成员函数的实现上,对类中成员的访问看起来像是要经过指针的间接访问,如obj->a,非静态成员函数的访问看起来更直接一点,直接可以对类中的成员进行存取,好像是非静态成员函数的效率更高一些,其实不然,非静态成员函数的调用,编译器会隐式的把它转换成另一种形式:

Object obj;
obj.print();
// 转换成:
print(&obj);
// print的定义转换成:
print(Object* const this) {printf("a=%d, b=%d\n", this->a, this->b);
}

        两者在本质上是一样的,查看生成的汇编代码也是一样的。另外也说明了为什么非静态成员函数要经由一个对象或对象的指针来调用。

  • 静态成员函数

        上面提到的非静态成员函数的调用,必须要经由类的对象来调用,是因为需要将对象的地址作为函数的参数,也就是隐式的this指针,这样在函数中访问类的非静态数据成员时将绑定到此地址上,也就是将此地址作为基地址,经过偏移得到数据成员的地址。但是如果函数中不需要访问非静态数据成员的话,是不需要this指针的,但目前的编译器并不区分这种情况。静态成员函数不能访问类中的非静态数据成员,所以是不需要this指针的,如Object类中定义了静态成员函数static int static_func(),通过对象调用:

        Object obj;

        obj.static_func();

或者通过对象的指针调用:

        Object* pobj = new Object;

        pobj->static_func();

最终都会转换成员如下的形式:

        Object::static_func();

        通过对象或者对象的指针来调用只是语法上的便利而已,它并不需要对象的地址作为参数(this指针)。

        那么静态成员函数存在的意义是什么?静态成员函数在C++诞生之初是不支持的,是在后面的版本中增加进去的。假设不支持静态成员函数时,类中有一个非公开的静态数据成员,如果外面的代码需要访问这个静态数据,那么就需要写一个非静态成员函数来存取它,而非静态成员函数需要经由对象来调用,但有时候在这个时间点没有创建一个对象或者没有必要创建一个对象,那么就有了以下的变通做法:

// 假设定义了get_static_var函数用于返回静态数据成员
((Object*) 0))->get_static_var();
// 编译器会转换成:
get_static_var((Object*) 0));

        上面的代码把0强制转换为Object类型的指针,然后经由它来调用非静态成员函数,编译器会把0作为对象的地址传递给函数,但函数中不会使用这个0,所以不会出现问题。由于有这些需求的存在,C++标准委员会增加了支持静态成员函数,静态成员函数可以访问类中的非公开的静态数据成员,且不需要经由类的对象来调用。

        静态成员函数和非静态成员函数、普通函数一样都是存储在代码段中的,也可以获取到它的地址,它是一个实际的内存的地址,是一个数据,如上面定义的static_func函数,它的类型为int (*)(),就是一个普通的函数类型。而非静态成员函数,返回的是一个“指向类成员函数的指针”,如上面定义的print函数,返回的类型是:

        void (Object::*) ();

静态成员函数基本上等同于普通函数,所以和C语言结合编程时,可以作为回调函数传递给C语言写的函数。

总结一下,静态成员函数具有以下的特性:

    • 静态成员函数不能存取类中的非静态数据成员。
    • 静态成员函数不能被声明为const、volatile或者是virtual。
    • 静态成员不需要经由类的对象来调用。
  • 虚函数

        虚函数是否也可以像非虚函数那样获取到它的地址呢?我们写个程序来测试一下。

#include <cstdio>class Object {
public:virtual void virtual_func1() {printf("this is virtual function 1\n");}virtual void virtual_func2() {printf("this is virtual function 2\n");}
};int main() {printf("Object::virtual_func1 = %p\n", &Object::virtual_func1);printf("Object::virtual_func2 = %p\n", &Object::virtual_func2);return 0;
}

        上面程序的输出:

Object::virtual_func1 = 0x0
Object::virtual_func2 = 0x8

        程序的输出结果并不是一个内存地址,而是一个数字,其实这是一个偏移值,对应的是这个虚函数在虚函数表中的位置,一个位置占用8字节大小,第一个是0,第二个是8,以此类推,每多一个虚函数,就在这个表中占用一个位置。看起来像是无法获取到虚函数的地址,其实不然,虚函数的地址就存放在虚函数表中,只是我们无法直接获取到它,但是我们记得,如果有虚函数时,对象的前面会被编译器插入一个虚函数表指针,这个指针就是指向类的虚函数表,我们可以通过它来获取到虚函数的地址,下面演示一下通过非常规手段来调用虚函数的做法:

#include <cstdio>class Object {
public:virtual void virtual_func1() {printf("this is virtual function 1\n");}virtual void virtual_func2() {printf("this is virtual function 2\n");}
};int main() {Object* pobj = new Object;using Fun = void (*)(void);Fun** ptr = (Fun**)pobj;printf("vptr = %p\n", *ptr);for (auto i = 0; i < 2; ++i) {Fun fp = *(*ptr + i);	//取得虚函数的内存地址printf("vptr[%d] = %p\n", i, fp);fp();	//此行调用虚函数}delete pobj;return 0;
}

        程序的输出结果:

vptr = 0x100264030
vptr[0] = 0x100263ea4
this is virtual function 1
vptr[1] = 0x100263ecc
this is virtual function 2

        可以看到,虚函数的地址不光可以获取得到,而且还可以直接调用它,调用它的前提是函数中没有访问类的非静态数据成员,不然就会出现运行错误。vptr就是写入到对象前面的虚函数表指针,它的值就是虚函数表在内存中的地址,虚函数表中记录了两项内容,对应了两个虚函数的地址,即vptr[0]是虚函数virtual_func1的地址,vptr[1]是虚函数virtual_func2的地址。把他们强制转换成普通函数的类型指针,然后可以直接调用他们,所以这里是没有对象的this指针的,也就不能访问类中的非静态数据成员了。

虚函数的实现

        从上一小节中我们已经窥探到虚函数的一般实现模型,每一个类有一个虚函数表,虚函数表中包含类中每个虚函数的地址,然后每个对象的前面会被编译器插入一个指向虚函数表的指针,同一个类的所有对象都共享同一个虚函数表。接下来的内容中将详细分析虚函数的实现细节,包括单一继承、多重继承和虚继承的情况。

        多态是C++中最重要的特性之一,也是组成面向对象编程范式的基石,虚函数则是为多态而生。那么何为多态?多态是在基类中定义一组接口,根据不同的业务场景派生出不同的子类,在子类中实现接口,上层代码根据业务逻辑调用接口,不关心接口的具体实现。在代码中,一般是声明一个基类的指针,此指针在运行期间可能指向不同的派生类,然后通过基类的指针调用一个接口,这个接口在不同的派生类中有不同的实现,所以根据基类的指针指向哪个具体的派生类,调用的就是这个派生类的实例。假设有一个名称为print的接口,p是基类类型的指针,那么下面的调用:

        p->print();

        是如何识别出要实施多态的行为?以及如何调用到具体哪个派生类中的print?如果是在指针类型上增加信息,以指明具体所指对象的类型,那么会改变指针原有的语义,造成和C语言的不兼容,而且也不是每个类型都需要这个信息,这会造成不必要的空间浪费。如果是在每个类对象中增加信息,那么在不需要多态的对象中也需要存放这些信息,也会造成空间上的浪费。因此增加了一个关键字virtual,用于修饰那些需要多态的函数,这样的函数就叫做虚函数,所以识别一个类是否支持多态,就看这个类中是否声明了虚函数。只要类中有虚函数,就说明需要在类对象中存储运行期的信息。

        那么在对象中要存储哪些信息才能够保证保证上面代码中print的调用是调用到正确的派生类中的实例呢?要调用到正确的print实例,我们需要知道:

  • p指向具体的对象类型,让我们知道要调用哪个print;
  • print的位置,以便我们可以正确调用它。

        要如何实现它,不同的编译器可能有不同的实现方法,通常是使用虚函数表的做法。编译器在编译的过程中,收集到哪些是虚函数,然后将这些虚函数的地址存放一个表格中,这些虚函数的地址在编译期间确定的,运行期间是不会改变的,虚函数的个数也是固定的,在程序的执行期间不能删除或者增加,所以表格的大小也是固定的,这个过程由编译器在编译期间完成。表格中虚函数的位置按照类中声明的顺序,位置是固定不变的,我们在上节中通过虚函数名称打印出来的值就是虚函数在虚函数表中的位置,即相对于表格首地址的偏移值。

        有了这个表格,那么如何寻址到这个表格呢?方法就是编译器根据类中是否有虚函数,如果有虚函数,就在类的构造函数里插入一些汇编代码,在构造对象时,在对象的前面插入一个指针,这个指针指向这个虚函数表,所以这个指针也叫做虚函数表指针。下面以具体的代码来看看虚函数是怎么调用的,把上面的例子main函数修改如下,其它地方不变:

int main() {Object* pobj = new Object;pobj->virtual_func1();pobj->virtual_func2();delete pobj;return 0;
}

        我们来看下生成的汇编代码,首先来看看虚函数表长什么样:

vtable for Object:.quad   0.quad   typeinfo for Object.quad   Object::virtual_func1().quad   Object::virtual_func2()

        它是汇编中定义在数据段的一组数据,“vtable for Object”是它的标签,代表了这个数据区的起始地址,每一行定义一条数据,第一列.quad表示数据的大小,占用8字节,第二列表示数据的值,可以是数字,也可以是标签,标签是地址的引用。其实这个完整的表叫做虚表,它包含了虚函数表、RTTI信息和虚继承相关的信息,Clang和Gcc编译器是把它们合在一起了,其它编译器可能是分开的。第一行是虚继承中用到,之前已经讲过了,第二行是RTTI信息,这个以后再讲。第三、四行是两个虚函数的地址。

        接着看看Object类的默认构造函数的代码:

Object::Object() [base object constructor]: 	# @Object::Object() [base object constructor]# 略...lea     rcx, [rip + vtable for Object]add     rcx, 16mov     qword ptr [rax], rcx# 略...

        之前已经讲过,有虚函数时编译器会为类生成默认构造函数,在默认构造函数里在类对象的前面设置了虚函数表指针。在这个默认构造函数里,主要的代码就是上面这三行,首先获取虚表(将上面)的起始地址存放在rcx寄存器,然后加上16的偏移值跳过第一、二行,这时指向第三行数据,也就是第一个虚函数的位置,然后将这个地址赋值给[rax],rax是存放的对象的首地址,这就完成了给对象设置虚函数表指针。

        接着看main函数中对虚函数的调用:

main:								# @main# 略...# 调用构造函数mov     rdi, raxmov     qword ptr [rbp - 32], rdi       # 8-byte Spillcall    Object::Object() [base object constructor]mov     rax, qword ptr [rbp - 32]       # 8-byte Reloadmov     qword ptr [rbp - 16], rax# 调用第一个虚函数mov     rdi, qword ptr [rbp - 16]mov     rax, qword ptr [rdi]call    qword ptr [rax]# 调用第二个虚函数mov     rdi, qword ptr [rbp - 16]mov     rax, qword ptr [rdi]call    qword ptr [rax + 8]# 略...

        上面汇编代码中的第4行rax是调用new函数后返回来的地址,也就是pobj指针,把它存放到rdi寄存器中作为参数,同时也保存到栈空间rbp - 32中,然后调用构造函数,构造完成之后再拷贝这个地址到栈空间rbp - 16中。接下来的第10到12行是第一个虚函数的调用,将对象的首地址加载到rdi寄存器中,然后对其取内容,也就是是相当于指针的解引用,即 (*pobj),取得的内容即是构造函数中设置的虚函数表的地址,它是一个指向第一个虚函数的地址,然后第12行对其取内容,也即是对这个地址解引用,取得第一个虚函数的地址,然后以rdi寄存器(即对象的首地址)为第一个参数调用它,相当于:virtual_func1(pobj)。第14到16行是对第二个虚函数的调用,流程和第一个基本一样,区别在于将虚函数表的地址加上8的偏移量以指向第二个虚函数。

        如果在一个虚函数中调用另一个虚函数又会怎样?第一个虚函数已经决议出是调用哪个对象的实例了,那么在其中调用其它虚函数还需要再动态决议吗?把main函数中对第二个虚函数的调用去掉,在第一个虚函数中增加以下代码:

virtual_func2();
Object::virtual_func2();

        来看下对应生成的汇编代码,其它代码都差不多,主要看virtual_func1函数的代码:

Object::virtual_func1():            # @Object::virtual_func1()# 略...mov     rdi, qword ptr [rbp - 16]       # 8-byte Reloadmov     rax, qword ptr [rdi]call    qword ptr [rax + 8]mov     rdi, qword ptr [rbp - 16]       # 8-byte Reloadcall    Object::virtual_func2()# 略...

        rbp - 16保存的是对象的首地址,第3到5行对应的是上面C++代码中第一句的调用,看起来在虚函数中调用另一个虚函数,用的还是动态决议的方法,这里编译器没有识别出已经决议出具体的对象了。从汇编代码的第6、7行看到,通过前面加类名的限定符,是直接调用到这个函数,如果你明确调用的是哪个函数的话,可以直接在函数的前面加上类名,这样就不需要用多态的方式去调用了。

        如果不是通过指针类型来调用虚函数,而是通过对象来调用,结果是什么情况?把main函数改成如下:

int main() {Object obj;obj.virtual_func1();obj.virtual_func2();return 0;
}

        查看main函数对应的汇编代码:

main:                           # @main# 略...lea     rdi, [rbp - 16]call    Object::virtual_func1()lea     rdi, [rbp - 16]call    Object::virtual_func2()# 略...

        可以看到通过对象来调用虚函数,是直接调用到这个对象的函数实例的,没有使用多态的方式,所以通过对象的方式调用是没有多态的行为的,只有通过类的指针或者引用类型来调用虚函数,才会有多态的行为。

单一继承下的虚函数

        假设有以下的类定义及继承关系:

class Point {
public:Point(int x = 0) { _x = x; }virtual ~Point() = default;virtual void drawLine() = 0;int x() { return _x; }virtual int y() { return 0; }virtual int z() { return 0; }
private:int _x;
};
class Point2d: public Point {
public:Point2d(int x = 0, int y = 0): Point(x) { _y = y; }virtual ~Point2d() = default;void drawLine() override { }virtual void rotate() { }int y() override { return _y; }
private:int _y;
};
class Point3d: public Point2d {
public:Point3d(int x = 0, int y = 0, int z = 0): Point2d(x, y) { _z = z; }virtual ~Point3d() = default;void drawLine() override { }void rotate() override { }int z() override { return _z; }
private:int _z;
};int main() {Point* p = new Point3d(1, 1, 1);printf("z = %d\n", p->z());delete p;return 0;
}

        先来看看生成的汇编代码中的虚函数表:

vtable for Point:.quad   0.quad   typeinfo for Point.quad   Point::~Point() [base object destructor].quad   Point::~Point() [deleting destructor].quad   __cxa_pure_virtual.quad   Point::y().quad   Point::z()vtable for Point2d:.quad   0.quad   typeinfo for Point2d.quad   Point2d::~Point2d() [base object destructor].quad   Point2d::~Point2d() [deleting destructor].quad   Point2d::drawLine().quad   Point2d::y().quad   Point::z().quad   Point2d::rotate()vtable for Point3d:.quad   0.quad   typeinfo for Point3d.quad   Point3d::~Point3d() [base object destructor].quad   Point3d::~Point3d() [deleting destructor].quad   Point3d::drawLine().quad   Point2d::y().quad   Point3d::z().quad   Point3d::rotate()

        每个类都有一个对应的虚函数表,虚函数表中的内容主要来自于三方面:

  • 改写基类中对应的虚函数,用自己实现的虚函数的地址写入到对应表格中的位置;
  • 从基类中继承而来的虚函数,直接拷贝基类虚函数的地址添加到虚函数表中;
  • 新增的虚函数,基类中没有,子类的虚函数表会增加一行容纳新条目;

        基类和子类使用各自的虚函数表,互不干扰,即使子类中没有改写基类的虚函数,也没有新增虚函数,编译器也会为子类新建一个虚函数表,内容从基类中拷贝过来,内容和基类完全一样。

        虚函数表中的虚函数的排列顺序是固定的,一般是按照在类中的声明顺序,如C++代码中的这行代码:

        p->z();

        要寻址到正确的z函数实例的地址,我们首先需要知道p指针所指向的具体对象,然后需要知道z函数在表格中的位置,如上例中,z函数在第5个条目,也就是说虚函数表的起始地址加上32的偏移量就可以寻址到它,这个位置保持不变,无论p指针指向哪个对象,都能找到正确的z函数。如果子类中有新增的虚函数,新增的虚函数声明的位置插在从基类中继承来的虚函数中间,编译器会做调整,把它安排在后面,在原有的顺序上再递增,如上例中的rotate函数。


如果您感兴趣这方面的内容,请在微信上搜索公众号iShare爱分享并关注,或者扫描以下二维码关注,以便在内容更新时直接向您推送。

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

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

相关文章

邮件接口的安全性如何保障?如何有效使用?

邮件接口故障时如何处理&#xff1f;怎么设置和配置邮件接口&#xff1f; 邮件接口作为企业内外部沟通的重要桥梁&#xff0c;其安全性显得尤为重要。那么&#xff0c;邮件接口的安全问题如何保障。AokSend将深入探讨如何保障邮件接口的安全性&#xff0c;确保企业信息的安全传…

什么是仪器校准报告?

在科学实验和工业生产中&#xff0c;仪器是一种非常重要的辅助工具&#xff0c;无论是测量数据、控制实验进程还是保证产品质量&#xff0c;仪器都发挥着至关重要的作用。为了确保仪器的准确性和稳定性&#xff0c;仪器校准报告这一概念应运而生。本文给大家详细介绍仪器校准报…

科研基础与工具(论文写作)

免责申明&#xff1a; 本文内容只是学习笔记&#xff0c;不代表个人观点&#xff0c;希望各位看官自行甄别 参考文献 科研基础与工具&#xff08;YouTube&#xff09; 学术写作句型 Academic Phrase bank 曼彻斯特大学维护的一个网站 写论文的时候&#xff0c;不不知道怎么…

简单使用Web3.js

随着区块链技术的快速发展&#xff0c;以太坊&#xff08;Ethereum&#xff09;作为其中的佼佼者&#xff0c;为开发者们提供了一个构建去中心化应用的强大平台。为了更轻松地与以太坊区块链进行交互&#xff0c;我们需要一个强大的工具&#xff0c;而Web3.js正是这样一个工具。…

【Linux】IO多路转接技术Epoll的使用

【Linux】IO多路转接技术Epoll的使用 文章目录 【Linux】IO多路转接技术Epoll的使用前言正文接口介绍工作原理LT模式与ET模式边缘触发&#xff08;ET&#xff09;水平触发&#xff08;LT&#xff09; 理解ET模式和非阻塞文件描述符ET模式epoll实现TCP服务器简单地封装epoll系统…

【软件测试】认识测试|测试岗位|软件测试和开发的区别|优秀的测试人员需要具备的素质

一、什么是测试 测试在⽣活中处处可⻅ 1.生活中的测试场景 案例⼀&#xff1a;对某款购物软件进⾏测试 *启动测试&#xff1a;点击软件图标&#xff0c;测试软件是否可以正常打开 搜索测试&#xff1a;点击输入框&#xff0c;输入关键词&#xff0c;点击搜索 商品测试&#…

【数据结构】图论(图的储存方式,图的遍历算法DFS和BFS、图的遍历算法的应用、图的连通性问题)

目录 图论一、 图的基本概念和术语二、图的存储结构1. 数组(邻接矩阵)存储表示无向图的数组(邻接矩阵)存储表示有向图的数组(邻接矩阵)存储表示 邻接表存储表示有向图的十字链表存储表示无向图的邻接多重表存储表示 三、图的遍历算法图的遍历——深度优先搜索&#xff08;DFS&a…

.net core webapi 添加日志管理看板LogDashboard

.net core webapi 添加日志管理看板LogDashboard 添加权限管理&#xff1a; 我们用的是Nlog文件来配置 <?xml version"1.0" encoding"utf-8" ?> <nlog xmlns"http://www.nlog-project.org/schemas/NLog.xsd"xmlns:xsi"http:/…

软考高项(已通过,E类人才)-学习笔记材料梳理汇总

软考高项&#xff0c;即软考高级信息系统项目管理师&#xff0c;全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试中的高级水平测试。适用于从事计算机应用技术、软件、网络、信息系统和信息服务等领域的专业人员&#xff0c;以及各级企业管理人员和从事项目…

Netty学习——实战篇7 ProtoBuf实战-多类型数据

1 需求&#xff1a; 1、客户端可以随机发送Student POJO 、Worker POJO 对象到服务器(通过protobuf编码) 2、服务端能接收 Student POJO 、Worker POJO 对象(需要判断是哪种类型)&#xff0c;并显示信息(通过ProtoBuf解码) 2 开发 2.1 编写proto文件 MyDate.proto syntax &…

力扣练习题(2024/4/18)

1不相交的线 在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 现在&#xff0c;可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线&#xff0c;这些直线需要同时满足&#xff1a; nums1[i] nums2[j]且绘制的直线不与任何其他连线&#xff08;非水平线…

省级客运、货运量及周转量数据(1990-2022年)

1、数据介绍 客运量和货运量是衡量交通运输行业发展状况的重要指标&#xff0c;可以反映一个地区或国家的经济发展水平和人民生活水平。而周转量则是反映运输行业效率的指标&#xff0c;即货物或旅客被运输的总距离。 省级客运、货运量及周转量是衡量一个地区交通运输行业发展…

Excel如何计算时间差

HOUR(B1-A1)&"小时 "&MINUTE(B1-A1)&"分钟 "&SECOND(B1-A1)&"秒"

手拉手安装Kafka2.13发送和消费消息

Kafka是一种高吞吐量的分布式发布订阅消息系统&#xff0c;它可以处理消费者在网站中的所有动作流数据。 Kafka启动方式有Zookeeper和Kraft&#xff0c;两种方式只能选择其中一种启动&#xff0c;不能同时使用。 Kafka下载https://downloads.apache.org/kafka/3.7.0/kafka_2.…

C语言 选择控制结构(1) 了解选择结构 关系运算符讲解 基本逻辑判断演示

接下来 我们来说 选择控制结构 在生活中 我们也有很多需要分支结构的例子 比如: 计算两个整数的最大值 计算n个数的最大值&#xff0c;最小值 判断三角形三边能否构成三角形? 判断某年是否是闰年? 判断输入的英文字母是大写还是小写? 我们在程序开发中 需要根据某种条件 进…

Mysql 、Redis 数据双写一致性 更新策略与应用

零、important point 1. 缓存双写一致性问题 2. java实现逻辑&#xff08;对于 QPS < 1000 可以使用&#xff09; public class UserService {public static final String CACHE_KEY_USER "user:";Resourceprivate UserMapper userMapper;Resourceprivate Re…

学生选课及成绩查询管理系统的设计与开发C#(winform + sqlserver)

源码来自网络 技术栈&#xff1a; C#的窗体程序开发 本系统未采用C#实现MDI——多文档窗口&#xff0c;因为考虑到C#的该技术与java类似&#xff0c;而暑期java实训时&#xff0c;曾用过类似的方法做过停车场管理系统&#xff0c;所以想为这次的系统注入一点新鲜的血液&#x…

HTML随机点名程序

案例要求 1.点击点名按钮&#xff0c;名字界面随机显示&#xff0c;按钮文字由点名变为停止 2.再次点击点名按钮&#xff0c;显示当前被点名学生姓名&#xff0c;按钮文字由停止变为点名 案例源码 <!DOCTYPE html> <html lang"en"> <head> <m…

虚拟机中安装的CentOS7的桌面右上角没有网络图标解决方案

问题描述 今天在打开CentOS7后&#xff0c;发现右上角的网络图标不见了&#xff0c;然后命令行访问百度也不通。然后上网查了一些解决方法。 原因分析及解决方案&#xff1a; 上网查了许多解决方法&#xff0c;其中一种成功解决了我的问题&#xff1b;我的是配置文件的问题。…

1-内核开发环境ubuntu+virtualbox+mobaXterm搭建

内核开发环境 ubuntuvirtualboxmobaXterm搭建 目录 内核开发环境 ubuntuvirtualboxmobaXterm搭建 1.virtualbox 安装 2.ubuntu 安装 3.网络设置 4.虚拟机安装ssh 服务&#xff0c;更新ubuntu 源安装基本软件 5.mobaXterm 个人免费版本安装 6.总结 本课程教程从0-1开始教…