C++11:右值引用

C++11:右值引用

    • 右值与左值
    • 右值引用语法
    • 右值引用底层
    • 移动语义
    • 引用折叠
    • 完美转发


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

右值与左值

在讲解右值引用之前,我们就需要先辨析一下左值右值的区别。

左值

左值是一个表示数据的表达式,我们可以获取它的地址并且对其赋值,左值可以出现在赋值操作符=的左边,但是右值不能。

比如以下代码中:

int i = 0;
int* p = &i;
double d = 3.14;

变量ipd都是左值,一方面来说,它们出现在了=的左边,另一方面来说,我们可以对其取地址,并修改它的值。

当然,我们也有const变量:

const int ci = 0;
int const* cp = &i;
const double cd = 3.14;

变量cicpcd都是左值,它们出现在了=的左边,我们可以对其取地址。但是由于具有const属性,我们不能修改它。

因此, 左值最显著的特征是可以取地址,但是不一定可以被修改

右值

右值也是一个表达数据的表达式,比如字面常量表达式返回值函数返回值等等,右值可以出现在赋值操作符=的右边,但是不能出现在=的左边,右值不能取地址

比如以下代码中:

double func()
{return 3.14;
}int x = 10;
int y = 20;
int z = x + y;
double d = func();

以上代码中,1020x + yfunc()都是右值,它们出现在=的右边。1020对应了字面常量x + y对应了表达式返回值func()对应函数返回值。这些都是右值,它们最显著的特点就是无法取地址

简单辨析了什么是左值,什么是右值,现在我们知道左值与右值的最大区别在于可不可以取地址,接下来我们就要讲解右值引用这个语法了。


右值引用语法

先回顾一下左值引用的语法:

int i = 0;
int* p = nullptr;int& ri = i;
int*& rp = p;

左值引用只需要在原本变量的类型后面,加一个&,就是一个左值引用的类型。左值引用后,新的变量相当于原先变量的别名,我们可以传引用传参,传引用返回等操作,来减少拷贝。

但是我们不能左值引用一个右值,比如这样:

int& ri = 0;
int*& rp = nullptr;
double& rd = 3.14;

以上代码中,=右侧都是字面常量,也就都是右值,而我们变量rirppd都是左值引用。我们不能拿左值引用来引用右值

左值引用的语法是:type&;右值引用的语法是:type&&

接下来我们尝试对刚刚的值进行右值引用:

int&& ri = 0;
int*&& rp = nullptr;
double&& rd = 3.14;

这样我们就完成了对右值的引用。

现在我们有了右值引用语法,那么再来考虑两个问题:

  1. 左值引用可以引用右值吗?
  2. 右值引用可以引用左值吗?

也许你会感到疑问,我刚刚已经证明过了无法直接通过左值引用引用右值,为什么我还要提出这个问题。这是因为刚刚的测试不全面,没有考虑特殊情况。

  1. 左值引用不能直接引用右值
  2. const左值引用 可以引用右值

在刚刚的测试用例中,我们尝试用左值引用直接引用右值:

int& i = 5; // 非法

这是不允许的,不然就没必要再推出右值引用语法了。

但是如果我们以const引用的形式,那么就可以引用右值:

const int& i = 5; // 合法

一个常量具有常性,也就是不能修改,如果我们直接把一个常量交给引用,那么我们就可能通过引用来修改这个常量,这就违背了常性。因此不能直接引用一个右值常量,但是当我们使用const引用,那么就可以引用了。

  1. 右值引用不能直接引用左值
  2. 右值引用可以引用move后的左值

右值引用不能直接引用左值:

int i = 5;
int&& rri = i; // 非法

但是C++11后,提供了一个函数move,其可以把一个左值强制转化为一个右值

就像这样:

int i = 5;
int&& rri = move(i); // 合法

要注意的是:  move并不会改变参数本身的左值属性,这一点可以参考强制类型转化:

double d = 3.14;
int i = (int)d;

在以上代码中,(int)d这个强制转化过程,并没有改变d是一个double类型的数据,只是在这个表达式中,(int)d返回了一个int类型的d

同理move(i)之后,i依然是左值,但是move(i)这个表达式返回了一个右值的i


右值引用底层

既然存在右值引用这个语法,那么我们来看看右值引用到底干了些啥。

右值引用的工作主要有两种情况,一种是右值引用了常量,另外一种是右值引用了move后的左值

右值引用了常量

当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据

看到一段代码:

int&& r = 5;
r = 10;

以上过程中,我们先用r右值引用了常量5,然后通过右值引用把5改为了10
这个过程中,右值常量5存储在常量区,r右值引用后如果r指向常量区的5,会发生什么?此时我们的r = 10操作,就相当于把常量区的5修改为了10,从此以后整个程序中只要去常量区拷贝5都会变成拷贝10,这可就完蛋了。因此我们的右值引用常量,绝对不能直接引用常量区的数据!!

因此,右值引用常量时的真实操作是把常量区的数据拷贝到栈区中,然后这个引用指向这一块栈区内存。

别忘了,我们的const左值引用也可以引用常量,那么这个引用又是如何进行的:

const int& r = 5;

你可能会想,反正const左值引用都不会修改数据,就算让r真的指向常量区的5也没啥问题。但是其实const左值引用常量时和右值引用是一样的,都是先把数据拷贝到栈区,再进行引用

总结如下:

  1. 当右值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,该数据可以修改
  2. const左值引用了常量,引用会把常量区中的数据拷贝一份到栈区,然后该引用指向栈区中拷贝后的数据,但是该数据是常量,不能修改

右值引用了move后的左值

  1. 当右值引用了move后的左值,右值引用直接指向该左值

看到以下代码:

int i = 5;
int&& rri = move(i);rri = 10;cout << i << endl;
cout << rri << endl;

程序输出结果为:

10
10

也就是说,我们可以通过修改右值引用来修改左值,或者说以通俗点的说法,此时右值引用就是这个左值的别名。

这一幕好像似曾相识,是不是左值引用也可以做到这个事情,甚至是一模一样的事情?

确实是这样的,当右值引用了move后的左值,其实和直接左值引用这个左值没有任何区别。那么为什么我们还需要右值引用?

为了搞清右值引用存在的意义,我们先来看看左值引用出现后,解决了那些问题,又没解决哪些问题:

  1. 左值引用解决了传参时存在的拷贝问题
string add_string(string& s1, string& s2)
{string s = s1 + s2;return s;
}int main()
{string str;string hello = "Hello";string world = "world";str = add_string(hello, world);return 0;
}

以上代码中,add_string函数需要接收两个string类型的参数,此时我们使用传引用传参,就可以避免两个string的拷贝消耗。

  1. 左值引用解决了一部分返回值的拷贝问题
string& say_hello()
{static string s = "hello world";return s;
}int main()
{string str1;string str2;str1 = say_hello();return 0;
}

以上代码中,函数say_hello生成了一个string,并把它返回给外部,如果我们直接返回,那么str1接收参数时,就会先拷贝构造出一个临时变量,然后临时变量再拷贝构造str1。这个过程发生了两次拷贝构造。但是返回值s指向的string是全局的,其出了函数依然存在,因此我们传引用返回,可以不用拷贝构造一个临时变量,直接拿返回值s去拷贝构造,节省了一次拷贝构造。

也就是说,左值引用通过传引用传参传引用返回节省了拷贝。

但是我们再看到以下情况:

string say_hello()
{string s = "hello world";return s;
}int main()
{string str;str = say_hello();return 0;
}

以上代码中,say_hello依然返回hello world这个字符串,但是s是一个局部变量,因为出了函数就会被销毁,如果str想要接收到s,那么就会先拷贝构造一个临时变量,然后临时变量再拷贝构造出str

但我们已经通过s创建好了一个字符串,我们为了得到一个字符串hello world,中间经过了这么多次拷贝。就因为这是一个局部变量,s不能出作用域。我们有没有办法直接把局部变量创建好的hello world移交给作用域外部的str,免去临时变量的拷贝构造

因此,右值引用应运而生。

我们先前说过,右值引用当引用一个被move左值的时候,其本质和左值引用没有区别。右值引用,其实更多的是一种标记

我们先来看看什么情况下会产生可以被右值引用的左值:

  1. 当一个左值被move后,可以被右值引用
  2. C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为将亡值 

回顾我们刚刚的情况:函数内部的局部变量s已经创建好了字符串hello world,但是s马上就要出函数作用域销毁了,于是把hello world拷贝一份给外部临时变量,s被销毁后,临时变量再拿拷贝到的hello world去拷贝构造str

这个过程中,变量s已经快要离开作用域了,马上就要被销毁,s被销毁没有问题,但是s内部的hello world是我们需要的。这种情况可以理解为:一个富翁快要死亡了,于是他在死前立遗嘱,把自己的金钱继承给谁。

同理,一旦左值得到了右值属性,相当于立好了遗嘱,不希望自己的资源被系统释放,而是被合适的对象继承走。

由于C++会把即将离开作用域的非引用类型的返回值当成右值,这种类型的右值也称为将亡值 s即将被销毁,此时s就是一个右值了,右值的意思就是:这个变量的资源可以被迁移走。这句话非常非常重要!!!

右值的意思就是:这个变量的资源可以被迁移走

我们再看到另外一种情况:

  1. 当一个左值被move后,可以被右值引用

C++之所以要给出一个move属性,是因为有一些变量,其生命周期还很长,C++不敢擅自把这个变量的资源迁移走。但是一旦程序员把这个变量move了,就得到了一个有右值属性的左值,此时相当于程序员亲自许可把这个变量的资源迁移走。

那么右值是如何把资源迁移走的呢?这就涉及到右值引用的移动语义了:


移动语义

为了讲解移动语义,我先写一个简单的mystring类:

class mystring
{
public://构造函数mystring(const char* str = ""){_str = new char[strlen(str) + 1];strcpy(_str, str);}//析构函数~mystring(){delete[] _str;}// 赋值重载mystring& operator=(const mystring& s){cout << "赋值重载" << endl;return *this;}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}private:char* _str = nullptr;
};

这个mystring类中,我没有具体实现每一个接口,因为移动语义中,更重要的是函数的调用关系,而不是函数的具体实现。在mystring类中,有一个成员_str,类型为char*指针,指向一块空间,内部存储了字符串的字符。

现在我们有如下过程:

mystring get_string()
{mystring str("hello");return str;
}int main()
{mystring s2 = get_string();return 0;
}

s2通过函数get_string来获得字符串,并构造自己。这个过程中,由于str是局部变量,会发生拷贝构造临时变量,临时变量再拷贝构造s2的过程。但是由于str是一个将亡值,具有右值属性,我们可以写一个函数直接把它的资源转移走

class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}
};

这个移动构造函数的参数是一个mystring&&类型,也就是一个右值引用。函数主体部分,通过一个swap函数把参数s_str指针成员与自己的_str成员进行交换。由于指针指向字符串数组,此时相当于把s的字符串数组交换给自己,这样就完成了对右值引用的数据转移

除了移动构造,我们还有原先的拷贝构造:

class mystring
{
public:// 移动构造mystring(mystring&& s){cout << "移动构造" << endl;std::swap(_str, s._str);}// 拷贝构造mystring(const mystring& s){cout << "拷贝构造" << endl;}
};

那么为什么get_string要去调用移动构造而不是调用拷贝构造呢?

  • 因为get_string的返回值是一个mystringstr,但是由于str要出作用域了,被判断为将亡值,因此str具有右值属性。那么str在出生命周期,构造临时变量的时候,就会去调用临时变量构造函数,由于str是构造函数的参数,具有右值属性,而不是左值属性,因此调用的是mystring&&的移动构造,而不是调用const mystring&的拷贝构造
  • 而我们的临时变量的生命周期,只在get_string这一行,马上就要被销毁了,因此临时变量也是一个将亡值,具有右值属性。当拿临时变量构造s2的时候,又会调用一次移动构造

流程如下:

get_string返回值str = =移动构造 = => 临时变量
临时变量 = =移动构造= => s2

可以看到,原先是进行两次拷贝构造,如果我们字符串有一亿个字符,那么总共要拷贝两亿个字符
但是移动构造出现后,我们只需要进行两次移动构造,一次移动构造只交换一个指针,共交换两个指针

现在可以看出,右值引用带来的移动构造有多么强悍。

虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的移动构造之所以这么叫,就是因为移走了别人的资源这部分资源之所以会被移走,就是因为它有右值属性而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走

就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义

移动:改语法实现了通过移走别人的资源,实现高效的创建对象,避免大量拷贝
语义:在这个过程中,右值引用只提供语义层面的功能,即许可一个对象资源被转移的右值语义

因为右值引用的出现,C++11后,类的默认成员函数从6个变成了8个。新增两个成员函数:移动构造移动赋值重载

比如我刚刚的mystring类的移动构造移动赋值

//移动赋值重载
mystring& operator=(mystring&& s)
{std::swap(_str, s._str);return *this;
}// 移动构造
mystring(mystring&& s)
{std::swap(_str, s._str);
}

它们的特点是:参数为右值引用,函数体内部通过交换别人的指针到自己手上,实现高效的资源转移

当然,STL库内部的所有容器,也都更新了移动构造移动赋值重载

这是C++11的vector构造函数:
在这里插入图片描述

这里多出来了一个move系列的构造函数,参数类型为vector&&右值引用,这就是vector的移动构造。

这是C++11的vectoroperator=
在这里插入图片描述
一样的,多出来一个系列的operator=,参数类型为vector&&右值引用,这是vector移动赋值重载。


引用折叠

看到以下代码:

template <class T>
void func(T&& t)
{cout << "T&& 右值引用" << endl;
}template <class T>
void func(const T& t)
{cout << "const T& const左值引用" << endl;
}int main()
{int a = 5;func(a);//左值func(move(a));//右值return 0;
}

以上代码中,有两个模板函数的特化,分别是func的右值引用特化T&&和const左值引用特化const T&&。请问:

向函数func传入一个左值a,会调用哪一个函数;像函数func传入一个右值move(a),会调用哪一个函数?

程序输出结果如下:

T&& 右值引用
T&& 右值引用

可以看到,不论是左值还是右值,都调用了这个右值的模板,这是为什么?/按理来说,虽然const T&int&类型不符,但是从一个一般的引用int&转为const int&是完全合理的,所以应该调用const T&版本才对。但是最后调用了T&&版本,是不是说明在模板中,T&左值引用可以转化为T&&右值引用?

这听起来太扯了,其根本原因在于,C++希望通过统一的方式来处理引用的模板:

因此C++在模板中推出了引用折叠,也叫做万能引用,规则如下:

T& && 推演为 T&
T&& && 推演为 T&&

如果你希望当参数为左值引用和右值引用的时候,函数的功能是一样的,你就可以只写一个函数:

template <class T>
void func(T&& t)
{
}

此时,参数T&&就已经是一个引用折叠了。现在我们来调用这个函数:

int a = 5;
func(a);
func(move(a));

我们共调用了两次函数,分别是左值引用传参和右值引用传参。

第一次传参,func(a);,模板参数T的类型为int&,但是参数类型为int& &&,此时根据折叠引用规则:int& &&等于int&
第二次传参,func(move(a));,模板参数T的类型为int&&,但是参数类型为int&& &&,此时根据折叠引用规则:int&& &&等于int&&

可见,其实这就是一个统一处理左值引用和右值引用的语法,你传入的参数是什么引用,最后T&&就是什么引用。当然,这套规则也对const引用生效。

因此我们刚才的模板,如果作用于int类型,就可以推演出四套函数重载:

void func(int&){};
void func(const int&){};
void func(int&&){};
void func(const int&){};

我们可以用一套模板,生成原先两套模板才能做的事情(前提是左值引用右值引用对函数的要求相同)。


完美转发

看到以下代码:

void fuc1(int& rri)
{cout << "func1 左值引用" << endl;
}void fuc1(int&& rri)
{cout << "func1 右值引用" << endl;
}int main()
{int i = 5;int&& rri = move(i);fuc1(rri);return 0;
}

请问输出结果是什么?
输出结果:

func1 左值引用

是不是有点出乎意料?

明明我们的rri是一个右值引用,却调用了左值引用的函数重载,这又是为啥?
这涉及到一个重要知识点:

右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性

比如说:int&& r = 5;这个代码,5的属性是右值,但是r的属性是左值。

因此我们在调用函数fuc1(rri);的时候,rri是一个左值,自然就以左值的形式来调用函数了。这该怎么办?

聪明的人就会想到,调用之前move一下不就好了,比如这样:

fuc1(move(rri));

这样确实没有问题,可以解决我们刚才的困境。那么我们再来看到一个案例:

void func2(int& x)
{cout << "func2 左值引用" << endl;
}void func2(int&& x)
{cout << "func2 右值引用" << endl;
}template <class T>
void fuc1(T&& t)
{func2(t);
}int main()
{int i = 5;fuc1(i);//左值fuc1(move(i));//右值return 0;
}

func1是一个引用折叠的函数模板,随后在func1中调用了func2,请问如何调用funx2参数的最开始的引用类型?
由于在func1中,我们经过了折叠引用这一步,T&&这个参数类型是不确定的。

如果T&&是右值的话,传参后t会变成左值,那么我们可以对其进行move操作
如果T&&是左值的话,传参后t还是左值,我们无需对其进行操作

这个地方就不能粗暴的进行move了,不然会把原本就是左值的参数,给move成右值。为了解决这个情况,C++提供了一个函数模板forward,称为完美转发,其可以识别到参数的左右值类型,从而将其转化为原来的值。

我们只需要在引用折叠中这样进行调用:

template <class T>
void fuc1(T&& t)
{func2(forward<T>(t));
}

forward的模板参数中传入引用折叠的模板参数T,那么forward<T>就可以根据t的类型自动返回其原始的左右值属性了。


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

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

相关文章

VR全景赋能智慧农业,打造沉浸式种植体验平台

随着人口的增长&#xff0c;传统农业也正在面临着不一样的挑战&#xff0c;加上很多人对农业的固有印象&#xff0c;很少有年轻人愿意下到农田里&#xff0c;那么该如何提高产量、降低成本以及引导年轻人深刻感受现代农业成为了急需解决的问题。 随着城市化脚步的推进&#xff…

数码管时钟--LABVIEW编程

一、程序的前面板 1.获取系统时钟&#xff0c;年月日&#xff0c;时分秒&#xff0c;用14个数码管显示。 2.闹钟设定小时和分钟。 二、程序的后面板 三、程序运行图 四、程序源码 源程序可以在百度网盘自行下载&#xff0c;地址链接见下方。 链接&#xff1a;https://pan.b…

LeetCode-54. 螺旋矩阵【数组 矩阵 模拟】

LeetCode-54. 螺旋矩阵【数组 矩阵 模拟】 题目描述&#xff1a;解题思路一&#xff1a;定义上下左右四个边界&#xff0c;进行模拟。解题思路二&#xff1a;5行 Python zip函数图一乐解题思路三&#xff1a;0 题目描述&#xff1a; 给你一个 m 行 n 列的矩阵 matrix &#xf…

SketchUp Pro中文---3D建模与设计领域的专业选手

SketchUp Pro是一款功能强大的3D建模软件&#xff0c;广泛应用于建筑、城市规划、室内设计等领域。它拥有用户友好的界面和丰富的建模工具&#xff0c;支持实时查看、高 级扩展以及智能提示等功能&#xff0c;使得建模过程更加直观、灵活和高效。SketchUp Pro还支持自定义插件&…

1033 To Fill or Not to Fill

是否有能到达的站点 无&#xff0c;输出当前距离&#xff08;最后一个到达站点距离满油箱状态下行走距离&#xff09;有 有价格更低的站点 如果油量不足以到达新站点&#xff0c;加刚好到达该站点的油量&#xff08;只加可到达范围内最便宜的油&#xff09;有价格更高的站点 在…

论文速览 | IEEE TCI, 2022 | 单光子级非视距成像:估计强度与优化重建

注1:本文系"计算成像最新论文速览"系列之一,致力于简洁清晰地介绍、解读非视距成像领域最新的顶会/顶刊论文(包括但不限于 Nature/Science及其子刊; CVPR, ICCV, ECCV, SIGGRAPH, TPAMI; Light‑Science & Applications, Optica 等)。 本次介绍的论文是:<2…

Dimitra:基于区块链、AI 等前沿技术重塑传统农业

根据 2023 年联合国粮食及农业组织&#xff08;FAO&#xff09;、国际农业发展基金&#xff08;IFAD&#xff09;等组织联合发布的《世界粮食安全和营养状况》报告显示&#xff0c;目前全球约有 7.35 亿饥饿人口&#xff0c;远高于 2019 年的 6.13 亿&#xff0c;这意味着农业仍…

为什么跟着高手还是亏损?fpmarkets10秒解答

各位投资者&#xff0c;不知道你们有没有遇见这样的情况&#xff1f;不管是别人能够持续盈利的技术指标&#xff0c;还是业内知名的行业专家&#xff0c;只要是我们这些普通的投资者一旦使用持续盈利的技术指标&#xff0c;或者跟随专家顾问的信号同时在同一个方向建仓&#xf…

python怎么处理txt

导入文件处理模块 import os 检测路径是否存在&#xff0c;存在则返回True&#xff0c;不存在则返回False os.path.exists("demo.txt") 如果你要创建一个文件并要写入内容 #如果demo.txt文件存在则会覆盖&#xff0c;并且demo.txt文件里面的内容被清空&#xff0c;如…

Machine Learning机器学习之文本分析的词法分析、句法分析、语义分析(详细讲解)

目录 前言 词法分析&#xff1a; 词义消歧&#xff1a; 句法分析&#xff1a; 语义分析&#xff1a; 文本分析应用 1、文本分类&#xff1a; 设计过程&#xff1a; 代码实现&#xff1a; 完整代码&#xff1a; 2、情感分析&#xff1a; 总结 博主介绍&#xff1a;✌专注于前后…

【Go】四、包名、访问范围控制、标识符、运算符

文章目录 1、_2、包名3、命名大小影响可访问范围4、运算符5、获取终端输入 1、_ 下划线"_"本身在Go中是一个特殊的标识符&#xff0c;称为空标识符用于忽略某个值 1&#xff09;忽略导入的没使用的包 2&#xff09;忽略某个返回值 2、包名 main包是程序的入口包&a…

关于 HEAP CORRUPTION DETECTED:after Normal block 错误的原因及解析

目录 一、HEAP CORRUPTION DETECTED:after Normal block 出现的报错情况&#xff1a; 二、问题原因&#xff08;重要&#xff09;&#xff1a; 三、举例 1.错误代码如下&#xff1a; 2.错误原因及分析&#xff08;重要&#xff09;&#xff1a; 3.解决方法 ​编辑 4.正…

【Linux C | 多线程编程】线程的连接、分离,资源销毁情况

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; ⏰发布时间⏰&#xff1a;2024-04-01 1…

SSM框架学习——MyBatis关联映射

MyBatis关联映射 为什么要关联映射 实际开发中&#xff0c;对数据库操作常常会涉及多张表&#xff0c;所以在OOP中就涉及对象与对象的关联关系。针对多表操作&#xff0c;MyBatis提供关联映射。 关联关系概述 一对一&#xff1a;A类中定义B类的属性b&#xff0c;B类中定义A…

MCU友好过渡MPU,米尔基于STM32MP135开发板裸机开发应用笔记

以前微处理器&#xff08;MPU&#xff09;与微控制器&#xff08;MCU&#xff09;是截然不同的两种设备&#xff0c;MPU支持丰富的软件系统&#xff0c;如Linux和相关的软件堆栈&#xff0c;而MCU通常将专注于裸机和RTOS。近年来&#xff0c;随着MCU的性能越来越高&#xff0c;…

【Spring源码】WebSocket做推送动作的底层实例

一、前瞻 Ok&#xff0c;开始我们今天的对Spring的【模块阅读】。 那就挑Web里的WebSocket模块&#xff0c;先思考下本次阅读的阅读线索&#xff1a; WebSocket在Spring里起到什么作用这个模块采用了什么设计模式我们都知道WebSocket可以主动推送消息给用户&#xff0c;那做推…

halcon图像膨胀

1、原理&#xff1a; 使用结构元素在图像上移动&#xff0c;如果结构元素中有任意一个像素和图像上的非零像素重叠&#xff0c;则保留此时结构元素中心所在位置&#xff0c;并将其像素值设置为非零。 2、halcon代码 其中圆形结构元素可设置半径&#xff0c;矩形结构元素设置…

MySQL故障排查与生产环境优化

一、MySQL单实例常见故障 1.逻辑架构图 MySQL逻辑架构图客户端和连接服务核心服务功能存储引擎层数据存储层 2.故障一 故障现象 ERROR 2002 (HY000): Cant connect to local MySQL server through socket/data/mysql/mysql.sock(2) 问题分析 数据库未启动或者数据库端口…

Yolo 自制数据集dect训练改进

上一文请看 Yolo自制detect训练-CSDN博客 简介 如下图&#xff1a; 首先看一下每个图的含义 loss loss分为cls_loss, box_loss, obj_loss三部分。 cls_loss用于监督类别分类&#xff0c;计算锚框与对应的标定分类是否正确。 box_loss用于监督检测框的回归&#xff0c;预测框…

蓝桥杯真题:成绩统计

这题思路简单&#xff0c;但是输出结果的位置容易出错&#xff0c;题目要求四舍五入&#xff0c;所以要用Math.round&#xff08;&#xff09;的方法