Rust语法:所有权引用生命周期

文章目录

    • 所有权
      • 垃圾回收管理内存
      • 手动管理内存
      • Rust的所有权
      • 所有权转移
      • 函数所有权传递
    • 引用与借用
      • 可变与不可变引用
    • 生命周期
      • 悬垂引用
      • 函数生命周期声明
      • 结构体的生命周期声明
      • Rust生命周期的自行推断
      • 生命周期约束
      • 静态生命周期

所有权

垃圾回收管理内存

Python,Java这类语言在管理内存时引用了一种叫做垃圾回收的技术,这种技术会为每个变量设置一个引用计数器(reference counter),来统计每个对象的引用次数。

一旦某个对象的引用数为0,垃圾回收器就会择取一个时机将其所占用的空间回收。
以Python为例子

x = [1, 2] # 列表对象[1, 2]被x引用,引用计数为1
y = x # 列表对象[1, 2]被y引用,引用计数为2
del x # 列表对象[1, 2]的引用x被删除,此时并不删除对象,引用计数为1
del y # 列表对象[1, 2]的引用y被删除,此时并不立即删除对象,引用计数为0
# 但此对象将会在下一次垃圾回收时被清理

这种垃圾回收的方式很好的避免了开发者去管理内存,但是也造成了一些列问题。也就是:

  1. 这种垃圾回收不会立刻进行,而是会选取某个时机进行,这无疑在这段时间内这段内存会被一直占用。
  2. 垃圾回收对性能的消耗比较大,因为编译器/解释器需要不停的追踪每个对象的引用计数来决定某个变量是否会被回收。

手动管理内存

C/C++就采取了手动管理内存的方式,也就是自己申请内存,自己需要去释放,如果不释放就可能导致内存占用的堆积,这种方式的好处在于可控性和高性能,坏处在于很容易出现各种错误。例如二次释放。

void test(int arr[]){free(arr); //此时该数组已经被释放
}int main(){int *arr = (int *)malloc(sizeof(int) * 10);test(arr);free(arr); //这里又进行了第二次释放return 0;
}

上述代码可能你觉着不会写的这么蠢,但是如果当代码逻辑复杂起来,你很难保证你不会犯这样的错误。
二次释放的危害很大,因为当一块内存被释放时可能会被新的对象再次占用,而地二次释放则会破坏新的对象的存储从而造成一系列严重的错误。

Rust的所有权

Rust采取了一种非传统的做法,即采用了一种叫做所有权的机制,其规则如下:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

来看下面的例子


fn main() {let x = 4; //x的作用域开始位置{//y的作用域开始位置let y = 10;}//y的作用域结束位置,作用域结束,y被丢弃
}//x的作用域结束位置,作用域结束,x被丢弃

此时就需要提一下堆和栈这两种内存了,其主要的区别如下:

栈是一种后进先出的内存,其内部高度组织化,并且栈内存的数据需要实现知道其大小,堆栈内存的存取速度很快

与栈内存不同,堆可以存储结果(指不能在编译时就确定大小)未知大小的数据,在存取时速度比较慢,OS会寻找一块可以存的下的内存然后分配给对应的进程。对于这块堆内存来说,会有一个指针指向这块,而这个指针是要存在栈中的。

存取速度
分配内存大小可不固定必须固定

在Rust中,有以下类型都是存储在栈内存上的,这些类型都实现了一个叫做copy的trait
在这里插入图片描述
(图片来自于b站杨旭)
由于这些类型都是存在栈上的,而栈的存储速度远高于堆,所以对于Rust来说,上述的数据在赋值传递时往往会直接进行拷贝,而不需要引用之类的技术。

fn main(){let x = 10;let y = x; //此时y有拷贝了一份10
}

所有权转移

由于我们知道上述的类型都是存在栈的,所以体现不出所有权,我们使用另一种存在堆上的类型String来演示所有权。

fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型
}

这里需要区分一下String和字符串字面值的区别,这里的String是一个存在堆内存的变量,其大小是可以扩充变化的。而字符串字面值(如let x = “hello”;)是固定的,在编译时就已经确定的了。

来看下面的代码:

fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let s2 = s1;println!("{}", s); //会报错
}

上述代码报错了,我们来看报错信息:

error[E0382]: borrow of moved value: `s1`--> src\main.rs:4:17|
2 |     let s1 = String::from("hello"); //从字符串字面值得到一个String类型|         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;|              - value moved here
4 |     println!("{}", s1); //会报错|                    ^ value borrowed here after move|= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

报错的内容说,我们借用了一个已经移动了的值s。
还记着开头所说的所有权规则么:

  1. Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者
  2. 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
  3. 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

String::from(“hello”);这个值最开始被s所拥有,但是当让s2 = s1时,这个值就不在归s所有了,而是发生了move(移动),所有权交给了s2。

这是因为有第二条: 一个值只能同时被一个变量所拥有的缘故,所以s1不再拥有String::from(“hello”);,自然也就无法再被使用了。

我们从内存上来看是这样的(图片来自于b站杨旭),刚开始s1是一个指针指向了hello这块堆内存
在这里插入图片描述
后来由于s2=s1,所以指针得到了复制,得到了如下的场景
在这里插入图片描述
但由于一个值只能有一个所有者,所以先前的s1指针将被视为无效,于是得到了如下的场景。
在这里插入图片描述
所以最终看起来就好像s1的所有权移动到了s2一样,发生了所有权(对hello这个值的拥有)的移动。

这么做有一个很大的好处,那就是由于一个值在同一时刻只有一个所有者,那么在离开作用域时,对应的值就不会被反复释放

fn main(){let s1 = String::from("hello"); //s1作用域开始let s2 = s1;//s2作用域开始,s1失去所有权
}//s1,s2作用域结束,由于s1没有所有权,所以只有s2会释放这块堆内存。

这样就有效的避免了二次释放的的问题,但是同时又给开发者引入了很多新的难题。

函数所有权传递

但一个变量被当做参数传递给函数时,其所有权将会被移交到函数内部的形式参数上。看如下例子:

fn get_len(s: String) -> usize{ //此时,这里的形式参数s得到了外部s1的所有权return s.len(); //返回s的长度
} //s的作用域结束,而s又具有hello的所有权,所以hello会被释放fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(s1); //把s1交给函数get_lenprintln!("{}", s1); //由于此时所有权已经交给了函数内部的形式参数s,所以s1没有所有权,导致报错
}

上面的注释已经说名了具体状况,其发生错误的原因就在于传递参数时会发生所有权的转移,而转移后有没有发生归还,而导致使用了没有所有权的变量。

解决方法1:
使用shadowing这个特性,将使用权再返回回去。

fn get_len(s: String) -> (usize, String){ //返回长度和字符串return (s.len(), s);
}fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型let (s, len) = get_len(s); //用Tuple拆包和shadowing的特性归还使用权println!("{}'s len is {}", s, len);
}

解决方法2:
参看下面的引用部分

如果你不想这么麻烦,你想直接复制一份hello传过去,而不是吧使用权交过去,你可以使用clone方法,这个方法会把堆上的数据克隆一份,然后给传给函数。

fn get_len(s: String) -> usize{ //返回长度和字符串return s.len();
}fn main(){let s = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(s.clone()); //把s克隆一份,此时新克隆的部分的使用权将交给形式参数sprintln!("{}'s len is {}", s, len);
}

注意,克隆意味着你要在堆上复制一份数据,这是相当耗时的。但好处在于,你可以不用为所有权而烦恼。

引用与借用

每次想要传一个值都会进行所有权转移显然十分的头疼,于是就有了借用,借用实际上就是一种引用,但是它的特殊之处在于他不会拿到所有权。 借用使用&操作符来表示。例子如下:

fn get_len(s: &String) -> usize{ //s是一个借用,而不是转交所有权return s.len();
}fn main(){let s1 = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(&s1); //传入s1的引用println!("{}'s len is {}", s1, len);
}

借用在内存上看是这个样子的:
在这里插入图片描述
可以看到引用s只是指向了s1,并不是指向了堆内存所在的位置。所以借用可以看做是变量的引用。因为有这个特性,所以借用并没有拿到了s1的所有权,只是暂时的借了过来。

所以在get_len函数结束之后,由于s只是借用所以无权释放hello。所有权还在s1手中。

可变与不可变引用

上述的借用很好的解决了传参的问题,但是还没完。有时我们希望修改一下对应的值,如果你直接修改就会发现报错这是因为你没加mut关键字。

fn get_len(s: &mut String) -> usize{ //可变字符串引用return s.len();
}fn main(){let mut s = String::from("hello"); //从字符串字面值得到一个String类型let len = get_len(&mut s);//传入可变字符串引用println!("{}'s len is {}", s, len);
}

问题似乎解决了,似乎皆大欢喜,但是,更大的问题随之出现了。我们来看下面的例子

fn main(){let mut s: String = String::from("hello");let mut re1: &String = &mut s;let mut re2: &String = &mut s;//此处报错println!("{} {}", re1, re2);
}

报错的理由很简单,同一作用域内不允许出现两个同一变量的可变引用。Rust这么做是为了防止数据竞争的发生。数据竞争会由一下行为引发:

1. 两个或更多的指针同时访问同一数据
2. 至少有一个指针被用来写入数据
3. 没有同步数据访问的机制
(参考来源: https://course.rs/)

同时为了保证不可变引用不会因为可变引用的修改而发生异常,于是规定,在同一作用域内不可变引用和可变引用不能同时出现。

fn main(){let mut s: String = String::from("hello");let mut re1: &String = &s;let mut re2: &String = &mut s; //不能同时出现println!("{} {}", re1, re2);
}

但是在同一作用域内,可以同时出现多个不可变引用。

如果实在需要用到多个可变引用,则可以通过大括号来创建新的作用域。

fn main(){let mut s: String = String::from("hello");{let mut re1: &String = &s;}let mut re2: &String = &mut s;println!("{}", re2); //此时已经无法调用re1,因为它的作用域在大括号内,在此处已经失效
}

生命周期

对于Rust来说每个变量都有一个自己的生命周期,也就是说,每个变量有一个有效地范围。

如果一个变量已经失效,而仍然使用他的引用,就会造成悬垂引用。而Rust的生命周期就是为了避免悬垂引用这个错误而设计的。

悬垂引用

来看下面的代码

fn main() {let result;{let tmp = String::from("abc");result = &tmp;}println!("{}", result);
}

上述代码会报错,这是因为result的生命周期在整个main内,但是tmp只是内部的一个局部变量,在print时,tmp已经失效可以认为是空的。

但是此时result仍然持有并想使用tmp的引用,因此引发了报错。来看一下报错内容:

error[E0597]: `tmp` does not live long enough--> src\main.rs:6:18|
5 |         let tmp = String::from("abc");|             --- binding `tmp` declared here
6 |         result = &tmp;|                  ^^^^ borrowed value does not live long enough
7 |     }|     - `tmp` dropped here while still borrowed
8 |     println!("{}", result);|                    ------ borrow later used hereFor more information about this error, try `rustc --explain E0597`.

内容说的很直白,说的是result借用了一个获得还没自己长的变量。

我们来看看两个变量的生命周期(就是存活时间)

fn main() {let result;-------------------------------+{                                         |<-result的生命周期let tmp = String::from("abc");-----+  |result = &tmp;      tmp的生命周期->  |  |}--------------------------------------+  |println!("{}", result);                   |
}---------------------------------------------+

从上图可以直观的看到,result的存活范围(生命周期)大于tmp的生命周期。一个大的生命周期的变量借用了一个小的,所以才会导致了错误的发生。

我们只需要修改上述代码就可以使其正确

fn main() {let result;let tmp = String::from("abc");result = &tmp;println!("{}", result);
}

此时在调用print时,tmp的生命周期还没有结束,但是此时你也能发现result的生命周期其实还是大于tmp的,也就意味着

大周期借用小周期不一定会出错。但是在很多情况下,Rust还是认为这种情况存在风险,会在编译阶段就拒绝我们。

但是小周期借用大周期确实一定不会出错的(因为此时小周期会率先失效,而不会借用到一个失效的大周期对象)

函数生命周期声明

先来看一个函数,有一个需求,要求返回两个字符串中最长的那一个的借用。此时你会想这么写代码

fn get_greater(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y}
}

此时你觉着则个代码写得没问题,但是你却发现编译器报错了。你会发现错误如下:

error[E0106]: missing lifetime specifier--> src\main.rs:9:37|
9 | fn get_greater(x: &str, y: &str) -> &str {|                   ----     ----     ^ expected named lifetime parameter|= help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y`

什么意思呢,也就是说Rust期望你手动添加一个生命周期的声明。

在Rust眼里,这个代码会返回一个字符串的引用&str。而这个引用可能来自x或者y,或者是其他的地方。

这取决于你如何写函数内部的实现,此时的Rust编译器犯迷糊了因为他不知道&str的生命周期大概是多长。
是和x一样长?还是和y一样长?还是什么?如果是和x一样长,且x的生命周期大于y的,那么返回一个y的引用就可能会出现悬空指针。
看下面例子:


fn main() {let result;let s1 = String::from("abc"); //s1生命周期大{let s2 = String::from("abcd");//s2生命周期小result = get_greater(s1.as_str(), s2.as_str()); //把小的引用赋给了大的}println!("{}", result); //此时s2已经销毁,result指针悬空
}fn get_greater(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y //此时返回了小的那一个}
}

上面的代码就产生了这种不安全的行为,归其原因在于我们可能会返回一个小的生命周期的借用给一个大的生命周期的变量。

而此时由于Rust并不清楚你的函数干了什么操作,所以推断不出返回值的借用的生命周期到底是大的还是小的。

所以此时Rust要求你对传入的参数的引用的生命周期加以限定,以保证返回值的生命周期是可以被Rust编译器推断的(没错,生命周期标注就是为了告诉Rust编译器你的返回值的生命周期是多大,从而让Rust能够检查出潜在的悬空指针错误)

生命周期标注符号使用’作为开头,例如

'a
'b
'abc

他么需要放在函数名后的尖括号里,表明这是一个生命周期标注变量

fn get_greater<'a>(x: &str, y: &str) -> &str {if x.len() > y.len() {x} else {y }
}

然后你就需要为后续的引用标注上生命周期。其写法是在&后面加上生命周期标注变量再加类型

fn get_greater<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y }
}

我们来看上面的代码,定义了一个生命周期标注变量’a, 其中x生命周期的大小是’a, y也是。返回值也是。也就是说x,y,返回值有同样的生命周期。

你可能会疑惑这是啥意思,x和y显然可能不同。此时你可以认为‘a的大小就是x和y中最小的那一个(显然让一个小的强行拥有大的是错误的)。

所以上述代码的标注在告诉编译器两件事:

  1. 对函数内部,返回的引用必须要和x和y的生命周期相同,否则函数内部的实现有问题。
  2. 对于函数外部,返回的值是x和y生命周期中最小的那一个,所以在外部进行检查时,如果将这个小的赋给了大的,那么编译器可以直接预判报错。

看完上述的两点,你会发现加了生命周期并不改变程序的一分一毫,只是给编译器指明了一条检查你错误的道路。

我们再来看下面代码加深对这句话的理解:

fn main() {let result;let s1 = String::from("abc");{let s2 = String::from("abcd");result = get_greater(s1.as_str(), s2.as_str());}println!("{}", result);
}fn get_greater<'a>(x: &'a str, y: &str) -> &'a str {if x.len() > y.len() {x} else {y //此处会报错}
}

上述代码只给x和返回值加上了生命周期的限制,也就是说返回值和x有一样的生命周期。
但是此时Rust编译器检查你的代码,发现了你逻辑上的漏洞,因为你返回了y,而y的生命周期并不是’a。所以此时在y处报错


fn main() {let result;let s1 = String::from("abc");{let s2 = String::from("abcd");result = get_greater(s1.as_str(), s2.as_str()); //在此处报错}println!("{}", result);
}fn get_greater<'a>(x: &'a str, y: &'a str) -> &'a str {if x.len() > y.len() {x} else {y}
}

此时我们对函数加以修正,Rust编译器会发现,此时函数内部返回值都有‘a的生命周期。所以函数内部的逻辑是正确的。

那么此时Rust编译器就会开始对外对调用者进行检查了,由于函数指明了x和y有相同的生命周期。

但是其中一个生命周期过小(s2的过小),导致’a就是过小的这个生命周期('a就是s2)的生命周期。此时编译器检查了一遍后就发现(编译器只看函数名就知道返回值的生命周期了,不需要知道函数内部的实现细节,这就是为啥要标注生命周期,就是给编译器看的),返回值的生命周期小于它要赋给的变量的生命周期。

也就是开头的大周期变量借用小周期变量。所以此时果断报错。

综上可以看出一件事,生命周期的标注就是为了让Rust能够在编译时,对函数内检查你的逻辑上的错误,对外检查调用时是否会发生错误。所以这个标注只起到检查作用(伺候好编译器老爷

结构体的生命周期声明

在结构体和枚举中也有可能出现引用,此时我们就需要给每个引用标注一下生命周期了。
如下:

struct Test<'a>{name: &'a String;
}

此时这个标注的含义就是,name这个生命周期至少要比结构体活得还要长。我们可以举一个反例,如下:

struct Test<'a>{name: &'a String
}fn main() {let test;{let name = String::from("abc");test = Test{name: &name //此处会报错,说name生命周期太短};}println!("{}",test.name);
}

此时Rust检查后发现了,name还没test活得长,所以果断报错。
主义在实现时实使用impl实现的时候也需要标注上生命周期,因为这个相当于是结构体名的一部分。

impl<'a> Test<'a>{fn print_hello(&self) -> (){}
}

Rust生命周期的自行推断

在某些情况下Rust可以自己推断出返回值的生命周期,主要按照如下的规则:
在这里插入图片描述
我们来看例子1:

fn test(s: &String) -> &String{}

上面的代码就没有报错,这是因为Rust已经推断出了返回值的生命周期。

  1. 根据第一条规则,每个引用类型的参数都有自己的生命周期,于是s有一个生命周期
  2. 根据第二条规则,只有一个输入,于是这个输入的生命周期将会给于返回值

于是Rust推断出了返回值引用的生命周期,这是因为返回值的生命周期只可能来自于输入,因为函数内部的创建的对象,在返回引用时会造成悬垂引用(因为函数一结束,被引用的对象就失效了)。

再来看另一种情况

impl<'a> Test<'a>{fn print_hello(&self, word: &String) -> &String{word}
}

上述情况也不会报错,理由如下:

  1. 根据第一条规则,每个引用类型的参数都有自己的生命周期,于是self和word有自己的生命周期
  2. 根据第三条原则,self的生命周期会被赋给word,和返回值,此时所有的参数的生命周期都已经推断出。

生命周期约束

对于生命周期标注来说,我们可以标注任意多的类别例如下面的这个式子:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &String{s1
}

当然,上述的代码是错误的,因为编译器无法推断出返回值的生命周期。

我们稍作修改后就得到了这个:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a String{s1
}

此时我们就得到了这个式子,就不报错了,如果我们突发奇想换一个式子呢?我们返回s2会如何

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a String{s2
}

此时编译器又报错了,因为编译器不知道s1和s2到底啥关系,返回的应该是’a的周期,但是实际返回的是’b的周期。

此时我们可以通过对生命周期的关系增加约束来达到解决问题的目的:

fn test<'a, 'b>(s1: &'a String, s2: &'b String) -> &'a Stringwhere 'b: 'a //'b生命周期大于'a
{s2
}

此时又不报错了,因为此时我们返回的是’b周期,而他比’a是要更大的。所以这个返回不会造成悬垂引用(因为返回的比’a要大,'a会造成的悬垂引用,'b不一定会造成

静态生命周期

对于一些变量它的生命周期可能是整个程序的运行期间,于是可以使用’static这个特殊的标注来声明一个整个程序期间的生命周期。

fn test(s: &'static str){}

其中,常见的字符串字面值&str采用的就是’static类型的生命周期

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

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

相关文章

【算法挨揍日记】day03——双指针算法_有效三角形的个数、和为s的两个数字

611. 有效三角形的个数 611. 有效三角形的个数https://leetcode.cn/problems/valid-triangle-number/ 题目描述&#xff1a; 给定一个包含非负整数的数组 nums &#xff0c;返回其中可以组成三角形三条边的三元组个数。 解题思路&#xff1a; 本题是一个关于三角形是否能成立…

【Diffusion】李宏毅2023机器学习Diffusion笔记

文章目录 1 想法概述2 实际过程阶段1 Add Noise阶段2 Denoise 3 数学原理4 为什么推理时要额外加入noise5 一些不知道对不对的Summary 1 想法概述 从一张充满噪声的图中不断denoise&#xff0c;最终得到一张clear的图片。为了确定当前图片中噪声占比的大小&#xff0c;同时输入…

42.SpringBoot—原理篇

一、SpringBoot原理篇。 &#xff08;1&#xff09;自动配置。 &#xff08;1.1&#xff09;bean加载方式。 &#xff08;1.1.1&#xff09;xml方式。(适用自定义bean与第三方bean&#xff09; &#xff08;1.1.2&#xff09;注解方式组件扫描。(适用于自定义bean&#xff…

【3Ds Max】布料命令的简单使用

简介 在3ds Max中&#xff0c;"布料"&#xff08;Cloth&#xff09;是一种模拟技术&#xff0c;用于模拟物体的布料、织物或软体的行为&#xff0c;例如衣物、帆布等。通过应用布料模拟&#xff0c;您可以模拟出物体在重力、碰撞和其他外力作用下的变形和动态效果。…

C#系统锁屏事件例子 - 开源研究系列文章

今天有个网友问了个关于操作系统锁屏的问题。 我们知道&#xff0c;操作系统是基于消息和事件处理的&#xff0c;所以我们只要找到该操作系统锁屏和解屏的那个事件&#xff0c;然后在事件里进行处理即可。下面是例子介绍。 1、 项目目录&#xff1b; 下面是项目目录&#xff1a…

【BUG】docker安装nacos,浏览器却无法访问到页面

个人主页&#xff1a;金鳞踏雨 个人简介&#xff1a;大家好&#xff0c;我是金鳞&#xff0c;一个初出茅庐的Java小白 目前状况&#xff1a;22届普通本科毕业生&#xff0c;几经波折了&#xff0c;现在任职于一家国内大型知名日化公司&#xff0c;从事Java开发工作 我的博客&am…

基于SSH框架实现的管理系统(包含java源码+数据库)

资料下载链接 介绍 基于SSH框架的管理系统 简洁版 &#xff1b; 实现 登录 、 注册 、 增 、 删 、 改 、 查 &#xff1b; 可继续完善增加前端、校验、其他功能等&#xff1b; 可作为 SSH&#xff08;Structs Spring Hibernate&#xff09;项目 开发练习基础模型&#xf…

前端打开后端返回的HTML格式的数据

前端打开后端返回的 HTML格式 的数据&#xff1a; 后端返回的数据格式如下示例&#xff1a; 前端通过 js 方式处理&#xff08;核心代码如下&#xff09; console.log(回调, path); // path 是后端返回的 HTML 格式数据// 必须要存进localstorage&#xff0c;否则会报错&am…

centos下使用jemalloc解决Mysql内存泄漏问题

参考&#xff1a; MySQL bug&#xff1a;https://bugs.mysql.com/bug.php?id83047&tdsourcetags_pcqq_aiomsg https://github.com/jemalloc/jemalloc/blob/dev/INSTALL.md &#xff08;1&#xff09;ptmalloc 是glibc的内存分配管理 &#xff08;2&#xff09;tcmalloc…

如何做好科技文献资料的翻译!

我们知道&#xff0c;科技文献是工程技术人员的重要参考文献&#xff0c;翻译科技文献资料有助于促进国内外科技知识和技术的传播。那么&#xff0c;如何做好科技文献资料的翻译&#xff0c;专业科技文献翻译哪家好&#xff1f; 据了解&#xff0c;科技文献翻译是一种以应用为主…

迈向通用听觉人工智能!清华电子系、火山语音携手推出认知导向的听觉大语言模型SALMONN

日前&#xff0c;清华大学电子工程系与火山语音团队携手合作&#xff0c;推出认知导向的开源听觉大语言模型SALMONN (Speech Audio Language Music Open Neural Network)。 大语言模型 SALMONN LOGO 相较于仅仅支持语音输入或非语音音频输入的其他大模型&#xff0c;SALMONN对…

insightface安装过程中提示 Microsoft Visual C++ 14.0 or greater is required.

pip install insightface安装过程中提示 Microsoft Visual C 14.0 or greater is required.Get it with "Microsoft C Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/ 根据提示网站访问官网下载生成工具 打开软件后会自动更新环境&#…

AI搜索引擎助力科学家创新

开发者希望通过帮助科学家从大量文献中发现联系从而解放科学家&#xff0c;让他们专注于发现和创新。 图片来源&#xff1a;The Project Twins 对于专注于历史的研究者Mushtaq Bilal来说&#xff0c;他在未来科技中投入了大量时间。 Bilal在丹麦南部大学&#xff08; Universit…

「UG/NX」Block UI 曲线收集器CurveCollector

✨博客主页何曾参静谧的博客📌文章专栏「UG/NX」BlockUI集合📚全部专栏「UG/NX」NX二次开发「UG/NX」BlockUI集合「VS」Visual Studio「QT」QT5程序设计「C/C+&#

机器人的运动范围

声明 该系列文章仅仅展示个人的解题思路和分析过程&#xff0c;并非一定是优质题解&#xff0c;重要的是通过分析和解决问题能让我们逐渐熟练和成长&#xff0c;从新手到大佬离不开一个磨练的过程&#xff0c;加油&#xff01; 原题链接 机器人的运动范围https://leetcode.c…

github以及上传代码处理

最近在github上传代码的时候出现了&#xff1a; /video_parser# git push -u origin main Username for https://github.com: gtnyxxx Password for https://gtny2010github.com: remote: Support for password authentication was removed on August 13, 2021. remote: Plea…

VB6编程IEEE浮点算法实践

纯代码实现浮点计算实际上对浮点算法的再实践。IEEE浮点表示法是Modbus RTU协议至今还在用的传送编码&#xff0c;更是WITS 1记录标准的基础。以往实现 MKI、CVI&#xff0c;MKL、CVL&#xff0c;MKS、CVS&#xff0c;MKD、CVD在高级语言里封装了现成的语句&#xff0c;现在Pow…

创建和运行 Ansible 临时命令

创建和运行 Ansible 临时命令 作为系统管理员&#xff0c;您需要在受管节点上安装软件。 请按照正文所述&#xff0c;创建一个名为 /home/curtis/ansible/adhoc.sh 的 shell 脚本&#xff0c;该脚本将使用 Ansible 临时命令在各个受管节点上安装 yum 存储库&#xff1a; 存储库…

【C++】函数指针

2023年8月18日&#xff0c;周五上午 今天在B站看Qt教学视频的时候遇到了 目录 语法和typedef或using结合我的总结 语法 返回类型 (*指针变量名)(参数列表)以下是一些示例来说明如何声明不同类型的函数指针&#xff1a; 声明一个不接受任何参数且返回void的函数指针&#xf…

OJ练习第151题——克隆图

克隆图 力扣链接&#xff1a;133. 克隆图 题目描述 给你无向 连通 图中一个节点的引用&#xff0c;请你返回该图的 深拷贝&#xff08;克隆&#xff09;。 示例 分析 对于一张图而言&#xff0c;它的深拷贝即构建一张与原图结构&#xff0c;值均一样的图&#xff0c;但是…