C++11特性:共享智能指针

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存泄露。解决这个问题最有效的方法是使用智能指针(smart pointer)。智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是引用计数,每使用它一次,内部引用计数加1,每析构一次内部的引用计数减1,减为0时,删除所指向的堆内存。

C++11中提供了三种智能指针,使用这些智能指针时需要引用头文件<memory>

1. std::shared_ptr:共享的智能指针
2. std::unique_ptr:独占的智能指针
3. std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的。

弱引用的智能指针是辅助共享的智能指针的。

共享的智能指针独占的智能指针区别

假设new出来一块int类型的内存,使用共享的智能指针可以有多个共享的智能指针同时管理。

在每个智能指针的内部都有一个引用计数,记录的当前的这块内存被多少个智能指针共同管理着。

当这个智能指针被析构或者重置的时候,计数就会减一。注意在最后一个智能指针析构前,会把管理的内存一并析构掉。

由上述这个例子不难看出,共享智能指针可以多个指针同时管理,那么独占智能指针不允许多个指针同时管理一块内存。

后来的这个独占智能指针想要管理这块内存,就需要进行资源的转移,也就是使用move函数实现资源的转移,这样原来的独占智能指针就不能管理这块内存了。所以独占智能指针的计数永远为1

1. shared_ptr的初始化:

共享智能指针是指多个智能指针可以同时管理同一块有效的内存,共享智能指针shared_ptr 是一个模板类,如果要进行初始化有三种方式:通过构造函数std::make_shared辅助函数以及reset方法。共享智能指针对象初始化完毕之后就指向了要管理的那块堆内存,如果想要查看当前有多少个智能指针同时管理着这块内存可以使用共享智能指针提供的一个成员函数use_count,函数原型如下:

// 管理当前对象的 shared_ptr 实例数量,或若无被管理对象则为 0。
long use_count() const noexcept;

 1.1 通过构造函数初始化

// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);

测试代码如下:

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    // 使用智能指针管理一块字符数组对应的堆内存
    shared_ptr<char> ptr2(new char[12]);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    // 创建智能指针对象, 不管理任何内存
    shared_ptr<int> ptr3;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    // 创建智能指针对象, 初始化为空
    shared_ptr<int> ptr4(nullptr);
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    return 0;
}

测试代码输出的结果如下:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 1
ptr3管理的内存引用计数: 0
ptr4管理的内存引用计数: 0

注意如果智能指针被初始化了一块有效内存,那么这块内存的引用计数+1,如果智能指针没有被初始化或者被初始化为nullptr空指针,引用计数不会+1。另外,不要使用一个原始指针初始化多个shared_ptr。

int *p = new int;
shared_ptr<int> p1(p);
shared_ptr<int> p2(p);		// error, 编译不会报错, 运行会出错

1.2 通过拷贝和移动构造函数初始化 

当一个智能指针被初始化之后,就可以通过这个智能指针初始化其他新对象。在创建新对象的时候,对应的拷贝构造函数或者移动构造函数就被自动调用了。

#include <iostream>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试程序输入的结果:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3

通过拷贝构造函数的初始化: 

上述代码中的调用拷贝构造函数初始化,首先用ptr1来初始化ptr2,然后调用拷贝构造来初始化ptr3,这时候ptr2和ptr3都指向相同的一块内存,并且引用计数加一。 

 通过移动构造函数的初始化: 

 移动构造就需要右值引用了,右值引用就需要右值来进行初始化了。右值分为纯右值和将亡值,这里使用的是将亡值。使用move函数就可得到将亡值了。上述代码中通过转移的方式将ptr2的资源转移给了ptr5,引用计数并不能加一

1.3 通过std::make_shared初始化

通过C++提供的std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );

1. T:模板参数的数据类型 

 2.  Args&&... args :要初始化的数据,如果是通过make_shared创建对象,需按照构造函数的参数列表指定

通过make_shared就能创建一块普通类型的内存,也可以去创建某一个对象对应的内存


测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Test
{
public:
    Test() 
    {
        cout << "construct Test..." << endl;
    }
    Test(int x) 
    {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) 
    {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test()
    {
        cout << "destruct Test ..." << endl;
    }
};

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;

    shared_ptr<Test> ptr2 = make_shared<Test>();
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;

    shared_ptr<Test> ptr3 = make_shared<Test>(520);
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;

    shared_ptr<Test> ptr4 = make_shared<Test>("我是要成为海贼王的男人!!!");
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    return 0;
}

使用std::make_shared()模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

1.4 通过 reset方法初始化

共享智能指针类提供的std::shared_ptr::reset方法函数原型如下:

void reset() noexcept;

template< class Y >
void reset( Y* ptr );

template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );

template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );

1. ptr:指向要取得所有权的对象的指针
2. d:指向要取得所有权的对象的指针
3. aloc:内部存储所用的分配器 

测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    shared_ptr<int> ptr2 = ptr1;
    shared_ptr<int> ptr3 = ptr1;
    shared_ptr<int> ptr4 = ptr1;
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    ptr4.reset();
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    shared_ptr<int> ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试代码输入的结果:

ptr1管理的内存引用计数: 4
ptr2管理的内存引用计数: 4
ptr3管理的内存引用计数: 4
ptr4管理的内存引用计数: 4
    
ptr1管理的内存引用计数: 3
ptr2管理的内存引用计数: 3
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 0
    
ptr5管理的内存引用计数: 1

对于一个未初始化的共享智能指针,可以通过reset方法来初始化,当智能指针中有值的时候,调用reset会使引用计数减1。

 简单来说reset有两个功能

1. 使指向某块内存的智能指针解除对这块内存的管理。

2. 让这个指针管理另外一块内存,相当于做了智能指针的重新初始化。

1.5 获取原始指针

通过智能指针可以管理一个普通变量或者对象的地址,此时原始地址就不可见了。当我们想要修改变量或者对象中的值的时候,就需要从智能指针对象中先取出数据的原始内存的地址再操作,解决方案是调用共享智能指针类提供的get()方法,其函数原型如下:

T* get() const noexcept;

当一个智能指针管理一块内存的时候,获取的原始指针是什么类型,这个T*返回的就是什么类型。 也就是智能指针管理的这块内存的类型

测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // 得到指针的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

 接下来通过一个示例程序来更加清楚的了解上述的前四个用法:

代码如下:

#include<iostream>
#include<memory>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "construct Test..." << endl;
	}

	Test(int x) : m_num(x)
	{
		cout << "construct Test,x = " << x << endl;
	}

	Test(string str)
	{
		cout << "construct Test,str = " << str << endl;
	}

	~Test()
	{
		cout << "destruct Test..." << endl;
	}

	void setValue(int v)
	{
		m_num = v;
	}

	void print()
	{
		cout << "m_num:" << m_num << endl;
	}

private:
	int m_num;
};

int main()
{
	// 通过构造函数初始化
	shared_ptr<int> ptr1(new int(3));
	cout << "ptr1 use_count:" << ptr1.use_count() << endl;

	// 移动构造和拷贝构造函数初始化
	// 移动构造:
	shared_ptr<int> ptr2 = move(ptr1);
	cout << "ptr1 use_count:" << ptr1.use_count() << endl;
	cout << "ptr2 use_count:" << ptr2.use_count() << endl;
	// 拷贝构造:
	// 需要一个实例对象来初始化,所以要使用ptr2来初始化
	// 因为ptr1的资源已经转移到ptr2中了
	shared_ptr<int> ptr3 = ptr2;
	cout << "ptr3 use_count:" << ptr3.use_count() << endl;
	cout << "ptr2 use_count:" << ptr2.use_count() << endl;

	// 通过std::make_shared初始化
	shared_ptr<int> ptr4 = make_shared<int>(8);// ()中的数就是给给new出来的这个int类型内存进行初始化
	shared_ptr<Test> ptr5 = make_shared<Test>(8);
	shared_ptr<Test> ptr6 = make_shared<Test>("luffy");

	// 通过reset初始化
	ptr6.reset();// 指针重置,引用计数变为0
	cout << "ptr6 use_count:" << ptr6.use_count() << endl;

	ptr5.reset(new Test("ace"));// 管理另一块内存
	cout << "ptr5 use_count:" << ptr5.use_count() << endl;

	// 获取原始指针




	return 0;
}

输出结果为:

ptr1 use_count:1
ptr1 use_count:0
ptr2 use_count:1
ptr3 use_count:2
ptr2 use_count:2
construct Test,x = 8
construct Test,str = luffy
destruct Test...
ptr6 use_count:0
construct Test,str = ace
destruct Test...
ptr5 use_count:1
destruct Test...

对输出结果进行分析:

当ptr1创建成功之后,他的引用计数就是1。然后通过移动拷贝构造函数转移ptr1的资源到ptr2,所以ptr1的引用计数变为0,ptr2的引用计数变为1。紧接着,ptr3进行的是一个拷贝构造,所以他的引用计数和ptr2一样为2,也就是说这块内存由ptr2和ptr3共同管理着。

ptr4和ptr5分别是整型和字符串类型,然后就是重置ptr6,ptr6管理的这个Test对象就被析构了,所以输出析构语句,引用计数也变为0。然后是让ptr5管理另一块内存,输出构造函数中的语句,所以ptr5之前管理的那块内存也析构了,输出析构语句。又因为ptr5管理着新的内存,所以引用计数仍然为1。最后,程序结束,ptr5管理的这块内存也析构了。

 那么怎么去使用这个初始化得到的共享智能指针呢?(有两种方式)

1. 通过智能指针取出原始地址:

// 获取原始指针
Test* t = ptr5.get();
t->setValue(1000);
t->print();

2.  通过智能指针对象直接进行操作 :

// 通过智能指针对象直接进行操作
// 将智能指针对象当作指针去使用
ptr5->setValue(999);
ptr5->print();

 2. 指定删除器

当智能指针管理的内存对应的引用计数变为0的时候,这块内存就会被智能指针析构掉了。另外,我们在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,我们只需要进行实现,其调用由智能指针完成的。

// 第二个参数是指定删除器,默认情况下可以不指定
// 删除器可以在外面创建一个或者写一个匿名函数(lambda表达式)
shared_ptr<Test> ppp(new Test(100), [](Test* t) {
	// 释放内存的操作 
	// 需要释放的是Test类型的指针
	// t指向的就是第一个参数
	cout << "----------------------" << endl;
	delete t;
});

输出结果为:

----------------------
destruct Test...
destruct Test...

由此可以看出这个删除器确定被调用了。

那么在什么时候必须要指定删除器函数呢?

 通过智能指针对象去管理一块数组内存的时候,如果不是数组内存,智能指针对象提供的默认删除器函数就能删除掉这块内存。如果管理的是一块数组内存就不能删除。

接下来用一段代码来演示:

#include<iostream>
#include<memory>
using namespace std;

class Test
{
public:
	Test()
	{
		cout << "construct Test..." << endl;
	}

	Test(int x) : m_num(x)
	{
		cout << "construct Test,x = " << x << endl;
	}

	Test(string str)
	{
		cout << "construct Test,str = " << str << endl;
	}

	~Test()
	{
		cout << "destruct Test..." << endl;
	}

	void setValue(int v)
	{
		m_num = v;
	}

	void print()
	{
		cout << "m_num:" << m_num << endl;
	}

private:
	int m_num;
};

int main()
{

	shared_ptr<Test> p1(new Test[5]);

	return 0;
}

输出结果为:

construct Test...
construct Test...
construct Test...
construct Test...
construct Test...
destruct Test...

由输出结果可以看出默认的删除器函数不能删除数组内存。

手动指定删除器:

shared_ptr<Test> p1(new Test[5], [](Test* t) {
	cout << "----------------------" << endl;
	delete[] t;
});

输出结果为:

construct Test...
construct Test...
construct Test...
construct Test...
construct Test...
----------------------
destruct Test...
destruct Test...
destruct Test...
destruct Test...
destruct Test...

在删除数组内存时,除了自己编写删除器,也可以使用C++提供的std::default_delete<T>()函数作为删除器,这个函数内部的删除功能也是通过调用delete来实现的,要释放什么类型的内存就将模板类型T指定为什么类型即可。具体处理代码如下:

int main()
{
    shared_ptr<Test> p2(new Test[5], default_delete<Test[]>());
    return 0;
}

注意:函数类型必须写[]不能写* 。而自己指定删除器中的参数类型可以也写成Test []t。

 输出结果和自己指定一样。

如果我们在程序中需要经常对数组类型的内存进行释放,我们可以去封装一个函数模板

代码如下:

#include <iostream>
#include <memory>
using namespace std;

template <typename T>
shared_ptr<T> make_share_array(size_t size)
{
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main()
{
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

 本文参考:共享智能指针 | 爱编程的大丙 (subingwen.cn)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2. 创建型模式 - 抽象工厂模式

亦称&#xff1a; Abstract Factory 意图 抽象工厂模式是一种创建型设计模式&#xff0c; 它能创建一系列相关的对象&#xff0c; 而无需指定其具体类。 问题 假设你正在开发一款家具商店模拟器。 你的代码中包括一些类&#xff0c; 用于表示&#xff1a; 一系列相关产品&…

【单调栈】LeetCode2334:元素值大于变化阈值的子数组

作者推荐 map|动态规划|单调栈|LeetCode975:奇偶跳 涉及知识点 单调栈 题目 给你一个整数数组 nums 和一个整数 threshold 。 找到长度为 k 的 nums 子数组&#xff0c;满足数组中 每个 元素都 大于 threshold / k 。 请你返回满足要求的 任意 子数组的 大小 。如果没有这…

Java对接腾讯多人音视频房间回调接口示例

在前面我们已经对接好了腾讯多人音视频房间相关内容&#xff1a;Java对接腾讯多人音视频房间示例 为了完善业务逻辑&#xff0c;我们还需要对接它的一些回调接口 官方文档地址 主要就下面这些 这里因为比较简单直接上代码 里面有些工具类和上一章一样这里就没贴&#xff0c;需要…

2023 英特尔On技术创新大会直播 | AI魅力的生活化

目录 前言正文 前言 依稀记得去年的直播大会&#xff0c;主要展现了其灵活、加速和半集成化的独特优势&#xff0c;广泛应用于人工智能、5G通信、边缘计算以及视觉图像处理等领域&#xff0c;不断提供领先的性能、能效和可编程性的创新。 如今又带来一些不一样的特色&#xf…

使用@jiaminghi/data-view实现一个数据大屏

<template><div class"content bg"><!-- 全局容器 --><!-- <dv-full-screen-container> --><!-- 第二行 --><div class"module-box" style"align-items: start; margin-top: 10px"><!-- 左 -->…

The Foundry NUKE 15 for mac/win:引领影视后期特效制作的创新力量

The Foundry NUKE 15 是一款领先的影视后期特效制作软件&#xff0c;为专业的视觉特效师和影视制作人员提供了强大的工具和功能。作为最新版本&#xff0c;NUKE 15不仅继承了之前版本的优点&#xff0c;更加强了其在创新和效率方面的能力&#xff0c;成为影视行业不可或缺的工具…

FLASH闪存的读取、擦除、编程(stm32f103c8t6)

一、stm32寄存器地址介绍 二、FLASH简介 &#xff08;1&#xff09;STM32F1系列的FLASH包含程序存储器、系统存储器和选项字节三个部分&#xff0c;通过闪存存储器接口可以对程序存储器和选项字节进行擦除和编程 &#xff08;2&#xff09; 读写FLASH的用途&#xff1a;利用程…

Excel排序怎么做?记好这些正确操作!

“我是个职场新手&#xff0c;对excel的使用还不是很熟悉。但是我需要处理一份文件。有朋友可以简单介绍一下excel排序的操作方法吗&#xff1f;” Excel作为一个实用的办公工具&#xff0c;给用户带来了很多的方便。在使用excel时&#xff0c;排序功能是比较重要且常用的。我们…

宕机后,Redis如何实现快速恢复?

Redis作为非常火热的内存数据库&#xff0c;其除了具有非常高的性能之外&#xff0c;还需要保证高可用&#xff0c;在故障发生时&#xff0c;尽可能地降低故障带来的影响&#xff0c;Redis也提供了完善的故障恢复机制&#xff1a;哨兵。 下面就来具体来看看Redis的故障恢复是如…

声音克隆定制丰富和的系统源码+完整的代码包+搭建教程

随着科技的进步&#xff0c;人工智能&#xff08;AI&#xff09;技术已经逐渐渗透到我们生活的各个领域。声音克隆技术&#xff0c;作为AI领域的一个重要分支&#xff0c;通过模仿人类的声音特征&#xff0c;生成与目标声音相似的语音。这项技术在语音合成、语音识别、虚拟现实…

机器学习——损失函数

【说明】文章内容来自《机器学习——基于sklearn》&#xff0c;用于学习记录。若有争议联系删除。 1、简介 损失函数(loss function)又称为误差函数(error function)&#xff0c;是衡量模型好坏的标准&#xff0c;用于估量模型的预测值与真实值的不一致程度&#xff0c;是一个…

深入剖析jsonp跨域原理

在项目中遇到一个jsonp跨域的问题&#xff0c;于是仔细的研究了一番jsonp跨域的原理。搞明白了一些以前不是很懂的地方&#xff0c;比如&#xff1a; 1&#xff09;jsonp跨域只能是get请求&#xff0c;而不能是post请求&#xff1b; 2&#xff09;jsonp跨域的原理到底是什么&…

这是最简单的轮播图,图片自己加

代码&#xff1a; <!DOCTYPE html> <html> <head> <title>轮播图</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } .container { position: relative; overflow: hid…

Golang 的内存管理

文章目录 1.内存管理角色1.常见的内存分配方法线性分配器空闲链表分配器TCMalloc 2.Go 内存管理组件mspanmcache初始化替换微分配器 mcentralmheap 3.内存分配4.内存管理思想参考文献 1.内存管理角色 内存管理一般包含三个不同的组件&#xff0c;分别是用户程序&#xff08;Mu…

Nginx快速入门:负载均衡upstream配置详解(四)

0. 引言 我们在第二章的时候简单演示了关于nginx实现负载均衡的演示&#xff0c;而实际上nginx支持很多负载均衡算法&#xff0c;并且多节点的转发也有多种策略。今天我们继续深入学习这块。 1. 负载均衡的应用场景 所谓负载均衡&#xff0c;Load Balance &#xff0c;就是将…

Jmeter自定义用户变量模拟多用户

java1234,56a801e9c869452fa092c9657cfc2051 jack,b6e528cca41143dea9c2c3e9ca5d6390

Linux环境安装Hadoop

&#xff08;1&#xff09;下载Hadoop安装包并上传 下载Hadoop安装包到本地&#xff0c;并导入到Linux服务器的/opt/software路径下 &#xff08;2&#xff09;解压安装包 解压安装文件并放到/opt/module下面 [roothadoop100 ~]$ cd /opt/software [roothadoop100 software…

基于SpringBoot的教学管理app的开发-计算机毕业设计源码65449

摘 要 信息化社会内需要与之针对性的信息获取途径&#xff0c;但是途径的扩展基本上为人们所努力的方向&#xff0c;由于站在的角度存在偏差&#xff0c;人们经常能够获得不同类型信息&#xff0c;这也是技术最为难以攻克的课题。针对教学管理等问题&#xff0c;对其进行研究分…

如何在本地安装Flask并将其web界面发布到公网上远程访问协同开发

目录 前言 1. 安装部署Flask 2. 安装Cpolar内网穿透 3. 配置Flask的web界面公网访问地址 4. 公网远程访问Flask的web界面 前言 本篇文章讲解如何在本地安装Flask&#xff0c;以及如何将其web界面发布到公网上并进行远程访问。 Flask是目前十分流行的web框架&#xff0c;…

电气 接近开关

npn&#xff1a;和负载&#xff08;控制器或者继电器&#xff09;共阳极&#xff0c;低电平响应 pnp&#xff1a;和负载共阴极&#xff0c;高电平响应