第四章 智能指针

2023-05-16

裸指针问题如下:

  1. 裸指针在声明中并未指出,裸指针指涉到的是单个对象还是一个数组。
  2. 裸指针在声明中也没有提示是不是要对其进行虚构。换言之,无法得知指针是否拥有其指涉的对象。或者是否空悬
  3. 指针的析构是不是拥有重载的delete操作符。
  4. 要防止多于一次的释放和析构。

C++存在的4种智能指针:

stdboost功能说明
auto_ptr(C++98)-独占指针对象
unique_ptr(C++11)scoped_ptr独占指针对象,并保证指针所指对象生命周期与其一致
shared_ptr(C++11)shared_ptr可共享指针对象,可以赋值给shared_ptr或weak_ptr。指针所指对象在所有的相关联的shared_ptr生命周期结束时结束,是强引用。
weak_ptr(C++11)weak_ptr它不能决定所指对象的生命周期,引用所指对象时,需要lock()成shared_ptr才能使用。

auto_ptr的问题

auto_ptr对指针进行独占,但是问题在于,独占需要使用移动操作,而C++98种没有移动语意,所以auto_ptr使用的是复制,所以要求独占却又可以复制,并且由于此问题,auto_ptr也不能存储于容器之中。

auto_ptr::get();    //返回原始指针
auto_ptr::release();    //将当前保存的普通指针删除并返回这个指针的值
auto_ptr::reset();  //将当前保存的指针所指向的地方释放并存储新的指针

十八 使用std::unique_ptr管理具备专属所有权的资源

默认情况下,基本可以认为unique_ptr和裸指针有着相同的尺寸,并且对于大多数操作(包括提领),它们都是执行了相同的指令。

unique_ptr是专属所有权语意,一个非空的unique_ptr总是拥有其指涉到的资源。移动一个unique_ptr会将所有权从源指针移动到默认指针,源指针被置空。unique_ptr不允许复制操作,为的就是防止破坏专属所有权。所以unique_ptr是一个只移型别。

默认的,资源的析构是调用默认的delete运算符。

例如:

template <typename... Ts>   //工厂函数
std::unique_ptr<classType> makeClass(Ts&&... params);
auto pClass = makeClass(arguments); //使用返回的unique_ptr

当使用自定义析构器的时候,只能使用构造函数(不能使用make_unique)来构造unique_ptr。

//自定义析构器
auto delClass = [](classType *pc){ makelog(pc); delete pc; };
template <typename... Ts>
std::unique_ptr<classType, decltype(delClass)> makeClass(Ts&&... params)
{
    std::unique_ptr<classType, decltype(delClass)> pc(nullptr, delClass);   //构造函数
    if(/*构建Stock类*/)
        pc.reset(new stock(std::forward<Ts>(params)...));
    else if(/*构建Bond类*/)
        pc.reset(new Bond(std::forward<Ts>(params)...));
    else if(/*创建Real类*/)
        pc.reset(new Real(std::forward<Ts>(params)...));
    return pc;
}

首先在这段代码种属于自定义析构器(delCLass),所有的自定义删除函数都是接受一个指涉到欲析构对象的裸指针,返回类型是void。其次是当使用自定义析构器,类型必须被指定为unique_ptr的第二个实参型别,所以这里工厂函数的返回类型是std::unique_ptr<classType, decltype(delClass)>。这里decltype(delClass)的类型是std::function<void(classType *)>。makeClass的基本策略是创建一个空的unique_ptr,使其指涉到适当型别的对象,然后将其返回。

将裸指针直接赋值给unique_ptr无法通过编译。因为这会形成裸指针到智能指针的隐式型别转换(发生裸指针到智能指针的转换后,当前指针将成为空指针,并且随着智能指针超出作用域而释放,当前内存将不再存在,将出现空悬指针)。

对每一次new运算符的调用结果,我们都使用forward将实参完美转发给makeClass。

自定义析构器接受基类类型,不管真实的类型是基类还是派生类,都会在lambda表达式种作为一个基类对象被删除。所以基类种必须声明虚析构函数。

C++14种,增加了函数返回型别推导,就使得makeClass可以通过以下更简单的、封装性更好的方法。

template <typename... Ts>
auto makeClass(Ts&&... params)
{
    //内存同上
}

若析构器是函数指针,那么unique_ptr长度尺寸一般会增加一到两个字长,而若析构器是函数对象,则带来的尺寸变化取决于该函数对象种存储了多少状态。无状态的函数指针不会浪费。

unique_ptr提供两种形式,一种是单个对象,另一种是数组(std::unique_ptr<T[]>),这样区分的结果是,指涉到的对象种类不会产生二义性。对于单个对象,不提供索引运算符(operator[]),数组形式不提供提领运算符(operator*operator->)。

unique_ptr可以高效的转换成shared_ptr。这是因为shared_ptr种有以unique_ptr类型为形参的右值构造函数。

shared_ptr<classType> sp = makeClass(arguments);

十九 使用shared_ptr管理具备共享所有权的资源

shared_ptr智能指针访问的对象采用共享所有权来管理对象的生存期,只有当最后一个指涉到对象的shared_ptr不再指涉到的时候,会析构其对象。

shared_ptr通过访问某资源的引用计数来确定是否自己是最后一个指涉到该资源的。也就是说,当引用计数归零的时候,会析构所指向的对象。否则只是引用对象进行自减。

shared_ptr的性能影响:

  • shared_ptr的尺寸是裸指针的两倍,因为内部包含一个指涉到该资源的裸指针,也包含一个直射到该资源的引用计数的裸指针。
  • 引用计数的内存必须动态分配,从技术上来说,引用计数于被指着到的对象相关联,然而被指涉到的对象却对此一无所知。这样所有的型别都可以被shared_ptr所管理。
  • 引用计数的递增和递减都必须是原子操作。因为在不同的线程种可能存在并发的读写器。因为不同线程可能会指涉到同一个对象,并做不同的操作。

当进行移动构造的时候是不需要进行计数递增的,因此移动操作比复制操作更快。复制操作要求递增引用计数,而移动不需要。这是因为移动构造后,原有的shared_ptr将不再指涉到此资源。

与unique_ptr类似,shared_ptr也使用delete来作为默认资源析构器,这种支持的设计却与unique_ptr不同,对于unique_ptr,析构器是智能指针类型型别的一部分,对于shared_ptr却不一样。

auto delandLog = [](classType *pc){ makelog(pc); delete pc; };
unique_ptr<Widget, decltype(delandLog)> upw(new Widget, delandLog); //析构器是智能指针的一部分
shared_ptr<Widget> upw(new Widget, delandLog);  //析构器型别不是智能指针的一部分

也就是说,传入了不同型别的析构器的unique_ptr将不再是同一个类型,尽管指涉到的对象类型是一致的。但是对于shared_ptr来说,传入不同的析构器并不会影响shared_ptr的型别,shared_ptr的型别只决定于所指对象的型别。

shared_ptr的析构器可能是lambda表达式,函数指针或函数对象,这些函数可能是有内存大小的,不存在于shared_ptr的类种,那存在哪里呢?

答案就是存储在shared_ptr的控制块中,这个控制块包括了引用计数,弱计数(weak_ptr所用)和其他数据(例如自定义的分配器或删除器等)。

如图:
这里写图片描述

一个对象的控制块应当由首个指涉到该对象的shared_ptr函数确定。毕竟正在创建指向某对象的控制块是不知道是否有其他对象指涉到该控制块。控制块的创建遵循以下规则:

  • make_shared()总是创建一个控制块。此函数会生产出一个用以指涉到的新对象,因此在调用make_shared的时刻,显然不会有针对该对象的控制块存在。
  • 从具备专属所有权的指针(unique_ptr或auto_ptr)出发构造一个shared_ptr时,会创建一个控制块。专属所有权指针不使用控制块,因此不应该存在所指涉的对象的控制块,并且当shared_ptr被指定了其所指涉到的对象的所有权,原有的专属所有权的指针被置空。
  • 当shared_ptr用裸指针创建的时候会创建控制块。如果想从一个已经拥有控制块的对象出发来创建一个shared_ptr,一般会使用shared_ptr或者weak_ptr,而非裸指针作为构造函数的实参。所以如果传入的实参是shared_ptr或者weak_ptr,则不会创造控制块,可以使用传入的智能指针的控制块。

这里有一个需要注意的地方,就是不能对同一个裸指针构造多个shared_ptr,这样会造成一个对象附有多个控制块,多重的控制块意味着该对象会被析构多次。

所以应尽量避免将裸指针传递给一个shared_ptr的构造函数。常用的替代方法是直接使用make_shared,但是对于自定义析构器的情况下,是不能使用make_shared的。其次如果必须将裸指针传递给shared_ptr,就直接传递new运算符的结果,而非传递一个裸指针变量。

上述的问题(多重控制块)很可能会隐式发生于this指针中,也就是说当类内部处理this指针的,并且又有外部的shared_ptr指向这个对象的时候,就会出现多控制块的问题。(实测在vs2017中无法通过编译,因为无法将裸指针隐式转换为shared_ptr,改函数被声明为explicit)

class A;
vector<shared_ptr<A>> classv;
class A
{
public:
    void process(){
        a = 100;
        classv.push_back(this);
    }
private:
    int a{0};
};

note: 原因如下: 无法从“A *”转换为“const std::shared_ptr”
note: class“std::shared_ptr”的构造函数声明为“explicit”

C++中提供了一个模板类std::enable_shared_from_this,该函数可以将内部的this指针包装为shared_ptr形式。

所以上述代码可以改为:

class A;
vector<shared_ptr<A>> classv;
class A : public enable_shared_from_this<A> //继承此模板类
{
public:
    void process(){
        a = 100;
        classv.push_back(shared_from_this());   //返回一个shared_ptr
    }
private:
    int a{0};
};

enable_shared_from_this是一个基类模板,其型别形参总是其派生类的类名。

enable_shared_from_this定义了一个成员函数,它会创建一个shared_ptr指涉到当前对象,但同时不会重复创建控制块。这个成员函数的名字是shared_from_this,每当需要一个和this指针指涉到相同对象的shared_ptr的时候,都可以在成员函数中使用它。正如上述代码。

从内部实现的角度,shared_from_this查询当前对象的控制块,并创建一个指向该控制块的新shared_ptr,这样的设计依赖于当前对象有一个新的shared_ptr。如果shared_ptr未定义,会产生错误。

为了避免这种情况的发生(即在shared_ptr指涉到对象之前就调用了引发shared_from_this的成员函数),继承自enable_shared_from_this的类,通常将其构造函数声明为private访问级别,并只允许用户通过调用返回shared_ptr的工厂函数来创建对象(通过静态函数调用)。

shared_ptr不可以被用来处理数组,只能用来处理指涉到单个对象的指针,shared_ptr还支持派生类到基类的指针型别转换,这也仅仅只对单个对象有意义。并且shared_ptr未提供operator[]。

shared_ptr实现多态

多态是虚函数+类对象指针/引用来实现的。但是对于shared_ptr来说,首先是该指针类并不了解具体的派生情况,其次是传入基类和派生类的shared_ptr其实是不同的类型的,那么如何在类内部实现多态呢?

方法就是通过成员函数来实现:

template<class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
        shared_ptr(const shared_ptr<_Ty2>& _Other) _NOEXCEPT
        {   // construct shared_ptr object that owns same resource as _Other
            this->_Copy_construct_from(_Other);
        }
template<class _Ty2>
        void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
        {   // implement shared_ptr's (converting) copy ctor
        if (_Other._Rep)
            {
            _Other._Rep->_Incref();
            }

        _Ptr = _Other._Ptr;
        _Rep = _Other._Rep;
        }

从这个函数我们可以知道,当父类的shared_ptr指向派生类的对象的时候,类内部是直接将裸指针形式进行派生类到基类的转换,也就是说,在成员函数内部,是基类的裸指针指向了派生类的裸指针,这就在shared_ptr中实现了多态。

二十 对于类似shared_ptr但有可能空悬的指针使用weak_ptr

对于两个互相指向对方来指针来说,如果使用shared_pr,有可能出现内存泄露。代码如下:

#include <iostream>
#include <memory>
using namespace std;
class A;
class B;
class A
{
public:
    A() { cout << "create A" << endl; }
    ~A() { cout << "destroy A" << endl; }
    shared_ptr<B> sp{nullptr};
};
class B
{
public:
    B() { cout << "create B" << endl; }
    ~B() { cout << "destroy B" << endl; }
    shared_ptr<A> sp{nullptr};
};
void test()
{
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->sp = pb;
    pb->sp = pa;
}
int main()
{
    test();
    return 0;
}

输出为:

create A
create B

很明显发现当超出了test的作用范围后,两个shared_ptr都没有被释放。

首先对这两个对象使用shared_ptr,这样每一个对象的控制块计数都是1。之后分别指向对方,会导致每一个的控制块计数都变成2。当超出作用域的时候,pa,pb检测到对象的控制块计数都是2,这样pa和pb的释放仅仅是把控制块计数减去1,而不会进行具体对象的析构。而由于这里退出test,我们已经无法再知道指向这两个对象的指针。就产生了内存泄露。

消除的方法就是把其中一个shared_ptr改为weak_ptr,这样不会改变其中之一对象的计数,这样退出作用域就会析构被shared_ptr和weak_ptr(计数是1),之后析构另一个对象。

class A;
class B;
class A
{
public:
    A() { cout << "create A" << endl; }
    ~A() { cout << "destroy A" << endl; }
    shared_ptr<B> sp{nullptr};
};
class B
{
public:
    B() { cout << "create B" << endl; }
    ~B() { cout << "destroy B" << endl; }
    weak_ptr<A> sp; //使用weak_ptr以防止循环引用
};
void test()
{
    shared_ptr<A> pa = make_shared<A>();
    shared_ptr<B> pb = make_shared<B>();
    pa->sp = pb;
    pb->sp = pa;
}

weak_ptr像shared_ptr那样运作,但是不影响其指涉对象的引用计数。这样这个指针就必须处理当前所指对象是否被析构的问题。

weak_ptr不能提领也不能检查是否未空,这是因为weak_ptr并不是一种独立的指针,而是shared_ptr的一种扩充。

weak一般是通过shared_ptr来创建的,当使用shared_ptr完成初始化weak_ptr的时刻,两者就指涉到相同的位置,但是weak_ptr不影响其指涉对象的引用计数。

class W
{
public:
    W(int _x, int _y) : x(_x), y(_y) { cout << "create W" << endl; }
    ~W() { cout << "destroy W" << endl; }
private:
    int x;
    int y;
};
int main()
{
    shared_ptr<W> spw = make_shared<W>(1, 2);
    weak_ptr<W> wpw(spw);
    cout << wpw.use_count() << endl;    //查看有多少shared_ptr指向该对象
    cout << wpw.expired() << endl;  //是否成为空悬指针
    auto pw = wpw.lock();   //从weak_ptr得到一个shared_ptr
    cout << typeid(pw).name() << endl;  //class std::shared_ptr<class W>
    cout << wpw.use_count() << endl;    //2

    return 0;
}

但是再多线程的情况下,如果先检测weak所指的对象是不是被析构了,如果未被析构再用shared_ptr去访问的话,可能会出现问题,因为再检测和访问之间,对象可能会被别的线程析构,所以需要一个原子操作来决定,这就是刚才用的lock()函数,用次函数,如果对象已经被析构,那么返回为空。

再工厂函数返回指针的时候,之前返回的是unique_ptr,但是这是独占的指针,如果想把指针放入到缓存中,则可以使用shared_ptr,但是如果这样,shared_ptr指向的对象将永远不会释放,换句话说,该释放的对象将永远因为缓存中的shared_ptr而不会释放。

所以可以在缓存中存储weak_ptr,这样缓存中的智能指针不会影响对象的生存期,并且也可以检测到对象的生存状态(expired())。

shared_ptr和weak_ptr配合使用最常见的就是在观察者模式中

在观察者模式中,主要是由两个类组成,其中之一是观察者(observer),另外一个就是主题(subject),主题中保存着一个链表或数组(通常用STL中的list或vector实现),主题中持有指向观察者的指针(weak_ptr实现),这样主题中发生变化的时候就会通过指针调用观察者的函数发起相对应的操作。并且当观察者被析构了之后,weak_ptr可以检测空悬。

从效率上看,weak_ptr和shared_ptr本质上一致,weak_ptr的对象和shared_ptr对象的尺寸相同,它们使用相同的控制块。事实上,控制块中也记录中weak_ptr的引用计数。

二十一 优先选用std::make_unique和std::make_shared,而非直接使用new

make_shared处于C++11中,而make_unique处于C++14中。

make_unqiue和make_shared仅仅是做了一次完美转发,这个完美转发接受一组可变参数模板,并将这些参数转发给类的构造函数执行动态内存分配,并将这个构造函数的结果返回给智能指针的构造函数。

通过make_XXX不能自定义析构器。

源代码如下:

//make_shared()源代码
template<class _Ty,
    class... _Types> inline
    shared_ptr<_Ty> make_shared(_Types&&... _Args)
    {   // make a shared_ptr
    const auto _Rx = new _Ref_count_obj<_Ty>(_STD forward<_Types>(_Args)...);

    shared_ptr<_Ty> _Ret;
    _Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
    return (_Ret);
    }
//make_unique源代码
template<class _Ty,
    class... _Types,
    enable_if_t<!is_array_v<_Ty>, int> = 0> inline
    unique_ptr<_Ty> make_unique(_Types&&... _Args)
    {   // make a unique_ptr
    return (unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...)));
    }

make_XXX函数较好的一点是,如果使用构造函数的形式,动态创建类的对象和依据类的对象构建智能指针是两步操作,这两个操作之间可能会被插入其他的操作,而一旦这个其他的操作抛出了异常,就会导致内存泄露,因为此时这个内存将不会被智能指针所托管。但是使用make_XXX就不会出这种问题,因为此时创建动态内存对象和智能指针托管之间没有其他操作。但是如果使用了自定义析构器将无法使用make_XXX,这个时候就需要把创建的代码单独提取出来,而不是写在形参中,但是这样会造成shared_ptr的拷贝,所以我们还需要用move将其强转为右值。

void test();
void Mydel(T *);
f(shared_ptr<T>(new T), test);  //可能存在内存泄露,如果test()抛出异常
f(make_shared<T>(), test);  //安全
f(shared_ptr<T>(new T, Mydel), test);   //无法使用make_XXX,可能有内存泄露
shared_ptr<T> sp(new T, Mydel);
f(sp, test);    //安全,但是效率不够高
f(move(sp), test);  //安全,并且右值传输,无需复制

并且由于make_shared结构的紧凑,产生的代码的运行速度会更快。首先对于构造函数来说,需要申请两次内存,一次是new类的对象,另一次是申请控制块。而对于make_shared来说,仅仅需要申请一次内存,因为make_shared会分配一块内存,同时保存控制块和类的对象

除了不能自定义析构器之外,对于make_XXX来说还有一个限制,就是对于initializer_list型别的形参,如果创建对象语句中使用大括号会优先匹配initializer_list类型的构造函数,而使用圆括号则不会匹配此类型构造函数。所以在make_XXX中是无法使用大括号进行初始化的。不过可以有折中的办法,那就是使用auto自动推导。

auto initl = {1,2,3,4,5};
auto spv = make_shared<vector<int>>(initl);

有些类会定义自身版本的operator new和operator delete,这些函数的存在意味着全局版本的内存分配和释放函数不适合这种对象。通常,类自定义的这两种函数被设计为仅仅释放该类精确尺寸的这种对象,例如A类,自定义new和delete恰好删除和构造sizeof(A)大小的内存,但是make_shared创建的内存是类的内存加上控制块的内存。所以对于拥有自定义new的类,make_shared并不是一个好办法。

前面说过控制块中还会有weak_ptr的引用计数,这个计数被称为弱计数。当shared_ptr引用计数归零的时候,控制块实际上不会删除,因为还要保留着weak_ptr的引用计数。所以其实控制块要等到所有的weak_ptr都超过作用域,才能被析构。

如果使用make_shared函数创建shared_ptr,那么类的对象的内存和控制块内存属于同一块内存,那么这一块内存将不会被释放。如果这个类对象比较大的话,则会产生很多不必要的内存占用,而对构造函数创建的shared_ptr则不会,类对象会在最后一个shared_ptr析构时被析构。

二十二 使用Pimpl习惯用法时,将特殊成员函数的定义放到实现文件中

Pimpl:pointer to implementation,即指涉到实现的指针。这种方法的技巧就是把某一类的数据成员用一个指涉到某实现类(或结构体)的指针代替,而后把原来主类中的数据成员放到实现类中,并通过指针间接访问这些数据成员。

class Widget
{
public:
    Widget();
    ~Widget();
private:
    /*原私有数据成员
    string name;
    vector<double> data;
    classType a, b, c;  //自定义类型
    */
    struct Impl;    //一个仅声明未定义的非完整类型
    Impl *pImpl;    //可声明指向这个非完整类型的指针
};

这里使用了Pimpl方法,可以减少头文件的引用,这样可以加快编译速度。

Pimpl用法的第一步就是申明一个指针类型的数据成员,指向一个非完整类型。第二步就是动态分配和回收持有从前在原始类里的那些数据成员的对象,而分配和回收代码则是放在实现文件中(包括Impl的实现)。这样也把原来在头文件中包含的头文件(如string、vector)也被转移到实现的.cpp文件中。

但是裸指针的使用不如智能指针,裸指针还会导致自己书写new,delete运算符,并时刻防止内存泄露,所以在C++11中,使用Pimpl方法的时候,最好是使用智能指针。并且由于这里是专属所有权,由外部类独占对象,所以可以使用unique_ptr。

裸指针风格的Pimpl方法中,析构函数要自己写delete,而现在使用了智能指针后,析构函数可以直接声明为= default(声明虚函数的理由是将其实现写在.cpp文件中,头文件中对非完整型别的Impl无法进行析构)。

对于移动操作也是一样,需要将其定义于.cpp文件中,因为在头文件中,Impl结构体还是非完整型别,而move函数在出现异常的时候,会生成析构代码,于是就会析构Impl,而这里的Impl无法被析构(原因同上述析构函数的原因)。

对于复制操作来说,这里要执行深复制,也就是说复制的代码需要重新撰写(如果没使用Pimpl就可以看数据成员支不支持复制操作,如果支持就无需写额外的代码)。

P.s.这里如果使用shared_ptr就不用考虑非完整类型,因为shared_ptr的析构器是不是智能指针类型的一部分,所以需要更大的运行时期数据结构和更慢的目标代码。而unique_ptr尺寸更小,速度更快,所以编译器要求其指涉到的对象必须是完整类型。

一个简单的示例代码如下:

//EMCPP_pimpl.h
#pragma once
#include <memory>
class Widget
{
public:
    Widget();
    ~Widget();
    Widget(Widget&& rhs);
    Widget& operator=(Widget&& rhs);
    Widget(const Widget& rhs);
    Widget& operator=(const Widget& rhs);
private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};
#if 1
#include <iostream>
#include <string>
#include <vector>
#include "EMCPP_pimpl.h"
using namespace std;

struct Widget::Impl //实现
{
    string name;
    vector<int> data;
};
Widget::Widget() : pImpl(make_unique<Impl>())
{}
Widget::~Widget() = default;
Widget::Widget(Widget&& rhs) = default;
Widget& Widget::operator=(Widget&& rhs) = default;
Widget::Widget(const Widget& rhs)
    :pImpl(make_unique<Impl>(*rhs.pImpl))
{}
Widget& Widget::operator=(const Widget& rhs)
{
    *pImpl = *rhs.pImpl;
    return *this;
}

#endif
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

第四章 智能指针 的相关文章

  • OpenWrt 内的阿里云盘 WebDAV 做磁盘使用

    最近在玩OpenwWrt的时候 xff0c 在刷的固件里看到预装的阿里云盘 WebDAV xff0c 加上最近刚刚开始用阿里云 xff0c 不限速 xff0c 非常快 xff0c 通过这个服务 xff0c 可以直接把阿里云的文件架挂载在本地
  • VS2022中使用Copilot

    Copilot可以自动帮你写代码 1 打开vs2022 点击扩展 xff0c 在里面搜索copilot安装 2 安装完成后 xff0c 左下角有个小图标就是copilot 3 点击登录 会弹框 点击确定后 xff0c 跳转到网站 xff0c
  • CSS/SCSS/LESS和自适应布局/响应式布局详解

    在开发前端的时候 xff0c 界面布局尤为重要 xff0c 要布局的非常合理 xff0c 好看 xff0c css是必不可少的 xff0c 然后是各种布局 xff0c 使用这些布局 xff0c 进行混合搭配 xff0c 最终的目的都是开发一
  • 响应式布局之viewport-超级简单

    之前文章CSS布局之详解 故里2130的博客 CSDN博客 上面的文章可以实现响应式布局 xff0c 根据浏览器的大小变化而变化 xff0c 但是相对于viewport来说 xff0c 之前的还是有点复杂 xff0c 而使用viewport
  • .net6API使用AutoMapper和DTO

    AutoMapper xff0c 是一个转换工具 xff0c 说到AutoMapper时 xff0c 就不得不先说DTO xff0c 它叫做数据传输对象 Data Transfer Object 通俗的来说 xff0c DTO就是前端界面需
  • 手机/移动端的UI框架-Vant和NutUI

    下面推荐2款手机 移动端的UI框架 其实还有很多的框架 xff0c 各个大厂都有UI框架 目前 xff0c 找来找去 xff0c 只有腾讯的移动端是setup语法写的TDesign xff0c 其他大厂 xff0c 虽然都是VUE3写的 x
  • 使用uniapp创建小程序和H5界面

    uniapp的介绍可以看官网 xff0c 接下来我们使用uniapp创建小程序和H5界面 xff0c 其他小程序也是可以的 xff0c 只演示创建这2个 xff0c 其实都是一套代码 xff0c 只是生成的方式不一样而已 uni app官网
  • 使用NutUI创建小程序和H5界面

    做开发的时间长了 xff0c 技术都是通用的 xff0c 创建小程序和H5界面有很多的UI xff0c 本章节演示使用NutUI来创建 xff0c 官网 xff0c NutUI 移动端 Vue3 小程序组件库 1 使用HBuilder X创
  • 如何开发微信小程序呢

    也许很多人对小程序 xff0c H5程序 xff0c Vue xff0c 网页程序 xff0c PC端程序认识比较模糊 xff0c 因为这些跨度非常的大 xff0c 很少人会一次性全部接触 xff0c 甚至只是听说过 xff0c 并不了解其
  • .NET6中使用GRPC详细描述

    Supported languages gRPC xff0c 官网 至于原理就不说了 xff0c 可以百度原理之后 xff0c 然后再结合代码 xff0c 事半功倍 xff0c 就能很好理解GRPC了 目录 一 简单使用 二 实际应用 一
  • spss统计分析基础教程(下)--自学

    目录 xff09 第十二章分布类型的检验12 1假设检验的基本思想12 2正态分布检验K S检验的原理 12 3二项分布检验12 4游程检验12 5蒙特卡罗方法 第十三章连续变量的统计推断 xff08 一 xff09 t检验13 1t检验概
  • uniapp学习记录

    目录 1 布局使用flex布局 2 rpx和界面自适应 xff0c 设计稿是750rpx 3 首页不显示tabBar 4 跳转页面 启动跳转页面 5 uniapp中页面生命周期 传值 6 颜色使用 7 字体使用 8 SCSS CSS中获取j
  • uniapp中调用.net6 webapi

    使用uniapp开发程序时 xff0c 不管是小程序 xff0c 还是H5界面 xff0c 它们只是一个显示界面 xff0c 也就是只充当前台界面 xff0c 那么我们后台使用 net6 webapi写业务逻辑 xff0c 然后前端访问后端
  • vue3中前端处理不同数据结构的JSON

    有时候 xff0c 后端返回的JSON数据格式 xff0c 是前端不需要的格式类型 xff0c 这时 xff0c 要么让后端修改 xff0c 你要什么格式 xff0c 那么让后端大哥哥给你返回什么格式 但是有时候不尽人意 xff0c 后端大
  • 在vue3中Element Plus切换主题

    一共2种方法 目录 第一种 第二种 第一种 暗黑模式 xff0c 使用useDark xff0c 可以不用安装Element Plus xff0c 只切换页面的背景颜色 xff0c 不改变Element Plus控件的颜色 xff0c 本案
  • IIS发布.net6 api+微信小程序/H5真机调试接口的流程

    我们创建 net6 api程序 xff0c 然后使用SqlSugar连接MySQL数据库 xff0c 再使用iis发布 xff0c 当然使用其他的也行 再开发一个微信小程序 xff0c 手机运行小程序 xff0c 手机运行H5 xff0c
  • 全栈开发小作品展示(有声音)

    不积跬步 xff0c 无以至千里 xff1b 不积小流 xff0c 无以成江海 目录 1 客户端 2 网站 3 小程序 4 H5演示 1 客户端 PC桌面 xff0c WPF 43 prism框架 xff0c 前后端分离 xff0c 主要是
  • rust学习

    Installation The Rust Programming Language https doc rust lang org book ch01 01 installation html 一 安装 安装了几个组件 curl prot
  • visual studio 2017出现MSB8020,MSB8036等SDK版本选择的错误

    1 xff0c 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 MSB8020 无法找到 v140 的生成 xff1b 2 xff0c 严重性 代码 说明 项目 文件 行 禁止显示状态 错误 MSB8036 找不到 Windows
  • C++中循环include问题的讨论

    问题 C语言中未避免头文件的重复引用 xff0c 一般都会使用include guard 如pragma once或 ifndef等 xff0c 但这样做以后并不是万事大吉了 循环使用include可能会出现一些意想不到的错误 如果代码较为

随机推荐

  • flask+gevent+gunicorn+supervisor+nginx异步高并发部署

    背景 flask是一款同步阻塞框架 xff0c 在调用外部http服务时 xff0c 当前进程将阻塞 多进程模式下 xff0c 无法响应其他用户的请求 xff0c 本文则是研究的是如何利用gevent提升flask的并发能力 xff0c 以
  • 小白学SAS--自学笔记

    64 TOC 目录 xff09 第一章初识SAS 数据集的命名 数据导入 建立永久数据集 用菜单新建文件夹 xff0c 并与电脑上已有文件夹关联 用libname语句指定文件夹名 xff0c 并与电脑上已有文件夹关联 用data语句直接指定
  • http协议常用请求头与响应头

    请求头 xff1a Accept 用于告诉服务器 xff0c 客户机支持的数据类型 Accept Charset xff1a 用于告诉服务器 xff0c 客户机所采用的编码 Accept Encoding xff1a 用于告诉服务器 xff
  • mysqlId 不能自启的问题(错误代号2003)

    计算机服务里看下有没有mysql的服务 xff0c 如果有 xff0c 把服务的启动类型改为自动 xff1b 如果没有 xff0c 则要进入安装目录的bin文件夹双击mysqld exe启动mysql 然后 cmd到 Mysql的安装目录的
  • RabbitMQ学习笔记1-"Hello World!"simple模型

    simple模型是RabbitMQ队列模型中最简单的一个模型 如图 xff1a P 是我们的生产者 xff08 producer xff09 xff0c C 是我们的消费者 xff08 consumer xff09 中间的红色框是队列 xf
  • RabbitMQ学习笔记2-Work queues

    接下来学习第二种模型 xff0c Work queues模型 xff0c 如图所示 xff1a 该模型描述的是 xff1a 一个生产者 xff08 P xff09 向队列发送一个消息 xff0c 然后多个消费者 xff08 P xff09
  • RabbitMQ学习笔记3-Publish/Subscribe

    在这部分中 xff0c 我们会做一些完全不同的事情 我们会向多个消费者传递相同的信息 这种模式就是 发布 订阅 该模型中生产者从不将任何消息直接发送到队列 xff0c 生产者通常甚至不知道要将消息发送到哪个队列 xff0c 通常是 xff0
  • ssh -X 使用遇到的问题

    ssh可以在登录时通过 X选项开启远程服务的X服务 xff0c 这样服务器端的需要调用X服务的程序就能开启了 最简单的例子就是可以使用服务器段的gedit程序编译文件了 问题是这样的 xff1a 在IDL程序中有个write gif程序 x
  • Docker部署Gitlab和gitlab-runner,搭建一站式DevOps平台

    一 首次安装Gitlab并配置Gitlab runner CI CD Gitlab Docker 官方安装文档 xff1a https docs gitlab cn jh install docker html 设置Gitlab数据和配置挂
  • 打开 word 显示内存或磁盘空间不足 ,Word 无法显示所请求的字体

    打开word显示内存或磁盘空间不足 xff0c Word无法显示所请求的字体 使用360加速球优化一下 xff0c 恢复正常
  • 使用win10工具远程连接树莓派

    win10远程连接树莓派 1 使用ssh远程连接1 1系统烧录1 2 SSH登录 2 使用win10的mstsc工具远程连接2 1进入远程ssh后 xff0c 修改软件源 xff0c 否则太慢 xff08 清华软件源地址 https mir
  • 网络 - 笔记本无线转为有线

    工具 路由器一个 计算机两台C1和C2 场景说明 xff1a 将C1计算机的无线网络通过路由器转为有线网络 xff0c 并提供给C2计算机使用 第一步 xff1a 硬件搭建 连接C1和路由器 xff1a 将网线的A端插入路由器的WAN口 x
  • 医学案例统计分析与SAS应用--自学笔记

    目录 第二章医学研究设计与SAS实现科研设计思路样本含量估计实验设计 科研设计的sas实现完全随机设计随机区组设计析因设计关系型研究 第三章 统计描述与SAS分析统计描述及sas命令简介定量资料的统计描述分类资料的统计描述 第四章 定量资料
  • 计算机基础 - 左移、右移和计算逻辑

    左移 指的是位移动 xff0c 左移就是将数据位向左移动 xff0c 例如十进制10 二进制为0000 1010 左移4位后得到1010 0000 xff0c 转为十进制后为160 如果是左移5位 xff0c 那么超出部分被丢弃得到的就是0
  • C++ 二叉树实现词频分析

    通过二叉树存单词 xff0c 并且对总共的单词数量进行计数 xff0c 二叉树自适应的将出现频率高的单词往上移动以减少二叉树的搜索时间 代码如下 span class hljs comment genSplay h span span cl
  • C++ cout输出字符

    cout输出字符时 xff0c 可以使用单引号 xff1a cout lt lt span class hljs string 39 39 span lt lt endl span class hljs regexp span 输出分号 s
  • Linux 多进程多线程编程

    一 创建进程 1 进程号 进程号的类型是pid t xff08 typedef unsigned int pid t xff09 获得进程和父进程ID的API如下 xff1a include lt sys types h gt includ
  • dpdk探究1-理解dpdk的运行逻辑

    DPDK介绍 DPDK主要功能 xff1a 利用IA xff08 intel architecture xff09 多核处理器进行高性能数据包处理 Linux下传统的网络设备驱动包处理的动作可以概括如下 xff1a 数据包到达网卡设备网卡设
  • C++11多线程实现的一道面试题

    题目 xff1a 子线程循环 10 次 xff0c 接着主线程循环 100 次 xff0c 接着又回到子线程循环 10 次 xff0c 接着再回到主线程又循环 100 次 xff0c 如此循环50次 xff0c 试写出代码 这里涉及到的问题
  • 第四章 智能指针

    裸指针问题如下 xff1a 裸指针在声明中并未指出 xff0c 裸指针指涉到的是单个对象还是一个数组 裸指针在声明中也没有提示是不是要对其进行虚构 换言之 xff0c 无法得知指针是否拥有其指涉的对象 或者是否空悬指针的析构是不是拥有重载的