当前位置: 首页 > news >正文

21天学会C++:Day14----模板

· CSDN的uu们,大家好。这里是C++入门的第十四讲。
· 座右铭:前路坎坷,披荆斩棘,扶摇直上。
· 博客主页: @姬如祎
· 收录专栏:C++专题

目录

1. 知识引入

2. 模板的使用

2.1 函数模板

2.2 类模板

3. 模板声明和定义分离

3.1 同一文件中的声明与定义分离

3.2 分文件的声明与定义分离

4. 非类型模板参数

5. 模版的特化

5.1 模板的全特化

5.2 模板的偏特化

6. 模板总结


1. 知识引入

有一天,我们在写C语言程序的时候,想要交换两个数的值,于是我们很快就写了一个交换两个整形变量的函数:

void Swap(int x, int y)
{int tmp = x;x = y;y = tmp;
}

但是写了一会代码,你发现你又要交换两个double的值,你又要重新写一个Swap double的函数。假如后来你还需要交换其他类型的变量,那么你就需要写更多的Swap函数。是不是偷一下子就变大了。不过别慌,C++带着新的语法走来了!

2. 模板的使用

听到模板这个名词,我们就想到了显示生活中的模具,通过一个模具我们就能制作出很多产品。同理通过一个模板,我们就能实现很多功能,满足你的各种需求!C++的模板是泛型编程的基础,所谓泛型编程:编写与类型无关的通用代码,是代码复用的一种手段

我们来看看模板的语法:

template <typaname T1, typename T2, ··· , typaname TN>

函数/类

 模板通过关键字 template 来定义,template 后面紧跟一个 尖括号 , 尖括号中加上关键字 typename (typename 换成class也行) 然后跟上模板参数,其中尖括号中的写法与函数的形参列表极为相似。其中的T1,T2 ··· 叫做模板参数,名字可以随意更改。根据template下方定义的类型,模板可以分为函数模板类模板

2.1 函数模板

我们先来看看函数模板的用法吧:我们就拿上面的Swap函数来试试吧,看看有了函数模板能省事多少!

//定义函数模板
template<typename T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}

模板起始就是将类型参数化,在上面的代码中我们将Swap函数的参数类型用模板参数代替,当我们调用函数时,编译器会根据传入参数的类型,自动为T实例化出对应的类型!

通过调试发现,即使我们传入不同类型的参数,也能够做到交换两个变量的值。

你可能会好奇,编译时怎么做到的呢?其实是这样的:当你使用 int 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 int 的函数,当你使用 double 类型调用 Swap 函数那么编译器就会根据函数模板生成一个参数类型为 double 的函数。你可能会说,这和直接些两个函数没有什么区别啊!但事实时,我们只写了一个函数模板,多余的事儿我们都交给了编译器,这不香吗?

通过观察汇编代码,可以看到确实是生成了连个不同参数类型的函数:

在C++中,通过函数模板生成函数的过程我们叫做模板的实例化。

上面我们使用函数模板的方式叫做隐式实例化 ,即不指定模板参数的类型,编译器根据参数的类型自动推导模板参数的类型。

但是并不是所有的情况都能通过隐式实例化来完成,那个时候就必须显示实例化啦:

template<typename T>
T* alloc(size_t n)
{return new T[n];
}int main()
{int* a1 = alloc(10);//显示实例化int* a2 = alloc<int>(10);return 0;
}

隐式实例化是会报错的,因为他无法通过你传入的参数推导出模板参数T的实际类型。 

2.2 类模板

类模板和函数模板差不多,只不过定义函数的地方改成定义类。

在下面的代码中,我们定义了一个名为Stack的模板类,根据模板实例化时传入的模板参数的类型不同,我们就能实例化出来栈内元素类型不同的栈。在C语言中我们只能通过 typedef 来实现变换栈内元素的类型,但是确做不到在一个工程中同时使用 一个数据元素是 int 的栈, 一个数据元素是 double 的栈(除非你不嫌麻烦,赋值一份栈的代码)。但是有了类模板就能轻松做到。

template<typename T>
class Stack
{
public:private:T* _a;int _size;int _capacity;
};int main()
{Stack<int> st1;Stack<double> st2;
}

我们可以看到:类模板在实例化的时候就只能显示实例化了!没法隐式实例化呢!即使你能够通过类中的构造函数推导出模板参数的类型 ,也不能隐式实例化呢!这是为什么呢?第 3 点会给你答案。

3. 模板声明和定义分离

3.1 同一文件中的声明与定义分离

 我们来看全局的函数模板的声明与定义分离该怎么书写:

//函数模板的声明
template<typename T>
void Swap(T& x, T& y);//函数模板的定义
template<typename T>
void Swap(T& x, T& y)
{T tmp = x;x = y;y = tmp;
}

在声明的时候需要有 template<typename T> 在定义的时候也需要有 template<typename T> 因为模板参数只能在它下面的第一个函数或者类中使用。

相比全局函数实现 声明与定义分离,我们更喜欢用的是类的成员函数的声明与定义分离:

我们在类中声明了一个push函数,虽然我们在外面实现了push函数,但是编译器依然报错,说明我们实现的方式有问题,你可能会说加一个类域?很棒,但是还是不完全正确!在解决这个问题之前还需要铺垫一个知识:普通类的类名即是一个类型!但是模板类的类名还是一个类型吗?我都这样说了,你肯定知道不是啦!的确不是,模板类的类型需要显示指定模板参数才是该类的类型。

像这样:Stack<int> 这就是一个Stack类型。

ok,我们现在大概能猜出类成员函数的声明与定义分离应该怎么写了吧:使得还需要在函数名的前面加上这个类的类型才行 。

template<typename T>
class Stack
{
public:int size(){return _size;}void push(const T& val);private:T* _a;int _size;int _capacity;
};template<typename T>
void Stack<T>::push(const T & val)
{_a[size++] = val;
}

像上面这样我们就实现了类成员函数的声明与定义分离了呢!在实际的编程中,我们习惯将那些短小的函数直接在类内定义(默认就是内联函数了),那些比较长的函数实现声明与定义分离。 

现在我们就知道为什么模板类不可能隐式实例化了嘛,因为模板类的类名不是类型,必须指定模板参数后才是类型,只有用类型才能定义变量!

3.2 分文件的声明与定义分离

我们在写C语言的时候就喜欢将函数的定义与声明分文件编写嘛!现在我们来看看模板类的成员函数如果声明与定义分文件编写会发生什么:

//test.h /
//
#pragma once
template<typename T>
class Stack
{
public:Stack(int capacity = 4){_a = new T[capacity];_size = 0;_capacity = capacity;}int size(){return _size;}void push(const T& val);void pop();private:T* _a;int _size;int _capacity;
};///
///  test.cpp /
///#include"test.h"template<typename T>
void Stack<T>::push(const T & val)
{_a[_size++] = val;
}template<typename T>
void Stack<T>::pop()
{_size--;
}///
///  main.cpp /
///
#include<iostream>
#include"test.h"using namespace std;int main()
{Stack<int> st;st.size(); // 不会出问题st.pop();st.push(1);}

还有一个奇怪的事儿就是当你注释掉 push 和 pop 函数的调用就不会报错了!这是因为 模板 函数会按需实例化 当你没有调用这个函数时 编译器是不会实例化出来对应的函数的! 

我们发现调用 push 和 pop 函数会报链接错误。这是为啥呢?链接时错误一般都是在函数有声明,没有定义的时候出现的,但是我在 test.cpp 确实是定义了 push 和 pop 函数的啊!

我们慢慢来分析,size函数没有报错是因为,size函数在声明的时候直接就定义了,编译的时候就能直接确定函数的地址。但是对于 push 和 pop 函数,因为他们的定义在另一个文件,只有在链接的时候才能确定函数的地址,当链接的时候去找 push 和 pop 函数的地址没找到,因此报了链接错误。

为什么就没找到呢?

是因为我们的 push 与 pop 的实现在另一个文件,在模板函数所在的cpp文件,不知道模板参数的具体类型,编译器不知道该实例化什么模板参数是什么类型的函数,从而无法为这两个函数确定函数地址。链接的时候自然就找不到这两个函数的地址了!

该怎么解决这个问题呢?

我们可以在 push 与 pop 所在的文件中,显示实例化模板参数,告诉编译器模板参数的类型:

告诉编译器帮我实例化模板参数为int 的函数,但是如果我们用到了 Stack<double> 那么我们还需要在这个文件中显示实例化模板参数为 double的函数!

因此 在实际的编程中我们更喜欢将模板类的类成员函数的声明与定义放在同一个文件里面!有的人为这样的文件取名为 .hpp 文件用来标识这是一个模板类!

为什么在同一个文件里面实现定义与分离就不会报错呢?我们在cpp文件中使用这个类,都需要包含这个类的头文件,包含这个头文件之后,我们就在一个cpp文件里面同时有了函数的声明与定义。当我们使用这个模板类,肯定会传入模板参数,从而确定了模板参数的类型,编译过程中,那些定义的函数就知道了模板参数的类型,只需要根据模板参数的类型实例化函数即可!

4. 非类型模板参数

模板的参数不仅仅可以通过< typename T> 将类型参数化。还允许使用整形值充当模板参数!(这里的整形值指的是整形家族,例如 int,char,unsigned int 等)

这有什么作用呢?

现在需要你实现一个静态栈,并且要求多个静态栈的大小要不相同!你会怎么做呢?使用#define 栈的大小能解决问题嘛?显然#define 和上面的typename 陷入了一样的困境,当实例化多个时都无法实现我们的需求,那看看非类型模板参数是怎么做的吧:

template<typename T, size_t N>
class Stack
{
public:private:T _a[N];int _size;int _capacity;
};int main()
{Stack<int, 100> st1; // 空间大小为 100 的静态栈Stack<int, 10> st2; // 空间大小为 10 的静态栈}

其中那个 N 就是非类型的模板参数, 观察到 N 可以直接用来当作数组的大小。因此这个 N 是一个常量哦!不允许被修改。

下面补充一下 typename 的另一层用途:

我定义了一个类:List,然后 List<T> 中将 ListNode<T> typedef 一下。在类 B 中,我们尝试去取List<T> 中的 Node 来定义一个变量,发现报出了编译错误,这是为什么呢?

原因就在于:向模板类里面取东西,编译器无法确定你取的东西是一个类型还是一个对象(例如:静态成员变量),假设你取的是一个类型,那么 List<T>::Node _node;就不会报错;单如果你取的是一个对象,这条语句就是有问题的!所以为了明确你取的东西,需要加上typename告诉编译器,你取的是一个类型! 这个语法在我后面实现STL容器时会用到!

5. 模版的特化

来看下面的代码:我们实现了一个打印的函数模板,传入什么值就打印什么值,于是我们写出了这样的代码:

template<typename T>
void Print(const T& val)
{cout << val << endl;
}

打印都没有问题,但是我有这样一个需求,就是当你传入指针的时候,我希望打印的是指针指向的内容而不是打印指针本身,这个时候应该怎么做呢?这就要使用我们的模板特化了!

通常情况下,使用模板可以实现一些与类型无关的代码,但对于一些特殊类型的可能会得到一些错误的结 果,需要特殊处理,比如:实现了一个专门用来进行小于比较的函数模板

5.1 模板的全特化

顾名思义全特化就是将模板参数全部都特化成具体的类型。

函数模板的特化步骤:

1. 必须要先有一个基础的函数模板

2. 关键字template后面接一对空的尖括号<>

3. 函数名后跟一对尖括号,尖括号中指定需要特化的类型

4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。

比如上面的例子:我们将模板参数T特化,当传入指针类型的时候,我们打印指针指向的内容。

template<typename T>
void Print(const T& val)
{cout << val << endl;
}template<typename T>
void Print(T* val)
{cout << (*val) << endl;
}int main()
{Print(5);int a = 10;Print(&a);double b = 20.5;Print(&b);
}

注意:一般情况下如果函数模板遇到不能处理或者处理有误的类型,为了实现简单通常都是将该函数直接给出。 该种实现简单明了,代码的可读性高,容易书写,因为对于一些参数类型复杂的函数模板,特化时特别给出,因此函数模板不建议特化 

我们来看看模板类的全特化:

template<class T1, class T2>
class Show
{
public:Show(){cout << "Show(T1, T2)" << endl;}
};template<>
class Show<int, int>
{
public:Show(){cout << "Show(int, int)" << endl;}
};template<>
class Show<int, double>
{
public:Show(){cout << "Show(int, double)" << endl;}
};int main()
{Show<double, double> s1;Show<int, int> s2;Show<int, double> s3;
}

我们看到我们写了特化之后就能针对指定的类型进行特殊处理了:

5.2 模板的偏特化

模板的偏特化,就是值针对一部分模板参数进行特化:

template<class T1, class T2>
class Show
{
public:Show(){cout << "Show(T1, T2)" << endl;}
};template<class T1>
class Show<T1, int>
{
public:Show(){cout << "Show(T1, int)" << endl;}
};template<class T1>
class Show<T1, double>
{
public:Show(){cout << "Show(T1, double)" << endl;}
};int main()
{Show<double, double> s1;Show<int, int> s2;Show<int, double> s3;
}

 总之,模板的特化能够使得我们更加方便的处理特殊化的情况。这一点在我们实现STL中的优先级队列会提到!

6. 模板总结

【优点】

1. 模板复用了代码,节省资源,更快的迭代开发,C++的标准模板库(STL)因此而产生。

2. 增强了代码的灵活性。

【缺陷】

1. 模板会导致代码膨胀问题,也会导致编译时间变长。其实这个问题不可避免,如果没有模板,那么就需要你手写这么多的代码了!

2. 出现模板编译错误时,错误信息非常凌乱,不易定位错误。(这个是真的令人头大!)

相关文章:

21天学会C++:Day14----模板

CSDN的uu们&#xff0c;大家好。这里是C入门的第十四讲。 座右铭&#xff1a;前路坎坷&#xff0c;披荆斩棘&#xff0c;扶摇直上。 博客主页&#xff1a; 姬如祎 收录专栏&#xff1a;C专题 目录 1. 知识引入 2. 模板的使用 2.1 函数模板 2.2 类模板 3. 模板声明和定义…...

MQ - 32 基础功能:消息查询的设计

文章目录 导图概述什么时候会用到消息查询消息队列支持查询的理论基础消息数据存储结构关于索引的一些知识点内核支持简单查询根据 Offset 查询数据根据时间戳查询数据根据消息 ID 查询数据借助第三方工具实现复杂查询第三方引擎支持查询工具化简单查询总结导图 概述 从功能上…...

c语言练习66:模拟实现offsetof

模拟实现offsetof #define offsetof(StructType, MemberName) (size_t)&(((StructType *)0)->MemberName) StructType是结构体类型名&#xff0c;MemberName是成员名。具体操作方法是&#xff1a; 1、先将0转换为一个结构体类型的指针&#xff0c;相当于某个结构体的首…...

数据库缓存服务器集群 redis集群

redis 提升数据库性能&#xff0c;缓解数据库压力 2.NoSql产品 产品: redis,mongodb,memcached 名词解释&#xff1a;非关系型数据库 以键值对的方式存储数据---&#xff08;Key-Value&#xff09;的形式 3.NoSql的优点 高可扩展性 分布式计算 低成本 架构的灵活性&…...

[密码学入门]仿射密码(Affine)

加密算法y(axb)mod N 解密算法x*(y-b)mod N(此处的为a关于N的乘法逆元&#xff0c;不是幂的概念&#xff09; 如何求&#xff0c;涉及的知识挺多&#xff0c;还没想好怎么写&#xff0c;丢番图方程&#xff0c;贝祖定理&#xff08;又译裴蜀定理&#xff09;&#xff0c;扩展欧…...

【Maven】SpringBoot多模块项目利用reversion占位符,进行版本管理.打包时版本号不能识别问题

问题原因&#xff1a; 多模块项目使用reversion点位符进行版管理&#xff0c;打包时生成的pom文件未将 {reversion}占位符替换为真实版本号。 而当子模块被依赖时&#xff0c;引入的pom文件中版本号是&#xff1a;{reversion}。而根据这个版本号去找相应父模块时肯定是找不到的…...

Vue watch实时计算器

watch实时计算器 可以自己选择、-、*、 参考代码 <!DOCTYPE html> <html> <head><meta charset"utf-8"><title></title><script src"https://cdn.bootcdn.net/ajax/libs/vue/2.7.10/vue.js"></script>…...

Java中的super关键字

super 是Java中的一个关键字&#xff0c;它可以用来引用当前对象的父类&#xff08;超类&#xff09;的成员变量或方法。主要有以下用途&#xff1a; 访问父类的成员变量&#xff1a; 当子类和父类中有同名的成员变量时&#xff0c;可以使用super关键字来访问父类的成员变量。 …...

MySQL数据库入门到精通6--进阶篇(锁)

5. 锁 5.1 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在数据库中&#xff0c;除传统的计算资源&#xff08;CPU、RAM、I/O&#xff09;的争用以外&#xff0c;数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决…...

js的继承

一、原型链继承 将父类的实例作为子类的原型 function Father(){this.name Tony }function Son() {}Son.prototype new Father()let son new Son();console.log(son.name) // Tony缺点&#xff1a; 父类所有的引用类型属性都会被所有子类共享&#xff0c;一个子类修改了属…...

HONEYWELLL 05701-A-0325 控制脉冲模块

运动控制&#xff1a; HONEYWELLL 05701-A-0325 控制脉冲模块可以用于运动控制应用&#xff0c;例如控制步进电机或伺服电机&#xff0c;以实现精确的位置和速度控制。 定位系统&#xff1a; 在自动化设备和机器人中&#xff0c;这些模块可以用于确定物体的位置和方向&#xf…...

Qt扩展-QCustomPlot 简介及配置

QCustomPlot 简介及配置 一、概述二、安装教程三、帮助文档的集成 一、概述 QCustomPlot是一个用于绘图和数据可视化的Qt 控件。它没有进一步的依赖关系&#xff0c;并且有良好的文档记录。这个绘图库专注于制作好看的、发布质量的2D绘图、图形和图表&#xff0c;以及为实时可…...

python教程:selenium WebDriver 中的几种等待--sleep(),implicitly_wait(),WebDriverWait()

大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 强制等待:sleep() import time sleep(5) #等待5秒设置固定休眠时间&#xff0c;单位为秒。 由python的time包提供, 导入 time 包后就可以使用。’ 缺点&#xff1a; 不…...

从裸机开始安装操作系统

目录 一、预置知识 电脑裸机 win10版本 官方镜像 V.S. 正版系统 二、下载微软官方原版系统镜像 三、使用微PE系统维护U盘 四、安装操作系统 五、总结 一、预置知识 电脑裸机 ●只有硬件部分&#xff0c;还未安装任何软件系统的电脑叫做裸机。 ●主板、硬盘、显卡等必…...

redhat 6.1 测试环境安装 yum

redhat 6.1 测试环境安装 yum 记录 1. 新建虚拟机 1.1 自定义建立虚拟机 自定义创建新的虚拟机 选择硬件兼容性 创建空白硬盘&#xff0c;稍后选择 iso 文件创建系统。 选择操作系统类型 为虚拟机命名 选择处理器配置 选择虚拟机内存 选择虚拟机网络类型 选择…...

WARNING:tensorflow:Your input ran out of data; interrupting training. 解决方法

问题详情&#xff1a; WARNING:tensorflow:Your input ran out of data; interrupting training. Make sure that your dataset or generator can generate at least steps_per_epoch * epochs batches (in this case, 13800 batches). You may need to use the repeat() funct…...

ChunJun(OldNameIsFlinkX)

序言 ChunJun主要是基于Flink实时计算框架,封装了不同数据源之间的数据导入与导出功能.我们只需要按照ChunJun的要求提供原始与目标数据源的相关信息给Chunjun,然后它会帮我们生成能运行与Flink上的算子任务执行,这样就避免了我们自己去根据不同的数据源重新编辑读入与读出的方…...

MySQL的时间差函数、日期转换计算函数

MySQL的时间差函数(TIMESTAMPDIFF、DATEDIFF)、日期转换计算函数(date_add、day、date_format、str_to_date) 时间差函数&#xff08;TIMESTAMPDIFF、DATEDIFF&#xff09; 需要用MySQL计算时间差&#xff0c;使用TIMESTAMPDIFF、DATEDIFF&#xff0c;记录一下实验结果 --0 …...

【神印王座】悲啸洞穴之物揭晓,圣采儿差点被骗,幸好龙皓晨聪明

Hello,小伙伴们&#xff0c;我是小郑继续为大家深度解析神印王座。 神印王座动漫现阶段已经出到龙皓晨等人接取新任务深入魔族地界的阶段&#xff0c;而龙皓晨等人接取的任务想必现在大家都知道了&#xff0c;那就是探索魔族地界中的悲啸洞穴。但是大家知道悲啸洞穴里面藏着什么…...

性能测试之使用Jemeter对HTTP接口压测

我们不应该仅仅局限于某一种工具&#xff0c;性能测试能使用的工具非常多&#xff0c;选择适合的就是最好的。笔者已经使用Loadrunner进行多年的项目性能测试实战经验&#xff0c;也算略有小成&#xff0c;任何性能测试&#xff08;如压力测试、负载测试、疲劳强度测试等&#…...

工业安全零事故的智能守护者:一体化AI智能安防平台

前言&#xff1a; 通过AI视觉技术&#xff0c;为船厂提供全面的安全监控解决方案&#xff0c;涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面&#xff0c;能够实现对应负责人反馈机制&#xff0c;并最终实现数据的统计报表。提升船厂…...

以下是对华为 HarmonyOS NETX 5属性动画(ArkTS)文档的结构化整理,通过层级标题、表格和代码块提升可读性:

一、属性动画概述NETX 作用&#xff1a;实现组件通用属性的渐变过渡效果&#xff0c;提升用户体验。支持属性&#xff1a;width、height、backgroundColor、opacity、scale、rotate、translate等。注意事项&#xff1a; 布局类属性&#xff08;如宽高&#xff09;变化时&#…...

《通信之道——从微积分到 5G》读书总结

第1章 绪 论 1.1 这是一本什么样的书 通信技术&#xff0c;说到底就是数学。 那些最基础、最本质的部分。 1.2 什么是通信 通信 发送方 接收方 承载信息的信号 解调出其中承载的信息 信息在发送方那里被加工成信号&#xff08;调制&#xff09; 把信息从信号中抽取出来&am…...

在WSL2的Ubuntu镜像中安装Docker

Docker官网链接: https://docs.docker.com/engine/install/ubuntu/ 1、运行以下命令卸载所有冲突的软件包&#xff1a; for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done2、设置Docker…...

深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏

一、引言 在深度学习中&#xff0c;我们训练出的神经网络往往非常庞大&#xff08;比如像 ResNet、YOLOv8、Vision Transformer&#xff09;&#xff0c;虽然精度很高&#xff0c;但“太重”了&#xff0c;运行起来很慢&#xff0c;占用内存大&#xff0c;不适合部署到手机、摄…...

Vue3中的computer和watch

computed的写法 在页面中 <div>{{ calcNumber }}</div>script中 写法1 常用 import { computed, ref } from vue; let price ref(100);const priceAdd () > { //函数方法 price 1price.value ; }//计算属性 let calcNumber computed(() > {return ${p…...

【若依】框架项目部署笔记

参考【SpringBoot】【Vue】项目部署_no main manifest attribute, in springboot-0.0.1-sn-CSDN博客 多一个redis安装 准备工作&#xff1a; 压缩包下载&#xff1a;http://download.redis.io/releases 1. 上传压缩包&#xff0c;并进入压缩包所在目录&#xff0c;解压到目标…...

JavaScript 标签加载

目录 JavaScript 标签加载script 标签的 async 和 defer 属性&#xff0c;分别代表什么&#xff0c;有什么区别1. 普通 script 标签2. async 属性3. defer 属性4. type"module"5. 各种加载方式的对比6. 使用建议 JavaScript 标签加载 script 标签的 async 和 defer …...

Docker、Wsl 打包迁移环境

电脑需要开启wsl2 可以使用wsl -v 查看当前的版本 wsl -v WSL 版本&#xff1a; 2.2.4.0 内核版本&#xff1a; 5.15.153.1-2 WSLg 版本&#xff1a; 1.0.61 MSRDC 版本&#xff1a; 1.2.5326 Direct3D 版本&#xff1a; 1.611.1-81528511 DXCore 版本&#xff1a; 10.0.2609…...

python可视化:俄乌战争时间线关键节点与深层原因

俄乌战争时间线可视化分析&#xff1a;关键节点与深层原因 俄乌战争是21世纪欧洲最具影响力的地缘政治冲突之一&#xff0c;自2022年2月爆发以来已持续超过3年。 本文将通过Python可视化工具&#xff0c;系统分析这场战争的时间线、关键节点及其背后的深层原因&#xff0c;全面…...