【c++随笔14】虚函数表
【c++随笔14】虚函数表
- 一、虚函数表(Virtual Function Table)
- 1、定义
- 2、查看虚函数表
- 2.1、 问题:三种类型,包含一个int类型的class、一个int类型的变量、int类型的指针:这三个大小分别是多少呢?
- 2.2、怎么发现虚函数表存在的?
- 2.3、查看虚函数表里面都有什么?
- 3、继承——虚函数的重写与覆盖
- 4、虚函数为何可以实现多态?
- 5、对象也能切片,为什么不能实现多态?(普通的继承为何不能实现多态?)
- 6、打印虚函数表
- 6.1同一个类型它们的虚表内存地址都是一样的,同一类型的对象共用一份虚表。
- 6.2打印虚函数表
- 二、多态原理
- 1、动态绑定、动态类型、静态类型
- 2、动态绑定和静态绑定对比
- 3、虚函数表的存储位置
- 4、汇编层面看多态实现原理
- 三、多继承中的虚函数表
- 1、多继承会有两张虚表(继承两个时),
- 2、派生类定义的虚函数,存放在第一张虚函数表中
- 四、经典问题
原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/131932164
qq技术交流群:921273910
一、虚函数表(Virtual Function Table)
1、定义
虚函数表是一个由虚函数组成的表格,用于实现动态绑定和多态性。每个包含虚函数的类都有自己的虚函数表,该表列出了该类及其所有基类的虚函数。当一个对象被创建时,它的类虚函数表也被创建,并且可以通过该对象的指针或引用来调用虚函数表中的函数。
虚函数表是一种实现动态多态性的机制。每个包含虚函数的类都有一个虚函数表,其中存储着该类的虚函数地址。当通过基类指针或引用调用虚函数时,程序会根据对象的实际类型查找对应类的虚函数表,并调用正确的虚函数。
2、查看虚函数表
2.1、 问题:三种类型,包含一个int类型的class、一个int类型的变量、int类型的指针:这三个大小分别是多少呢?
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:void Func1() {cout << "Func1()" << endl;}void Func2() {cout << "Func2()" << endl;}void Func3() {cout << "Func3()" << endl;;}
};int main(void)
{Base b;cout << sizeof(b) << endl;cout << sizeof(int) << endl;cout << sizeof(int *) << endl;return 0;
}
输出
-
答案:(64位系统)
- class 对象实例:占4个字节
- int 变量:占4个字节
- int 指针:占用8个字节
-
其他结论:
- 类class占用内存的大小,就是类calss成员变量占用内存的大小;
- 类class占用内存的大小,和成员函数无关;
- 类也可以作为一种数据类型来看待;
-
Base实例b里面有什么
只有成员变量_b
2.2、怎么发现虚函数表存在的?
- 2.2.1加了virtual后,虚函数Base的大小
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:virtual void Func1() {cout << "Func1()" << endl;}virtual void Func2() {cout << "Func2()" << endl;}virtual void Func3() {cout << "Func3()" << endl;;}
};int main(void)
{Base b;cout << sizeof(b) << endl;return 0;
}
输出
结论:
加了virtua后,虚函数Base大小为16;
- 2.2.2、加了虚函数表后为何变成了16字节?
调试查看Base类的实例b里面都有什么
调试截图如下,除了 _b 成员外,还有了一个 _vfptr 在 b1对象中
结论:
- 由于类里面,除了 _b 成员外,还增加了_vfptr,所以由4字节变成了16字节;
- _vfptr就是虚函数表指针(virtual function pointer),指向虚函数表;
扩展:虚函数表指针占用8字节,int类型占用4字节;那8+4应该是12字节,为何总的内存变成了18字节呢,这里面有个内存对齐的问题。打个比方,你int类型占用4字节,double占用8字节,那么会总的便会占用16字节,int类型也会分配8字节的空间。
2.3、查看虚函数表里面都有什么?
结论:
- 虚函数表里面是一个数组;
- 数组里面存储的是每一个虚函数的地址;
扩展
其实,看了我之前写的文章就知道,其实类的实例的每一个成员函数,都有一个单独的地址,存储在代码段.text段。
3、继承——虚函数的重写与覆盖
代码:现在我们增加一个子类 Derive 去继承 Base:
#include <iostream>
using namespace std;class Base {
private:int _b = 1;public:virtual void Func1() {cout << "Func1()" << endl;}virtual void Func2() {cout << "Func2()" << endl;}virtual void Func3() {cout << "Func3()" << endl;;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main(void)
{Derive d;cout << sizeof(d) << endl;return 0;
}
输出
父类 b 对象和子类 b 对象虚表是不一样的,这里看我们发现 Func1 完成了重写,
所以 d 的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫做覆盖。
就可以理解为:子类的虚表拷贝了父类的虚表,子类的 Func1 覆盖掉了父类上的 Func1。
(覆盖指的是虚表中虚函数的覆盖)
- 虚函数重写:语法层的概念,子类对继承父类虚函数实现进行了重写。
- 虚函数覆盖:原理层的概念,子类的虚表,拷贝父类虚表进行了修改,覆盖重写那个虚函数。
🔺 总结:虚函数的重写与覆盖,重写是语法层的叫法,覆盖是原理层的叫法。
4、虚函数为何可以实现多态?
多态调用实现是依靠运行时去指向对象的虚表中查,调用函数地址。
#include <iostream>
using namespace std;class Base {
private:int _b = 1;public:virtual void Func1() {cout << "Base::Func1()" << endl;}virtual void Func2() {cout << "Base::Func2()" << endl;}virtual void Func3() {cout << "Base::Func3()" << endl;;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main(void)
{Base b;Derive d;Base* ptr = &b;ptr->Func1(); // 调用的是父类的虚函数ptr = &d;ptr->Func1(); // 调用的是子类的虚函数return 0;
}
输出
5、对象也能切片,为什么不能实现多态?(普通的继承为何不能实现多态?)
既然指针和引用可以实现多态,那父类赋值给子类对象也可以切片,
根本原因是:对象切片时,子类对象只会拷贝成员给父类对象,并不会拷贝虚表指针。(没有虚函数表)
之前我们讨论过,为何没有静态多态的概念,除了当时说的部符合多态的定义外,本质的原因就在这里。
6、打印虚函数表
6.1同一个类型它们的虚表内存地址都是一样的,同一类型的对象共用一份虚表。
#include <iostream>
using namespace std;class Base {
public:int _b = 1;public:virtual void Func1() {cout << "Base::Func1()" << endl;}virtual void Func2() {cout << "Base::Func2()" << endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout << "Derive::Func1()" << endl;}virtual void Func3() {cout << "Derive::Func3()" << endl;;}
public:int _d = 2;
};using pf = void(*)();
//typedef void(*pf)(void); //和上面的写法相等,看不懂的可以看下我的另外一篇博客《函数指针》int main(void)
{Base b;Derive d1;Derive d2;return 0;
}
查看局部变量的窗口
可以看到子类继承自父类的虚函数表中Func1函数地址是重写之后的函数地址,已经将父类的func函数地址覆盖掉。
- 如果父类中的虚函数没有被子类重写,那么子类的虚函数表中的地址仍然是父类中虚函数的地址。
- 只有虚函数才会进虚函数表,非虚函数是不进虚函数表的。
- 如果派生类中存在新增加的虚函数,那么就会按照在派生类中的声明顺序依次添加到派生类的虚函数表的最后。
- 虚函数表本质就是一个虚函数指针数组,而虚函数表指针本质就是这个数组的首元素地址。虚函数表的最后一个字段通常置为nullptr。
6.2打印虚函数表
我们例子中,每一个虚函数返回值类型void,参数无,所以,虚函数指针数组中元素的类型为void(*)(void);(不懂的可以查看我的另外一篇博客《【c++随笔09】函数指针》
- 注意:派生类的虚函数,visual并没显示出来,但是我们打印出来了,可见:visual的可视化是有些问题的。
#include <iostream>
using namespace std;class Base {
public:int _b = 1;public:virtual void Func1() {cout << "Base::Func1()" << endl;}virtual void Func2() {cout << "Base::Func2()" << endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout << "Derive::Func1()" << endl;}virtual void Func3() {cout << "Derive::Func3()" << endl;;}
public:int _d = 2;
};using pf = void(*)();
//typedef void(*pf)(void); //和上面的写法相等,看不懂的可以看下我的另外一篇博客《函数指针》int main(void)
{Base b;Derive d1;Derive d2;Base* ptr = &d1;//ptr->Func1(); // 调用的是父类的虚函数//Base* ptr2 = new Derive();pf* pfun = (pf*)*(long long*)ptr;//2.pp这个指针是函数指针数组的首元素的地址。while (*pfun){cout << *pfun << endl;(*pfun)();cout << endl;pfun++;}cout << "----------------------------------------" << endl;ptr = &d2;pfun = (pf*)*(long long*)ptr;while (*pfun){cout << *pfun << endl;(*pfun)();cout << endl;pfun++;}cout << "----------------------------------------" << endl;ptr = &b;pfun = (pf*)*(long long*)ptr;while (*pfun){cout << *pfun << endl;(*pfun)();cout << endl;pfun++;}return 0;
}
输出
二、多态原理
1、动态绑定、动态类型、静态类型
我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p575-576页)
-
动态绑定(dynamic binding) 直到运行时才确定到底执行函数的哪个版本。在C++语言中,动态绑定的意思是在运行时根据引用或指针所绑定对象的实际类型来选择执行虚函数的某一个版本。
-
动态类型(dynamic type) 对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。
-
静态类型(static type) 对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。
《C++ Primer 第5版》第15.2章节(p529页)
- 以动态绑定有时又被称为运行时绑定(run-time binding)。
- 在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。
2、动态绑定和静态绑定对比
由于《C++ Primer 第5版》并没有给出静态绑定的概念,我们暂时把在程序编译期间确定了程序的行为,也称为静态绑定。比如函数重载。
- 动态绑定
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:void Func1() {cout << "Func1()" << endl;}
};int main(void)
{Base *ptr1 = new Base();ptr1->Func1();return 0;
}
- 静态绑定
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:virtual void Func1() {cout << "Func1()" << endl;}
};int main(void)
{Base *ptr1 = new Base();ptr1->Func1();return 0;
}
- 汇编层面分析静态绑定和动态绑定的区别
g++ main.cpp
objdump -h -d -x ./a.out
对比反汇编的代码,如下截图
3、虚函数表的存储位置
推断:虚表存储在只读数据段上。
4、汇编层面看多态实现原理
- 多态
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:virtual void Func1() {cout << "Func1()" << endl;}
};// 子类 Derive
class Derive : public Base {
public:virtual void Func1() {cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};void pf(Base *b)
{b->Func1();
}int main(void)
{Base *ptr1 = new Base();pf(ptr1);Base *ptr2 = new Derive();pf(ptr2);return 0;
}
- 非多态
#include <iostream>
using namespace std;class Base {
private:int _b = 1;
public:void Func1() {cout << "Func1()" << endl;}
};// 子类 Derive
class Derive : public Base {
public:void Func1() {cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};void pf(Base *b)
{b->Func1();
}int main(void)
{Base *ptr1 = new Base();pf(ptr1);Base *ptr2 = new Derive();pf(ptr2);return 0;
}
g++ main.cpp
objdump -h -d -x ./a.out
对比反汇编的代码,如下截图
三、多继承中的虚函数表
// 基类A
class A {
public:virtual void func1() {std::cout << "A::func1()" << std::endl;}virtual void func2() {std::cout << "A::func2()" << std::endl;}};// 基类B
class B {
public:virtual void func1() {std::cout << "B::func1()" << std::endl;}/*virtual void func2() {std::cout << "B::func2()" << std::endl;}*/};// 派生类C,多继承自A和B
class C : public A, public B {
public:virtual void func1() override {std::cout << "C::func1()" << std::endl;}/*virtual void func2() override {std::cout << "C::func2()" << std::endl;}*/virtual void func3() {std::cout << "C::func3()" << std::endl;}
};int main() {C c;
}
我们先透过监视简单看一下:
2、派生类定义的虚函数,存放在第一张虚函数表中
#include <iostream>// 基类A
class A {
public:virtual void func1() {std::cout << "A::func1()" << std::endl;}virtual void func2() {std::cout << "A::func2()" << std::endl;}};// 基类B
class B {
public:virtual void func1() {std::cout << "B::func1()" << std::endl;}/*virtual void func2() {std::cout << "B::func2()" << std::endl;}*/};// 派生类C,多继承自A和B
class C : public A, public B {
public:virtual void func1() override {std::cout << "C::func1()" << std::endl;}/*virtual void func2() override {std::cout << "C::func2()" << std::endl;}*/virtual void func3() {std::cout << "C::func3()" << std::endl;}
};int main() {C c;// 打印A的虚函数表std::cout << "A's vtable: " << std::endl;void** aVTable = *(void***)(&c);for (int i = 0; aVTable[i] != nullptr; i++) {std::cout << " [" << i << "]" << aVTable[i]<<" -> "<<" 函数执行";void(*func)() = (void(*)())(aVTable[i]);func();}// 打印B的虚函数表std::cout << "B's vtable: " << std::endl;void** bVTable = *(void***)(((char*)&c) + sizeof(A));for (int i = 0; bVTable[i] != nullptr; i++) {std::cout << " [" << i << "]" << aVTable[i]<<" -> " << " 函数执行";void(*func)() = (void(*)())(bVTable[i]);func();} 根据虚函数表地址运行虚函数//std::cout << "Running virtual function using A's vtable: " << std::endl;//void(*aFunc)() = (void(*)())(aVTable[0]);//aFunc();//std::cout << "Running virtual function using B's vtable: " << std::endl;//void(*bFunc)() = (void(*)())(bVTable[0]);//bFunc();return 0;
}
输出
四、经典问题
-
- inline函数可以是虚函数嘛?
inline函数可以是虚函数,但是其内联的特性也就没有了,因为inline只是对编译器的建议。内联函数是在调用的地方展开,没有函数地址,而虚函数的地址是要写入虚函数表的,所以内联函数和虚函数只能为其中的一个,不可兼得。
-
- 静态成员函数可以是虚函数嘛?
不能,因为静态成员函数没有this指针,使用类名::成员函数的调用方式 无法访问虚函数表,所以静态成员函数无法放进虚函数表。
-
- 构造函数可以是虚函数嘛?
不可以,因为虚函数表指针是在构造函数的初始化列表初始化的,但是虚函数又要借助虚函数表指针来调用虚函数,两者矛盾,所以不可以为虚函数。
-
- 析构函数可以是虚函数嘛?
可以,并且建议将析构函数定义为虚函数,因为这样可以避免内存泄漏的问题。如果子类对象是动态开辟的,使用父类指针指向子类对象,在delete时如果构成多态那么就会调用子类析构函数,而调用子类析构函数前系统会默认先调用父类析构函数,这样可以避免内存泄漏。
-
- 对象访问普通函数快还是访问虚函数快?
如果是通过实例化的对象访问那么是一样快的,如果是指针或引用对象访问的话是访问普通函数快的,因为指针或引用去访问虚函数时走的是多态调用是一个晚绑定,需要在运行时去需表中找函数的地址。
-
- 虚函数表是在什么时候形成的?存在哪?
和虚函数相关的字符,字符在只读数据区(.rodata),但是虚函数的实现代码应该是和其他的函数一样,存储在代码段(.text)
-
- 什么是抽象类?
函数纯虚函数的类叫做抽象类,此类不能实例化出对象,这也强制了其派生类如果想要实例化出对象那么就必须重写纯虚函数。
-
- C++菱形继承解决方案和多态原理?
菱形继承具有数据冗余和二义性的问题,解决的方法是通过虚继承的方式,虚继承的派生类中会产生一个虚基表指针,该指针指向虚基表,表中的内容是一个到冗余数据的偏移量,而原本冗余的数据会被放到派生类对象的最后。
多态的原理是通过重写虚函数,达到在派生类的虚函数表中重写的虚函数地址覆盖掉原本的地址,然后通过基类的指针或者引用指向派生类对象时,调用虚函数调用的时子类重写后的虚函数,而执行基类对象时调用的就是基类的虚函数达到多态的行为。
不要将虚基表和虚函数表搞混。
相关文章:

【c++随笔14】虚函数表
【c随笔14】虚函数表 一、虚函数表(Virtual Function Table)1、定义2、查看虚函数表2.1、 问题:三种类型,包含一个int类型的class、一个int类型的变量、int类型的指针:这三个大小分别是多少呢?2.2、怎么发现…...

分布式链路追踪实战篇-日志库集成opentelemetry的思路
由上文分布式链路追踪入门篇-基础原理与快速应用可以知道分布式链路追踪的作用,但是距离应用到项目中,我们还需要对项目中一些关键组件进行opentelemetry的集成,例如日志库,ORM、http框架、rpc框架等。 一、日志库如何集成opentel…...

电脑投屏到电视的软件,Mac,Linux,Win均可使用
电脑投屏到电视的软件,Mac,Linux,Win均可使用 AirDroid Cast的TV版,可以上笔记本电脑或台式电脑直接投屏到各种安卓电视上。 无线投屏可以实现本地投屏及远程投屏,AirPlay协议可以实现本地投屏,大家可以按需…...

基于vue+element-plus+echarts编写动态绘图页面
我们都知道网页的echarts可以画图,但是很多情况下都需要编码实现绘图逻辑,如果有一个前端页面可以让我输入数据然后动态生成图表的话那么该多好,其实这个需求不难实现,先看效果。 整体页面分为左右两个部分,其中左边的…...

无人机巡检如何做到实时识别,从数据到模型全流程解读
在数字化和自动化飞速发展的今天,AI识别算法正在加速进入行业生产系统。 基于巡检数据的智能开发,识别算法突破性进展的核心驱动力在于需求——从全天候巡视的平安城市,到潮汐变化的交通网络,从广阔的水域,到繁忙的街道…...

zlmediakit实现rtsp流服务器
本次实现是将内存中的H264数据经过zlmediakit实现为rtsp流。 我是用的是CAPI的方式,将zlmediakit作为一个sdk嵌入到自己的程序中而不是作为一个独立的进进程服务。 1.编译完成zkmedialit后会得到bin include lib三个文件夹如图 其中bin中的MediaServer是作为独立的…...

保姆级 ARM64 CPU架构下安装部署Docker + rancher + K8S 说明文档
1 K8S 简介 K8S是Kubernetes的简称,是一个开源的容器编排平台,用于自动部署、扩展和管理“容器化(containerized)应用程序”的系统。它可以跨多个主机聚集在一起,控制和自动化应用的部署与更新。 K8S 架构 Kubernete…...

耶鲁博弈论笔记
编辑记录: 1126:开个新坑,耶鲁大学的博弈论课程, 和专业相关不大,纯兴趣,尽量写好一点吧 1. 首先指出博弈论是一种研究策略形式的方法,对于经济学中,完全竞争市场只能被动接受均衡…...
一个简易的URL爬虫程序(java)
该程序是一个简单的Java程序,用于从指定的URL中获取网页内容并保存到本地文件。通过URL类打开指定的URL链接,并使用openStream()方法获取输入流。然后使用Scanner类读取输入流中的内容,并使用PrintWriter类将读取到的内容写入到本地文件中。 …...

Deep Learning(wu--46)
文章目录 ContentsBeginBasic逻辑回归SGD导数计算图(反向传播)向量化广播numpy Neural Network向量化激活函数梯度下降深层表示反向传播 Contents Begin Basic 逻辑回归 SGD 导数 计算图(反向传播) 向量化 广播 numpy Neural Netw…...

Java网络爬虫实战
List item 文章目录 ⭐️写在前面的话⭐️📌What is it?分类网络爬虫按照系统结构和实现技术,大致可以分为以下几种类型:通用网络爬虫(General Purpose Web Crawler)、聚焦网络爬虫(Focused Web Crawler&a…...

srs的webrtc信令分析
关于webrtc的流信令只有四个 /rtc/v1/publish/,这是推流接口,是推流客户端跟SRS交换SDP的接口 /rtc/v1/play/,这是拉流接口,是拉流客户端跟SRS交换SDP的接口 /rtc/v1/whip/,这也是推流接口,作用是也是交换…...

实现简单的操作服务器和客户端(上)
一、说明 描述:本教程介绍如何使用 simple_action_server 库创建斐波那契动作服务器。此示例操作服务器生成斐波那契序列,目标是序列的顺序,反馈是计算的序列,结果是最终序列。 内容 创建操作消息编写一个简单的服务器 代码...

基于Java SSM框架+Vue实现药品销售进销存网站项目【项目源码+论文说明】
基于java的SSM框架Vue实现药品销售进销存网站演示 摘要 随着科学技术的飞速发展,各行各业都在努力与现代先进技术接轨,通过科技手段提高自身的优势;对于药品管理系统当然也不能排除在外,随着网络技术的不断成熟,带动了…...

【刷题笔记】加油站||符合思维方式
加油站 文章目录 加油站1 题目描述2 思路3 解题方法 1 题目描述 https://leetcode.cn/problems/gas-station/ 在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i1 个加油站需要消…...

【ArcGIS Pro微课1000例】0037:ArcGIS Pro中模型构建器的使用---以shp批量转kml/kmz为例
文章目录 一、ArcGIS Pro模型构建器介绍二、shp批量转kml/kmz1. 打开模型构建器2. 添加工作空间4. 添加【创建要素图层】工具5. 添加【图层转kml】工具6. 输出文件命名7. 运行模型三、模型另存为1.py文件2. 保存为工具一、ArcGIS Pro模型构建器介绍 模型构建器是一种可视化编程…...

前端 vue 面试题(二)
文章目录 如何让vue页面重新渲染组件间通信vue为什么要mutation、 action操作插槽、具名插槽、作用域插槽vue编译使用的是什么库?vue怎么实现treeshakingwebpack实现treeshaking为什么只有es module 能支持 tree shaking mixin 的作用mixin的底层原理nexTick原理vue…...

MySQL 高可用架构
MySQL 是实际生产中最常用的数据库,生产环境数据量极为庞大,对性能和安全要求很高,单机的 MySQL 是远远达不到的,所以必须搭建一个主从复制架构,同时可以基于一些工具实现高可用架构,在此基础上,…...

JVM虚拟机:G1垃圾回收器的日志分析
本文重点 本文我们将学习G1垃圾回收器的日志 使用 执行命令 java -Xms20M -Xmx20M -XX:PrintGCDetails -XX:UseG1GC 类名 分析 前面我们学习了G1垃圾回收器,它的回收有三种可能: YGC FGC MixedGC GC pause表示STW,Evacuation表示复制对象,…...

解决视口动画插件jquery.aniview.js使用animate.css时无效的问题(最新版本网页视口动画插件的使用及没作用、没反应)
当网站页面元素进入视口时自动应用过渡效果。CSS过渡效果可以为网页添加动画效果,并提供了一种平滑的转换方式,使元素的变化更加流畅和生动。而通过jQuery插件来获取页面滚动位置决定合适调用动画效果。 一、官网 animate.css官网 一款强大的预设css3动…...

微软PowerBI考试 PL300-选择 Power BI 模型框架【附练习数据】
微软PowerBI考试 PL300-选择 Power BI 模型框架 20 多年来,Microsoft 持续对企业商业智能 (BI) 进行大量投资。 Azure Analysis Services (AAS) 和 SQL Server Analysis Services (SSAS) 基于无数企业使用的成熟的 BI 数据建模技术。 同样的技术也是 Power BI 数据…...
SciencePlots——绘制论文中的图片
文章目录 安装一、风格二、1 资源 安装 # 安装最新版 pip install githttps://github.com/garrettj403/SciencePlots.git# 安装稳定版 pip install SciencePlots一、风格 简单好用的深度学习论文绘图专用工具包–Science Plot 二、 1 资源 论文绘图神器来了:一行…...

如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
力扣-35.搜索插入位置
题目描述 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 class Solution {public int searchInsert(int[] nums, …...
快刀集(1): 一刀斩断视频片头广告
一刀流:用一个简单脚本,秒杀视频片头广告,还你清爽观影体验。 1. 引子 作为一个爱生活、爱学习、爱收藏高清资源的老码农,平时写代码之余看看电影、补补片,是再正常不过的事。 电影嘛,要沉浸,…...
NPOI Excel用OLE对象的形式插入文件附件以及插入图片
static void Main(string[] args) {XlsWithObjData();Console.WriteLine("输出完成"); }static void XlsWithObjData() {// 创建工作簿和单元格,只有HSSFWorkbook,XSSFWorkbook不可以HSSFWorkbook workbook new HSSFWorkbook();HSSFSheet sheet (HSSFSheet)workboo…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
前端高频面试题2:浏览器/计算机网络
本专栏相关链接 前端高频面试题1:HTML/CSS 前端高频面试题2:浏览器/计算机网络 前端高频面试题3:JavaScript 1.什么是强缓存、协商缓存? 强缓存: 当浏览器请求资源时,首先检查本地缓存是否命中。如果命…...

【免费数据】2005-2019年我国272个地级市的旅游竞争力多指标数据(33个指标)
旅游业是一个城市的重要产业构成。旅游竞争力是一个城市竞争力的重要构成部分。一个城市的旅游竞争力反映了其在旅游市场竞争中的比较优势。 今日我们分享的是2005-2019年我国272个地级市的旅游竞争力多指标数据!该数据集源自2025年4月发表于《地理学报》的论文成果…...