【C++ 第十八章】C++11 新增语法(2)

news2024/11/26 5:36:44

 

前情回顾:

【C++11 新增语法(1):1~6 点】

        C++11出现与历史、花括号统一初始化、initializer_list初始化列表、 auto、decltype、nullptr、STL的一些新变化

本文会使用到自己模拟实现的 string 和 list 类,为了更好的观察各种函数的构造过程,建议先将本文最后的 string 和 list 代码拷贝下来创建一个 string.h / list.h 文件

7 右值引用和移动语义

 7.1 左值引用和右值引用

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

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

int main()
{
	// 以下的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;
	return 0;
}

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

int main()
{
	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; 
	return 0;
}

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,
这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

这里也隐含的说明了,右值引用 其实也是一种左值(使用一个 “左值” 存储右值,所以可以取地址和修改值)

int main()
{
	double x = 1.1, y = 2.2;
	int&& rr1 = 10;   // 10 是右值,rr1 是右值引用
	const double&& rr2 = x + y;   // x + y 是右值,rr2 是右值引用
	rr1 = 20;
	rr2 = 5.5;  // 报错:rr2 被 const 修饰了
	return 0;
}

7.2 左值引用与右值引用比较

左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a; // ra为a的别名

	//int& ra2 = 10;   // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;  // 10 是右值
	const int& ra4 = a;   // a 是左值
	return 0;
}

右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。

int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;

	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;

	// 右值引用可以引用move以后的左值
	int&& r3 = std::move(a);
	return 0;
}

 move是一个函数,传入左值对象或右值对象,返回一个带有右值属性的该对象


7.3 右值引用使用场景和意义

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

⭐我们之后的讲解会围绕我们自己模拟实现 string 的代码 ,该 string.h 代码在文章的末尾 

🚩关于 move 函数的作用

当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值。C++11中,std::move() 函数位于 头文件中,该函数名字具有迷惑性,
它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义

🚩左值引用的使用场景:

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

void func1(my::string s)
{}
void func2(const my::string& s)
{}
int main()
{
	my::string s1("hello world");
	// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
	func1(s1);   // 传值过去 func1,需要一次拷贝
	func2(s1);  //   传值过去 func2 ,引用接收,无需拷贝

	// 在上面的 string.h 代码中
	// string operator+=(char ch) 传值返回存在深拷贝
	// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
	s1 += '!';
	return 0;
}

🚩左值引用的短板:

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

两次拷贝构造:先拷贝构造生成一个临时对象,然后再传递临时对象回去,拷贝构造给 ret2

这个过程通常会被编译器优化,不生成临时对象,直接使用返回的对象拷贝给 ret2(少了一次拷贝)


因为本质就是 传值返回,因此这里至少还是会有一次拷贝构造

🚩使用右值引用 移动构造函数

为了更好的优化传值返回,这里引出 使用右值引用的 移动构造函数

移动构造:右值引用接收的 拷贝构造函数

string(string&& s)
{}

右值又分为:

纯右值:内置类型右值

将亡值:类内置的右值(如 自定义类型的 匿名对象)

将亡值,顾名思义即是即将要销毁消亡的数据值

传值返回会生成一个临时对象,该临时对象即是将亡值,拷贝给其他变量后会销毁掉,既然使用完就会销毁,不如直接将该 临时对象的资源 swap 转移交换给 其他变量,这样只需几步swap交换程序,无需进行深拷贝(消耗不少效率),这就是移动构造的原理

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

调用拷贝构造函数时,识别到参数为右值,则会匹配到下面这个移动构造,使用 string&& 右值引用接收,然后将该右值s(将亡值)的资源直接交换

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动拷贝" << endl;

	swap(s);
	return *this;
}

 

小结:如果拷贝对象是右值(将亡值),则调用移动构造函数,直接资源交换;否则调用深拷贝构造,执行深拷贝

移动构造使得 传值返回 真正的没有了 拷贝构造:

(1)编译器优化前传值返回,一次拷贝构造生成临时对象(右值),再调用移动构造

(2)编译器优化后传值返回,不生成临时对象(右值),本来返回对象需要一次拷贝构造,此时编译器会将 返回对象move成一个右值属性的对象,则整个过程只需要调用一个移动构造

(编译器自动将 函数返回值 move 成一个右值,即用即销毁,出了函数作用域就销毁,因此此处转换成 右值不会有事)


深拷贝的类,移动构造才有意义,像是日期类这样的浅拷贝类型,就没有必要上 移动构造

(日期类只有内置类型的成员变量)

🚩不仅仅有移动构造,还有移动赋值:

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

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) -- 移动拷贝" << endl;

	swap(s);
	return *this;
}

调用 to_string 函数,传值 1234 ,函数传值返回一个临时对象(右值)此时 ret 进行赋值操作,识别到函数返回一个右值,则调用移动赋值函数,直接交换资源即可,避免了深拷贝

my::string to_string(int value) {
	//....
	return str;
}

my::string ret;
ret = to_string(1234);

一一解析这里每条打印信息:

main函数中创建一个 string的 ret ,调用直接构造

to_string 函数中创建一个 string 的 str,调用直接构造

to_string 函数返回 str,由于这里是赋值操作,不是拷贝操作(编译器无法直接优化),需要生成临时对象,不过编译器聪明的识别出 str 是即将销毁的值(出了作用域销毁),因此将 str move 转换为 右值,调用 移动拷贝构造 生成临时对象(右值),该临时对象传回main函数中,因为是右值,则调用 移动赋值函数,赋值给 ret 变量

🚩push_back 函数重载 右值引用版本(借助 list<string> 的 push-back 使用举例)

由于 右值是将亡值,可以调用移动拷贝 和 移动赋值,极大提高效率

因此,C++11 右值出现后,其他各种库函数都增加了 以右值引用作为参数的重载,提高不少效率

如上图:push_back 一个对象 s1,则调用深拷贝

push_back 一个匿名对象 s1 ,调用 移动构造(匿名对象是右值)

 

因此,如果自己模拟实现 list 的话,list 内部还要写另一个右值引用版本的 push_back 函数

insert 函数也要写 右值引用版本

void push_back(const T& data){
    insert(it, data);
}
// 右值引用版本
void push_back(T&& data){
    insert(it, data);
}


void insert(iterator it, const T& data)
{}
void insert(iterator it, T&& data)
{}
int main() {
    my::list<my::string> lt;
    // 匿名对象,具有常性的临时对象,在 push_back 函数参数识别成右值
    lt.push_back(my::string("22222222222222222222222222222"));

    return 0;
}

我们这里 push_back 一个右值,会调用右值版本的 push_back 函数,但是调试可以发现,insert 函数却没有如愿的调用 右值版本的 insert 函数

因为 右值引用本身的属性是左值:因此 push_back 函数的右值引用 data 是 左值

因此会调用到 非右值版本的 insert 函数 insert(iterator it, const T& data)

为什么说 右值引用 需要是左值:上面刚学的移动构造中 swap(s),这个 右值引用 s 必须是可以修改的,才能进行资源交换,如果是右值则不能修改

因此,需要使用 move 将右值引用的左值属性 修改成 右值属性

// 右值引用版本
void push_back(T&& data){
    insert(it, move(data)); // move一下
}


void insert(iterator it, T&& x){
    Node* newNode = new Node(move(x));  // move一下
    //....
}


// 构造函数也要右值版本
ListNode(T&& x)
    :_data(move(x))  // move一下
    // ....
{}

从这里就知道,传递一个 右值,容易“退化成”左值,如果想要 右值属性保持不变,需要在右值传递的路径上 使用 move将 右值引用转化为右值继续传递

🚩【代码演示】对 list 的 push_back 的各种使用

注释已经讲得比较清楚了 

int main()
{
    my::list<my::string> lt;    // 创建一个以string为节点数据类型的 list:其中创建头节点,会 “直接构造”一个 string,调用头节点构造函数,给 string 赋初值 T(),调用 string 的“拷贝构造”
    my::string s1("111111111111111111111");  // 调用“直接构造”
    lt.push_back(s1);  // 因为 list 节点类型为 string,这里会调用“拷贝构造”存储数据(相当于给新节点里面已存在的 string,重新拷贝)


    // 匿名对象,具有常性的临时对象,在 push_back 函数参数识别成右值:调用右值引用版本的 push_back,这个push_back 调用 右值引用版本的 insert ,这个 insert 里面需要创建新节点,则调用 移动构造函数(因为一路上的传递都使用 move 保持了右值属性,最后就能被识别成右值而调用移动构造)
    lt.push_back(my::string("22222222222222222222222222222"));

    // 隐式类型转换成 string,中间生成string的临时对象识别成右值:"直接构造" 一个 string,这个string是临时对象,push进链表调用 "移动构造"
    lt.push_back("3333333333333333333333333333");

    // 这里直接将 s1 变成右值,则底层直接调用移动构造则不用拷贝构造:将左值 s1 move返回一个 右值属性的 s1对象
    lt.push_back(move(s1));


    // 注意:move是一个表达式,将 s1 放进去,返回一个 右值类型的 s1,而并不会将 s1 本身属性改变,因此下面的 push_back 还是原来的 s1
    move(s1); // 没有改变 s1 属性,只是move返回一个 右值类型的 s1
    lt.push_back(s1);

    my::string&& r1 = my::string("22222222222222222222222222222");
    // r1(右值引用本身)的属性是左值还是右值?-> 左值

    return 0;
}

因此,以后传数据,使用 匿名对象或隐式类型转换的数据,会调用移动函数,代码运行效率都会大幅提升

7.4 左右值引用的底层

在汇编底层,左值引用和右值引用都是 指针,原理都相同!!

左值引用和右值引用 底层都是相同的程序概念

只是在 语法层面有了限制和区分,如果左值引用一个右值,会报错,也只是表面的语法层不允许,底层是可以的

因此 一个左值可以 move 或者类型强制转换 成 一个带有右值属性的对象

int x = 10;
string&& r1 = move(x);
string&& r2 = (string&&)x;

⭐关于 C++的语法的“口是心非”

在语法层,说右值不能取地址,其实底层是使用一块临时空间存储该右值,且该空间也是有地址的

还有说 左右值引用就是没有取地址,没有开空间,只是取了一个别名 而底层确实取地址开空间

因此,C++的语法很多时候都“口是心非”,当语法层和底层的表现不一样时,一定要注意区分,学习C++不要过度关注底层,明白底层和语法层两者有时的表现是不一样的就行

但也不能不了解 底层,否则下面这句代码可能就难理解

将左值 x 强转只是为了让其通过语法层的检验而已,他们底层本来就相同

string&& r2 = (string&&)x;

其实左右值没有这么复杂,两者是可以通过类型转换来回直接切换的

void func(const my::string& s)
{
    cout << "void func(bit::string& s)" << endl;
}

void func(my::string&& s)
{
    cout << "void func(bit::string&& s)" << endl;
}


int main()
{
    my::string s1("1111111");
    func(s1);

    func((my::string&&)s1); // 左值强转成右值


    func(my::string("1111111"));  // 右值
    func((my::string&)my::string("1111111"));   // 右值强转成左值

    return 0;
}

⭐为什么平时一般都使用 move 而不是 强转来将左值变成右值?

因为强转需要写具体类型,限制性大

7.5 万能引用

关于 函数的左右值引用 参数

// 函数参数可以是左值引用也可以是右值引用
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; }

关于 模板的左右值引用参数 

(T&& x) 这里不叫右值引用:是万能引用,同时可以接收 左右值引用,会自己推导数据是 左值 or 右值(因此具有模板的”适配性“)

template<class T>
void Func(T&& x) {
	​
}

前段文章中讲解的某些函数,传过来的既有左值也有右值,因此既要考虑左值引用,也要考虑右值引用,(可能就需要写两个不同参数的 重载函数),这个函数模板就可以帮助解决这个问题

template<typename T>
void PerfectForward(T&& t)
{
	Fun(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;
}

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

7.6 完美转发

// 函数参数可以是左值引用也可以是右值引用
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; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(t);
}

int main() {
	PerfectForward(10);  // 右值
	return 0;
}

模板 PerfectForward 接收右值,右值退化成 左值,如果想要维持住 右值属性,可以使用完美转发

完美转发会自动识别 该对象的属性,并维持其本身的属性:

模版实例化是左值引用,保持属性直接传参给 Fun
模版实例化是右值引用,右值引用属性会退化成左值,转换成右值属性再传参给Fun

为什么不能使用move 直接将 右值引用修改成 右值属性?

因为这个是模板,也可能接收 左值,如果一律使用 move 会导致左值也变成右值

因此,在这类可能会接收左右值的模板,就需要 使用完美转发,维持属性

其他场景,一般使用 move 即可

// 函数参数可以是左值引用也可以是右值引用
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; }

template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));  // 使用完美转发
}

int main() {
	PerfectForward(10);  // 右值
	return 0;
}

再次声明,模板中的双引用&& 不是右值引用,而是万能引用!

下面这个 Args&& 不是右值引用, 这个是函数模板,Args&& 是识别 左右值的“万能引用”


8. 新的类功能

8.1 新增的两个默认类

默认成员函数
原来C++类中,有6个默认成员函数:
1. 构造函数
2. 析构函数
3. 拷贝构造函数
4. 拷贝赋值重载
5. 取地址重载
6. const 取地址重载

最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。

C++11 新增了两个:移动构造函数和移动赋值运算符重载

针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:

如果你自己没有实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(指这三种一个都没显式实现)。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。

如果你自己没有实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个(指这三种一个都没显式实现),那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)

图示过程:

如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

🚩为什么默认的两个移动函数的生成条件如此苛刻?

需要没有显式实现析构函数 、拷贝构造、拷贝赋值重载,且没有实现 移动函数

🚩理清一个道理:

当需要显示写析构,说明有资源需要释放

1、说明需要显示写拷贝构造和赋值重载

2、说明需要显示写移动构造和移动赋值

因此他们几个都是同时出现同时消失的

🚩为什么需要默认生成移动函数?

像日期类这样的,成员都是内置类型,没有什么资源管理,是否有移动构造都差不多,因此可以不用显式写拷贝构造和赋值重载

而像是 Person 类,没有成员管理资源,但是有 string 这种非内置类型的成员(万一string很大呢?),这种情况就需要调用默认的移动构造

Person 类的 移动构造 是为了解决成员的拷贝效率问题,不是为了解决自身的拷贝效率问题

自动生成移动构造,对于下面Person这样的类是很有意义的

因为 Person  是右值时,他内部的 string 也是右值,string就可以走移动构造,提高效率了。

#include<utility>
class Person
{
public:
	Person(const char* name = "111111111111", int age = 0)
		:_name(name)
		, _age(age)
	{}


private:
	string _name;
	int _age;
};

int main() {
	Person p1;
	p1 = "2222222222222222222222";

	return 0;
}

这里可以发现,上面的赋值过程,Person 类的 成员string,地址位置变了,就是调用了 string内部 默认的移动赋值

8.2 关于 禁止生成默认函数的关键字 delete

如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成 private,并且只声明不实现,这样只要其他人想要调用就会报错(而且也不能自己实现)。

在C++11中更简单,只需在该函数声明加上 = delete 即可,该语法指示编译器不生成对应函数的默认版本,称 =delete修饰的函数为删除函数。

class Person
{
public:
	Person(const char* name = "111111111111", int age = 0)
		:_name(name)
		, _age(age)
	{}

	// C++98
	//1、只声明不实现,且声明为私有
	
private:
	Person(const Person& p);
	Person& operator=(const Person & p);

	// C++11
	// 设置 delete 关键字
	Person(const Person& p) = delete;
	Person& operator=(const Person& p) = delete;
private:
	string _name;
	int _age;
};

会有一些场景:不想被别人拷贝的类,则限制不生成拷贝构造

因为 IO流 中存在缓冲区,直接被拷贝,影响效率,就禁了

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

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

相关文章

笔记整理—内核!启动!—uboot部分(2)

上文中&#xff0c;我们说到了使用uboot去启动kernel支持的几种方式以及压缩kernel的几种形式&#xff0c;本章节将要接着内核的启动说起。 上一章我们对uImage格式进行了初步的说明&#xff0c;并说这样的格式已经被废弃&#xff0c;但是依然保留了相应的代码。boot_get_kerne…

MATLAB生成mif文件

MATLAB代码 % 参数设置 N 4096; % 数据点数量 t linspace(0, 2*pi, N); % 时间向量 width 12; % 位宽% 正弦波 sine_wave 2.5 * sin(t) 2.5; % 幅度在0到5之间% 三角波 tri_wave 5 - abs(mod(t/(2*pi)*4, 2) - 1);% 方波 square_wave 2.5 * (square(t) 1); % 将范围调…

Hive Tutorial For Beginners

Hive Tutorial For Beginners 一、Hive历史&#xff08;History of Hive&#xff09; Facebook 在面对日益增长的大数据时&#xff0c;选择了 Hadoop 作为解决方案。 但问题在于&#xff0c;许多用户并不熟悉 Java 或其他编程语言&#xff0c;这使得使用 Hadoop 的 MapReduc…

代码随想录——回文子串(Leetcode 647)

题目链接 我的题解&#xff08;双指针&#xff09; 思路&#xff1a; 当然&#xff0c;以下是对您提供的代码的解释&#xff1a; class Solution {public int countSubstrings(String s) {// 初始化回文子字符串的数量int count 0;// 遍历字符串的每个字符&#xff0c;使用…

无人机之电池篇

无人机电池作为无人机的重要组成部分&#xff0c;其性能、使用、保养及选择都至关重要。以下是对无人机电池的综合介绍&#xff1a; 一、无人机电池的基本参数 电池容量&#xff1a;电池容量直接影响无人机的续航能力。大容量电池&#xff0c;如5000mAh的电池&#xff0c;能提…

无人机道通布局的讲究详解!!!

一、通道分配与功能对应 基本通道&#xff1a;无人机遥控器通常至少包含四个基本通道&#xff0c;分别对应无人机的上下&#xff08;升降&#xff09;、左右&#xff08;副翼&#xff09;、前后&#xff08;俯仰&#xff09;和旋转&#xff08;方向舵&#xff09;控制。这些通…

关于报错 SLF4J: Failed to load class “org.slf4j.impl.StaticLoggerBinder“ 的可能原因

1. 絮絮叨叨 学习或工作中&#xff0c;如果需要从头建立日志打印体系&#xff0c;笔者通常直接照抄之前的博客&#xff1a;《Java maven工程配置slf4j》&#xff0c;直接粘贴、复制相关依赖除了上述博客提到的slf4j-api、logback-classic&#xff0c;也看到过slf4j-simple、lo…

区域生长算法详解与Python实现

图像分割是计算机视觉中一个重要的任务&#xff0c;区域生长算法是其中的一种常见方法。本文将详细介绍区域生长算法的原理&#xff0c;并通过Python代码实现&#xff0c;带你一步步理解它的实际应用。 1. 区域生长算法简介 区域生长算法是一种基于像素相似性进行图像分割的方…

2025款宋L EV全面升级加量不加价,仅18.98万元起

8月30日&#xff0c;2024成都车展正式开幕&#xff0c;备受期待的比亚迪王朝网B级先锋猎装SUV——2025款宋L EV正式上市&#xff0c;搭载“天神之眼”高阶智能驾驶辅助系统DiPilot 100&#xff0c;e平台3.0、CTB、云辇-C三大明星技术&#xff0c;并新增外观内饰配色。凭借智驾、…

【2024】Datawhale X 李宏毅苹果书 AI夏令营 Task2

本文是关于李宏毅苹果书”线性模型“学习内容的记录。 线性模型 线性模型&#xff08;linear model&#xff09;&#xff1a;将输入的特征 x x x&#xff08;或 x \bold{x} x&#xff09;乘上权重 ω \omega ω&#xff08;或 ω \bold{\omega} ω&#xff09;&#xff0c;再…

Python简易IDE工作界面制作

、 休闲一下&#xff0c;学习编程还是要学习一些界面编程&#xff0c;能够根据需要制作图形操作界面&#xff0c;这样我们开发的程序才能方便操作和使用&#xff0c;同时获得更友好的人机交互体验。下面是一个用PyQt5制作的简易界面&#xff0c;供大学参考。如下图所示&a…

罪人的终幕(原题)

题目背景 而我承诺你&#xff0c;一切都将在一场盛大的&#xff0c;如同戏剧般的审判中结束…… 小小地旋转&#xff0c;轻轻地跳跃&#xff0c;然后便是「罪人」的谢幕。 题目描述 定义函数 &#x1d44e;(&#x1d465;)a(x) 表示自然数 &#x1d465;x 的不同的质因子的和。…

数据增强在Sentence Transformers中的作用:提高句子评分任务的性能

Sentence Transformers 是一个强大的 Python 库&#xff0c;它基于 Transformer 模型架构&#xff0c;如 BERT、RoBERTa 和 XLM-RoBERTa 等&#xff0c;用于学习和操作句子级别的向量表示。这个库特别适合于处理自然语言处理&#xff08;NLP&#xff09;任务&#xff0c;能够为…

实用好软-----电脑端 开源的视频无损剪切与合并工具

这个是一个开源项目LosslessCut 无损剪切就是基于关键帧的剪切&#xff0c;不需要重编码&#xff0c;因此速度非常快&#xff0c; 缺点就是切割时间无法达到非常精确&#xff0c;可能前后会有几秒的差距&#xff0c; 要做到精确的剪切&#xff0c;只能重编码。 LosslessCut在切…

学习之SQL语句之DQL(数据库查询语言)

DQL英文全称是Data Query Language(数据查询语言)&#xff0c;数据查询语言&#xff0c;用来查询数据库中表的记录 查询关键字:SELECT 数据准备&#xff1a; CREATE TABLE emp ( id INT COMMENT “编号”, workno VARCHAR ( 10 ) COMMENT “工号”, NAME VARCHAR ( 10 ) COMME…

windows配置hadoop环境

目录 一、windows配置hadoop环境1、下载文件2、解压3、移动winutils.exe4、移动hadoop.dll5、配置代码内容 一、windows配置hadoop环境 1、下载文件 首先下载所需要的文件内容 共有三个文件&#xff0c;可以通过 这个链接 获得 2、解压 使用解压工具将 hadoop-3.0.0.tar.gz…

堆的时间复杂度分析

一&#xff0c;建堆的时间复杂度分析 堆是一颗完全二叉树&#xff0c;满二叉树又是一颗特殊的完全二叉树。 对于满二叉树来说&#xff0c;第一层的节点个数为2^0,第二层的节点个数为2^1,......所以可以得到第h层的节点个数为2^(h-1)。总结点个数N2^02^1...2^(h-1)2^h-1。那么就…

桌面日历工具

Desktop Calendar 官网 设置安装目录&#xff0c;防止默认装到C盘修改为自己想要安装的位置调整位置和大小

ip地址变化是什么意思?手机地址ip一直变化怎么办

IP地址作为互联网设备的唯一标识&#xff0c;‌其稳定性对于网络连接至关重要。‌然而&#xff0c;‌手机IP地址频繁变动可能带来一系列问题。‌本文将深入探讨IP地址变化的含义、‌IP地址频繁变动的原因&#xff0c;‌以及提供手机地址IP一直变化的有效应对策略。‌ 一、IP地址…

申请商标及版权时千万要注意字体!

近日有个渠道合作的朋友申请版权&#xff0c;就是几行文字&#xff0c;普推知产老杨一看这个字体有点特别&#xff0c;不是免费字体&#xff0c;一问也不是他们美工自己设计&#xff0c;是在网上找的字体&#xff0c;一检索果然是商业字体&#xff0c;赶紧建议换字体。 以前经常…