C++ 学习系列 -- C++ 中的多态行为
一 多态是什么?
多态是面向对象三大特征中重要一项,另外两项分别是封装与继承。
所谓多态,指的是多种不同的形态,也就是去完成某个具体的行为,多个不同的对象去操作同一个函数时,会产生不同的行为,进而出现不同的状态。
二 C++ 类中的普通成员函数
普通成员函数面临着两个问题:
1. 无法实现多态行为
2. 派生类同名函数会覆盖基类的同名函数,即使函数的参数不同也会导致覆盖
// base.h
#include<iostream>class Base
{
public:Base(){}~Base(){}void func1(){std::cout << "Base::func1()---" << std::endl;}void func2(){std::cout << "Base::func2()---" << std::endl;}
};class Derive1 : public Base
{
public:Derive1(){}~Derive1(){}void func1(){std::cout << "Derive1::func1()---" << std::endl;}void func2(int num){std::cout << "Derive1::func2(int num)---" << std::endl;}
};// main.cpp
#include"base.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数// 1. 普通函数 是没有多态行为的// 2. 派生类的同名函数会覆盖基类同名函数,即使参数不同也会被覆盖Base* b1 = new Derive1;b1->func1(); // 调用的是 Base::func1() 函数delete b1;// b1->func2(2); // 编译不过,因为 Base::func2() 函数是不带参数的Derive1* d1 = new Derive1;// d1->func2(); // 编译不过,Derive1::func2(int num) 覆盖了同名函数 Base::func2() 函数,delete d1;return 0;
}
输出:
Base::func1()---
Derive1::func1()---
三 C++ 中的虚函数
1. virtual 关键字
c++ 中的 virtual 关键字来常有以下两种使用,
1.1 修饰函数
被 virtual 关键字修饰的类成员函数,被称为虚函数。虚函数分为两种:
1. 纯虚函数 (也叫做抽象函数)
含有纯虚函数的类是无法被实例化的,因为在编译器看来,纯虚函数并不是一个完整的函数,它没有具体的实现,若是在使用时调用它,编译器也不知道到底该怎么办
2. 非纯虚函数
若是类的虚函数都是非纯虚函数,那么这个类是可以实例化的
友元函数、构造函数与 static 函数 是不能被 virtual 关键字修饰的。
虚函数具有继承性,基类中 virtual关键字修饰的虚函数,派生类重写时,可以不再用 virtual 修饰,重写的基类虚函数会自动成为虚函数。
// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }// 虚函数virtual void vfunc1(){std::cout << "A::vfunc1()---" << std::endl;}// 纯虚函数virtual void vfunc() = 0;private:int m_data1;
};class B : public A
{
public:B(){ }~B() { }void vfunc1(){std::cout << "B::vfunc1()---" << std::endl;}void vfunc(){std::cout << "B::vfunc()---" << std::endl;}
private:int m_data2;
};// main.cpp
#include"A.h"int main()
{// A a1; // 类中有纯虚函数,无法实例化,编译不过// A* a2 = new A; // 类中有纯虚函数,无法实例化,编译不过B b1; // B class 实现了 A class 的纯虚函数,可以实例化B* b2 = new B; // B class 实现了 A class 的纯虚函数,可以实例化delete b2;return 0;
}
1.2 修饰类继承
C++虚继承和虚基类详解 - 知乎 (zhihu.com)
1.2.1 public、protected 与 private 继承的区别
我们知道 类之间的继承有三种:public、protected 与 private
三者区别为:
- public 继承
- 基类中原 public 的成员 在派生类中仍然是 public
- 基类中原 protected 的成员 在派生类中仍然是 protected
- 基类中原 private 的成员 在派生类中仍然是 private
- protected
- 基类中原 public 的成员 在派生类中变为了 protected
- 基类中原 protected 的成员 在派生类中仍然是 protected
- 基类中原 private 的成员 在派生类中仍然是 private
- private
- 基类中原 public 的成员 在派生类中变为了 private
- 基类中原 protected 的成员 在派生类中仍然是 private
- 基类中原 private 的成员 在派生类中仍然是 private
class A
{};class B1 : public A
{};class B2 : protected A
{};class B3 : private A
{};
那么 virtual 继承是用于解决什么问题呢?下面让我们看以下的场景:
有这样一种继承关系,菱形继承:

// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){// m_a = a; // 编译不通过,这里有歧义,可能是 BB::m_a ,也可能是 CC::m_a}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{D d;return 0;
}
输出:

通过以上代码与输出我们可以得出下面两点结论:
1. AA 类的构造函数与析构函数分别被执行了两次,分别是 DD 继承 BB 类时执行,DD继承 CC 类时执行。
2. m_a = a 这行代码是无法通过编译的,因为编译器无法判断 m_a 是从路径 AA -> BB -> DD 还是从路径 AA -> CC -> DD 得来的,产生了歧义。
那么是否有办法消除歧义呢?
答案是有,可以通过如下的代码来消除歧义
// 使用 BB 类路径的 m_a
void set_a(int a)
{BB::m_a = a;
}
// 使用 CC 类路径的 m_a
void set_a(int a)
{CC::m_a = a;
}
上面的多继承的方式主要有两点问题:
1. 派生类 DD 有两个 m_a 成员变量,分别来自于 AA -> BB -> DD 于 AA -> CC -> DD,这样会造成冗余与内存的浪费
2. 同名变量与函数会出现命名的冲突
1.2.2 虚继承

为了解决多继承这两点问题,c++引入了虚继承的概念
虚继承代码如下:
// AA.h
#include<iostream>class AA
{
public:AA(int a = 0):m_a(a){std::cout << "AA constructor." << std::endl;}~AA(){std::cout << "AA destructor." << std::endl;}public:int m_a;
};class BB : virtual public AA
{
public:BB(int a = 0):m_b(a){std::cout << "BB constructor." << std::endl;}~BB(){std::cout << "BB destructor." << std::endl;}public:int m_b;
};class CC : virtual public AA
{
public:CC(int a = 0):m_c(a){std::cout << "CC constructor." << std::endl;}~CC(){std::cout << "CC destructor." << std::endl;}public:int m_c;
};class DD : public BB, public CC
{
public:DD(int a = 0):m_d(a){std::cout << "DD constructor." << std::endl;}~DD(){std::cout << "DD destructor." << std::endl;}void set_a(int a){m_a = a; // 编译通过}void set_b(int b){m_b = b;}void set_c(int c){m_c = c;}void set_d(int d){m_d = d;}public:int m_d;
};// main.cpp
#include"AA.h"int main()
{DD dd;return 0;
}
输出:
通过输出可以看出,AA类的构造函数与析构函数只被执行了一次,这说明在虚继承时,不再存在两种路径 AA -> BB -> DD 与 AA -> CC -> DD。
上述的虚继承代码解决了菱形继承中 m_a 数据冗余的问题,所以 D 类中直接访问 m_a 就不再有歧义的问题了。
// 虚继承中访问 m_a 无冗余问题
void set_a(int a)
{m_a = a;
}
总结:
虚继承的目的是为了表明某个类的基类是可以被这个类的所有派生类共享,这个基类也被称作虚基类(Virtual Base Class),上述代码中的 class AA 就是 class BB 与 class CC 的虚基类。
不管虚基类在派生类中出现多少次,最终在派生类中都只有一份虚基类的成员变量与成员函数。
2. 虚函数指针与虚函数表
2.1 c++多态原理
c++ 实现多态的原理就是利用 虚函数表指针与虚函数表。
若是定义的类实现了一个或者多个虚函数,那么这个类会有一张对应的虚函数表,表中存放的是对应虚函数的指针。
如下图所示:
若是派生类 B 重写了基类 A 中的 虚函数 vfunc1,那么在 class B 的虚函数表中对应的 vfunc1 虚函数的地址就是派生类 B 重写的虚函数 B::vfunc1 地址;
若是 派生类 B 没有重写了基类 A 中的 虚函数 vfunc2,那么在 class B 的虚函数表中对应的 vfunc2 虚函数的地址就是基类 A 的虚函数 A::vfunc2 地址。
当新生成 class B或者 class C 对象时,会在对象内部自动生成一个指向虚函数表地址的虚函数表指针 vptr,如果我们用 sizeof(A) 发现其大小为 16 ,计算方式为 两个 int 类型的成员变量大小分别为 4 ,有一个默认的虚函数指针大小为 8 (这里是 64 位操作系统,如果是 32 位操作系统的话,指针大小为 4)所以 sizeof(B) = 4 * 2 + 8

// A.h
#include<iostream>class A
{
public:A() { }virtual ~A() { }virtual void vfunc1() override{std::cout << "A::vfunc1()---" << std::endl;}virtual void vfunc2(){std::cout << "A::vfunc2()---" << std::endl;}
private:int m_data1;int m_data2;
};class B : public A
{
public:B(){ }~B() { }void vfunc1() override{std::cout << "B::vfunc1()---" << std::endl;}
private:int m_data2;int m_data3;};class C : public B
{
public:C(){ }~C(){ }void vfunc1() override{std::cout << "C::vfunc1()---" << std::endl;}
private:int m_data1;int m_data4;
};// main.cpp
#include"A.h"int main()
{// 多态三要素// 1. 指针 2. 向上转型 3. 调用虚函数A* a = new A;A* b = new B;A* c = new C;a->vfunc1(); // A::vfunc1a->vfunc2(); // A::vfunc2b->vfunc1(); // B::vfunc1b->vfunc2(); // A::vfunc2c->vfunc1(); // C::vfunc1c->vfunc2(); // A::vfunc2return 0;
}
输出

通过输出可以看出,指针为 A* 的 B 类对象调用的 vfunc1 函数为 B 类中重写的 vfunc1 虚函数; 指针为 A* 的 C 类对象调用的 vfunc1 函数为 C 类中重写的 vfunc1 虚函数。
2.2 虚函数表分析
以下通过类的内存布局来看一下虚函数表是什么样的:
通过debug 方式,查看 A类、B类 与 C 类生成对象中虚函数表中的内存分配,操作系统是 windows 10 ,64位。



A类虚函数表 vptr 中的头两个元素存放的是 其析构函数 A::~A() 的地址,第三个元素是虚函数 A::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
B类虚函数表 vptr 中的头两个元素存放的是 其析构函数 B::~B() 的地址,第三个元素是虚函数 B::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
C类虚函数表 vptr 中的头两个元素存放的是 其析构函数 C::~C() 的地址,第三个元素是虚函数 C::vfunc1() 的地址,第四个元素 是虚函数 A::vfunc2() 的地址;
如何获取到类 Object 的虚函数表呢?对于有虚函数的对象 object 来说:
1. &object 代表对象 object 的起始地址
2. (intptr_t *)&object 代表获取对象起始地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),而这前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)则是虚函数表的指针
3. *(intptr_t *)&object 则是取前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节)中的数据,也就是虚函数表 vptr 的地址
4. 取 虚函数表 vptr 地址的前 4 个字节(32 位操作系统位 4 字节,64位操作系统位 8 字节),为虚函数表中第一个元素的地址 (intptr_t *) *(intptr_t *)&object,取出虚函数表中第一个元素 *((intptr_t *) *(intptr_t *)&object), 则取出虚函数表中第 n 个元素为 *((intptr_t *) *(intptr_t *)&object + n),所取元素即为 虚函数的地址
5. 定义一个函数指针:typedef void(*pFunc)(); 函数指针 pFunc 代表 形如 void print(); 的函数的指针类型。通过前面的 1 - 4 步,获取道虚函数的地址,将地址赋给函数指针 pFunc ,就可以通过 pFunc 调用对应的虚函数了。 深入浅出——理解c/c++函数指针 - 知乎 (zhihu.com)
我们可以把类对象的虚函数表中的虚函数指针获取出来,直接调用虚函数,代码如下所示:
// main.cpp
#include"a.h"int main()
{typedef void(*pFunc)() ;A a;printf("the addr of A::vfunc1 is 0x%p\n", &A::vfunc1);printf("the addr of the third function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+2));printf("the addr of the fourth function pointer in class A vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&a+3));pFunc pa_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&a+2);pFunc pa_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&a+3);pa_vfunc1();pa_vfunc2();B b;printf("the addr of B::vfunc1 is 0x%p\n", &B::vfunc1);printf("the addr of the third function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+2));printf("the addr of the fourth function pointer in class B vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&b+3));pFunc pb_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&b+2);pFunc pb_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&b+3);pb_vfunc1();pb_vfunc2();C c;printf("the addr of B::vfunc1 is 0x%p\n", &C::vfunc1);printf("the addr of the third function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+2));printf("the addr of the fourth function pointer in class C vptr is: 0x%p\n", *((intptr_t *)*(intptr_t *)&c+3));pFunc pc_vfunc1 = (pFunc)*((intptr_t *)*(intptr_t *)&c+2);pFunc pc_vfunc2 = (pFunc)*((intptr_t *)*(intptr_t *)&c+3);pc_vfunc1();pc_vfunc2();return 0;
}
输出:

2.3 c++ 多态实现条件
C++ 多态的实现条件有三条:
1. 指针
2. 指针向上转型
3. 虚函数
多态可以通过下面的例子来去实现:
// main.cpp
#include"A.h"int main()
{// 多态三要素:1. 指针 2. 向上转型 3. 调用虚函数A* a = new A; // 指针a->vfunc1();a->vfunc2();B b;a = &b; // 向上转型a->vfunc1(); // 调用虚函数a->vfunc2();C c;a = &c; // 向上转型a->vfunc1(); // 调用虚函数a->vfunc2();return 0;
}
输出:

四 常见问题
1. 虚函数指针属于类还是对象?
答:虚函数指针属于对象,每个含有虚函数的类对象都有一个默认的虚函数指针,通过虚函数指针可以获取到虚函数表,进而实现动态调用虚函数,实现多态行为
2. 虚函数表属于类还是对象?
答:虚函数表属于类,让我们站在设计者的角度思考一下,虚函数表本身是占有一定的内存空间的,而每个对象的虚函数表都是相同的,要是每个对象都有一个,那得耗费多少冗余内存啊,所有对象共用一份虚函数表即可。
3. 基类的析构函数为什么要加 virtual 关键字
答:若是基类的析构函数不加 virtual 关键字的话,则 delete 多态时的对象指针是,会出现只调用基类的析构函数,未调用派生类的析构函数,那么派生类的数据就可能未被释放掉,会出现内存泄漏的现象。
实验如下:
// base.h
#include<iostream>class Base
{
public:Base(){std::cout << "Base constructor." << std::endl;}~Base(){std::cout << "Base destructor." << std::endl;}virtual void func(){std::cout << "virtual Base::func()---" << std::endl;}};class Derive1 : public Base
{
public:Derive1(){std::cout << "Derive1 constructor." << std::endl;}virtual ~Derive1(){std::cout << "Derive1 constructor." << std::endl;}void func() override{std::cout << "virtual Derive1::func()---" << std::endl;}};// main.cpp
#include"base.h"int main()
{Base* d1 = new Derive1;d1->func();delete d1;return 0;
}
输出:

相关文章:
C++ 学习系列 -- C++ 中的多态行为
一 多态是什么? 多态是面向对象三大特征中重要一项,另外两项分别是封装与继承。 所谓多态,指的是多种不同的形态,也就是去完成某个具体的行为,多个不同的对象去操作同一个函数时,会产生不同的行为&…...
Spring Cloud中实现Feign声明式服务调用客户端
可以通过OpenFeign从一个服务中调用另一个服务,我们一般采用的方式就是定义一个Feign接口并使用FeignClient注解来进行标注,feign会默认为我们创建的接口生成一个代理对象。 当我们在代码中调用Feign接口的方法的时候,实际上就是在调用我们Fe…...
【网络编程】网络通信基础——简述TCP/IP协议
个人主页:兜里有颗棉花糖 欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】【Java系列】 本专栏旨在分享学习网络编程的一点学习心得,欢迎大家在评论区交流讨论💌 目录 一、ip地…...
观察者模式 Observer
观察者模式属于行为型模式。在程序设计中,观察者模式通常由两个对象组成:观察者和被观察者。当被观察者状态发生改变时,它会通知所有的观察者对象,使他们能够及时做出响应。 三要素:观察者(Observer&#…...
Hadoop入门学习笔记——七、Hive语法
视频课程地址:https://www.bilibili.com/video/BV1WY4y197g7 课程资料链接:https://pan.baidu.com/s/15KpnWeKpvExpKmOC8xjmtQ?pwd5ay8 Hadoop入门学习笔记(汇总) 目录 七、Hive语法7.1. 数据库相关操作7.1.1. 创建数据库7.1.2…...
采用SpringBoot框架+原生HTML、JS前后端分离模式开发和部署的电子病历编辑器源码(电子病历评级4级)
概述: 电子病历是指医务人员在医疗活动过程中,使用医疗机构信息系统生成的文字、符号、图表、图形、数据、影像等数字化信息,并能实现存储、管理、传输和重现的医疗记录,是病历的一种记录形式。 医院通过电子病历以电子化方式记录患者就诊的信息,包括&…...
HTML表单
<!DOCTYPE html> <html><head><meta charset"utf-8"><title>招聘案列</title></head><body><h1>午睡操场传来蝉的声音</h1><hr /><form>昵称:<input type"text" …...
Http 请求体和响应体中重要的字段
Http 请求体 Accept:用于告诉服务器客户端能够处理哪些媒体类型。Accept 头中的值通常是一个或多个 MIME 类型,并按优先级排序。服务器会根据 Accept 头中的值来决定响应的内容类型。例如,Accept: text/plain, text/html。Content-Type&…...
最新国内可用使用GPT4.0,GPT语音对话,Midjourney绘画,DALL-E3文生图
一、前言 ChatGPT3.5、GPT4.0、GPT语音对话、Midjourney绘画,相信对大家应该不感到陌生吧?简单来说,GPT-4技术比之前的GPT-3.5相对来说更加智能,会根据用户的要求生成多种内容甚至也可以和用户进行创作交流。 然而,GP…...
【量化金融】证券投资学
韭菜的自我修养 第一章: 基本框架和概念1.1 大盘底部形成的技术条件1.2 牛市与熊市1.3 交易系统1.3.1 树懒型交易系统1.3.2 止损止损的4个技术 第二章:证券家族4兄弟2.1 债券(1)债券,是伟大的创新(2&#x…...
【Bash】重点总结
文章目录 1. 总体认识1.1. Shell概述1.2. 第一个Shell脚本 2. 变量2.1. 定义变量2.2. 使用变量2.3. 只读变量2.4. 删除变量2.5. 变量类型2.5.1. 字符串变量 1. 总体认识 1.1. Shell概述 Shell是一个用C语言编写的程序,这个程序提供了一个界面,用户通过…...
Git安装和使用教程,并以gitee为例实现远程连接远程仓库
文章目录 1、Git简介及安装2、使用方法2.1、Git的启动与配置2.2、基本操作2.2.1、搭建自己的workspace2.2.2、git add2.2.3、git commit2.2.4、忽略某些文件不予提交2.2.5、以gitee为例实现git连接gitee远程仓库来托管代码 1、Git简介及安装 版本控制(Revision cont…...
Hadoop入门学习笔记——一、VMware准备Linux虚拟机
视频课程地址:https://www.bilibili.com/video/BV1WY4y197g7 课程资料链接:https://pan.baidu.com/s/15KpnWeKpvExpKmOC8xjmtQ?pwd5ay8 Hadoop入门学习笔记(汇总) 目录 一、VMware准备Linux虚拟机1.1. VMware安装Linux虚拟机1.…...
CSS3新增特性
CSS3 CSS3私有前缀 W3C 标准所提出的某个CSS 特性,在被浏览器正式支持之前,浏览器厂商会根据浏览器的内核,使用私有前缀来测试该 CSS 特性,在浏览器正式支持该 CSS 特性后,就不需要私有前缀了。 查询 CSS3 兼容性的网…...
Unity中Shader观察空间推导
文章目录 前言一、本地空间怎么转化到观察空间二、怎么得到观察空间的基向量1、Z轴向量2、假设 观察空间的 Y~假设~ (0,1,0)3、X Y 与 Z 的叉积4、Y X 与 Z 的叉积 三、求 [V~world~]^T^1、求V~world~2、求[V~world~]^T^ 四、求出最后在Unity中使用的公式1、偏移坐标轴2、把…...
信息学奥赛一本通2034:【例5.1】反序输出
2034:【例5.1】反序输出 时间限制: 1000 ms 内存限制: 65536 KB 提交数: 79280 通过数: 35643 【题目描述】 输入nn个数,要求程序按输入时的逆序把这nn个数打印出来,已知整数不超过100100个。也就是说,按输入相反顺序打印这nn个…...
使用教程之【SkyWant.[2304]】路由器操作系统,破解移动【Netkeeper】校园网【小白篇】
许多高校目前饱受Netkeeper认证的痛苦,普通路由器无法使用, 教你利用SkyWant的Netkeeper认证软件来使你的SkyWant路由器顺利认证上网,全宿舍又可以合作共赢了! 步骤一:正确连接网线,插电开机 正确连接网…...
模式识别与机器学习(十):梯度提升树
1.原理 提升方法实际采用加法模型(即基函数的线性组合)与前向分步算法。以决策树为基函数的提升方法称为提升树(boosting tree)。对分类问题决策树是二叉分类树,对回归问题决策树是二叉回归树。提升树模型可以表示为决…...
《剑指offer》Java版--12.矩阵中的路径(DFS+剪枝)
剑指offer原题:矩阵中的路径 请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再…...
AI智能体的介绍
最近几个月 随着大语言模型的持续火爆 利用大模型来构建AI智能体的研究呢 也陆续进入了人们的视野 AI智能体这个概念呢 也逐渐的流行开来 先是斯坦福大学谷歌的研究者们 成功的构建了一个虚拟小镇 小镇上的居民呢不再是人 而是25个AI的智能体 他们的行为呢 比人类角…...
Linux云原生安全:零信任架构与机密计算
Linux云原生安全:零信任架构与机密计算 构建坚不可摧的云原生防御体系 引言:云原生安全的范式革命 随着云原生技术的普及,安全边界正在从传统的网络边界向工作负载内部转移。Gartner预测,到2025年,零信任架构将成为超…...
ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
土地利用/土地覆盖遥感解译与基于CLUE模型未来变化情景预测;从基础到高级,涵盖ArcGIS数据处理、ENVI遥感解译与CLUE模型情景模拟等
🔍 土地利用/土地覆盖数据是生态、环境和气象等诸多领域模型的关键输入参数。通过遥感影像解译技术,可以精准获取历史或当前任何一个区域的土地利用/土地覆盖情况。这些数据不仅能够用于评估区域生态环境的变化趋势,还能有效评价重大生态工程…...
C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南
1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发,使用DevEco Studio作为开发工具,采用Java语言实现,包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...
代理篇12|深入理解 Vite中的Proxy接口代理配置
在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...
零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...
力扣-35.搜索插入位置
题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...
HarmonyOS运动开发:如何用mpchart绘制运动配速图表
##鸿蒙核心技术##运动开发##Sensor Service Kit(传感器服务)# 前言 在运动类应用中,运动数据的可视化是提升用户体验的重要环节。通过直观的图表展示运动过程中的关键数据,如配速、距离、卡路里消耗等,用户可以更清晰…...
JavaScript基础-API 和 Web API
在学习JavaScript的过程中,理解API(应用程序接口)和Web API的概念及其应用是非常重要的。这些工具极大地扩展了JavaScript的功能,使得开发者能够创建出功能丰富、交互性强的Web应用程序。本文将深入探讨JavaScript中的API与Web AP…...
