[C++] 剖析多态的原理及实现

news2024/9/25 7:14:30

文章目录

  • 多态的概念及定义
    • 编译时多态(静态多态)
    • 运行时多态(动态多态)
      • 动态多态的原理
          • 示例:运行时多态
    • 两种多态的区别
  • 多态的实现
    • 基本条件
    • 虚函数
    • 虚函数的重写与覆盖
    • 虚函数重写的其他问题
      • 协变
      • 析构函数的重写
    • C++11 中的 `override` 和 `final` 关键字
    • 重载、重写和隐藏的对比
      • 重载(Overloading)
      • 重写(Overriding)
      • 隐藏(Hiding)
  • 纯虚函数和抽象类
    • 纯虚函数(Pure Virtual Function)
    • 抽象类(Abstract Class)
  • 多态的原理
    • 虚函数表指针(vptr)
    • 多态实现的原理
      • 如何实现多态?
      • 动态绑定和静态绑定
      • 虚函数表
  • 练习题
    • 多态场景的⼀个选择题
      • 执行流程:

多态的概念及定义

多态(Polymorphism)是面向对象编程中的一个重要概念,它使得同一个行为可以针对不同类型的对象表现出不同的形态。通俗来讲,多态就是“多种形态”的实现。

根据执行的时机,多态可以分为两种类型:

  1. 编译时多态(静态多态)
  2. 运行时多态(动态多态)

编译时多态(静态多态)

编译时多态,顾名思义,就是在编译期间决定函数调用的行为。它通过以下两种方式实现:

  • 函数重载:同名函数可以根据不同的参数类型或数量,做出不同的实现。
  • 模板:函数模板或类模板能够针对不同的类型参数生成不同的代码。

静态多态的特点是函数调用的解析过程在编译时就完成了。例如,函数重载通过传入不同的参数类型,编译器在编译时选择正确的函数版本。

函数重载示例:

void print(int i) {
    std::cout << "Integer: " << i << std::endl;
}

void print(double d) {
    std::cout << "Double: " << d << std::endl;
}

int main() {
    print(10);    // 输出: Integer: 10
    print(3.14);  // 输出: Double: 3.14
    return 0;
}

编译时多态通常称为静态绑定,因为在编译阶段就已经确定了实际调用的函数。这使得编译时多态非常高效,但不具备灵活的运行时决策能力。

运行时多态(动态多态)

运行时多态是在程序运行时,根据实际传入的对象类型来决定函数的具体实现。这种形式的多态依赖于继承虚函数

动态多态的原理

动态多态的核心思想是基类定义了接口(虚函数),而派生类根据自己的需求对这些接口进行不同的实现。在运行时,调用具体派生类的实现,而不是基类的实现。

实现动态多态有两个**必要条件****:**

  1. 基类的函数必须是虚函数,即使用<font style="background-color:#FBDE28;">virtual</font>关键字声明。
  2. 必须通过基类的指针或引用来调用虚函数。
示例:运行时多态

假设有一个“买票”行为,不同类型的对象执行该行为时有不同的表现:

class Person {
public:
    virtual void BuyTicket() {
        std::cout << "买票-全价" << std::endl;
    }
};

class Student : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-打折" << std::endl;
    }
};

class Soldier : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-优先" << std::endl;
    }
};

void BuyTicketForPerson(Person* person) {
    person->BuyTicket();  // 调用具体对象的BuyTicket函数
}

int main() {
    Person p;
    Student s;
    Soldier so;

    BuyTicketForPerson(&p);  // 输出: 买票-全价
    BuyTicketForPerson(&s);  // 输出: 买票-打折
    BuyTicketForPerson(&so); // 输出: 买票-优先
    return 0;
}

在这个例子中,BuyTicketForPerson 函数接收一个基类Person的指针,但是在实际调用时,动态多态会根据具体传入的对象类型,调用派生类的BuyTicket函数。对应不同人群的买票价格不一样。

两种多态的区别

  • 编译时多态:通过函数重载和模板实现,函数调用在编译阶段确定,效率高,但灵活性较低。
  • 运行时多态:通过虚函数和继承实现,基类指针或引用根据实际对象类型调用对应的函数实现,具有更大的灵活性,但需要在运行时进行决策。

多态的实现

基本条件

  1. 通过基类的指针或引用调用虚函数:多态的前提是通过基类的指针或引用来访问派生类对象。只有基类的指针或引用才能够指向不同的派生类对象,并且根据派生类对象的实际类型,决定具体调用哪个函数。
  2. 函数必须是虚函数:要想在运行时根据对象的实际类型调用不同的函数实现,基类中的函数必须声明为虚函数(virtual)。虚函数机制使得调用操作在运行时决定,而不是在编译时。
  3. 派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到

虚函数

虚函数是实现多态的核心。在C++中,类的成员函数前加上virtual关键字,就将其声明为虚函数。虚函数允许派生类重写该函数,并在运行时根据实际对象类型调用具体实现

class Person {
public:
    virtual void BuyTicket() {
        std::cout << "买票-全价" << std::endl;
    }
};

BuyTicket是一个虚函数,允许派生类重写它。

虚函数的重写与覆盖

重写(Override)是指派生类对基类的虚函数提供新的实现。派生类中的虚函数必须和基类虚函数的签名完全相同,即**返回类型、函数名、参数列表**必须一致。只有这样,才能保证派生类重写了基类的虚函数。

**注意:**尽管在派生类中可以省略virtual关键字,但不建议这样做,因为它可能会导致可读性和可维护性的下降。

现在可以分析开头引入多态概念的带代码:

  1. 虚函数的重写
class Person {
public:
    virtual void BuyTicket() {
        std::cout << "买票-全价" << std::endl;
    }
};

class Student : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-打折" << std::endl;
    }
};

Student类重写了Person类的BuyTicket函数。当通过基类指针或引用调用时,实际执行的是派生类的BuyTicket函数。

  1. 使用虚函数实现多态
void Func(Person* ptr) {
    // person* 的ptr会对传入的派生类对象进行切片操作
    // 尽管ptr是Person类型的指针,实际调用的函数由ptr指向的对象决定
    ptr->BuyTicket();
}

int main() {
    Person ps;
    Student st;
    
    Func(&ps);  // 输出: 买票-全价
    Func(&st);  // 输出: 买票-打折
    return 0;
}

通过Person类型的指针调用BuyTicket,具体执行的函数取决于指针实际指向的对象。对于Student对象,将调用其重写的BuyTicket函数。

虚函数重写的其他问题

协变

当派生类重写基类的虚函数时,如果基类虚函数返回基类类型的指针或引用,派生类虚函数可以返回派生类类型的指针或引用。这种情况称为协变

class A {};
class B : public A {};

class Person {
public:
    virtual A* BuyTicket() {
        std::cout << "买票-全价" << std::endl;
        return nullptr;
    }
};

class Student : public Person {
public:
    B* BuyTicket() override {
        std::cout << "买票-打折" << std::endl;
        return nullptr;
    }
};

析构函数的重写

在使用多态时,基类的析构函数应该声明为虚函数,否则会出现内存泄漏问题。如果基类析构函数不是虚函数,那么通过基类指针删除派生类对象时,只会调用基类的析构函数,派生类的析构函数不会被调用,导致资源无法释放。

虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成<font style="color:rgb(31,35,41);">destructor</font>,所以基类的析构函数加了<font style="color:rgb(31,35,41);">vialtual</font>修饰,派⽣类的析构函数就构成重写

class A {
public:
    virtual ~A() {
        std::cout << "~A()" << std::endl;
    }
};

class B : public A {
public:
    ~B() {
        std::cout << "~B()" << std::endl;
    }
};

int main() {
    A* p = new B;
    delete p;  // 正确调用B的析构函数
    return 0;
}

C++11 中的 overridefinal 关键字

为了防止虚函数重写时出现意外情况,C++11引入了overridefinal关键字。

  • override:确保派生类的函数确实是重写了基类的虚函数。如果函数签名不匹配,编译器会报错。
  • final:用于禁止派生类进一步重写某个虚函数。
class Car {
public:
    virtual void Drive() final {
        std::cout << "Car driving" << std::endl;
    }
};

class Benz : public Car {
    // 编译错误,不能重写final函数
    // void Drive() override { std::cout << "Benz driving" << std::endl; }
};

重载、重写和隐藏的对比

重载(Overloading)

重载是指在同一个类中,存在多个同名函数,它们的参数列表不同(参数类型或数量)。重载函数在编译时通过传递给函数的参数类型或数量来确定调用哪个函数。

特点:

  • 发生在同一个作用域中(同一类或同一个函数)。
  • 函数名相同,但参数列表必须不同(类型或数量不同)。
  • 重载与返回值无关,返回值类型不能用于区分重载。
class Example {
public:
    void print(int i) {
        std::cout << "Integer: " << i << std::endl;
    }

    void print(double d) {
        std::cout << "Double: " << d << std::endl;
    }

    void print(std::string s) {
        std::cout << "String: " << s << std::endl;
    }
};

print函数被重载了三次,分别接受intdoublestd::string类型的参数。调用时,根据参数类型选择相应的print函数。

重写(Overriding)

重写是指在继承关系中,派生类对基类的虚函数重新实现。当基类中有虚函数时,派生类可以重写该虚函数,从而在运行时根据实际对象的类型调用对应的函数实现。

特点

  • 发生在继承层次结构中。
  • 基类中的函数必须是虚函数virtual),且派生类的函数与基类虚函数具有相同的签名(即返回值、参数列表必须一致)。
  • 运行时根据对象的实际类型调用对应的派生类或基类函数,实现动态多态
  • 派生类函数可以使用override关键字明确表示重写。
class Base {
public:
    virtual void show() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void show() override {
        std::cout << "Derived class" << std::endl;
    }
};

Derived类重写了Base类的show函数。当通过基类指针调用show函数时,具体调用哪个函数取决于实际的对象类型。

隐藏(Hiding)

隐藏是指在派生类中定义了一个与基类同名但非虚的函数,此时基类的同名函数会被隐藏。隐藏的函数在派生类中无法通过对象或指针访问,除非显式地使用作用域解析符调用基类版本的函数。

特点

  • 发生在继承层次结构中。
  • 隐藏的函数与重写不同,隐藏的函数不是虚函数,因此不会参与动态多态机制。
  • 派生类函数的签名可以与基类相同,也可以不同,但一旦存在同名函数,基类函数就会被隐藏。
  • 可以通过基类的作用域解析符调用基类函数。
class Base {
public:
    void show() {
        std::cout << "Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void show(int i) {
        std::cout << "Derived class with int: " << i << std::endl;
    }
};

Derived类的show(int i)函数隐藏Base类的show()函数。通过Derived类的对象,无法调用Base类的show()函数。

如果需要访问基类函数,可以如下使用:

Derived d;
d.show(10);          // 调用 Derived 类的 show(int)
d.Base::show();      // 调用 Base 类的 show()

画板

纯虚函数和抽象类

纯虚函数(Pure Virtual Function)

在C++中,虚函数后加= 0,就将该函数声明为纯虚函数。纯虚函数没有具体实现,只提供接口,要求派生类必须实现该函数。通过纯虚函数,C++允许程序设计者定义一个抽象的接口,并要求任何继承该接口的类必须实现这些接口方法。

  • 定义:虚函数在声明时,末尾加= 0,表明它是一个纯虚函数,无法在基类中实现。
  • 特点:纯虚函数只需要声明,不需要定义。
class Car {
public:
    virtual void Drive() = 0;  // 纯虚函数
};

Car类不能直接实例化,因为它包含了纯虚函数,必须由派生类来实现。

抽象类(Abstract Class)

抽象类是指包含一个或多个纯虚函数的类。抽象类不能被实例化,必须通过派生类进行实例化。抽象类的作用是为派生类提供统一的接口,使得多个派生类可以通过相同的接口进行调用,从而实现多态。

  • 特点:抽象类不能被直接实例化,它只能作为基类存在。
  • 派生类要求:派生类必须实现抽象类中的所有纯虚函数,否则派生类也将成为抽象类,无法实例化。
class Car {
public:
    virtual void Drive() = 0;  // 纯虚函数
};

class Benz : public Car {
public:
    void Drive() override {
        std::cout << "Benz-舒适" << std::endl;
    }
};

class BMW : public Car {
public:
    void Drive() override {
        std::cout << "BMW-操控" << std::endl;
    }
};

int main() {
    // Car car; // 错误,Car是抽象类,不能实例化

    Car* pBenz = new Benz;
    pBenz->Drive();  // 输出: Benz-舒适

    Car* pBMW = new BMW;
    pBMW->Drive();   // 输出: BMW-操控

    delete pBenz;
    delete pBMW;
    return 0;
}

Car是一个抽象类,因为它包含了纯虚函数DriveBenzBMW继承自Car,并且实现了Drive函数。因此,BenzBMW对象可以通过Car类型的指针实现多态调用。

多态的原理

虚函数表指针(vptr)

每个包含虚函数的对象都有一个隐藏的指针,称为虚函数表指针(vptr)。该指针指向该类的虚函数表,虚函数表中存储了类中虚函数的地址。

  • vptr的作用:vptr用于指向对象的虚函数表,帮助程序在运行时通过虚函数表找到具体的函数实现。
  • vptr的存储位置:vptr通常位于对象内存布局的开头,但这取决于编译器的实现。在某些平台上,vptr可能会位于对象的最后。

虚函数表指针用来指向当前对象对应的虚函数表(虚表)

多态实现的原理

如何实现多态?

class Person {
public:
    virtual void BuyTicket() {
        std::cout << "买票-全价" << std::endl;
    }
};

class Student : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-打折" << std::endl;
    }
};

class Soldier : public Person {
public:
    void BuyTicket() override {
        std::cout << "买票-优先" << std::endl;
    }
};

void Func(Person* ptr) {
    ptr->BuyTicket();  // 通过指针调用虚函数
}

int main() {
    Person ps;
    Student st;
    Soldier sr;

    Func(&ps);  // 输出: 买票-全价
    Func(&st);  // 输出: 买票-打折
    Func(&sr);  // 输出: 买票-优先
    return 0;
}

Person*类型的ptr作为Func的形参,用来指向接受的对象并进行切片。当运行的时候,多态展现出来,并不是使用PersonBuyTicket。通过上述图片发现当每次将不同类型传入Funcptr调用的都是接受的那个对象的类的BuyTicket。这样就是实现了指针或引用指向基类或者派生类直接调用指向类的虚函数。实现了多态。

动态绑定和静态绑定

  • 静态绑定:编译器在编译时已经确定了函数调用的地址,通常用于普通函数(不满足多态条件)。由于函数地址在编译时已经确定,静态绑定非常高效。
  • 动态绑定:程序在运行时根据对象的实际类型确定函数的调用地址,通常用于虚函数。这种方式提供了极大的灵活性,但运行时效率相对静态绑定较低。
// ptr是指针+BuyTicket是虚函数满⾜多态条件。

// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();

00EF2001 mov eax,dword ptr [ptr]
00EF2004 mov edx,dword ptr [eax]
00EF2006 mov esi,esp
00EF2008 mov ecx,dword ptr [ptr]
00EF200B mov eax,dword ptr [edx]
00EF200D call eax

// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();

00EA2C91 mov ecx,dword ptr [ptr]
00EA2C94 call Student::Student (0EA153Ch)

虚函数表

  • 基类的虚函数表:基类的虚表中存放该类所有虚函数的地址。当基类中的虚函数未被派生类重写时,派生类的虚表会继承这些地址。
  • 派生类的虚函数表:当派生类重写了基类的虚函数,派生类的虚表中的相应条目会替换为派生类的虚函数地址。派生类的虚表包含三类地址:
  1. 基类的虚函数地址:未被派生类重写的基类虚函数。
  2. 重写的虚函数地址:派生类对基类虚函数的重写。
  3. 派生类特有的虚函数地址:派生类定义的独有虚函数。
    1. 派生类独有的虚函数地址被存放在虚函数表的最后
    2. 如果一个派生类继承了多个有虚函数的类,一个类对应一个虚函数表,派生类独有的虚函数地址存放在第一个虚函数表的后面。
  • 派生类的虚函数表与基类相独立

  • 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函
    数地址。

  • 虚函数表的底层工作原理

    • 当通过基类指针调用虚函数时,程序会:
  1. 通过对象的vptr访问该对象的虚函数表。
  2. 在虚表中找到对应函数的地址。
  3. 通过动态绑定机制)调用函数,函数的具体实现取决于虚表中存储的地址。

虚函数表是一个由虚函数指针构成的数组,虚表的最后可能会存储一个标记(如0x00000000),用来表示数组的结束(不同的编译器可能会有不同的实现)。

  • 虚函数存储在哪?

虚函数和普通函数一样,编译后会变成一段机器指令,并被存储在代码段(Code Segment)中。虚函数表本质上是指向这些指令的指针数组。

+-------------------------+
|       代码段            |   --> 存储所有的函数(包括虚函数)的指令代码
+-------------------------+
|    只读数据段/常量区    |   --> 存储常量字符串、只读数据(如虚表)
+-------------------------+
|       全局数据区        |   --> 存储全局和静态变量
+-------------------------+
|       堆(Heap)        |   --> 动态分配的对象(如new分配的对象)
+-------------------------+
|       栈(Stack)       |   --> 存储局部变量和函数调用的上下文
+-------------------------+

练习题

多态场景的⼀个选择题

以下程序输出结果是什么( )
A:A->0B:B->1C:A->1D:B->0E:编译出错F:以上都不正确

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(int argc, char* argv[])
{
    B* p = new B;
    p->test();

    return 0;
}
  1. 虚函数与默认参数
    • 在C++中,默认参数的绑定是在编译时完成的,而虚函数调用的解析是在运行时完成的。尽管函数调用的解析在运行时根据对象的类型调用了B类的func,但是默认参数的值是在编译时绑定的,它依然使用了基类**A**的默认参数值
  2. 详细过程
    • 当调用p->test()时,程序首先执行test函数,这个函数是A类中的虚函数。
    • test()函数内部调用了func(),由于func是虚函数,调用的是B类重写的func
    • 虽然B类的func函数被调用了,但是默认参数val是在编译时绑定的,所以val的值仍然是基类**A**的默认值**1**,而不是B类中的0
    • 因此,func()的输出是B->1

执行流程:

  1. 调用p->test()
    • 调用的是A类中的test函数,执行test()中的func()调用。
  2. func()是虚函数,实际调用的是B类的func
  3. 虽然调用了B类的func,但是由于默认参数val在编译时已绑定为1(基类A的默认参数),所以输出B->1

或者说:重写的是函数体部分

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

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

相关文章

【数据结构】8——图3,十字链表,邻接多重表

数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表 文章目录 数据结构8——图3&#xff0c;十字链表&#xff0c;邻接多重表前言一、十字链表结构例子 复杂例子 二、邻接多重表&#xff08;Adjacency Multilist&#xff09;例子 前言 除了之前的邻接矩阵和邻接表 …

Kubernetes部署及示例

目录 一、实验环境 二、部署 1、添加解析 2、安装docker&#xff0c;确保登录成功 3、所有禁用swap和本地解析 4、 安装K8S部署工具 5、集群初始化 6、安装flannel网络插件 7、节点扩容 三、kubernetes 中的资源 1、资源管理介绍 2、资源管理方式 &#xff08;…

【Kubernetes】服务账号 Service Account

《K8s 的安全认证》系列&#xff0c;共包含以下文章&#xff1a; K8s 的安全框架和用户认证K8s 的鉴权管理&#xff08;一&#xff09;&#xff1a;基于角色的访问控制&#xff08;RBAC 鉴权&#xff09;K8s 的鉴权管理&#xff08;二&#xff09;&#xff1a;基于属性 / 节点…

Mac导入iPhone的照片怎么删除?快速方法讲解

随着Apple生态系统的高度整合&#xff0c;Mac与iPhone之间的照片同步和导入变得异常便捷。但这种便利有时也会带来一些管理上的困扰&#xff0c;比如Mac导入iPhone的照片怎么删除&#xff1f; 从iPhone直接删除照片 Mac导入iPhone的照片怎么删除&#xff1f;如果你的照片是通…

思维商业篇(1)—如何判断商业效率

思维商业篇(1)—如何判断商业效率 我们评价一个公司&#xff0c;很大程度上其实就是看其商业效率高不高以及规模大不大。 规模是一个企业的大小&#xff0c;效率是一个企业的节奏。 一个小企业如果效率很高&#xff0c;在未来就会有很多的机会。只要其所在行业在&#xff0c…

深入理解Python中的魔法参数 *args 和 **kwargs

在Python编程中&#xff0c;函数的灵活性是其强大之处之一。其中&#xff0c;*args 和 **kwargs 是实现函数参数可变性的重要工具。 无论我们是Python初学者还是经验丰富的开发者&#xff0c;充分理解这两个概念都有助于编写更加灵活、高效的代码。 本文将深入探讨*args和**kw…

【JavaScript】数据结构之树

什么是树形结构&#xff1f; 一种分层数据的抽象模型&#xff0c;用来分层级关系的。虚拟dom它所组织的那个数据原理就是树形结构 深度优先搜索&#xff08;遍历&#xff09;- 递归 从根出发&#xff0c;尽可能深的搜索树的节点技巧 访问根节点对根节点的children挨个进行深…

三、(JS)JS中常见的表单事件

一、onfocus、onblur事件 这个很容易理解&#xff0c;就不解释啦。 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"&…

【JS|第27期】网页文件传输:Blob与Base64的对决

日期&#xff1a;2024年9月12日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xf…

【SQL】百题计划:SQL最基本的判断和查询。

[SQL]百题计划 Select product_id from Products where low_fats "Y" and recyclable "Y";

java重点学习-JVM组成

十二 JVM 12.1 JVM运行原理 Java Virtual Machine Java程序的运行环境(java二进制字节码的运行环境) 好处: 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收机制 12.2 什么是程序计数器? 程序计数器:线程私有的&#xff08;不存在线程安全问题&#xff09;&…

美团图床设置教程

大厂图床&#xff0c;CDN加速 项目地址&#xff1a;https://github.com/woniu336/mt-img 使用方法 在mt.php填上你的token即可&#xff0c;然后打开index.html上传图片 获取token方法 注册https://czz.meituan.com/发布视频&#xff0c;上传封面&#xff0c;注意在上传封面后…

java项目之企业级工位管理系统源码(springboot)

项目简介 企业级工位管理系统实现了以下功能&#xff1a; 企业级工位管理系统的主要使用者管理员功能有个人中心&#xff0c;部门信息管理&#xff0c;工位信息管理&#xff0c;使用情况管理&#xff0c;工位分配管理。员工可以查看个人中心&#xff0c;部门信息&#xff0c;…

linux第二课(docker的安装使用)

目录 一.关于docker (1)背景引入 (2)docker介绍 (3)功能 (4)Docker架构 二.docker的安装及相关的命令 (1)docker的安装 (2)docker的配置 (3)docker镜像命令 (4)容器命令 三.docker安装myaql ​编辑 四.数据卷挂载 1.数据卷挂载引入 2.数据卷挂载图解 3.数据卷的安装…

通用四期ARM架构银河麒麟桌面操作系统V10【安装、配置FTP服务端】

一、操作环境 服务端&#xff1a;银河麒麟桌面操作系统V10SP1 &#xff08;服务端包链接&#xff1a;https://download.csdn.net/download/AirIT/89747026&#xff09; 客户端&#xff1a;银河麒麟桌面操作系统V10SP1 &#xff08;客户端包链接&#xff1a;https://downloa…

List<Map<String, Object>>汇总统计排序

开发环境&#xff1a;jdk 1.8 需求一&#xff1a; 1、统计每个小时(升序)不同事件的产品产量 2、统计不同事件&#xff08;OK 、NG&#xff09;的总产量 public static void main(String[] args) {//数据源List<Map<String, Object>> list new ArrayList<Map…

微信小程序开发第三课

1 wxml语法 1.1 模版语法 # 1 在页面 xx.js 的 Page() 方法的 data 对象中进行声明定义 # 2 在xx.wxml 中使用 {{}} 包裹&#xff0c;显示数据 # 3 可以显示如下&#xff0c;不能编写js语句或js方法-变量-算数运算-三元运算-逻辑判断# 4 只是单纯通过赋值&#xff0c;js中…

[Python学习日记-22] Python 中的字符编码(下)

[Python学习日记-22] Python 中的字符编码&#xff08;下&#xff09; 简介 编码的战国时代 Unicode 和 UTF 现代计算机系统通用的字符编码工作方式 简介 在[Python学习日记-21] Python 中的字符编码&#xff08;上&#xff09;中我们讲了字符编码中的 ASCII 码和 GB2312/G…

算法刷题:300. 最长递增子序列、674. 最长连续递增序列、718. 最长重复子数组、1143. 最长公共子序列

300. 最长递增子序列 1.dp定义&#xff1a;dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度 2.递推公式&#xff1a;if (nums[i] > nums[j]) dp[i] max(dp[i], dp[j] 1); 注意这里不是要dp[i] 与 dp[j] 1进行比较&#xff0c;而是我们要取dp[j] 1的最大值…

【前端】ref引用的作用

首先&#xff0c;我们要明确一点&#xff0c;使用vue的好处是&#xff1a; 想要减少开发者直接操作dom元素。使用组件模版&#xff0c;实现代码的服用。 ref的属性的实现是为了取代原生js中使用id、class等标识来获取dom元素。 helloworld组件 <template><div clas…