• UE 智能指针的介绍


    此文章为下面文章的整理,感谢前辈

    智能指针的介绍

    UE 中的 UObject 本身就可以帮我们做回收,做释放的。我们可以看到 UE4 中所有的类都是继承自UObject,这样的话当我们引擎被关闭的时候,它会帮我们自动释放这些资源,或者是我们手动去释放掉它,这样能保证能彻底删掉它。

    UE4 对原生对象(即不继承 UObject 的类)不提供垃圾回收,需要手动进行清理。

    原生对象创建和销毁的方式有:malloc / freenew / delete。new 与 malloc 的区别在于,new 在分配内存完成之后会调用构造函数。

    我们会遇到这样的问题:

    1. 创建一个类之后,如果忘记把它销毁,则它占的内存可能永远都无法被释放掉。
    2. 创建一个类之后,有其它的指针指向了它,结果不小心把它释放了,其它指针依然保留了原始的信息,导致我们在访问这个类的时候而产生野指针奔溃。

    为了防止这些问题的出现,智能指针就出现了,它就是用来管理这些非继承 UObject 的类。

    智能指针

    UE4 提供共享指针库来管理内存,它是 C++11 智能指针的自定义实现,虚拟智能指针库和C++智能指针在功能和效率方面都有一些共同的特点。

    智能指针优点

    1. 可以像常规的 C++ 指针那样复制,解引用,比较共享指针等
    2. 防止内存泄漏:共享引用不存在时,智能指针(弱指针除外)会自动删除对象。
    3. 弱引用:弱指针会中断引用循环并阻止悬挂指针。
    4. 包含了可以通过多线程安全地进行访问的“线程安全”版本
    5. 可以创建任何类型的对象的共享指针
    6. 支持针“const”、前置声明的不完全类型、类型转换等

    为什么使用 UE4 的智能指针库?

    1. std::shared_ptr不是在所有的平台都能用的
    2. 可以和其它虚幻容器及类型无缝协作
    3. 更好的控制平台特性,包括线程处理和优化

    可以查看详细的官方文档

    如何使用智能指针?

    智能指针的主要类型分为:TSharedPtr, TSharedRef, TWeakPtr, TUniquePtr。

    先声明一个测试类

    class TestA
    {
    public:
        int32 a;
        float b;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    TSharedPtr 共享指针

    TSharedPtr 为共享指针。

    创建(MakeShareable)/初始化/重置(Reset

    用法如下:

    void TestSharedPtr()
    {
        //声明
        TSharedPtr MyTestA;
        //分配内存
        MyTestA = MakeShareable(new TestA());
        //先判读智能指针是否有效
        if (MyTestA.IsValid() || MyTestA.Get())
        {
            //访问
            int32 a = MyTestA->a;
            //复制指针
            TSharedPtr MyTesta = MyTestA;
            //获取共享指针引用计数
            int32 Count = MyTestA.GetSharedReferenceCount();
            //销毁对象
            MyTesta.Reset();
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    TSharedPtr 不能指向 UObject。如果想要指向UObject,可以使用 TWeakObjectPtr。TSharedPtr 可以对 FStructures 使用。

    TSharedPtr 可以在 .h 中定义,并且可以置为空(= nullptr)。

    TShareRef 不允许在 .h 中定义的,并且一直有值,下面会介绍。

    TSharedPtr 的 .Get()方法将 TSharedPtr 转换成 C++ 原生指针。在访问 TSharedPtr 时,需要先通过 .IsValid() 或者 .Get() 判断共享指针是否有效。如果指针是无效的,调用重载的操作符 -> 去获取值时,直接会导致断言(check)失败,将会导致奔溃。

    FORCEINLINE
    ObjectType* operator->() const 
    { 
        // 可以看到这里的check
        check( IsValid() );  
        return Object;
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    注意:MyTestA.IsValid()中 “.” 是访问共享指针的成员,而 MyTestA->a中 “->” 是访问这个指针指向的对象中的成员。

    Reset() 用于重置共享指针。

    TSharePtr 和 C++11 的 shareptr 一样,内部都是基于引用计数的。所以可以通过GetSharedReferenceCount()获取到当前的引用计数

    复制

    复制共享指针

    // 复制共享指针
    TSharedPtr simObjectPtr_copy = simObjectPtr;
    UE_LOG(LogTemp, Warning, TEXT("引用计数: simObjectPtr[%d], simObjectPtr_copy[%d],"),
    	simObjectPtr.GetSharedReferenceCount(), simObjectPtr_copy.GetSharedReferenceCount());
    
    • 1
    • 2
    • 3
    • 4

    输出:

    引用计数: simObjectPtr[2], simObjectPtr_copy[2]

    转移

    转移可以通过 MoveTempMoveTempIfPossible

    // 转移共享指针
    TSharedPtr simObjectPtr_MoveTemp = MoveTemp(simObjectPtr_copy);  // 另 MoveTempIfPossible()
    UE_LOG(LogTemp, Warning, TEXT("引用计数: simObjectPtr[%d], simObjectPtr_copy[%d], simObjectPtr_MoveTemp[%d]"),
    	simObjectPtr.GetSharedReferenceCount(), simObjectPtr_copy.GetSharedReferenceCount(), simObjectPtr_MoveTemp.GetSharedReferenceCount()
    
    • 1
    • 2
    • 3
    • 4

    输出:

    引用计数: simObjectPtr[2], simObjectPtr_copy[0], simObjectPtr_MoveTemp[2]

    条件判断 / 对比 / 解引用与访问

    • -> 运算符
    • Get() 函数
    • IsValid() 函数
    • ==!= 运算符
    if (simObjectPtr)					// 条件判断
    {
    	simObjectPtr->ExeFun();			// 解引用
    }
    if (simObjectPtr.Get() != nullptr)	// 条件判断
    {
    	simObjectPtr.Get()->ExeFun();	//解引用
    }
    if (simObjectPtr.IsValid())			// 条件判断
    {
    	(*simObjectPtr).ExeFun();		// 解引用
    }
    if (simObjectPtr == simObjectPtr_copy)	// 对比
    {
    	UE_LOG(LogTemp, Warning, TEXT("simObjectPtr_copy == simObjectPtr"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    TSharedRef 共享引用

    TSharedRef 为共享引用,共享引用不可为空,所以它没有 IsValid 方法,因为它一直是有效的。TSharedRef 不可用于 UObject 对象。

    创建/初始化

    MakeShareable() / MakeShared() 函数

    void TestSharedRef()
    {
        // 创建共享引用
        TSharedRef objRef(new SimpleObject());
        TSharedRef objRef2 = MakeShareable(new SimpleObject());
        TSharedRef objRef3 = MakeShared();
    
        //访问:
        int32 a = objRef->a;//方法一
        int32 b = (*objRef).a;//方法二
        
        //销毁对象
        objRef.Reset();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意:

    1. 与共享指针类似,固定引用非空对象,使用时候直接用->即可,因为一直有有效值;
    2. ShareRef 不允许在 .h 中定义的,会崩溃;
    3. 不能 =nullptr 这么写;
    4. 也可以通过 GetSharedReferenceCount() 获取引用技术。

    TSharedPtr 和 TSharedRef 之间的相互转换

    TSharedRef 转为 TSharedPtr,支持隐式转换,直接赋值即可。

    TSharedPtr 转为 TSharedRef,通过 ToSharedRef()进行转换。

    void ATestSharedRefAndPtr()
    {
        //创建普通指针
        TestA* MyTestC = new TestA();
        //创建共享指针
        TSharedPtr MyTestA;
        //创建共享引用
        TSharedRef MyTestB(new TestA());
    
        //共享引用转换为共享指针,支持隐式转换
        MyTestA = MyTestB;
        //普通的指针转换为共享指针
        MyTestA = MakeShareable(MyTestC);
        //共享指针转换为共享引用,共享指针不能为空
        MyTestB = MyTestA.ToSharedRef();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    TWeakPtr(弱指针)

    TWeakPtr 为弱指针,保持对一个对象的弱引用,与 TSharedPtr 相比,它不参与引用计数,也就不会阻止对象的销毁。当 TWeakPtr 指向的类销毁时,那么这个弱指针(TWeakPtr)也会为null,所以,使用时需要判断有效性。

    TWeakPtr 打破 TSharedPtr 共享指针带来的循环引用问题。

    创建/初始化/重置

    • 通过 TSharedPtr 创建
    • 通过 TSharedRef 创建
    • 运算符 = 赋值
    • IsValid() 函数判断有效性
    • Reset() 或 nullptr 重置

    TWeakPtr 的创建必须基于一个 TSharedPtr 或者 TSharedRef。

    // 强指针创建弱指针
    TSharedPtr ObjPtr = MakeShared();
    TWeakPtr ObjWeakPtr(ObjPtr);
    UE_LOG(LogTemp, Warning, TEXT("step1 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
    
    // 强引用创建弱指针
    TSharedRef objRef = MakeShareable(new SimpleObject());
    TWeakPtr ObjWeakPtr2(objRef);
    
    TWeakPtr ObjWeakPtr_Copy = ObjWeakPtr;
    UE_LOG(LogTemp, Warning, TEXT("step2 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
    
    // 判断有效性
    if (ObjWeakPtr.IsValid())
    {
    	TSharedPtr ObjPtr2 = ObjWeakPtr.Pin();
    	ObjPtr2->ExeFun();
    }
    
    // 清空强指针
    ObjPtr.Reset();
    UE_LOG(LogTemp, Warning, TEXT("step3 引用计数:ObjPtr[%d]"), ObjPtr.GetSharedReferenceCount());
    
    TSharedPtr ObjPtr2 = ObjWeakPtr.Pin();
    // 判断有效性
    if (!ObjPtr2)
    {
    	UE_LOG(LogTemp, Warning, TEXT("弱指针已空"));
    }
    
    // 重置
    ObjWeakPtr.Reset();
    ObjWeakPtr_Copy = nullptr;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    输出:

    step1 引用计数:ObjPtr[1]
    step2 引用计数:ObjPtr[1]
    step3 引用计数:ObjPtr[0]
    弱指针已空

    转换

    TWeakPtr 的 Pin() 函数可以将 WeakPtr 转成 TSharedPtr。

    • Pin() 函数转成 TSharedPtr ,再解引用访问对象
    void MyWeakPtrToSharePtr()
    {
        TSharedPtr ptr = MakeShareable(new TestA());
        //通过SharePtr初始化TWeakPtr
        TWeakPtr WeakPtr = ptr;
    
        //利用 WeakPtr的.Pin() 函数将 WeakPtr 转成SharedPtr
        TSharedPtr WeakPtrToSharePtrObj = WeakPtr.Pin();
        if (WeakPtrToSharePtrObj.IsValid())
        {
            //tdo smth...
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    TWeakPtr 无法直接转换为 TSharedRef,需要先将 WeakPtr 转成 SharedPtr,再将 SharedPtr 转 SharedRef。

    void MyWeakPtrToShareRef()
    {
        TSharedPtr ptr = MakeShareable(new TestA());
        //通过SharePtr初始化TWeakPtr
        TWeakPtr WeakPtr = ptr;
    
        //利用WeakPtr的.Pin()函数将WeakPtr转成SharedPtr
        TSharedPtr WeakPtrToSharePtrObj = WeakPtr.Pin();
        if (WeakPtrToSharePtrObj.IsValid())
        {
            //利用TSharedPtr的ToSharedRef()函数将TSharedPtr转换成TSharedRef
            TSharedRef ref = WeakPtrToSharePtrObj.ToSharedRef();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    注意:TWeakPtr 一定是从 TSharedPtr 或者 TSharedRef 过来的。

    TUniquePtr 唯一指针

    TUniquePtr 指向的对象只能被唯一指向,因而 Unique 指针不能赋值给其它指针。

    不要为共享指针或共享引用的对象创建唯一指针。

    创建/初始化/判断/解引用/重置

    • MakeUnique()
    • IsValid()
    • -> 运算符
    • Get() 函数
    • Release() 释放并返回指针
    • Reset() 或 nullptr 重置
    // 创建唯一指针
    TUniquePtr ObjUniquePtr = MakeUnique();
    UE_LOG(LogTemp, Warning, TEXT("Validity: ObjUniquePtr[%d]"), ObjUniquePtr.IsValid());
    
    // 判断有效性
    if (ObjUniquePtr.IsValid()) 
    {
        ObjUniquePtr->ExeFun(); // 解引用
    }
    
    // 释放指针,移交
    TUniquePtr ObjUniquePtr2(ObjUniquePtr.Release());
    UE_LOG(LogTemp, Warning, TEXT("Validity: ObjUniquePtr[%d], ObjUniquePtr2[%d]"), ObjUniquePtr.IsValid(), ObjUniquePtr2.IsValid());
    
    // 重置
    ObjUniquePtr.Reset();		
    ObjUniquePtr2 = nullptr;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    基类与派生类的智能转换

    先声明两个类

    class Father
    {
    public:
    	int32 a = 10;
    };
    
    class Child :public Father
    {
    public:
    	int32 b = 20;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    共享指针:基类与派生类的转换

    派生类转换为基类

    派生类转基类为隐式转换。

    //声明基类共享指针_father
    TSharedPtr_father;
    //创建派生类共享指针_Child
    TSharedPtr_child = MakeShareable(new Child);
    //派生类转换为基类,支持隐式转换
    _father = _child;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    基类转换为派生类

    通过 StaticCastSharedRef 进行转换。

    //基类转换为派生类
    TSharedPtr_child = StaticCastSharedPtr(_father);
    //_child调用派生类的成员
    if (_child.IsValid())
    {
        _child->b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    常量基类转换为派生类

    常量基类转换为派生类,即将 Const TSharePtr<基类> 转换为 TSharePtr<派生类>,有两种方式:

    方法一:

    1. 先通过 ConstCastSharedRef 转换城非 Const 的基类
    2. 再通过 StaticCastSharedRef 转换
    //创建派生类转为基类的共享指针,支持隐式转换
    const TSharedPtr const_father = MakeShareable(new Child);
    //先将基类指针转换为指向基类的指针
    TSharedPtr const_father_a = ConstCastSharedPtr(const_father);
    //然后基类转换为派生类
    TSharedPtr _child_a = StaticCastSharedPtr(const_father_a);
    //调用派生类成员
    if (_child_a.IsValid())
    {
        _child_a->b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    方法二:直接通过 StaticCastSharedRef 转换

    TSharedPtr _child_a = StaticCastSharedRef(const_father);
    if (_child_a.IsValid())
    {
        _child_a->b;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    注意:不能通过 ConstCastSharedPtr 直接转成派生类.

    //错误写法, 不能通过 ConstCastSharedPtr 直接转成派生类. 		
    TSharedPtr ptrcast = ConstCastSharedPtr(const_father);
    
    • 1
    • 2

    共享引用:基类与派生类的转换

    用法和共享指针一样,只需修改以下即可

    • TsharedPtr 改为 TSharedRef,
    • StaticCastSharedPtr 改为 StaticCastSharedRef,
    • ConstStaticCastSharedPtr 改为 ConstStaticCastSharedRef

    TSharedFromThis

    继承了 TSharedFromThis 其实里面会多一个弱引用指针(WeakPtr),存了一个对象,同时继承了 AsShared 方法,来获取这个类的共享引用,方便我们直接做转换。

    将一个类继承自 TSharedFromThis 之后,那么这个类就会知道自己是属于哪一个共享指针了。

    TSharedFromThis 使用场景

    TSharedFromThis 哪些场合适合使用?

    1. 比如我们要写一个单例管理类,可以将数据类继承自 TSharedFromThis;
    2. 需要转换成原生 C++ 类,又想在某一处将原生 C++ 类再次转换成 TSharedPtr 的需求;
    3. TSharedFromThis 是支持线程安全的。

    用法

    先声明一个类 ClassBase 继承自 TSharedFromThis

    class ClassBase : public TSharedFromThis
    {
    public:
        int32 nBase;
    };
    
    class ClassTop : public ClassBase
    {
    public:
        void MyFunc() {}
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    TSharedFromThis 的使用:

    • AsShared() 将裸指针转智共享引用,可再隐式转为共享指针
    • SharedThis(this) 会返回具备"this"类型的TSharedRef

    不要在构造函数中调用 AsShared 或 Shared,共享引用此时并未初始化,将导致崩溃或断言。

    void MySharedFromThisTest()
    {
        TSharedPtr ptr = MakeShareable(new ClassTop());
        if (ptr.IsValid())
        {
            //我们通过.Get()将TSharedPtr转换成C++ 原生指针
            ClassTop* pOriginPtr = ptr.Get();
    
            //现在我们想将pOriginPtr这个原生C++指针怎么转回智能指针呢?
            {
                //错误的用法
                //TSharedPtr ptr = MakeShareable(pOriginPtr);
    
                //错误的用法, AsShared()不能用派生类接
                //TSharedPtr ptr22 = pOriginPtr->AsShared();
            }
            {
                //正确的用法, AsShared()不能用基类接(基类指的是你继承自TSharedFromThis的那个类)
                TSharedPtr ptr2 = pOriginPtr->AsShared();
                TSharedPtr ptr3 = StaticCastSharedPtr(ptr2);
                if (ptr3.IsValid())
                {
                        ptr3->MyFunc();
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    注意

    1. 错误的用法:通过 Get() 将 TSharedPtr 转换成 C++ 原生指针,在创建 TSharedPtr。
    TSharedPtr ptr = MakeShareable(pOriginPtr);
    
    • 1
    1. 错误的用法(AsShared() 不能用派生类接)
    TSharedPtr ptr22 = pOriginPtr->AsShared();
    
    • 1
    1. 正确的用法,AsShared() 需要用基类接(基类指的是指直接继承自TSharedFromThis的那个类)
    TSharedPtr ptr2 = pOriginPtr->AsShared();
    TSharedPtr ptr3 = StaticCastSharedPtr(ptr2);
    if (ptr3.IsValid())
    {
        ptr3->MyFunc();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    SharedThis 示例:

    // 基类
    class BaseClass :public TSharedFromThis
    {
    public:
    	BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
    	virtual ~BaseClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
    	virtual void ExeFun() { 
    		TSharedRef ThisAsSharedRef = AsShared();
    	}
    };
    
    // 派生类
    class ChildClass :public BaseClass 
    {
    public:
    	ChildClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
    	virtual ~ChildClass() { UE_LOG(LogTemp, Warning, TEXT(__FUNCTION__)); }
    	virtual void ExeFun() override{
            // AsShared()返回 TSharedRef, 因而编译不通过
    		// TSharedRef AsSharedRef = AsShared(); 
    
    		TSharedRef AsSharedRef = SharedThis(this);
    	}
    };
    
    TSharedPtr BaseClassPtr = MakeShared();
    UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d]"), BaseClassPtr.GetSharedReferenceCount());
    
    BaseClass* tempPtr = BaseClassPtr.Get();
    TSharedPtr BaseClassPtr_Shared = tempPtr->AsShared();
    UE_LOG(LogTemp, Warning, TEXT("引用计数:BaseClassPtr[%d], BaseClassPtr_Shared[%d]"), 
    	BaseClassPtr.GetSharedReferenceCount(), 
        BaseClassPtr_Shared.GetSharedReferenceCount());
    
    // 使用下面语句运行,程序死机
    // TSharedPtr BaseClassPtr_New = MakeShareable(tempPtr);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    输出:

    引用计数:BaseClassPtr[1]
    引用计数:BaseClassPtr[2], BaseClassPtr_Shared[2]

    注意事项

    • 避免将数据作为 TSharedRef 或 TSharedPtr 参数传到函数,此操作将因取消引用和引用计数而产生开销。相反,建议将引用对象作为 const & 进行传递。
    • 可将共享指针向前声明为不完整类型。
    • 共享指针与虚幻对象(UObject及其衍生类)不兼容。引擎具有UObject管理的单独内存管理系统,两个系统未互相重叠。
    • 避免智能指针和原生指针混用,可能会遇到内存释放等问题。
    • 切记不能用 UPROPERTY 这些反射修饰符来修饰这些智能指针变量
    • 切记 TSharedRef 变量是不允许定义在头文件里面作为成员变量出现的
    • C++ 的原生 cast 方法(static_cast、dynamic_cast、const_cast、reinterpret_cast)不能用于 UE4 智能指针的转换,请使用 ConstCastSharedRef、ConstCastSharedPtr、StaticCastSharedPtr、StaticCastSharedRef。
    • 在UE4里面还是使用UE4的智能指针,就别用C++11的了,因为UE的智能指针能和UE的代码比如一些容器能够方便的使用。
    • UE的智能指针要比C++占得字节要大
    • 这些智能指针都是支持线程安全的,有Fast和Safe两种模式。默认是Fast,也就是非线程安全。可以按需要标记成Safe模式。
  • 相关阅读:
    6.3 Cookie对象操作
    Fastweb - Lua操作SQLite数据库
    网络技术二十二:NAT&PPP
    Go 面向对象,多态
    C# 中关于 T 泛型【C# 基础】
    Linux服务器如何处理无法删除的文件?
    基于STM32CubeMX和keil采用RTC时钟周期唤醒和闹钟实现LED与BEEP周期开关
    C++常用脚本合集
    初识树结构和二叉树
    UnoCSS框架常用语法
  • 原文地址:https://blog.csdn.net/fuyoufang1/article/details/126178795