C/C++语言基础--C++神奇的多态

本专栏目的

  • 更新C/C++的基础语法,包括C++的一些新特性

前言

  • 通过前面几节课,我们学习了抽象、封装、继承相关的概念,接下来我们将讲解多态,多态他非常神奇,正式有了他,类才能出现多样性特征;
  • C语言后面也会继续更新知识点,如内联汇编;
  • 欢迎收藏 + 关注,本人将会持续更新。

文章目录

    • 问题思考?
    • 面向对象新需求
    • 多态的意义探究
      • 面向对象三大概念:
      • 多态成立的三要素:(结合解决方案)
    • 虚析构
    • 函数的重载、重写、重定义
      • 函数重载
      • 函数重定义
      • 虚函数重写
    • 纯虚函数和抽象类
      • 纯虚函数
      • 抽象类与接口
        • 抽象类特征
      • 关键字
        • abstract
        • final
    • 多态探究
      • 多态的理论基础
      • 获得虚函数表
      • 多态的本质(原理)
      • 虚函数简单介绍:
      • 虚函数图像
      • 如何证明vptr指针存在
      • 如何找到vptr指针呢

问题思考?

如果子类定义了与父类中定义相同函数会发生什么?如下面代码所示:

#include <iostream>using namespace std;class Parent
{
public:void show() {cout << "I am father" << endl;}
};class Son : public Parent
{
public:void show() {cout << "I am son" << endl;}
};void print(Parent& p) {p.show();
}int main()
{Parent pa;Son so;print(pa);print(so);  // 子赋值给父亲,可以当作父亲用return 0;
}

输出:

I am father
I am father

但是,如何在传入不同对象的时候输出相应的数据呢? 这个就是我们接下来要学的多态。

面向对象新需求

上面的这一种场景,需要C++需要做的事情是:

  • print函数中,传递什么对象调用什么对象的show函数,传递父类的,就调用父类的,传递子类的,就调用子类的。

解决方案:虚函数

  • 在父类中,在能让子类重写的函数前面必须加上virtual关键字
  • 在子类中,在重写的父类的虚函数后面加上override关键字,表示是虚函数重写(非必须,但是加上可以防止重写的虚函数写错)

虚函数重写概念

派生类(父类)中有一个跟基类(子类)完全相同的函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同)

多态的意义探究

面向对象三大概念:

封装:提取事物的属性与方法
继承:代码复用——可以用父类的代码
多态:在代码复用基础上,实现不同功能

案例:打印矩形和圆形坐标和面积

矩形:x,y,length,width

圆形:x,y,radius

在这个案例中,我们可以划分:

  • 封装:将矩形和圆形共有属性抽象出来,这里是x,y坐标,同时将共有方法抽象出来,这里是打印坐标和面积,将这些封装成一个基类A
  • 继承:分别定义矩形、圆形类,继承基类A,同时定义属于自己的属性或者方法,这里是矩形中定义属性length,width,圆形定义radius;
  • 多态:基类中定义了方法(打印坐标和面积),在圆形和矩形中分别重写这两个方法;
  • 测试:利用子类可以赋值给父类的特征,实现传入什么类就输出什么类对应的API。

代码实现如下:

#include <iostream>using namespace std;class Geometry
{
public:Geometry(int x, int y) : m_x(x), m_y(y) {}virtual void print_coordinates() {}virtual void print_area() {}int m_x;			// 测试:整形int m_y;
};class Rectangle : public Geometry
{
public:Rectangle(int x, int y, int width, int length): Geometry(x, y),m_width(width),m_length(length) {}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Rectangle area: " << m_width * m_length << std::endl;}int m_width;int m_length;
};class Round : public Geometry
{
public:Round(int x, int y, int riduas): Geometry(x, y),m_riduas(riduas){}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;}int m_riduas;
};void test(Geometry& various)
{various.print_coordinates();various.print_area();
}int main()
{Rectangle rect(1, 2, 3, 4);Round round(5, 6, 1);test(rect);test(round);return 0;
}

输出:

x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

多态成立的三要素:(结合解决方案)

  1. 要有继承:多态发生在父子类之间
  2. 要有虚函数重写:重写了虚函数,才能进行动态绑定
    1. (解决方案)
  3. 要有父类指针(引用)指向子类对象,传递参数的时候必须为引用或者指针,推荐常引用

虚析构

前置知识:

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数

析构函数可以是虚的。通过父类指针释放所有的子类资源

虚析构:

虚析构:通过父类去释放子类的时候,如果分类没有虚析构不会调用子类的析构函数的,会调用子类的析构函数,想要通过父类去释放子类, 必须在父类定义虚析构


让我们来看一下,这段代码结果会是什么:

#include <iostream>using namespace std;class Base
{
public:Base(){cout << __FUNCSIG__ << endl;}~Base(){cout << __FUNCSIG__ << endl;}
};class Derive : public Base
{
private:char* _str;
public:Derive(){_str = new char[10] { "wy" };cout << __FUNCSIG__ << endl;}~Derive(){delete _str;cout << __FUNCSIG__ << endl;}
};int main()
{Base* base = new Derive;       delete base;			return 0;
}

结果:

__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Base::~Base(void)

但是这个时候,子类的内存没有释放(_str),这样就造成了内存泄露问题😵😵😵😵😵😵


解决方法:

🔥虚析构:

  • 在父类析构函数中,加上关键字vartual
class Base
{
public:Base(){cout << __FUNCSIG__ << endl;}virtual ~Base()   // 加上virtual{cout << __FUNCSIG__ << endl;}
};

结果:

__cdecl Base::Base(void)
__cdecl Derive::Derive(void)
__cdecl Derive::~Derive(void)
__cdecl Base::~Base(void)

这样子类通过父类去释放,这样就能够自动识别是父类还是子类了,从而避免内存泄露🌝🌝🌝

函数的重载、重写、重定义

函数重载

  • 必须在同一个作用域相同
  • 子类无法重载父类的函数,父类同名函数将被名称覆盖
  • 重载是在编译期间根据参数类型和个数决定函数调用
int add(int a, int b) {  // 函数1return a + b;
}int add(int a, int b, int c) {   // 函数2return a + b + c;
}add(2, 3);  // 调用函数1
add(2, 3, 4);   // 调用函数2

函数重定义

  • 发生于父类和子类之间,如果子类写了个和父类函数原型一样的函数,并且父类中的函数没有声明为虚函数,则子类会直接覆盖掉父类的函数
  • 但是要注意,通过父类指针或引用执行子类对象时,会调用父类的函数

子类继承父类函数,且子类直接调用父类函数:

#include <iostream>using namespace std;class Parent
{
public:void show() {cout << "I am father" << endl;}
};class Son : public Parent
{
public:};int main()
{Parent pa;Son so;pa.show();so.show();return 0;
}

结果

I am father
I am father

但是如果这样:

// 在子类中
class Son : public Parent
{
public:void show() {   // 重新写show函数cout << "I am son" << endl;}
};

结果

I am father
I am son

📖 📖📖📖 ​ ​ 这样通过子类调用show,就调用的是子类定义的show的。

虚函数重写

  • 必须发生于父类和子类之间
  • 并且父类与子类中的函数必须有完全相同的原型
  • 必须使用virtual声明之后能够产生多态(如果不使用virtual,那叫重定义)
  • 多态是在运行期间根据具体对象的类型决定函数调用
// 如这个打印面积的案例
#include <iostream>using namespace std;class Geometry
{
public:Geometry(int x, int y) : m_x(x), m_y(y) {}virtual void print_coordinates() {}   // virtualvirtual void print_area() {}int m_x;			// 测试:整形int m_y;
};class Rectangle : public Geometry
{
public:Rectangle(int x, int y, int width, int length): Geometry(x, y),m_width(width),m_length(length) {}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Rectangle area: " << m_width * m_length << std::endl;}int m_width;int m_length;
};class Round : public Geometry
{
public:Round(int x, int y, int riduas): Geometry(x, y),m_riduas(riduas){}// 重写void print_coordinates() override{std::cout << "x: " << m_x << " y: " << m_y << std::endl;}void print_area() override{std::cout << "Round area: " <<  3.14 * m_riduas * m_riduas << std::endl;}int m_riduas;
};void test(Geometry& various)
{various.print_coordinates();various.print_area();
}int main()
{Rectangle rect(1, 2, 3, 4);Round round(5, 6, 1);test(rect);test(round);return 0;
}

结果:

统一调用:test(rect),test(round)的时候输出的:
x: 1 y: 2
Rectangle area: 12
x: 5 y: 6
Round area: 3.14

纯虚函数和抽象类

纯虚函数

纯虚函数也可以叫抽象函数,一般来说它只有函数名、参数和返回值类型,不需要函数体,这意味着它没有函数的实现需要让派生类去实现

C++中的纯虚函数,一般在函数签名后使用=0作为此类函数的标志。Java、C#等语言中,则直接使用abstract作为关键字修饰这个函数签名,表示这是抽象函数(纯虚函数)。

简单理解:如果类里面声明了纯虚函数,那么这个类就叫做抽象类,且抽象类无法定义对象

class Animal
{
public:virtual void cry() = 0;     //virtual 为虚函数标志,后面赋值 = 0,代表为这个为纯虚函数,则这个类为抽象类
}

抽象类与接口

接口:在C++里面,就是通过抽象类来实现接口的(不要在接口里面存放任何变量,一般只放虚函数

抽象类:是对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。

  • 通常在编程语句中用 abstract 修饰的类是抽象类。在C++中,含有纯虚拟函数的类称为抽象类,它不能生成对象;在java中,含有抽象方法的类称为抽象类,同样不能生成对象。
  • 抽象类是不完整的,它只能用作基类。
抽象类特征
  1. 抽象类不能实例化
  2. 抽象类和包含抽象方法(纯虚函数)、非抽象方法和属性
  3. 从抽象类派生的非抽象类,必须对继承过来的所有抽象方法实现

关键字

abstract

MSVC独有的关键字,申明类为抽象类

class  Animal abstract
{
};int main()
{Animal a;	//error C3622: “Animal”: 声明为“abstract”的类不能被实例化return 0;
}
final

C++标准关键字,结束的意思

  • 禁用虚函数重写

    class  Animal 
    {
    protected:virtual void show() final{}
    };class Dog final :public Animal
    {
    public:void show()override	//error C3248: “Animal::show”: 声明为“final”的函数无法被“Dog::show”重写{}
    };
  • 禁止该类被继承

    class  Animal  final
    {
    };class Dog final :public Animal //error C3246: "Dog": 无法从 "Animal" 继承,因为它已声明为 "final"
    {
    };

多态探究

参考博客: 详解

🌺 提示:多态概念很重要,但是概念同时也很容易忘记,可以先较为深入学习一下,记一下笔记,收藏一点资料,等要用到的时候再看,可以快速回忆。

多态的理论基础

静态联编和动态联编:联编是指一个程序模块、代码之间互相关联的过程。

  • 静态联编(关联),是程序的匹配、连接在编译阶段实现,也称为早期匹配。
    重载函数使用静态联编。

  • 动态联编(关联),是指程序联编推迟到运行时进行,所以又称为动态联编(迟绑定),将函数体和函数调用关联起来,就叫绑定
    switch 语句和 if 语句是动态联编的例子。

    那么C++中的动态联编是如何实现的呢?
    如果我们声明了类中的成员函数为虚函数,那么C++编译器会为类生成一个虚函数表,通过这个表即可实现动态联编

获得虚函数表

#include<iostream>
//1、得到虚函数
//2、验证不同兄弟类虚函数都是一样的
class A
{
public:virtual void a(){std::cout << __FUNCSIG__ << std::endl;}virtual void b(){std::cout << __FUNCSIG__ << std::endl;}virtual void c(){std::cout << __FUNCSIG__ << std::endl;}
private:int x;int y;
};typedef void(*func)();  //使用函数指针,强制转换成函数int main()
{A a, b;uint64_t* p = (uint64_t*)&a;uint64_t* arr = (uint64_t*)*p;func fa = (func)arr[0];func fb = (func)arr[1];func fc = (func)arr[2];fa();fb();fc();uint64_t* pp = (uint64_t*)&b;uint64_t* arr2 = (uint64_t*)*pp;std::cout << arr << " " << arr2 << std::endl;return 0;
}
  • 继承虚函数
#include<iostream>
/*
* 1、父类虚函数和子类虚函数
* 2、兄弟虚函数
* 3、继承虚函数
*///继承虚函数表和重写
class A
{
public:virtual void a(){std::cout << __FUNCSIG__ << std::endl;}virtual void b(){std::cout << __FUNCSIG__ << std::endl;}virtual void c(){std::cout << __FUNCSIG__ << std::endl;}
private:int x;int y;
};class B : public A
{
public:void b() override{ std::cout << __FUNCSIG__ << std::endl; } 
};typedef void(*func)();  //使用函数指针,强制转换成函数int main()
{A a;B b;uint64_t* pa = (uint64_t*)&a;uint64_t* arra = (uint64_t*)*pa;uint64_t* pb = (uint64_t*)&b;uint64_t* arrb = (uint64_t*)*pb;func faa = (func)arra[0];func fab = (func)arra[1];func fac = (func)arra[2];func fba = (func)arrb[0];func fbb = (func)arrb[1];func fbc = (func)arrb[2];faa();fab(); fac();fba(); fbb(); fbc();return 0;
}

ss

多态的本质(原理)

虚函数表是顺序存放虚函数地址的,虚表是顺序表(数组),依次存放着类里面的虚函数。
虚函数表是由编译器自动生成与维护的,相同类的不同对象的虚函数表是一样的

在这里插入图片描述

既然虚函数表,是一个顺序表,那么它的首地址存放在哪里呢?

  • 当我们在类中定义了virtual函数时,C++编译器会偷偷的给对象添加一个vptr指针,vptr指针就是存的虚函数表的首地址

虚函数简单介绍:

  • 虚函数表存放了类的虚函数(就是一个函数指针数组)
  • 虚函数的指针分布初始化:创建子类对象的时候,会先构造父类,构造父类的时候,父类的虚函数指针,指向自己的虚函数(父类构造完后会构造子类,这个时候父类的虚函数指针,会指向类的虚函数标)
  • 在构造函数里面禁止使用虚函数(因为分布初始化还没有完成,可能得到不正确的结果)

虚函数图像

  • 三层

如何证明vptr指针存在

我们可以通过求出类的大小判断是否有vptr的存在

class Dog
{void show() {}
};class Cat
{virtual void show() {}
};int main()
{cout << "Dog size:" << sizeof(Dog) << " Cat size:" << sizeof(Cat) << endl;return 0;
}
output: Dog size:1 Cat size:8

通过调试确实能看到vptr指针的存在,而且存放在对象的第一个元素

在这里插入图片描述

如何找到vptr指针呢

既然vptr指针存在,那么能不能拿到vptr指针,手动来调用函数呢?

答案是可以的,利用它存在对象的第一个元素特征,但是操作起起来很麻烦,以下过程也是我收集资料学习到的。

思路:核心(存放在第一个对象元素)

  • 首先定义一个子类,拿取子类地址;
  • 接着将子类地址转化成long long*类型,再次解引用,这样就告诉编译器,这个指向子类指针,没有子类约束,并且这个类型是long long类型了,这个时候就拿到了对象第一个元素,很绕,但是没办法;
  • 再接着,将这个类型重新转化为指针long long
  • 这个时候就可以通过指针转化成不同定义的函数指针,转化成相应的函数调用。

步骤:

  1. 因为vptr指针在对象的第一个元素(通过证明vptr指针的存在可以看出),所以对对象t取地址可以拿到对象的地址

    Parent* p = &obj;
  2. 现在拿到的指针的步长是对象的大小,因为vptr是指针,只有4/8个字节,所以需要把p强转成int*指针,这样对(int*)&t就得到了vptr指针

    int vptr = *(int*)p;	//拿到了vptr指针的指针
    int* pvptr = (int*)vptr; //把vptr的值转成指针
  3. 因为vptr指针是指向的存储指针数组的首地址,所以拿到vptr指针后先把vptr转成int*指针,这样进行取值的话,刚好是每个指针

    FUN foo = (FUN)*(pvptr+0)  // 获取元素
  4. 接着吧得到的数组里面的元素(指针)转成函数指针,即可直接使用了

🤠 结果

#include <iostream>using namespace std;using FUN = void(*)();   // (*) 代表是一个指针,指向一个void类型函数class Parent
{
public:virtual void func1(){cout << "Parent::func1()" << endl;}virtual void func2(){cout << "Parent::func2()" << endl;}
};class Child : public Parent
{
public:void func1() override{cout << "Child::func1()" << endl;}void func2() override{cout << "Child::func2()" << endl;}
};int main()
{Child obj;Parent* p = &obj;long long vptr = *(long long*)p;long long* pvptr = (long long*)vptr;auto foo = (FUN) * (pvptr + 1);foo();return 0;
}

🍼 输出:

Child::func2()

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

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

相关文章

【C++差分数组】P1672何时运输的饲料

本文涉及知识点 C差分数组 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 P1672何时运输的饲料 原文比较啰嗦&#xff0c;我简述一下&#xff1a; 第x天运来F1(1<F1<1e6)千克的饲料&#xff0c;第D&#xff08;1<2e3)天还剩F2&…

DBA | 如何将 .mdf 与 .ldf 的数据库文件导入到SQL Server 数据库中?

[ 知识是人生的灯塔&#xff0c;只有不断学习&#xff0c;才能照亮前行的道路 ] 原文链接&#xff1a;DBA | 如何将 .mdf 与 .ldf 的数据库文件导入到SQL Server 数据库中? 如何将 (.mdf) 和 (.ldf) 的SQL Server 数据库文件导入到当前数据库中? Step 1.登录到 Sql Server 服…

【Nginx系列】Nginx启动失败

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

数学建模算法与应用 第12章 现代优化算法

目录 12.1 粒子群优化算法 Matlab代码示例&#xff1a;粒子群优化算法求解函数最小值 12.2 遗传算法 Matlab代码示例&#xff1a;遗传算法求解函数最小值 12.3 蚁群算法 Matlab代码示例&#xff1a;蚁群算法求解旅行商问题 12.4 Matlab 遗传算法工具 使用遗传算法工具箱…

PyCharm打开及配置现有工程(详细图解)

本文详细介绍了如何利用Pycharm打开一个现有的工程&#xff0c;其中包括编译器的配置。 PyCharm打开及配置现有工程 1、打开工程2、配置编译器 1、打开工程 双击PyCharm软件&#xff0c;点击左上角 文件 >> 打开(O)… 选中想要打开的项目之后点击“确定” 2、配置编译器…

余承东直播论道智能驾驶:激光雷达不可或缺,华为ADS 3.0引领安全创新

华为余承东:激光雷达,智能驾驶安全性的关键 9月29日,华为消费者业务集团CEO余承东在一场引人注目的直播中,与知名主持人马东就智能驾驶技术的最新进展进行了深入交流。在这场直播中,余承东针对激光雷达在智能驾驶中的必要性问题,发表了明确且深刻的观点,引发了业界和公众…

uniapp自定义导航,全端兼容

我们在用uniapp 开发应用的时候&#xff0c;有的页面需要自定义导航&#xff0c; 1.如果普通的直接使用uni 扩展柜组件的 uni-nav-bar 也基本够用&#xff0c; 2.如果稍微带点自定义的这个值无法支持的&#xff0c;特别在小程序端&#xff0c;胶囊是会压住右边的按钮的 自定…

小白必看web专题!PHP-WebShell免杀(基础版)!!真的很简单!(全网最详细版本)

大家好&#xff0c;我是Dest1ny&#xff01; 最近一直在搞辅导啥的&#xff0c;所以没啥时间搞写&#xff5e; 也谢谢大家一直的点赞&#xff0c;今天特意把之前的web专题再发一个。 废话不多说&#xff0c;我们直接开始&#xff01; CLASS-1 WebShell免杀测试 渊龙Sec团队导…

PyQt5常用功能三

日历 QCalendarWidget 提供了基于⽉份的⽇历插件&#xff0c;⼗分简易⽽且直观 from PyQt5.QtWidgets import (QWidget, QCalendarWidget,QLabel, QApplication, QVBoxLayout) from PyQt5.QtCore import QDate import sysclass Example(QWidget):def __init__(self):super().…

Linux高阶——0928—Github本地仓库与云端仓库关联

1、安装代理软件 steam 选择Github和系统代理模式&#xff0c;一键加速即可 2、 安装Git 3、访问Github网站&#xff0c;创建新用户 4、Github探索 &#xff08;1&#xff09;Explore探索标签 &#xff08;2&#xff09;工程结构 用户名/仓库名 自述文件&#xff0c;用markdo…

SPI通信——FPGA学习笔记14

一、简介 SPI(Serial Periphera Interface&#xff0c;串行外围设备接口)通讯协议&#xff0c;是 Motorola 公司提出的一种同步串行接口技术&#xff0c;是一种高速、全双工、同步通信总线&#xff0c;在芯片中只占用四根管脚用来控制及数据传输&#xff0c;广泛用于 EEPROM、F…

Redis配置篇 - 指定Redis配置的三种方式,以及Redis配置文件介绍

文章目录 1 指定Redis配置的三种方式1.1 通过命令行参数来指定Redis配置1.2 通过配置文件来指定Redis配置1.3 在服务器运行时更​​改 Redis 配置 2 关于Redis配置文件 1 指定Redis配置的三种方式 1.1 通过命令行参数来指定Redis配置 在redis启动时&#xff0c;可以直接通过命…

绿野仙踪不仅是童话,还是便宜又好用的产品测试法!

以 ChatGPT 为代表的大语言模型爆火后&#xff0c;推动了对话类人工智能产品的高速发展&#xff0c;我们已经看到了如智能助理、问答系统、自动写作等多种类型的个性化对话类 AI 服务。 AI 能力的提升让人们对智能 AI 产品的期望越来越高&#xff0c;相关产品的用户体验也因此变…

豆包MarsCode 合伙人计划限时招募中,推广最高赢万元现金!

豆包MarsCode 合伙人计划正式上线啦&#xff01;作为官方推出的推广激励项目&#xff0c;豆包MarsCode 编程助手号召和鼓励所有用户向我们推荐新用户。 现在正式开启首轮合伙人招募&#xff0c;诚邀各位有意愿推广普及 AI 编程产品的伙伴成为我们的合伙人&#xff0c;全国限量…

jmeter输出性能测试报告(常见问题处理与处理)

问题1&#xff1a;报错 WARNING: Could not open/create prefs root node Software\JavaSoft\Prefs at root 0x80000002. Windows R 意思是&#xff1a;报没有权限 处理&#xff1a; 操作非gui生成测试报告的方法 cmd界面进入到 jmeter的bin目录 jmeter –n –t -l -e –o …

对后端返回的日期属性进行格式化(扩展 Spring MVC 的消息转换器)

格式化之前 格式化之后&#xff1a; 解决方式 方式一 在属性中加上注解&#xff0c;对日期进行格式化 JsonFormat(pattern "yyyy-MM-dd HH:mm:ss")private LocalDateTime createTime;//JsonFormat(pattern &quo…

利用FnOS搭建虚拟云桌面,并搭建前端开发环境(一)

利用FnOS搭建虚拟云桌面&#xff0c;并搭建前端开发环境 一 飞牛FnOS官方文档一、安装FnOS【Win11系统】1.下载VirtualBox2.下载FnOS镜像3.创建虚拟机4.启动完成后&#xff0c;会进入这样一个界面&#xff0c;这个基本上后续就后台了 本人在网上冲浪了很久&#xff0c;一直也没…

DGX的优势

NVIDIA DGX 的 AI 领导力 文章目录 前言一、概述推动跨行业的 AI 创新二、优势客户体验到哪些好处?1. 利用生成式 AI 释放研究人员的潜力2. 加快现代应用程序的上市时间3. 利用 AI 改善客户体验三、性能性能很重要1. 为世界上最先进的超级计算机提供动力2. 打破世界纪录3. 提高…

ES6总结

1.let和const以及与var区别 1.1 作用域 var&#xff1a; 变量提升&#xff08;Hoisting&#xff09;&#xff1a;var 声明的变量会被提升到其作用域的顶部&#xff0c;但赋值不会提升。这意味着你可以在声明之前引用该变量&#xff08;但会得到 undefined&#xff09;。 con…

CSS元素显示类型

display 属性是 CSS 中最重要的属性之一&#xff0c;主要用来控制元素的布局&#xff0c;通过 display 属性您可以设置元素是否显示以及如何显示。 根据元素类型的不同&#xff0c;每个元素都有一个默认的 display 属性值&#xff0c;例如<div>默认的 display 属性值为 …