【C++】深入理解类和对象(1)

news2024/11/15 23:45:05

自己打败自己是最可悲的失败,自己战胜自己是最可贵的胜利。💓💓💓 

目录

  ✨说在前面

🍋知识点一:类的定义

• 🌰1.类定义格式

• 🌰2.访问限定符

• 🌰3.类域

🍋知识点二:实例化

• 🌰1.什么是实例化?

• 🌰2.对象的大小

• 🌰3.this指针

🍋C与C++实现Stack对比

• 🌰1.封装

• 🌰2.Stack的实现对比

 • ✨SumUp结语


  ✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章中我带大家学习了C++最基本的基础语法和C++的发展历史如果大家没有掌握好,可以再回去看看,复习一下,再进入今天的内容。

今天我们将要学习C++中很重要的一部分,也是C++学习中的第一大关卡——类和对象。如果大家准备好了,那就接着往下看吧~

  👇👇👇
💘💘💘知识连线时刻(直接点击即可)

【C++】入门基础知识【C++】入门基础知识

  🎉🎉🎉复习回顾🎉🎉🎉

        

   博主主页传送门:愿天垂怜的博客

 

🍋知识点一:类的定义

• 🌰1.类定义格式

🔥class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面的分号是不能省略的。类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数,

🔥为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。

🔥C++中struct也可以定义类,C++兼容C中struct的语法,同时struct升级成了类,明显的变化就是struct中可以定义函数,一般情况下我们还是推荐用class定义类。

🔥定义在类里面的成员函数默认为inline。

class Stack
{
	void Push(int x)//入栈
	{
		//...
	}
	void Pop()//出栈
	{
		//...
	}
	int Top()//取栈顶数据
	{
		//...
	}
	int* a;
	int top;
	int capacity;
};

 

• 🌰2.访问限定符

🔥C++一种实现封装的方式,用类将对象的属性和方法结合在一块,让对象更加完善,通过的访问权限选择性的将其接口提供给外部的用户使用。

🔥public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是一样的,在学习继承的时候才能体现出他们的区别。

🔥访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就要到类结束。

🔥class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

🔥一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。

示例1:访问限定符的使用

class Stack
{
public:
	void Push(int x)//入栈
	{
		//...
	}
	void Pop()//出栈
	{
		//...
	}
	int Top()//取栈顶数据
	{
		//...
	}
private:
	int* a;
	int top;
	int capacity;
};

示例2:区分成员变量

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	//为了区分成员变量,一般习惯上成员变量会加一个特殊标识_或者m开头
	int _year;//year m_year
	int _month;//声明,没有开空间
	int _day;
};

int main()
{
	Date d;
	d.Init(2024, 7, 9);

	return 0;
}

 

• 🌰3.类域

🔥类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

🔥类域影响的是编译的查找规则,下面程序中Init如果不指定类域Date,那么编译器就会把Init当成全局函数,那么编译时,找不到成员的声明/定义在哪里,就会报错。指定类域Date,就是直到Init是成员函数,当前域找不到成员,就会到类域中区查找。

示例:声明与定义分离

class Date
{
public:
	void Init(int year, int month, int day);
private:
	int _year;
	int _month;
	int _day;
};

//类外定义成员函数
void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

注意:声明与定义分离,Init函数就不属于内联函数。

 

🍋知识点二:实例化

• 🌰1.什么是实例化?

🔥用类类型在物理内存中创建对象的过程,称为类实例化对象。

🔥类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员链表只是声明,没有分配空间,用类实例化出来对象时,才会分配空间。

🔥一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出的对象就像现实中使用建筑设计图建造出房子,类就是设计图,设计图规划了有多少个房间,房间大小功能等,但并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。

示例:类实例化出对象,才会分配空间。 

#include <iostream>
using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	//这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	//Date类实例化出对象d1和d2
	Date d1;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();

	return 0;
}

 

• 🌰2.对象的大小

分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一端指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢?Date示例化出d1和d2的成员函数Init/Print指针确是一样的,存储在各自的数据,如果用Date示例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要在额外说一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接的时候,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,这个以后我们会说。

上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。

我们回顾一下结构体的内存对齐规则:

🔥第一个成员在与结构体偏移量为0的地址处。

🔥其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数 = min{ 编译器默认的一个对齐数, 该成员大小 },VS中默认对齐数为8。

🔥结构体总大小为最大对齐数(所有变量最大者与默认对齐参数取最小)的整数倍。

🔥如果嵌套了结构体的情况,嵌套的结构体对其到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含有嵌套结构体的对齐数)的整数倍。

问:为什么结构体需要结构对齐?

CPU在读取内存中的数据时,一次读取的数据量是有限的,通常是按照字(word)或双字(dword)等单位进行读取,这些单位的大小通常是2的幂次方(如4字节或8字节)。如果结构体中的成员变量不满足对齐要求,CPU可能需要多次读取操作才能获取完整的数据,并且,CPU不能从任意位置开始读取(整数倍位置开始读取),如一个int类型的变量会被拆成两次取读取,这会降低读取效率。而对齐后的结构体成员变量地址是连续的,CPU可以通过一次读取操作将整个结构体读入,从而提高读取效率。

#include <iostream>
using namespace std;

class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};

class B
{
public:
	void Print()
	{
		//...
	}
};

class C {};

int main()
{
	A a;
	B b;
	C c;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;

	return 0;
}

我们以上面的代码为例,思考A、B、C三种类类型所实例化出的对象a,b,c大小分别是多少。

根据对齐规则,我们很容易知道a的大小是8个字节,那b和c呢?b好歹有一个成员函数,c什么都没有,难道大小是0吗,那如果是0,就是没有开空间,那怎么证明b和c存在?

然而,上面程序运行后,我们看到没有成员变量的B和C类对象的大小是1,这个1,纯粹是为了占位,标识对象存在。

 

• 🌰3.this指针

我们之前写的Date类中有Init与Print两个成员函数,函数体内没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了一个隐含的this指针解决这里的问题。

🔥编译器编译后,类的成员函数默认都会在形参的第一个位置,增加一个当前类类型的指针,叫做this指针。如Date类的Init的真实原型为:void Init(Date* const this, int year, int month, int day)

🔥类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,this->year = year

🔥C++规定不能再形参和实参的位置显式的写this指针(编译时编译器会处理),但是可以在函数体内显式使用this指针。

#include<iostream>
using namespace std;

class Date
{
public:
	//void Init(Data* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	//void Print(Data* const this);
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}
private:
	//这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	//Date类实例化出对象d1和d2
	Date d1;
	Date d2;
	
	d1.Init(2024, 3, 31);//d1.Init(&d1, 2024, 3, 31);
	d1.Print();//d1.Print(&d1);

	d2.Init(2024, 7, 5);//d2.Init(&d2, 2024, 7, 5);
	d2.Print();//d2.Print(&d2);
	
	return 0;
}

下面通过两个选择题测试一下前面只是到底有没有真正理解。

1.下面程序编译运行的结果是( )

A.编译报错        B.运行崩溃        C.正常运行

#include <iostream>
using namespace std;

class A
{
public:
    void Print()
   {
        cout << "A::Print()" << endl;
   }
private:
   int _a;
};

int main()
{
   A* p = nullptr;
   p->Print();
 
   return 0;
}

正确答案为C.

那为什么是正常运行呢?很多人会认为p是一个空指针,那么p->Print就是空指针解引用。实际这个想法是错误的,因为编译后p->Print所对应的指令是[call + 地址],而后[mov ecx p],这个地址并不存在p对象中,所以不会造成空指针的解引用,代码正常运行。

2.下面程序编译运行的结果是( )

A.编译报错        B.运行崩溃        C.正常运行

#include < iostream>
using namespace std;

class A
{
public:	
	void Print()
	{
		cout << "A::Print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();

	return 0;
}

正确答案是B. 

那为什么会崩溃呢?崩溃的点不在于p->Print,而在于Print函数中会打印出_a的值,而_a是确确实实存在类对象中的,相当于解引用,所以会崩溃。

3.this指针存在内存中的哪个区域( )

A.栈        B.堆        C.静态区        D.常量区        E.对象里面

正确答案是A.

首先我们再计算类实例化的对象的大小时,并没有计算this指针,所以肯定是不在对象里的;而由于this指针是形参,形参存放在函数栈帧,也就是在栈区,所以这道题选A更合适。

为什么说更合适而不是正确呢,因为这还得看编译器,如VS下其实是存放在寄存器里的。

 

🍋C与C++实现Stack对比

 

• 🌰1.封装

面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。 

🔥C++中数据结构和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,也是最重要的变化。这里封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。

🔥C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多;成员函数不需要传对象地址,因为有this指针隐含地传递了,方便了很多;使用类型不再需要typedef用类名就很方便。

🔥我们再这个C++的入门阶段实现Stack看起来变了很多,实际上本质变化不大,如果用后面的STL中用适配器实现的Stack,大家就能体会到C++的魅力了。

 

• 🌰2.Stack的实现对比

C语言实现Stack代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int STDataType;
typedef struct Stack
{
	STDataType * a;
	int top;
	int capacity;
}ST;

//栈的初始化
void STInit(ST * ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

//栈的销毁
void STDestroy(ST * ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

//压栈
void STPush(ST * ps, STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType * tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

//判断栈是否为空
bool STEmpty(ST * ps)
{
	assert(ps);
	return ps->top == 0;
}

//弹栈
void STPop(ST * ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	ps->top--;
}

//取栈顶元素
STDataType STTop(ST * ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	return ps->a[ps->top - 1];
}

//栈的大小
int STSize(ST * ps)
{
	assert(ps);
	return ps->top;
}

int main()
{
	ST s;
	STInit(&s);
	
	STPush(&s, 1);
	STPush(&s, 2);
	STPush(&s, 3);
	STPush(&s, 4);
	while (!STEmpty(&s))
	{
		printf("%d\n", STTop(&s));
		STPop(&s);
	}
	
		STDestroy(&s);
		return 0;
}

C++实现Stack代码如下:

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	//栈的初始化
	void Init(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	//压栈
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType * tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
			if (tmp == nullptr)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	//弹栈
	void Pop()
	{
		assert(_top > 0);
		--_top;
	}
	//判断栈是否为空
	bool Empty()
	{
		return _top == 0;
	}
	//取栈顶元素
	int Top()
	{
		assert(_top > 0);
		return _a[_top - 1];
	}
	//栈的销毁
	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	//成员变量
	STDataType * _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);
	while (!s.Empty())
	{
		printf("%d\n", s.Top());
		s.Pop();
	}

	s.Destroy();

	return 0;
}

 

 • ✨SumUp结语

到这里本篇文章的内容就结束了,本节初步带大家学习了类和对象的第一大内容。这是类和对象的基础,后面的内容会更加有难度,下一章节的内容也格外重要。希望大家能够认真学习,打好基础,迎接接下来的挑战,希望大家继续捧场~

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

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

相关文章

人工智能时代,程序员当如何保持核心竞争力?

目录 前言 一.AI辅助编程对程序员工作的影响 二.程序员应重点发展的核心能力 三.人机协作模式下的职业发展规划 结束语 前言 随着AIGC&#xff08;如chatgpt、midjourney、claude等&#xff09;大语言模型接二连三的涌现&#xff0c;AI辅助编程工具日益普及&#xff0c;程序…

C++第三十一弹---C++继承机制深度剖析(下)

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 1.菱形继承及菱形虚拟继承 1.1 单继承 单继承&#xff1a;一个子类只有一个直接父类时称这个继承关系为单继承。 Student的直接父类是Person&#xff…

双向循环链表和内核链表

目录 双向循环链表 结构设计 初始化 插入 删除 遍历&#xff08;顺序/逆序&#xff0c;打印输出&#xff09; 查找 主函数 内核链表 内核链表初始化定义 内核链表的插入定义 内核链表的遍历定义 内核链表剔除节点定义 内核链表如何移动节点定义 内核链表的应用 临时补充…

身在职场,不得不提防的几个问题,能让少走许多弯路

职场路本就崎岖&#xff0c;如果再走了弯路&#xff0c;脚下的路将会更漫长且难走。 谁不想一帆风顺&#xff0c;可谁又能一帆风顺&#xff1f;不是人心险恶&#xff0c;而是立场本就不同&#xff0c;为了各自的利益考虑无可厚非。 你可以说凭借能力获取利益&#xff0c;为什…

CVE-2023-37569~文件上传【春秋云境靶场渗透】

# 今天我们拿下CVE-2023-37569这个文件上传漏洞# 经过简单账号密码猜测 账号&#xff1a;admin 密码&#xff1a;password# 找到了文件上传的地方# 我们直接给它上传一句话木马并发现上传成功# 上传好木马后&#xff0c;右键上传的木马打开发现上传木马页面# 直接使用蚁剑进行连…

Linux5:Shell编程——函数、重定向

目录 前言 一、函数 1.函数结构 2.函数实例 3.函数传参 二、重定向 1.输出重定向 2.输入重定向 3.同时使用 4.重定向深入了解 5.垃圾桶 总结 前言 Shell编程将会在本章完结 一、函数 1.函数结构 #!/bin/sh # 函数function fun1() {echo "this is a funtion&q…

【有手就行】:从无到有搭建后端SpringBoot项目

前言 想静下心来写点东西&#xff0c;但是确实想不到该写点啥&#xff0c;可能是少了点感觉吧 &#x1f622;。前面刚整理了下前端VUE&#xff0c;就想了下把后端也一起整理下吧&#xff0c;免得换电脑了安装环境又要弄半天&#xff0c;那就开搞吧 首先 准备环境 1.安装IDEA…

云计算实训21——mysql-8.0.33-linux-glibc安装及使用

一、mysql-8.0.33-linux-glibc安装 安装步骤 1.解压 tar -xvf mysql-8.0.33-linux-glibc2.12-x86_64.tar.xz 2.清空其他环境 rm -rf /etc/my.cnf 3.安装依赖库 yum list installed | grep libaio 4.创建用户 useradd -r -s /sbin/nologin mysql 查看 id mysql 5.创建mysql-fi…

PXE批量网络装机(超详细实验教程)教会你自动化批量安装linux 系统 红帽7

1.创建自动化安装服务器 1.1. 搭建本地厂库 写入rpm.re文件内容 [rhel7]namerhel7baseurlfile:///rhel7gpgcheck0 Yum makecache 测试是否挂载成功 1.2.关闭虚拟机的本地DHCP 1.3下载必要软件 下载图形化脚本自动生成工具方便编写脚本 下载dhcp分配ip httpd 搭建网页 …

数据排序之旅

1、排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递增或递减的排列起来的操作。 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序…

vue3 + i18n 实现国际化并动态切换语言

安装 npm install vue-i18n// index.ts import { createI18n } from vue-i18n // 语言包 import ch from ./ch import en from ./enconst lang localStorage.getItem(localeLangD) || ch if (!localStorage.getItem(localeLangD)) {localStorage.setItem(localeLangD, lang) …

linux文本命令:文本处理工具awk详解

目录 一、概述 二、基本语法 1、awk 命令的基本语法 2、常用选项 3、获取帮助 三、工作原理 四、 功能特点 五、分割字段 六、 示例 1. 打印所有行 2. 计算总和 3. 过滤特定模式 4. 使用多个模式 5. 复杂的脚本 6. 自定义分隔符 7. 打印指定列 8. 使用 BEGIN …

微信小程序教程011-1:京西购物商城实战

文章目录 1、起步1.1 uni-app简介1.2 开发工具1.2.1 下载HBuilderX1.2.2 安装HBuilderX1.2.3 安装scss/sass编译1.2.4 快捷键方案切换1.3 创建uni-app项目1.4 目录结构1.5 把项目运行到微信开发者工具1.6 使用Git管理项目1.6.1 本地管理1.6.2 把项目托管到码云1、起步 1.1 uni…

【Unity】3D功能开发入门系列(五)

Unity3D功能开发入门系列&#xff08;五&#xff09; 一、预制体&#xff08;一&#xff09;预制体&#xff08;二&#xff09;预制体的创建&#xff08;三&#xff09;预制体实例&#xff08;四&#xff09;预制体的编辑 二、动态创建实例&#xff08;一&#xff09;动态创建实…

2024/8/4 汇川变频器低压产品分类选型

VF就是通过电压、频率控制 矢量就是通过开环&#xff08;svc&#xff09;和闭环&#xff08;fvc&#xff09; MD310、MD200 开环&#xff0c;不支持闭环&#xff0c;无法接编码器 290 、200s、280、都是VF控制

有哪些供应链管理方法?详解四种常用的供应链管理方法!

在当今复杂多变的商业环境中&#xff0c;供应链管理已成为企业获取竞争优势的关键。有效的供应链策略不仅能提升企业的响应速度和市场适应性&#xff0c;还能显著降低成本、提高效率。本文将深入探讨几种主流的供应链管理方法&#xff0c;包括快速反应、有效客户反应、基于活动…

LeetCode 0572.另一棵树的子树:深搜+广搜(n^2做法就能过,也有复杂度耕地的算法)

【LetMeFly】572.另一棵树的子树&#xff1a;深搜广搜&#xff08;n^2做法就能过&#xff0c;也有复杂度耕地的算法&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/subtree-of-another-tree/ 给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 s…

DEBUG:sw模板不对

问题 sw自带模板不合适 解决 工具 选项 文件位置 自己新建一个文件夹 放入模板 &#xff08;三维 二维各一个 一般就是统一标准 可以自己新建个模板&#xff09;

深度学习笔记(神经网络+VGG+ResNet)

深度学习 主要参考博客常用英语单词 概念应用神经网络基础神经网络基本结构 超参数超参数是什么常用超参数超参数搜索过程常用超参数调优办法&#xff08;通过问题复杂度和计算资源选择&#xff09; 激活函数介绍为什么要使用激活函数推荐博客 sigmoid激活函数&#xff08;使用…

【教程-时间序列预测】PyTorch 时间序列预测入门

文章目录 from博客: https://zhajiman.github.io/post/pytorch_time_series_tutorial/#%E9%AB%98%E7%BA%A7%E6%96%B9%E6%B3%95%E8%87%AA%E5%9B%9E%E5%BD%92%E6%A8%A1%E5%9E%8B 数据集产生 窗口 也是难点&#xff01;