C++学习记录——삼십 智能指针

news2024/12/23 18:10:04

文章目录

  • 1、为什么需要智能指针?
  • 2、内存泄漏
  • 3、智能指针的使用及原理
    • 1、RAII思想
    • 2、拷贝问题
      • 1、unique_ptr
      • 2、shared_ptr
        • 1、多线程
        • 2、循环引用
        • 3、定制删除器


1、为什么需要智能指针?

看一个场景

int div()
{
	int a, b;
	cin >> a >> b;
	if (b == 0)
		throw invalid_argument("除0错误");
	return a / b;
}

void Func()
{
	int* p1 = new int;
	int* p2 = new int;
	cout << div() << endl;
	delete p1;
	delete p2;
}

int main()
{
	try
	{
		Func();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
	return 0;
}

new是可能开辟失败,抛异常的。上述代码中,如果p1抛异常,那么可以外面的catch可以捕获到,打印出消息;如果p1异常,p2也要抛异常,那么在这之前,应当销毁p1,再去抛;同理,到了div()那里如果也抛异常,那么得销毁p1和p2,整体就得这样写

void Func()
{
	// 1、如果p1这里new 抛异常会如何?
	// 2、如果p2这里new 抛异常会如何?
	// 3、如果div调用这里又会抛异常会如何?
	int* p1 = new int;
	try
	{
		int* p2 = new int;
	}
	catch (...)
	{
		delete p1;
		throw;
	}
	try
	{
		cout << div() << endl;
	}
	catch (...)
	{
		delete p1;
		delete p2;
		throw;
	}
	delete p1;
	delete p2;
}

一下子就能看出来,这太麻烦了,如果有多个new呢?

2、内存泄漏

在这里插入图片描述

在这里插入图片描述

Windows和Linux都有检测内存泄漏的工具,不过Windows下的VLD不太靠谱,Linux中valgrind是比较出名的

Linux下几款C++程序中的内存泄露检查工具

为了预防内存泄漏,常用的办法就是用智能指针或者事后检测。

3、智能指针的使用及原理

1、RAII思想

template <class T>
class SmartPtr
{
public:
	SmartPtr(T* ptr)
		:_ptr(ptr)
	{}

	~SmartPtr()
	{
		if (_ptr)
		{
			cout << _ptr << endl;
			delete _ptr;
		}
	}

private:
	T* _ptr;
};

void Func()
{
	SmartPtr<int> sp1(new int);
	SmartPtr<int> sp2(new int);
	cout << div() << endl;
}

和封装锁的思路来类似,都是RAII。用临时变量来构造,出了作用域就自动销毁。

RAII利用对象生命周期来控制程序资源,对象构造时获取资源,析构时释放资源

上面的SmartPtr不像一个指针,它不能解引用数据,不过我们可以写对应的函数。

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
	
	cout << *sp1 << endl;//如果模板参数是自定义类型的话就可以用->了。

2、拷贝问题

智能指针如何拷贝?

int main()
{
	SmartPtr<int> sp1(new int(1));
	SmartPtr<int> sp2(sp1);
	return 0;
}

采用默认拷贝会浅拷贝,导致同一空间重复释放。这里应当如何写拷贝构造?是要用深拷贝吗?其实不是,我们要的浅拷贝,sp1和sp2指向同一个资源,以前的链表等这些迭代器结构不需要释放资源,而智能指针需要管理资源,所以不能单纯地浅拷贝,但是又不能要深拷贝。

C++98时已经有智能指针了,那个版本中有一个auto_ptr,它的方法是管理权转移,我们写到SmartPtr类中

	//管理权转移
	auto_ptr(auto_ptr<T>& ap)
		:_ptr(ap._ptr)
	{
		ap._ptr = nullptr;
	}
	auto_ptr<int> sp2(sp1);

这样看就像是用ap指向sp1,然后用sp1的_ptr来初始化sp2的_ptr,然后把sp1的_ptr给置空。虽然看起来是可以的,能解决问题,但有很大隐患,这会导致sp1悬空,如果不知道管理权转移的实际写法,那么下面代码中如果有*sp1就出问题了。程序员用它的时候需要时刻提醒自己,被拷贝对象已经悬空了,不能去解引用它。

在C++11之前,有个可移植的C++库——Boost库,不是标准库,但也胜似标准库,是有C++标准委员会库工作组成员发起的,C++中有很多标准都从Boost中吸收过来,像右值引用,线程库。Boost库有scoped_ptr,weak_ptr,_shared_ptr,C++11中把scoped_ptr改名成unique_ptr。

1、unique_ptr

它的思路是防拷贝

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以会把它放在私有里
	//C++11思路:函数后= delete
	//这里拷贝构造和赋值都写上
	unique_ptr(const unique_ptr<T>& up) = delete;
	unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

不需要拷贝的场景就用它。

2、shared_ptr

引用计数的思路。有多少个指针指向一个空间,那么这个空间的引用计数就是多少。当一个指针要释放时,如果引用计数大于0,那就不做操作,如果等于0,那就做一次释放资源,这个空间的指针也都用完了。

引用计数这个变量不能放在静态区,因为如果static修饰后,它属于类的每个对象,但我们要的是指向同一空间的所有指针。定义一个int* pcount。

	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	~shared_ptr()
	{
		if (--(*_pcount) == 0)
		{
			cout << _ptr << endl;
			delete _ptr;
			delete _pcount;
		}
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		++(*_pcount);
	}

赋值函数,比如sp1 = sp3,那么sp1的引用计数需要–,因为它要指向新空间了;假设sp1的空间还有别的指针指向,而sp3的空间只有sp3这一个指针,sp3 = sp1,那么就是sp3–。

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			if (--(*_pcount) == 0)//处理空间上只有一个指针的情况
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			++(*_pcount);
		}
		return *this;
	}

1、多线程

整体改成这样的形式来配合加锁

	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
	{}

	void Release()
	{
		if (--(*_pcount) == 0)
		{
		    if(_ptr)//如果为空那就不需要释放
		    {
		        delete _ptr;
		    }
			delete _pcount;
		}
	}

	void AddCount()
	{
		++(*_pcount);
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			AddCount();
		}
		return *this;
	}

多线程比较常见的场景就是线程安全问题。同一个数会出现多次操作,导致结果不是我们想要的。多线程情况下,像传给接收引用的参数时,要写成ref(…),ref是库中的函数,否则会被认为是传值传参。

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}

	void Release()
	{
		_pmtx.lock();
		if (--(*_pcount) == 0)
		{
		    if(_ptr)
		    {
		        delete _ptr;
		    }
			delete _pcount;
		}
		_pmtx.unlock();
	}

	void AddCount()
	{
		_pmtx.lock();
		++(*_pcount);
		_pmtx.unlock();
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp->_pmtx;
			AddCount();
		}
		return *this;
	}

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
	//C++11思路:函数后= delete
	//unique_ptr(const unique_ptr<T>& up) = delete;
	//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* pcount;
	mutex* _pmtx;
};

在Release那里,到了引用计数减到0时,需要释放引用计数,释放锁。如果是在if里释放锁,那么外面的解锁操作就有问题了。 解决办法是可以设置一个状态位

	void Release()
	{
		_pmtx.lock();
		bool deleteFlag = false;
		if (--(*_pcount) == 0)
		{
		    if(_ptr)
		    {
		        delete _ptr;
		    }
			delete _pcount;
			deleteFlag = true;
		}
		_pmtx.unlock();
		if (deleteFlag)
		{
			delete _pmtx;
		}
	}

shared_ptr本身是线程安全的,因为计数是加锁保护的,它实例化的对象不是线程安全的,想要线程安全,那么在对对象操作时用锁保护就行。

2、循环引用

写一个场景,还是用上面的shared_ptr

template <class T>
class shared_ptr
{
public:
	shared_ptr(T* ptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}

	void Release()
	{
		_pmtx.lock();
		bool deleteFlag = false;
		if (--(*_pcount) == 0)
		{
			if (_ptr)
			{
				delete _ptr;
			}
			delete _pcount;
			deleteFlag = true;
		}
		_pmtx.unlock();
		if (deleteFlag)
		{
			delete _pmtx;
		}
	}

	void AddCount()
	{
		_pmtx.lock();
		++(*_pcount);
		_pmtx.unlock();
	}

	~shared_ptr()
	{
		Release();
	}

	shared_ptr(const shared_ptr<T>& sp)
		:_ptr(sp._ptr)
		, _pcount(sp._pcount)
		, _pmtx(sp._pmtx)
	{
		AddCount();
	}

	shared_ptr<T>& operator=(const shared_ptr<T>& sp)
	{
		if (_ptr != sp._str)
		{
			Release();
			_ptr = sp._ptr;
			_pcount = sp._pcount;
			_pmtx = sp->_pmtx;
			AddCount();
		}
		return *this;
	}

	//防拷贝
	//C++98思路:只声明不实现,但是还可以在外面强行定义,所以放在私有里
	//C++11思路:函数后= delete
	//unique_ptr(const unique_ptr<T>& up) = delete;
	//unique_ptr<T>& operator=(const unique_ptr<T>& up) = delete;

	T& operator*()
	{
		return *_ptr;
	}

	T* operator->()
	{
		return _ptr;
	}
private:
	T* _ptr;
	int* pcount;
	mutex* _pmtx;
};

struct ListNode
{
	ListNode* _next;
	ListNode* _prev;
	int _val;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	shared_ptr<ListNode> n1 = new ListNode;
	shared_ptr<ListNode> n2 = new ListNode;
	return 0;
}

当尝试连接两个节点时就发生了错误

	n1->_next = n2;
	n2->_prev = n1;

n1和n2是智能指针类型,而next和prev是ListNode类型的,无法赋值,那把ListNode里的两个指针换成shared_ptr< ListNode >类型的,但这样还不行,因为我们在定义next和prev时没有传参,是无参构造,所以在智能指针的类里应当写上缺省参数。

	shared_ptr(T* ptr = nullptr)
		:_ptr(ptr)
		, _pcount(new int(1))
		, _pmtx(new mutex)
	{}
	
struct ListNode
{
	shared_ptr<ListNode> _next;
	shared_ptr<ListNode> _prev;
	int _val;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

现在有一个问题

	n1->_next = n2;
	n2->_prev = n1;

如果两句都写,程序会不释放资源,而如果只写一句或者两句都不写,那就会释放资源,就会打印"~ListNode",使用库中的智能指针也是这样,这就是智能指针引起的循环引用问题。

n1和n2都有各自的next和prev,如果不相互连接,也就是什么都不写,那么next和prev随着n1n2销毁而销毁。

写了一句,比如n1->_next = n2,那么n2这个节点除了它本身,还有n1的next指向它,n2析构时,引用计数–,但是空间不销毁,n1析构时,里面的成员变量也会随着析构,那么整体也可以完好地退出。

但是两句都写就出问题了。

	n1->_next = n2;
	n2->_prev = n1;

出了作用域,n2先析构,引用计数–,但是还不能销毁空间,引用计数没有为0,也还有一个指针指向它;n1析构时,n1也是一样,也不能析构,引用计数–,现在这两个空间的引用计数都为1,n1的next指向n2的空间,n2的prev指向n1的空间,那么n1这个空间什么时候析构?要看prev,prev析构,n1这个空间就析构,但是n2这个空间由next指向,next析构,n2才能析构,prev才能析构,所以next和prev已经形成了相互制约的关系,没办法全部析构了。这就是循环引用,会导致内存泄漏。

为了解决这个问题,标准库中有个weak_ptr来辅助shared_ptr,也叫做弱指针。weak_ptr不是RAII的,也就是它不是常规的智能指针,但是支持像指针一样,专门用来解决shared_ptr的辅助引用问题。用weak这样写。

struct ListNode
{
	std::weak_ptr<ListNode> _next;
	std::weak_ptr<ListNode> _prev;
	int _val;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

weak_ptr不会增加引用计数。标准库中weak_ptr实现得很复杂,我们这里只模拟实现一个简单的

	template <class T>
	class weak_ptr
	{
	public:
		weak_ptr()
			:_ptr(nullptr)
		{}

		weak_ptr(const shared_ptr<T>& sp)
			:_ptr(sp.get())
		{}

		T& operator*()
		{
			return *_ptr;
		}

		T* operator->()
		{
			return _ptr;
		}

		T* get() const
		{
			return _ptr;
		}

		int use_count()
		{
			return *_pcount;
		}
	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
	};
struct ListNode
{
	zyd::weak_ptr<ListNode> _next;
	zyd::weak_ptr<ListNode> _prev;
	int _val;

	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};

int main()
{
	zyd::shared_ptr<ListNode> n1 = new ListNode;
	zyd::shared_ptr<ListNode> n2 = new ListNode;

	n1->_next = n2;
	n2->_prev = n1;
	return 0;
}

3、定制删除器

在实例化的时候,传new int[10]这样的话,可能会崩溃,是因为new []会在开辟的空间前再开辟一个存放元素个数的空间,但是delete的时候会从开辟的空间开始释放,而不包含那个存储个数的空间,所以本质上是释放的位置不对。

定制删除器本质上是一个可调用对象,函数指针,仿函数,lambda都可以。

template <class T>
struct DeleteArray
{
	void operator()(T* ptr)
	{
		cout << "仿函数" << endl;
		delete[] ptr;
	}
};

int main()
{
	//zyd::shared_ptr<ListNode> n1 = new ListNode;
	//zyd::shared_ptr<ListNode> n2 = new ListNode;

	//n1->_next = n2;
	//n2->_prev = n1;
	std::shared_ptr(int) spa1(new int[10], DeleteArray<int>());//仿函数
	std::shared_ptr(int) spa2(new int[10], [](int* ptr) {delete[] ptr; });//lambda
	return 0;
}

库中的做法是把这个删除器放到构造函数里,实例化的时候传过来,保存起来,析构时用它去析构。这里的重点在于如何保存这个删除器。一个是我们可以在总的模板参数那里加一个模板参数,那么析构函数就可以直接用,也不用在构造函数那里在写上一个模板参数;或者用包装器。这里写包装器。

		template <class D>
		shared_ptr(const shared_ptr<T>& sp, D del)
			:_ptr(sp._ptr)
			, _pcount(sp._pcount)
			, _pmtx(sp._pmtx)
			, _del(del)
		{
			AddCount();
		}
    
        void Release()
		{
			_pmtx.lock();
			bool deleteFlag = false;
			if (--(*_pcount) == 0)
			{
				if (_ptr)
				{
					//delete _ptr;
					_del(_ptr);
				}
				delete _pcount;
				deleteFlag = true;
			}
			_pmtx.unlock();
			if (deleteFlag)
			{
				delete _pmtx;
			}
		}
        
    private:
	   	T* _ptr;
		int* _pcount;
		mutex* _pmtx;
		functional<void(T*)> _del;

这样写其实会有问题,如果用不到这个删除器就会调用默认构造,删除器没有初始化,到了析构时,删除器就是被编译器默认初始化的,用它来析构就容易出问题。我们可以用缺省

	private:
		T* _ptr;
		int* _pcount;
		mutex* _pmtx;
		functional<void(T*)> _del = [](T* ptr) {
			cout << "lambda delete:" << ptr << endl;
			delete ptr;
		};

定制删除器当作了解,重点在于shared_ptr的实现。

本篇gitee

结束。

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

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

相关文章

初探---Qt

目录 一、介绍Qt 二、软件安装 三、QT工具介绍 四、Assistant帮助文档的使用 五、设计师界面的介绍 ​编辑 六、QT工程项目各文件初始程序的介绍 1> 配置文件&#xff1a;.pro文件 2> 头文件 3> 源文件 4> 主程序 5> 各文件之间调用方式 七、第一个…

一篇文章搞定《WebView的优化及封装》

一篇文章搞定《WebView的优化及封装》 前言WebView的过程分析确定优化方案一、预加载&#xff0c;复用缓冲池&#xff08;初始化优化&#xff09;优化的解析说明具体的实现 二、预置模版&#xff08;请求、渲染优化&#xff09;优化的解析说明具体的实现1、离线包2、预获取数据…

【云原生进阶之PaaS中间件】第一章Redis-2.3.3集群模式

1 集群模式 Redis集群是一个提供在多个Redis节点之间共享数据的程序集。它并不像Redis主从复制模式那样只提供一个master节点提供写服务,而是会提供多个master节点提供写服务,每个master节点中存储的数据都不一样,这些数据通过数据分片的方式被自动分割到不同的master节点上…

手写实现call() apply() bind()函数,附有详细注释,包含this指向、arguments讲解

手写实现call() apply() bind()函数是很经典的问题&#xff0c;但是能掰扯清楚的文章确实不算多&#xff0c;于是笔者才决定写下本文&#xff0c;希望能给读者带来一些启发&#xff0c;如有错误欢迎指正。 目录 补充知识 函数中的this指向 类数组对象arguments call() 原理…

Leedcode19. 删除链表的倒数第 N 个结点

给你一个链表&#xff0c;删除链表的倒数第 n 个结点&#xff0c;并且返回链表的头结点。 输入&#xff1a;head [1,2,3,4,5], n 2 输出&#xff1a;[1,2,3,5] 示例 2&#xff1a; 输入&#xff1a;head [1], n 1 输出&#xff1a;[] 示例 3&#xff1a; 输入&#xff1…

MPI之持久化通信句柄与非持久化通信句柄

MPI_Isend & MPI_Send 创建临时通信句柄 在前面的文章中举了例子&#xff0c;我们使用MPI_Isend接口发送数据时&#xff0c;有个传出参数request&#xff0c;该参数是创建的通信句柄&#xff0c; 实际上该句柄是一个临时句柄&#xff0c;即只用于一次性发送数据的场景&…

uniapp iOS打包证书申请流程——mac

如何在 Mac 创建 iOS 打包证书&#xff1f; 文章目录 如何在 Mac 创建 iOS 打包证书&#xff1f;会员 VS 非会员权限步骤添加设备创建标识符生成证书生成描述文件 前提&#xff1a; Mac 电脑Apple ID 申请Apple ID成为开发者 developer 注意&#xff1a; 登录 Apple ID 成为开…

2023年数字孪生行业研究报告

第一章 行业概况 1.1 定义 数字孪生&#xff08;Digital Twin&#xff09;是一种先进的建模技术&#xff0c;它通过创建一个物理实体的虚拟复制品&#xff0c;以实时模拟、预测和优化实体的行为和性能。这个虚拟模型会同步收集和分析来自其物理对应物的数据&#xff0c;从而提…

【DRONECAN】(三)WSL2 及 ubuntu20.04 CAN 驱动安装

【DRONECAN】&#xff08;三&#xff09;WSL2 及 ubuntu20.04 CAN 驱动安装 前言 这一篇文章主要介绍一下 WSL2 及 ubuntu20.04 CAN 驱动的安装&#xff0c;首先说一下介绍本文的目的。 大家肯定都接触过 ubuntu 系统&#xff0c;但是我们常用的操作系统都是 Windows&#x…

JavaSE基础(2)

1 方法的使用 思维导图 1.1 什么是方法 方法就是一个代码片段. 类似于 C 语言中的 “函数”。方法存在的意义(不要背, 重在体会): 是能够模块化的组织代码(当代码规模比较复杂的时候).做到代码被重复使用, 一份代码可以在多个位置使用.让代码更好理解更简单.直接调用现有方法…

基于金枪鱼群算法优化的BP神经网络(预测应用) - 附代码

基于金枪鱼群算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于金枪鱼群算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.金枪鱼群优化BP神经网络2.1 BP神经网络参数设置2.2 金枪鱼群算法应用 4.测试结果&#xff1a;5…

肠道微生物群肾衰竭

编者推荐 该研究应用多组学分析&#xff08;代谢组学分析微生物组学分析&#xff09;分析了人类ESRD肠道微生物组组成、尿毒症毒素和肾衰竭之间的关系&#xff0c;使用独立队列和无菌动物模型对多组学结果及研究提出的ESRD机制假设进行验证&#xff0c;首次从肠道微生物的角度…

qt第一天

#include "widget.h" #include "ui_widget.h" #include "QDebug" Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);this->resize(QSize(800,600)); //使用匿名对象&#xff0c;调用重…

嵌入式Linux开发实操(十五):nand flash接口开发

# 前言 flash memory,分NAND和NOR: 如果说nor flash有个特点就是能执行代码,NOR并行接口具有地址和数据总线,spi flash更是主要用于存储代码,SPI(或QSPI)NOR代码可就地执行(XiP),一般系统要求flash闪存提供相对较高的频率和数据缓存的clocking。而nand flash主要用于…

QT Creator工具介绍及使用

一、QT的基本概念 QT主要用于图形化界面的开发&#xff0c; QT是基于C编写的一套界面相关的类库&#xff0c;如进程线程库&#xff0c;网络编程的库&#xff0c;数据库操作的库&#xff0c;文件操作的库等。 如何使用这个类库&#xff1a;类库实例化对象(构造函数) --> 学习…

Django静态文件媒体文件文件上传

文章目录 一、静态文件和媒体文件1.在django中使用静态文件实践2.在django中使用媒体文件 二、文件上传单文件上传实践多文件上传 一、静态文件和媒体文件 媒体文件: 用户上传的文件&#xff0c;叫做media 静态文件:存放在服务器的css,js,image,font等 叫做static1.在django中…

【Flutter】使用Android Studio 创建第一个flutter应用。

前言 首先下载好 flutter sdk和 Android Studio。 FlutterSDK下载 Android Studio官网 配置 我的是 windows。 where.exe flutter dart查看flutter安装环境。 如果没有&#xff0c;自己在环境变量的path添加下flutter安装路径。 在将 Path 变量更新后&#xff0c;打开一个…

QTday1(第一个QT界面、常用类与组件)

一、Xmind整理&#xff1a; Assistant帮助文档的使用&#xff1a; 设计师界面的介绍&#xff1a; 各文件之间调用方式&#xff1a; 二、上课笔记整理&#xff1a; 1.第一个QT界面 ①创建自定义类时需要指定父类 ②第一个界面的相关操作 #include "mainwindow.h"…