C++ 智能指针的原理、分类、使用

news2024/11/24 20:58:25

1. 智能指针介绍

为解决裸指针可能导致的内存泄漏问题。如:

        a)忘记释放内存;

        b)程序提前退出导致资源释放代码未执行到。

就出现了智能指针,能够做到资源的自动释放


2. 智能指针的原理和简单实现

2.1 智能指针的原理

将裸指针封装为一个智能指针类,需要使用该裸指针时,就创建该类的对象;利用栈区对象出作用域会自动析构的特性,保证资源的自动释放。

2.2 智能指针的简单实现

代码示例:

template<typename T>
class MySmartPtr {
public:
    MySmartPtr(T* ptr = nullptr):mptr(ptr) { // 创建该对象时,裸指针会传给对象
    }

    ~MySmartPtr() {  // 对象出作用域会自动析构,因此会释放裸指针指向的资源
        delete mptr;
    }
    
    // *运算符重载
    T& operator*() {  // 提供智能指针的解引用操作,即返回它包装的裸指针的解引用
        return *mptr; 
    }

    // ->运算符重载
    T* operator->() { // 即返回裸指针
        return mptr;
    }
private:
    T* mptr;
};

class Obj {
public:
    void func() {
        cout << "Obj::func" << endl;
    }
};

void test01() {

    /*创建一个int型的裸指针,
    使用MySmartPtr将其封装为智能指针对象ptr,ptr对象除了作用域就会自动调用析构函数。
    智能指针就是利用栈上对象出作用域自动析构这一特性。*/
    MySmartPtr<int> ptr0(new int);
    *ptr0 = 10;

    MySmartPtr<Obj> ptr1(new Obj);
    ptr1->func();
    (ptr1.operator->())->func(); // 等价于上面

    /*  中间异常退出,智能指针也会自动释放资源。
    if (xxx) {
        throw "....";
    }
    
    if (yyy) {
        return -1;
    }
    */
}

3. 智能指针分类

3.1 问题引入

 接着使用上述自己实现的智能指针进行拷贝构造:

void test02() {
    MySmartPtr<int> p1(new int); // p1指向一块int型内存空间
    MySmartPtr<int> p2(p1);      // p2指向p1指向的内存空间
    
    *p1 = 10;   // 内存空间的值为10
    *p2 = 20;   // 内存空间的值被改为20
}

但运行时出错:

原因在于p1和p2指向同一块int型堆区内存空间,p2析构将该int型空间释放,p1再析构时释放同一块内存,则出错。

那可否使用如下深拷贝解决该问题?

MySmartPtr(cosnt MySmartPtr<T>& src) {
    mptr = new T(*src.mptr);
}

不可以。因为按照裸指针的使用方式,用户本意是想将p1和p2都指向该int型堆区内存,使用指针p1、p2都可改变该内存空间的值,显然深拷贝不符合此场景。


3.2 两类智能指针

不带引用计数的智能指针:只能有一个指针管理资源。

        auto_ptr;

        scoped_ptr;

        unique_ptr;.

带引用计数的智能指针:可以有多个指针同时管理资源。

        shared_ptr;强智能指针。

        weak_ptr: 弱智能指针。这是特例,不能控制资源的生命周期,不能控制资源的自动释放!


3.3 不带引用计数的智能指针

只能有一个指针管理资源。

3.3.1 auto_ptr (不推荐使用)

void test03() {
	auto_ptr<int> ptr1(new int);
	auto_ptr<int> ptr2(ptr1);
	*ptr2 = 20;
	// cout << *ptr2 << endl; // 可访问*ptr2
	cout << *ptr1 << endl; //访问*ptr1却报错
}

如上代码,访问*ptr1为何报错?

因为调用auto_ptr的拷贝构造将ptr1的值赋值给ptr2后,底层会将ptr1指向nullptr;即将同一个指针拷贝构造多次时,只让最后一次拷贝的指针管理资源,前面的指针全指向nullptr

不推荐将auto_ptr存入容器。

3.3.2 scoped_ptr (使用较少)

scoped_ptr已将拷贝构造函数赋值运算符重载delete了。

scoped_ptr(const scoped_ptr<T>&) = delete; // 删除拷贝构造
scoped_ptr<T>& operator=(const scoped_ptr<T>&) = delete;  // 删除赋值重载

3.3.3 unique_ptr (推荐使用)

unique_ptr也已将拷贝构造函数赋值运算符重载delete

unique_ptr(const unique_ptr<T>&) = delete; // 删除拷贝构造
unique_ptr<T>& operator=(const unique_ptr<T>&) = delete;  // 删除赋值重载

unique_ptr提供了右值引用参数的拷贝构造函数赋值运算符重载,如下:

void test04() {
	unique_ptr<int> ptr1(new int);
	// unique_ptr<int> ptr2(ptr1);  和scoped_ptr一样无法通过编译
	unique_ptr<int> ptr2(std::move(ptr1)); // 但可使用move得到ptr1的右值类型
    // *ptr1  也无法访问
}

3.4 带引用计数的智能指针

可以有多个指针同时管理资源。

原理:给智能指针添加其指向资源的引用计数属性,若引用计数 > 0,则不会释放资源,若引用计数 = 0就释放资源。

具体来说:额外创建资源引用计数类,在智能指针类中加入该资源引用计数类的指针作为其中的一个属性;当使用裸指针创建智能指针对象时,创建智能指针中的资源引用计数对象,并将其中的引用计数属性初始化为1,当后面对该智能指针对象进行拷贝(使用其他智能指针指向该资源时)或时,需要在其他智能指针对象类中将被拷贝的智能指针对象中的资源引用计数类的指针获取过来,然后将引用计数+1;当用该智能指针给其他智能指针进行赋值时,因为其他智能指针被赋值后,它们就不指向原先的资源了,原先资源的引用计数就-1,直至引用计数为0时delete掉资源;当智能指针对象析构时,会使用其中的资源引用计数指针将共享的引用计数-1,直至引用计数为0时delete掉资源。

shared_ptr:强智能指针;可改变资源的引用计数

weak_ptr:弱智能指针;不可改变资源的引用计数

带引用计数的智能指针的简单实现:

/*资源的引用计数类*/
template<typename T>
class RefCnt {
public:
    RefCnt(T* ptr=nullptr):mptr(ptr) {
        if (mptr != nullptr) {
            mcount = 1; // 刚创建指针指针时,引用计数初始化为1
        }
    }

    void addRef() {  // 增加引用计数
        mcount++;
    }

    int delRef() {   // 减少引用计数
        mcount--;
        return mcount;
    }
private:
    T* mptr;  // 资源地址
    int mcount; // 资源的引用计数
};

/*智能指针类*/
template<typename T>
class MySmartPtr {
public:
    MySmartPtr(T* ptr = nullptr) :mptr(ptr) { // 创建该对象时,裸指针会传给对象
        mpRefCnt = new RefCnt<T>(mptr);
    }

    ~MySmartPtr() {  // 对象出作用域会自动析构,因此会释放裸指针指向的资源
        if (0 == mpRefCnt->delRef()) {
            delete mptr;
            mptr = nullptr;
        }
    }

    // *运算符重载
    T& operator*() {  // 提供智能指针的解引用操作,即返回它包装的裸指针的解引用
        return *mptr;
    }

    // ->运算符重载
    T* operator->() { // 即返回裸指针
        return mptr;
    }

    // 拷贝构造
    MySmartPtr(const MySmartPtr<T>& src):mptr(src.mptr),mpRefCnt(src.mpRefCnt) {
        if (mptr != nullptr) {
            mpRefCnt->addRef();
        }
    }

    // 赋值重载
    MySmartPtr<T>& operator=(const MySmartPtr<T>& src) {
        if (this == &src) // 防止自赋值
            return *this;

        /*若本指针改为指向src管理的资源,则本指针原先指向的资源的引用计数-1,
        若原资源的引用计数为0,就释放资源*/
        if (0 == mpRefCnt->delRef()) {  
            delete mptr;
        }

        mptr = src.mptr;
        mpRefCnt = src.mpRefCnt;
        mpRefCnt->addRef();
        return *this;
    }
private:
    T* mptr;  // 指向资源的指针
    RefCnt<T>* mpRefCnt; // 资源的引用计数
};

3.4.1 shared_ptr

强智能指针。可改变资源的引用计数。

(1)强智能指针的交叉引用问题

class B;

class A {
public:
    A() {
        cout << "A()" << endl;
    }
    ~A() {
        cout << "~A()" << endl;
    }
    shared_ptr<B> _ptrb;
};

class B {
public:
    B() {
        cout << "B()" << endl;
    }
    ~B() {
        cout << "~B()" << endl;
    }
    shared_ptr<A> _ptra;
};

void test06() {
    shared_ptr<A> pa(new A());
    shared_ptr<B> pb(new B());

    pa->_ptrb = pb;
    pb->_ptra = pa;

    /*打印pa、pb指向资源的引用计数*/
    cout << pa.use_count() << endl;
    cout << pb.use_count() << endl;
}

输出结果:

 

可见pa、pb指向的资源的引用计数都为2,因此出了作用域导致pa、pb指向的资源都无法释放,如下图所示:

解决: 

建议定义对象时使用强智能指针,引用对象时使用弱智能指针,防止出现交叉引用的问题。

什么是定义对象?什么是引用对象?

定义对象:

        使用new创建对象,并创建一个新的智能指针管理它。

引用对象:

        使用一个已存在的智能指针来创建一个新的智能指针。

        定义对象和引用对象的示例如下:

shared_ptr<int> p1(new int());              // 定义智能指针对象p1
shared_ptr<int> p2 = make_shared<int>(10);  // 定义智能指针对象p2

shared_ptr<int> p3 = p1;  // 引用智能指针p1,并使用p3来共享它
weak_ptr<int> p4 = p2;    // 引用智能指针p2,并使用p4来观察它

如上述代码,因为在test06函数中使用pa对象的_ptrb引用pb对象,使用pb对象的_ptra引用pa对象,因此需要将A类、B类中的_ptrb_ptra的类型改为弱智能指针weak_ptr即可,这样就不会改变资源的引用计数,能够正确释放资源。

3.4.2 weak_ptr 

弱智能指针。不可改变资源的引用计数。不能创建对象,也不能访问资源(因为weak_ptr未提供operator->operator*重载运算符),即不能通过弱智能指针调用函数、不能将其解引用。只能从一个已有的shared_ptr或weak_ptr获得资源的弱引用。

弱智能指针weak_ptr若想用访问资源,则需要使用lock方法将其提升为一个强智能指针,提升失败则返回nullptr。(提升的情形常使用于多线程环境,避免无效的访问,提升程序安全性)

注意:弱智能指针weak_ptr只能观察资源的状态,但不能管理资源的生命周期,不会改变资源的引用计数,不能控制资源的释放。

weak_ptr示例:

void test07() {
    shared_ptr<Boy> boy_sptr(new Boy());
    weak_ptr<Boy> boy_wptr(boy_sptr);
    // boy_wptr->study(); 错误!无法使用弱智能指针访问资源
    cout << boy_sptr.use_count() << endl; // 引用计数为1,因为弱智能指针不改变引用计数

    shared_ptr<int> i_sptr(new int(99));
    weak_ptr<int> i_wptr(i_sptr);
    // cout << *i_wptr << endl; 错误!无法使用弱智能指针访问资源
    cout << i_sptr.use_count() << endl; // 引用计数为1,因为弱智能指针不改变引用计数

    /*弱智能指针提升为强智能指针*/
    shared_ptr<Boy> boy_sptr1 = boy_wptr.lock();
    if (boy_sptr1 != nullptr) {
        cout << boy_sptr1.use_count() << endl; // 提升成功,引用计数为2
        boy_sptr1->study(); // 可以调用
    }

    shared_ptr<int> i_sptr1 = i_wptr.lock();
    if (i_sptr1 != nullptr) {
        cout << i_sptr1.use_count() << endl; // 提升成功,引用计数为2
        cout << *i_sptr1 << endl; // 可以输出
    }  
}

4. 智能指针与多线程访问共享资源的安全问题

有如下两种启动线程的方式:

方式1:主线程调用test08函数,在test08函数中启动子线程执行线程函数,如下:

void handler() {
	cout << "Hello" << endl;
}

void func() {
	thread t1(handler);
}

int main(int argc, char** argv) {
	func();
	this_thread::sleep_for(chrono::seconds(1));
	system("pause");
	return 0;
}

运行报错:

 方式2:主线程中直接创建子线程来执行线程函数,如下:

void handler() {
	cout << "Hello" << endl;
}

int main(int argc, char** argv) {
    thread t1(handler);
	this_thread::sleep_for(chrono::seconds(1));
	system("pause");
	return 0;
}

运行结果:无报错

上面两种方式旨在通过子线程调用函数输出Hello,但为什么方式1就报错了?很简单,不再赘述。


回归本节标题的正题,有如下程序:

class C {
public:
    C() {
        cout << "C()" << endl;
    }

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

    void funcC() {
        cout << "C::funcC()" << endl;
    }
private:

};

/*子线程执行函数*/
void threadHandler(C* c) {
    this_thread::sleep_for(chrono::seconds(1));
    c->funcC();
}

/* 主线程 */
int main(int argc, char** argv) {
    C* c = new C();
    thread t1(threadHandler, c);
    delete c;
	t1.join();
	return 0;
}

运行结果:

结果显示c指向的对象被析构了,但是仍然使用该被析构的对象调用了其中的funcC函数,显然不合理。

因此在线程函数中,使用c指针访问A对象时,需要观察A对象是否存活

使用弱智能指针weak_ptr接收对象,访问对象之前尝试提升为强智能指针shared_ptr,提升成功则访问,否则对象被析构。

情形1:对象被访问之前就被析构了:

class C {
public:
    C() {
        cout << "C()" << endl;
    }

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

    void funcC() {
        cout << "C::funcC()" << endl;
    }
private:

};

/*子线程执行函数*/
void threadHandler(weak_ptr<C> pw) {  // 引用时使用弱智能指针
    this_thread::sleep_for(chrono::seconds(1));
    shared_ptr<C> ps = pw.lock();  // 尝试提升
    if (ps != nullptr) {
        ps->funcC();
    } else {
        cout << "对象已经析构!" << endl;
    }
}

/* 主线程 */
int main(int argc, char** argv) {
    {
        shared_ptr<C> p(new C());
        thread t1(threadHandler, weak_ptr<C>(p));
        t1.detach();
    }
    this_thread::sleep_for(chrono::seconds(5));
	return 0;
}

运行结果:

情形2: 对象访问完才被析构:

class C {
public:
    C() {
        cout << "C()" << endl;
    }

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

    void funcC() {
        cout << "C::funcC()" << endl;
    }
private:

};

/*子线程执行函数*/
void threadHandler(weak_ptr<C> pw) {  // 引用时使用弱智能指针
    this_thread::sleep_for(chrono::seconds(1));
    shared_ptr<C> ps = pw.lock();  // 尝试提升
    if (ps != nullptr) {
        ps->funcC();
    } else {
        cout << "对象已经析构!" << endl;
    }
}

/* 主线程 */
int main(int argc, char** argv) {
    {
        shared_ptr<C> p(new C());
        thread t1(threadHandler, weak_ptr<C>(p));
        t1.detach();
        this_thread::sleep_for(chrono::seconds(5));
    }  
	return 0;
}

运行结果:

 很显然shared_ptr与weak_ptr结合使用,能够较好地保证多线程访问共享资源的安全。


5.智能指针的删除器deleter

删除器是智能指针释放资源的方式,默认使用操作符delete来释放资源。

但并非所有智能指针管理的资源都可通过delete释放,如数组、文件资源、数据库连接资源等。

有如下智能指针对象管理一个数组资源:

unique_ptr<int> ptr1(new int[100]);

此时再用默认的删除器则会造成资源泄露。因此需要自定义删除器。

/* 方式1:类模板 */
template<typename T>
class MyDeleter {
public:
    void operator()(T* ptr) const {
        cout << "数组自定义删除器1." << endl;
        delete[] ptr;
    }
};

/* 方式2:函数 */
void myDeleter(int* p) {
    cout << "数组自定义删除器2." << endl;
    delete[] p;
}

void test09() {
    unique_ptr<int, MyDeleter<int>> ptr1(new int[100]);
    unique_ptr<int, void(*)(int*)> ptr2(new int[100], myDeleter);

    /* 方式3:Lambda表达式 */
    unique_ptr<int, void(*)(int*)> ptr3(new int[100], [](int* p) {
        cout << "数组自定义删除器3." << endl;
        delete[] p;
        });
}

void test10() {
    unique_ptr<FILE, void(*)(FILE*)> ptr2(fopen("1.txt", "w"), [](FILE* f) {
        cout << "文件自定义删除器." << endl;
        fclose(f);
        });
}

运行结果:

 待补充

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

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

相关文章

讯飞星火 VS 文心一言:谁是中文大语言模型的TOP1?

在百度发布文心一言一个多月后&#xff0c;科大讯飞也发布了自己的大模型“讯飞星火大模型”。本篇博客就测评一下这两个在中文圈最受好评的大语言模型&#xff0c;顺便辅以ChatGPT为参考。大家一起来看看到底谁是中文大语言模型的TOP1&#xff1f; 目录 体验网址 1、旅游攻…

Class类文件的结构

1 class文件介绍 Class文件是一组以8个字节为基础单位的二进制流&#xff1b; 各个数据项目严格按照顺序紧凑地排列在文件之中&#xff0c;中间没有添加任何分隔符; 采用一种类似C语言结构体的伪结构来存储数据&#xff0c;只要两种数据类型&#xff1a;“无符号数”和“表”…

execl函数总结以及扩展

为什么要用exec族函数&#xff0c;有什么作用&#xff1f; (1)一个父进程希望复制自己&#xff0c;使父、子进程同时执行不同的代码段。这在网络服务进程中是常见的一一父进程等待客户端的服务请求。当这种请求到达时&#xff0c;父进程调用fork&#xff0c;使子进程处理此请求…

移动机器人运动规划---基于图搜索的基础知识---图和图搜索算法的基本概念

移动机器人运动规划---基于图搜索的基础知识---图和图搜索算法的基本概念 图和图搜索算法的基本概念图的基础概念图搜索基本概念图搜索算法图搜索算法框架 图和图搜索算法的基本概念 图的基础概念 图是有节点和边的一种表达方式 各节点由边连起来 边可以是有向的&#xff0c;…

Java经典笔试题—day07

Java经典笔试题—day07 &#x1f50e;选择题&#x1f50e;编程题&#x1f95d;Fibonacci数列&#x1f95d;合法括号序列判断 &#x1f50e;结尾 &#x1f50e;选择题 (1)Java属于&#xff08; &#xff09; A.操作系统 B.办公软件 C.数据库系统 D.计算机语言 D (2)类声明中&a…

大数据Doris(十八):Properties配置项和关于ENGINE

文章目录 Properties配置项和关于ENGINE 一、Properties配置项 二、关于ENGINE Properties配置项和关于ENGINE 一、Properties配置项 在创建表时,可以指定properties设置表属性,目前支持以下属性: replica

Kali-linux系统指纹识别

现在一些便携式计算机操作系统使用指纹识别来验证密码进行登录。指纹识别是识别系统的一个典型模式&#xff0c;包括指纹图像获取、处理、特征提取和对等模块。如果要做渗透测试&#xff0c;需要了解要渗透测试的操作系统的类型才可以。本节将介绍使用Nmap工具测试正在运行的主…

前端面试题汇总大全 -- 持续更新!

文章目录 一、html 系列 ⭐⭐⭐⭐⭐1、H5新增特性和css3新增特性&#xff1f;2、什么是HTML5&#xff0c;以及和HTML的区别是什么&#xff1f;3、说说你对 Dom 树的理解&#xff1f;4、跨域时怎么处理 cookie&#xff1f;5、说说你对 SSG 的理解&#xff1f;6、从输入url&#…

Azure API 管理缺陷突出了 API 开发中的服务器端请求伪造风险

微软最近修补了其 Azure API 管理服务中的三个漏洞&#xff0c;其中两个漏洞启用了服务器端请求伪造 (SSRF) 攻击&#xff0c;这些攻击可能允许黑客访问内部 Azure 资产。 概念验证漏洞用于突出开发人员在尝试为自己的 API 和服务实施基于黑名单的限制时可能犯的常见错误。 W…

JQuery 详细教程

文章目录 一、JQuery 对象1.1 安装和使用1.2 JQuery包装集对象 二、JQuery 选择器2.1 基础选择器2.2 层次选择器2.3 表单选择器 三、JQuery Dom 操作3.1 操作元素3.1.1 操作属性3.1.2 操作样式3.1.3 操作内容 3.2 添加元素3.3 删除元素3.4 遍历元素 四、JQuery 事件4.1 ready 加…

C/C++每日一练(20230513) 二叉树专场(7)

目录 1. 翻转二叉树 &#x1f31f; 2. 二叉树的最小深度 &#x1f31f; 3. 填充每个节点的下一个右侧节点指针 &#x1f31f;&#x1f31f; 附&#xff1a;二叉树的序列化与反序列化 &#x1f31f; 每日一练刷题专栏 &#x1f31f; Golang每日一练 专栏 Python每日一…

消息队列中间件介绍

消息队列介绍 消息队列中间件是大型系统中的重要组件&#xff0c;已经逐渐成为企业系统内部通信的核心手段。它具有松耦合、异步消息、流量削峰、可靠投递、广播、流量控制、最终一致性等一系列功能&#xff0c;已经成为异步RPC的主要手段之一。 目前常见的消息中间件有Active…

知名高校博士:我改了这2个地方,一开始被秒拒的论文很快就成功发表了~

手稿被拒后&#xff0c;你会怎么做&#xff1f;是直接换期刊重投&#xff0c;还是先仔细修改下论文呢&#xff1f; 伊利诺伊大学博士Sara E. Skrabalak分享了自己在论文被秒拒后&#xff0c;修改了文章部分内容就成功发表的经验。我们来看看她到底做了哪些修改吧 ~ Sara E. Sk…

Java技术总结

Java技术总结 1.高并发1.什么是高并发2.如何提升系统的并发能力3.常见的互联网分层架构整个系统各层次的水平扩展&#xff0c;又分别是如何实施的呢&#xff1f;4.分层水平扩展架构实践5.总结 2.Java 线程的 5 种状态1.新建状态(New):2.就绪状态(Runnable):3.运行状态(Running)…

【企业信息化】第1集 免费开源ERP: Odoo 16 CRM客户关系管理系统

文章目录 前言一、概览二、使用功能1.加快销售速度2.销售线索3.机会4.客户5.高效沟通6.报告7.集成 三、总结 前言 世界排名第一的免费开源ERP: Odoo 16 CRM客户关系管理系统。真正以客户为中心的CRM。 一、概览 获得准确预测 使用可操作数据&#xff0c;以做出更好的决定。 获…

Java --- redis7之缓存预热+雪崩+穿透+击穿

目录 一、缓存预热 二、缓存雪崩 三、缓存穿透 3.1、解决方案 3.1.1、空对象缓存或者缺省值 3.1.2、Goolge布隆过滤器Guava解决缓存穿透 四、缓存击穿 4.1、危害 4.2、解决方案 4.3、模拟百亿补贴活动案例 一、缓存预热 场景&#xff1a;MySQL有N条新记录&#xff…

基于Java的班级管理系统的设计与实现(论文+源码)_kaic

摘 要 伴随着信息技术不断的飞速发展&#xff0c;信息技术给现在的生活也在带来翻天覆地的变化。信息时代的到来已经成为一种趋势&#xff0c;人类的发展历史正在进入到一个新的时代。信息技术已经在生活中的各个领域得到普及以及被应用。班级管理在信息技术发展之前一直都是…

金子数量——c++

金子数量 Description 某地区有n条(编号依次为1到n)互不交叉的道路&#xff0c;每条道路上都有m个数字&#xff0c;其中能被8整除的数称为金子数&#xff0c;这个数字表示其重量。 如下表是3条道路&#xff0c;每条道路中有5个数的一种可能情况。 苏海想在n条道路中走一条金子…

[架构之路-195]-《软考-系统分析师》- MVC、MVP、MVVM架构各自的优缺点

目录 一、MVC 1.1 MVC的基本结构 1.2 MVC的工作流程 1.3 MVC的优缺点 二、MVP 2.1 MVP的基本结构 2.2 MVP的工作流程 2.3 MVP的优缺点 三、MVVM 3.1 MVVM的基本结构 3.2 MVVM的工作流程 3.3 MVVM的优缺点 前言&#xff1a; MVC&#xff1a;一个controller对应多个v…

Windows SSH远程连接Linux服务器 - 无公网IP内网穿透

文章目录 前言视频教程1、安装OpenSSH2、vscode配置ssh3. 局域网测试连接远程服务器4. 公网远程连接4.1 ubuntu安装cpolar内网穿透4.2 创建隧道映射4.3 测试公网远程连接 5. 配置固定TCP端口地址5.1 保留一个固定TCP端口地址5.2 配置固定TCP端口地址5.3 测试固定公网地址远程 转…