<C++> C++11右值引用

news2024/9/28 1:21:51

C++11右值引用

1.左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),**我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。**定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

void test() {
    // 以下的p、b、c、*p都是左值

    int *p = new int(0);
    int b = 1;
    const int c = 2;
    // 以下几个是对上面左值的左值引用
    int *&rp = p;
    int &rb = b;
    const int &rc = c;
    int &pvalue = *p;
}

什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量表达式返回值函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。

右值引用是C++11引入的一种引用类型,用于绑定到右值。它通过使用双引号&&来声明。右值引用可以将其绑定到一个右值,允许对其进行移动语义完美转发

void test() {
    double x = 1.1, y = 2.2;
    // 以下几个都是常见的右值
    10;
    x + y;
    fmin(x, y);
    // 以下几个都是对右值的右值引用
    int &&rr1 = 10;
    double &&rr2 = x + y;
    double &&rr3 = fmin(x, y);
    // 这里编译会报错:error C2106: “=”: 左操作数必须为左值
    10 = 1;  
    x + y = 1;
    fmin(x, y) = 1;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1去引用

void test() {
    double x = 1.1, y = 2.2;
    int &&rr1 = 10;
    const double &&rr2 = x + y;
    rr1 = 20;
    rr2 = 5.5;// 报错
    return 0;
}

const左值引用右值

在 C++ 中,const 左值引用可以绑定到右值,但是需要一些特定的条件。在 C++11 引入了右值引用和移动语义之后,可以将右值绑定到左值引用的 const 版本上。

当将右值绑定到 const 左值引用时,编译器会创建一个临时对象,该对象是右值的副本,并且该临时对象的生命周期与左值引用的生命周期相同。这样,通过 const 左值引用,可以安全地访问右值的内容。

void test() {
    int x = 10;
    cout << x << endl;   // 正确,x 是左值
    cout << 20 << endl;  // 正确,20 是右值
    const int &ref = x;  // 正确,将左值绑定到 const 左值引用
    const int &temp = 30;// 正确,将右值绑定到 const 左值引用,创建一个临时对象
}

const 左值引用可以绑定到右值,但是会创建一个临时对象来容纳右值的副本。这种绑定方式可以安全地访问右值的内容,并且在某些情况下可以提供更高的灵活性和代码复用性。

2.左值引用和右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
void test() {
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int &ra1 = a; // ra为a的别名
    int &ra2 = 10;// 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    // 编译器会生成一个临时的常量对象,并将const左值引用绑定到该临时对象上。
    const int &ra3 = 10;
    const int &ra4 = a;
}

const左值引用右值,编译器会生成一个临时的常量对象,并将const左值引用绑定到该临时对象上。

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以引用move之后的左值。
void test() {
    // 右值引用只能右值,不能引用左值。
    int &&r1 = 10;

    // message : 无法将左值绑定到右值引用
    int a = 10;
    int &&r2 = a;   // error C2440: “初始化”: 无法从“int”转换为“int &&”  
    // 右值引用可以引用move以后的左值
    int &&r3 = std::move(a);
}

3.move

std::move 是 C++ 标准库中的一个函数模板,位于 <utility> 头文件中。它用于将对象的所有权从一个对象转移到另一个对象,通常用于实现移动语义和避免不必要的对象拷贝操作。

std::move 的函数原型如下:

template<class T>
constexpr typename std::remove_reference<T>::type&& move(T&& t) noexcept;

该函数接受一个对象 t,并将其转换为右值引用,返回一个指向转换后的右值引用的对象。右值引用表示对象的所有权可以被移动或转移,而不是进行拷贝。std::move 本质上是将左值强制转换为右值引用,从而告诉编译器该对象可以被移动而非拷贝。

使用 std::move 的主要用途是在实现移动语义时,将对象的资源转移给其他对象,以提高效率。移动语义允许在对象所有权的转移过程中,将资源(如动态分配的内存或打开的文件句柄)从一个对象转移到另一个对象,而不进行不必要的拷贝操作。

下面是一个简单的示例,演示了 std::move 的用法:

#include <iostream>
#include <utility>

class MyClass {
public:
    MyClass() {
        std::cout << "无构造" << std::endl;
    }

    MyClass(const MyClass& other) {
        std::cout << "拷贝构造" << std::endl;
    }

    MyClass(MyClass&& other) noexcept {
        std::cout << "移动构造" << std::endl;
    }
};

int main() {
    MyClass obj1;
    MyClass obj2 = std::move(obj1);  // 调用移动构造函数
    return 0;
}

在上面的示例中,obj1obj2 都是 MyClass 类型的对象。通过调用 std::move(obj1),我们将 obj1 的所有权转移给了 obj2,因此在转移过程中会调用移动构造函数。这样做可以避免调用拷贝构造函数,提高了程序的效率。

需要注意的是,使用 std::move 之后,原对象的状态是不确定的,它可能处于有效状态、空状态或不可用状态。因此,在使用 std::move 之后,对原对象的操作应该谨慎,通常应该避免使用原对象。

std::move 是 C++ 中用于转移对象所有权的函数模板。它将左值转换为右值引用,从而告诉编译器该对象可以进行移动操作。通过使用 std::move,可以实现移动语义,避免不必要的对象拷贝,提高程序的效率。

4.右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

下面是string类的实现

namespace phw {
    class string {
    public:
        typedef char *iterator;
        iterator begin() {
            return _str;
        }
        iterator end() {
            return _str + _size;
        }
        string(const char *str = "")
            : _size(strlen(str)), _capacity(_size) {
            //cout << "string(char* str)" << endl;
            _str = new char[_capacity + 1];
            strcpy(_str, str);
        }
        // s1.swap(s2)
        void swap(string &s) {
            ::swap(_str, s._str);
            ::swap(_size, s._size);
            ::swap(_capacity, s._capacity);
        }
        // 拷贝构造
        string(const string &s)
            : _str(nullptr) {
            cout << "string(const string& s) -- 深拷贝" << endl;
            string tmp(s._str);
            swap(tmp);
        }
        // 赋值重载
        string &operator=(const string &s) {
            cout << "string& operator=(string s) -- 深拷贝" << endl;
            string tmp(s);
            swap(tmp);
            return *this;
        }
        // 移动构造
        string(string &&s)
            : _str(nullptr), _size(0), _capacity(0) {
            cout << "string(string&& s) -- 移动语义" << endl;
            swap(s);
        }
        // 移动赋值
        string &operator=(string &&s) {
            cout << "string& operator=(string&& s) -- 移动语义" << endl;
            swap(s);
            return *this;
        }
        ~string() {
            delete[] _str;
            _str = nullptr;
        }
        char &operator[](size_t pos) {
            assert(pos < _size);
            return _str[pos];
        }
        void reserve(size_t n) {
            if (n > _capacity) {
                char *tmp = new char[n + 1];
                strcpy(tmp, _str);
                delete[] _str;
                _str = tmp;
                _capacity = n;
            }
        }
        void push_back(char ch) {
            if (_size >= _capacity) {
                size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
                reserve(newcapacity);
            }
            _str[_size] = ch;
            ++_size;
            _str[_size] = '\0';
        }
        //string operator+=(char ch)
        string &operator+=(char ch) {
            push_back(ch);
            return *this;
        }
        const char *c_str() const {
            return _str;
        }

    private:
        char *_str;
        size_t _size;
        size_t _capacity;// 不包含最后做标识的\0
    };
}// namespace phw

左值引用的使用场景

做参数和做返回值都可以提高效率。

void func1(phw::string s)
{}

void func2(const phw::string& s)
{}

int main()
{
	phw::string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);   //深拷贝
	func2(s1);   //没有深拷贝
    //下面是string类中+=的定义,一个是传值,一个是传引用
	//string operator+=(char ch);  //传值返回存在深拷贝
	//string& operator+=(char ch);  //传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

左值引用的短板

当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回, 只能传值返回。例如:string to_string(int value)函数中可以看到,这里只能使用传值返回, 传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

string to_string(int value) {
    bool flag = true;
    if (value < 0) {
        flag = false;
        value = 0 - value;
    }
    string str;
    while (value > 0) {
        int x = value % 10;
        value /= 10;
        str += ('0' + x);
    }
    if (flag == false) {
        str += '-';
    }

    std::reverse(str.begin(), str.end());
    return str;
}
// str是一个局部对象,只能传值返回,因为局部对象出了函数外就销毁了,不能引用

这个函数的返回值是一个右值。在C++中,当一个函数返回一个临时对象时,该对象被视为右值。

对于该函数的返回值,它是一个临时创建的string对象,它的生命周期仅限于函数返回后的瞬间。由于这个临时对象没有持久性,编译器可以对其进行优化,例如通过将返回值直接移动(move)到调用者的位置,而不是执行复制操作。

根据C++标准库的规范,标准函数std::reverse不会改变迭代器的有效性,所以在调用std::reverse后,str仍然是一个有效的右值。因此,编译器可能会继续优化,例如对str进行移动操作,而不是执行复制操作。

int main()
{
   	string ret1 = to_string(1234);
	string ret2 = to_string(-1234);
 	return 0;
}

在这里插入图片描述
在这里插入图片描述

string to_string(int value)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。使用移动构造可以解决多次拷贝构造的问题。

5.移动构造和移动语义

移动构造(move constructor)是一种特殊的构造函数,用于从一个对象移动(或者说窃取)资源而不是复制资源。移动构造函数通常采用右值引用作为参数,并将资源从传入的对象转移到正在构造的对象中。移动构造函数可以通过使用移动语义来提高性能,因为它可以避免昂贵的复制操作。

移动语义(move semantics)是一种语言特性,允许在适当的情况下将资源从一个对象移动到另一个对象,而不是进行复制操作。移动语义的实现依赖于移动构造函数和移动赋值运算符。

在string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

// 拷贝构造
string(const string& s)
	: _str(nullptr) {
	cout << "string(const string& s) -- 深拷贝" << endl;
	string tmp(s._str);
	swap(tmp);
}

// 移动构造
string(string &&s)
    : _str(nullptr), _size(0), _capacity(0) {
    cout << "string(string&& s) -- 移动语义" << endl;
    swap(s);
}


int main() {
    string ret2 = to_string(-1234);
    return 0;
}

再运行上面to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。

to_string的返回值是一个右值,用这个右值构造ret2,如果既有拷贝构造又有移动构造,调用会匹配调用移动构造,因为编译器会选择最匹配的参数调用。那么这里就是一个移动语义。

6.移动赋值

在string类中增加移动赋值函数,再去调用to_string(1234),不过这次是将 to_string(1234)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

// 移动赋值
string &operator=(string &&s) {
    cout << "string& operator=(string&& s) -- 移动语义" << endl;
    swap(s);
    return *this;
}

int main() {
    string ret1;
    ret1 = to_string(1234);
    return 0;
}
// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后,我们看到调用了一次移动构造和一次移动赋值。因为如果是用一个已经存在的对象接收,编译器就没办法优化了。to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为to_string函数调用的返回值赋值给ret1,这里调用的移动赋值。

在C++11中STL中的容器都是增加了移动构造和移动赋值

7.右值引用引用左值及其一些更深入的使用场景分析

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件 <utility> 中,该函数名字具有迷惑性, 它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

左值引用没解决的问题:

  • 局部对象返回问题(局部对象出了作用域被销毁)
  • 插入接口,对象拷贝问题(传参,会发生拷贝)

下面看个例子:

template<class T>
T func(){
    T ret;

    //...
    return ret;
}

T x  = Func();

T是一个自定义类型:

1、如果T是浅拷贝的类,这里就是拷贝构造,因为对于浅拷贝的类,移动构造是没什么意义的。

2、如是T是深拷贝的类,这里就是移动构造,对于深拷贝,移动构造可以转移右值的资源,没有拷贝

int main() {
    string s1("hello world");
    // 这里s1是左值,调用的是拷贝构造
    string s2(s1);
    // 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
    // 但是这里要注意,一般是不要这样用的,因为我们会发现s1的资源被转移给了s3,s1被置空了。
    string s3(std::move(s1));
    return 0;
}

在这里插入图片描述

STL容器插入接口函数也增加了右值引用版本:

void push_back(value_type &&val);
int main() {
    list<string> lt;
    string s1("1111");
    // 这里调用的是拷贝构造
    lt.push_back(s1);
    // 下面调用都是移动构造
    lt.push_back("2222");
    lt.push_back(std::move(s1));
    return 0;
}
运行结果:
// string(const string& s) -- 深拷贝
// string(string&& s) -- 移动语义
// string(string&& s) -- 移动语义
void Fun(int &x) { cout << "左值引用" << endl; }
void Fun(const int &x) { cout << "const 左值引用" << endl; }

在这里插入图片描述

8.完美转发

完美转发(perfect forwarding)是一种技术,允许函数模板将其参数传递给其他函数,并保留原始参数的值类别(左值或者右值)。这是在C++11中引入的新功能,通过引入两个新的引用限定符&&来实现。

完美转发的主要目的是解决函数模板中参数传递的问题。通常情况下,当我们将一个参数传递给函数模板的另一个函数时,参数的值类别会发生改变,比如一个右值可能会被转换为左值。这可能会导致一些问题,特别是在涉及重载和模板的情况下。

通过使用完美转发,我们可以确保参数的值类别保持不变。下面是一个示例代码,展示了如何使用完美转发:

#include <utility>

// 接受任意参数类型的函数模板
template <typename T>
void forwardFunction(T&& arg){
    otherFunction(std::forward<T>(arg)); // 完美转发参数
}

// 接受一个左值引用的函数
void otherFunction(int& arg){
    // 处理左值引用
}

// 接受一个右值引用的函数
void otherFunction(int&& arg){
    // 处理右值引用
}

void otherFunction(const int& arg){
    // 处理const左值引用
}

void otherFunction(const int&& arg){
    // 处理const右值引用
}

int main(){
    int value = 42;
    const int value2 = 10;
    forwardFunction(value);        // 传递左值
    forwardFunction(123);          // 传递右值
    forwardFunction(value2);       // 传递const左值
    forwardFunction(std::move(value2));   //传递const右值
    
    return 0;
}

在上面的示例中,forwardFunction是一个接受任意类型参数的函数模板。通过使用T&&作为参数类型,我们实现了完美转发。然后,通过std::forward<T>(arg)来传递参数给otherFunction,确保参数的值类别保持不变。

otherFunction有四个重载版本,分别接受左值引用和右值引用以及对应的const版本。在forwardFunction中,我们可以传递左值value和右值123,也可以传递const左值,它们分别将被正确地转发到相应的otherFunction

  • 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
  • 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
  • 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
  • 我们希望能够在传递过程中保持它的左值或者右值的属性

std::forward 完美转发在传参的过程中保留对象原生类型属性

void Fun(int &x) { cout << "左值引用" << endl; }
void Fun(const int &x) { cout << "const 左值引用" << endl; }
void Fun(int &&x) { cout << "右值引用" << endl; }
void Fun(const int &&x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T &&t) {
    Fun(std::forward<T>(t));
}

int main() {
    PerfectForward(10);// 右值
    int a;
    PerfectForward(a);           // 左值
    PerfectForward(std::move(a));// 右值
    const int b = 8;
    PerfectForward(b);           // const 左值
    PerfectForward(std::move(b));// const 右值
    return 0;
}

完美转发实际中的使用场景

template<class T>
struct ListNode {
    ListNode *_next = nullptr;
    ListNode *_prev = nullptr;
    T _data;
};

template<class T>
class List {
    typedef ListNode<T> Node;

public:
    List() {
        _head = new Node;
        _head->_next = _head;
        _head->_prev = _head;
    }
    
    void PushBack(T &&x) {
        //Insert(_head, x);
        Insert(_head, std::forward<T>(x));
    }
    
    void PushFront(T &&x) {
        //Insert(_head->_next, x);
        Insert(_head->_next, std::forward<T>(x));
    }
    
    void Insert(Node *pos, T &&x) {
        Node *prev = pos->_prev;
        Node *newnode = new Node;
        newnode->_data = std::forward<T>(x);// 关键位置
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }
    
    void Insert(Node *pos, const T &x) {
        Node *prev = pos->_prev;
        Node *newnode = new Node;
        newnode->_data = x;// 关键位置
        // prev newnode pos
        prev->_next = newnode;
        newnode->_prev = prev;
        newnode->_next = pos;
        pos->_prev = newnode;
    }

private:
    Node *_head;
};

int main() {
    List<bit::string> lt;
    lt.PushBack("1111");
    lt.PushFront("2222");
    return 0;
}

完美转发被用于将参数从PushBackPushFront函数传递到Insert函数中。

在List类的定义中,PushBackPushFront成员函数都接受右值引用参数T&& x。当调用PushBack("1111")PushFront("2222")时,字符串字面值被转换为右值引用。

然后,在PushBackPushFront函数中,Insert函数被调用,参数x被通过std::forward<T>(x)完美转发到Insert函数中。这里使用std::forward来确保参数的值类别(左值或者右值)保持不变。

Insert函数中,x被再次通过std::forward<T>(x)完美转发给新创建的Node对象的_data成员。这样做可以避免不必要的复制或移动,并保持原始参数的值类别。

通过使用完美转发,程序可以正确地将参数传递给Insert函数,并保持参数的值类别不变,从而避免不必要的拷贝和移动操作。这种方式提高了代码的效率,并允许程序处理不同类型的参数(左值或者右值)。

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

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

相关文章

sonar-scanner-Windows本地Python代码检查使用方法【免费下载sonar-scanner验证有效】

背景介绍&#xff1a; sonar作为开源的代码扫描工具&#xff0c;sonar-scanner是windows扫描器。SonarQube是一个开源的代码质量管理平台&#xff0c;可以将 sonar-scanner扫描的结果进行分析。 公司有搭建SonarQube质量管理平台&#xff0c;支持本地扫描和gitlab集成扫描。现…

locust学习教程(7) - docker运行单个locust脚本

目录 1、安装 docker 2、下载镜像 3、运行脚本 4、开始压测 &#x1f381;更多干货 1、安装 docker widnows安装docker客户端blog.csdn.net/weixin_4545… 实施步骤&#xff1a; 第一步、启动docker客户端 2、下载镜像 cmd窗口下载locust镜像文件&#xff1a;docker pul…

隐私链或成监管和虚拟货币犯罪打击新挑战?

匿名币、混币器等是大家在当前案件侦办中常遇到的资金追踪“拦路虎”&#xff0c;而在区块链中还有一些隐私保护方案&#xff08;隐藏交易相关信息&#xff09;&#xff0c;可能大家较少涉猎&#xff0c;在当前的区块链相关案件中也还没有明显的表现&#xff0c;我们也希望通过…

深度解析:分布式事务解决方案大盘点,助你轻松应对复杂业务场景

随着互联网的发展&#xff0c;分布式系统已经成为了现代软件开发的主流。在分布式系统中&#xff0c;多个节点之间需要协同工作&#xff0c;以完成一些复杂的任务。然而&#xff0c;由于节点之间的网络延迟、故障等问题&#xff0c;这些节点之间可能会出现数据不一致的情况&…

华为OD机试真题 JavaScript 实现【最多获得的短信条数】【2023Q1 100分】,附详细解题思路

一、题目描述 某云短信厂商&#xff0c;为庆祝国庆&#xff0c;推出充值优惠活动。现在给出客户预算&#xff0c;和优惠售价序列&#xff0c;求最多可获得的短信总条数。 二、输入描述 第一行客户预算M&#xff0c;其中 0 ≤ M ≤ 10^6第二行给出售价表&#xff0c; P1, P2,…

一切美好如夏而至,中国人民大学与加拿大女王大学金融硕士项目陪你逐梦硕士

流光半夏&#xff0c;美好日长。愿所有春天里的酝酿&#xff0c;都在夏天热烈绽放。你春天酝酿的读研梦有实现吗&#xff1f;在这个最长的白昼&#xff0c;让我们与中国人民大学与加拿大女王大学金融硕士项目邂逅&#xff0c;一起在盛夏里追寻诗与远方。 都说有梦想&#xff0…

【07】STM32·HAL库开发-新建寄存器版本MDK工程 |下载STM32Cube固件包 | 新建MDK工程步骤

目录 1.新建工程前的准备工作&#xff08;了解&#xff09;1.1下载相关STM32Cube 官方固件包&#xff08;F1/F4/F7/H7) 2.新建寄存器版本MDK工程步骤&#xff08;熟悉&#xff09;2.1新建工程文件夹2.1.1Drivers文件夹2.1.2Middlewares文件夹2.1.3Output文件夹2.1.4Projects文件…

零基础入门网络安全,收藏这篇不迷茫【2022 最新】

前言 最近收到不少关注朋友的私信和留言&#xff0c;大多数都是零基础小友入门网络安全&#xff0c;需要相关资源学习。其实看过的铁粉都知道&#xff0c;之前的文里是有过推荐过的。新来的小友可能不太清楚&#xff0c;这里就系统地叙述一遍。 01.简单了解一下网络安全 说白…

ASEMI代理光宝光耦LTV-61L的工作原理与应用探析

编辑-Z 本文将对光耦LTV-61L进行深入的探讨&#xff0c;主要从其工作原理、应用领域、使用注意事项以及市场前景四个方面进行详细的阐述。光耦LTV-61L是一种常用的光电器件&#xff0c;其工作原理简单&#xff0c;应用领域广泛&#xff0c;但在使用过程中也需要注意一些问题。…

1分钟!免费将AI图像创作能力接入办公系统

随着人工智能技术的日新月异&#xff0c;各行各业都在尝试将AI技术融入到自己的生产和服务中以提升效率和用户体验&#xff0c;绘画领域也在迎来一轮新的生产方式革新。在数字绘画领域&#xff0c;AI绘图软件的出现&#xff0c;为数字绘画领域注入了新的活力。 但我们在使用AI绘…

【正点原子STM32连载】第三十八章 CAN通讯实验 摘自【正点原子】STM32F103 战舰开发指南V1.2

1&#xff09;实验平台&#xff1a;正点原子stm32f103战舰开发板V4 2&#xff09;平台购买地址&#xff1a;https://detail.tmall.com/item.htm?id609294757420 3&#xff09;全套实验源码手册视频下载地址&#xff1a; http://www.openedv.com/thread-340252-1-1.html# 第三…

VXLAN 主机VTEP(OVN)

EVE环境模拟搭建一个基于主机VTEP的VXLAN数据中心网络。 实验里vtep是在linux主机上&#xff0c;同时linux主机还得有路由功能使VTEP的端点IP可达&#xff0c;所以两台linux服务器需要安装FRR。 数据转发平面使用VXLAN封装&#xff1b;在控制平面我打算选择使用EVPN和OVN两种不…

Golang每日一练(leetDay0101) 最长递增子序列I\II\个数

目录 300. 最长递增子序列 Longest Increasing Subsequence &#x1f31f;&#x1f31f; 2407. 最长递增子序列 II Longest Increasing Subsequence ii &#x1f31f;&#x1f31f;&#x1f31f; 673. 最长递增子序列的个数 Number of Longest Increasing Subsequence &a…

YOLOv5/v7 添加注意力机制,30多种模块分析⑥,S2-MLPv2模块,NAM模块

目录 一、注意力机制介绍1、什么是注意力机制&#xff1f;2、注意力机制的分类3、注意力机制的核心 二、S2-MLPv2模块1、 S2-MLPv2模块的原理2、实验结果3、应用示例 三、NAM模块1、NAM 的原理2、实验结果3、应用示例 大家好&#xff0c;我是哪吒。 &#x1f3c6;本文收录于&a…

【Duilib】资源打包入EXE

环境 VS版本&#xff1a;VS2013 概述 资源打包成ZIP&#xff0c;ZIP文件放置EXE内部。 步骤 1、按上一篇建好工程。 2、RC文件添加ZIP资源。 这一步比较复杂&#xff0c;工程 添加资源&#xff0c;弹窗如下右侧对话框后&#xff0c;按①②③④步骤&#xff0c;找到theme.z…

Springboot项目使用原生Websocket

目录 1.启用Websocket功能2.封装操作websocket session的工具3.保存websocket session的接口4.保存websocket session的类5.定义websocket 端点6.创建定时任务 ping websocket 客户端 1.启用Websocket功能 package com.xxx.robot.config;import org.springframework.context.a…

机器学习实践(1.2)XGBoost回归任务

前言 XGBoost属于Boosting集成学习模型&#xff0c;由华盛顿大学陈天齐博士提出&#xff0c;因在机器学习挑战赛中大放异彩而被业界所熟知。相比越来越流行的深度神经网络&#xff0c;XGBoost能更好的处理表格数据&#xff0c;并具有更强的可解释性&#xff0c;还具有易于调参…

Axure教程—树

本文将教大家如何用AXURE中的动态面板制作树 一、效果 预览地址&#xff1a;https://1rmtjd.axshare.com 二、功能 1、点击“”&#xff0c;展开子节点 2、点击“-”子节点折叠 三、制作 1、父节点制作 拖入一个动态面板&#xff0c;进入&#xff0c;如图&#xff1a; 拖入一…

【LeetCode】HOT 100(18)

题单介绍&#xff1a; 精选 100 道力扣&#xff08;LeetCode&#xff09;上最热门的题目&#xff0c;适合初识算法与数据结构的新手和想要在短时间内高效提升的人&#xff0c;熟练掌握这 100 道题&#xff0c;你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

【玩转Docker小鲸鱼叭】Docker容器常用命令大全

在 Docker 核心概念理解 一文中&#xff0c;我们知道 Docker容器 其实就是一个轻量级的沙盒&#xff0c;应用运行在不同的容器中从而实现隔离效果。容器的创建和运行是以镜像为基础的&#xff0c;容器可以被创建、销毁、启动和停止等。本文将介绍下容器的这些常用操作命令。 1、…