Rust学习笔记:深度解析内存管理(二)

7f39db826ad89d2987bd97da1544abcd.jpeg

在这个信息爆炸的时代,学习一门新的编程语言不仅仅是为了找到一份好工作,更是为了打开思维的新窗口。Rust,作为一门注重安全、速度和并发的系统编程语言,正吸引着越来越多的年轻开发者的目光。今天,我们将一起深入探讨Rust的内存管理机制,包括它独特的所有权系统、借用规则以及引用/指针的使用,带你领略Rust语言的魅力所在。

在Rust中,内存管理是其核心特性之一,不同于其他语言需要开发者手动管理内存或完全依赖垃圾回收机制,Rust通过所有权、借用和生命周期等概念,有效防止了内存泄露和数据竞争等问题,确保了代码的安全性和高效性。

a189ce73f1cfdf17420c497928aceaf0.jpeg

内存管理入门:从传统到Rust的革新之路

在软件开发的世界里,如何高效、安全地管理内存是每个开发者都必须面对的挑战。不同的编程语言采取了不同的策略来解决这一问题,而Rust语言在这方面采用了一种独特且革命性的方法——所有权系统。在深入了解Rust的所有权之前,让我们先回顾一下其他语言是如何管理内存的。

传统内存管理方法

  • 垃圾收集(Garbage Collection):Java、Go等语言通过垃圾收集器自动查找并释放不再使用的内存。这种方法虽然减轻了开发者的负担,但可能会对性能产生不利影响。

  • 手动内存管理:C/C++等语言要求程序员手动分配和释放内存。这增加了程序的灵活性,但同时也增加了内存安全风险,需要开发者承担更多的责任。

  • 引用计数:Python等语言使用引用计数来跟踪每个对象的引用数量。当对象的引用计数降至零时,该对象被视为不再需要,并由垃圾收集器回收。

Rust的革新之路:所有权系统

Rust采用了一种全新的内存管理模型——所有权系统,它通过在编译时检查规则,并定义运行时行为来决定何时释放内存,从而实现了内存安全和性能的平衡。Rust的所有权系统基于三条基本规则:

  • Rust中的每个值都有一个所有者。

  • 一次只能有一个所有者拥有该值。

  • 当所有者离开作用域时,这个值会被自动释放。

这种方法不仅提高了内存安全性,还通过将大部分内存处理功能的检查放在编译时,提高了程序的性能。与传统的内存管理方法相比,Rust的所有权机制无疑提供了一种更为高效和安全的解决方案。

Rust内存管理:所有权与作用域

在Rust的学习之路上,理解内存管理是一道不可或缺的关卡。Rust通过所有权(Ownership)机制来管理内存,这一机制的核心在于:内存的每一块资源只能有一个所有者,当所有者结束生命周期时,相关资源将被自动释放。这听起来可能有些抽象,但通过几个简单的例子,我们可以更深入地理解这一概念。

所有权与变量作用域

让我们从最基本的例子开始:

fn main() {
    let s = String::from("Brian");
    println!("{}", s);
}

在这个例子中,当变量s被声明时,它在堆上分配了内存。根据Rust的规则,当拥有该内存的变量s离开作用域后,Rust会自动释放这部分内存。在这个例子里,变量s在main函数执行完毕后离开作用域。

再来看一个稍微复杂一点的例子,引入了内部作用域:

fn main() {
    {
        let s = String::from("Brian");
        println!("{}", s);
        // 内部作用域
    }
    let s2 = String::from("Brian 2");
    println!("{}", s2); 
    // 外部作用域
}

在这里,由于增加了额外的大括号,s的作用域被限制在了内部大括号里,因此,当内部作用域结束时,s所占用的内存就会被释放。然后,外部作用域的s2同样在main函数结束时被释放。

所有权转移与克隆

Rust中的所有权机制确保了内存的安全使用,但这也意味着一块内存的所有权在任一时刻只能属于一个变量。看看下面这个例子:

fn main() {
    let s = String::from("Brian");
    let s2 = s;
    println!("{}", s); // 编译错误,因为s的所有权已经转移给了s2
}

要解决这个问题,我们可以使用克隆:

fn main() {
    let s = String::from("Brian");
    let s2 = s.clone();
    println!("{} : {}", s, s2); // 正常工作,因为s被克隆,所有权没有被转移
}

在Rust中,所有权(Ownership)是其内存管理的核心概念,通过一系列规则确保内存安全和程序效率。理解所有权的转移和借用是掌握Rust的关键。以下是对上述内容的补充和详细解释:

所有权的转移

  • 通过赋值或变量绑定改变所有权:当一个变量赋值给另一个变量时,原始变量的所有权会转移给新变量。这意味着之前的变量将无法再被访问,从而防止了悬垂指针或重复释放内存的问题。

  • 通过函数传递数据改变所有权:将变量作为参数传递给函数或从函数返回值时,所有权也可能发生转移。如果函数取得了某个值的所有权,那么原始变量将无法再次使用,除非这个值被返回。

防止问题的策略

为了避免由于所有权系统导致的使用限制,Rust提供了一些策略:

  • 使用引用:当不需要完全拥有值时,可以使用引用(&T和&mut T)来借用值。这样可以在不转移所有权的情况下访问或修改数据,同时保持内存安全。

  • 复制值:如果类型实现了Copy trait,那么在赋值或函数传递时,原始数据将被自动复制,而不是移动所有权。这适用于一些简单的类型,如整数类型和布尔类型,但不适用于如String这样的需要堆分配的类型。

  • 减少长寿命对象数量:通过重构代码来减少需要长时间持有的对象,可以减少内存占用和复杂度,提高程序效率。

  • 包装数据类型:通过创建或使用结构体(Structs)等类型来包装数据,可以更有效地管理数据的所有权和借用,尤其是在处理复杂数据结构时。

避免双重释放错误

所有权的一个重要原因是避免双重释放错误(double free error)。如果允许多个变量拥有同一块内存的所有权,当这些变量被销毁时,相同的内存会被释放多次,导致程序崩溃或安全漏洞。Rust通过确保每块内存只有一个所有者来防止这种情况发生。

函数与所有权

在Rust中,将变量传递给函数时,可能会发生所有权的移动或复制,这取决于变量的类型:

fn main() {
    let s = String::from("Brian");
    print_string(s);
    // println!("{}", s);  这将失败,因为s的所有权已经移动到了函数中
    
    let i = 192;
    print_int(i);
    println!("{}", i); // 这可以工作,因为i是基本类型,其大小已知且在栈上分配
}

fn print_string(s_in : String) {
    println!("{}", s_in);
}

fn print_int(i_in : i32) {
    println!("{}", i_in);
}

在Rust中,处理堆上分配的值(如String类型)与处理栈上分配的基本类型值(如i32)时,所有权的规则表现出明显的不同。通过前面提到的例子,我们可以深入探讨这一差异及其对函数调用和返回值的影响。

堆上分配的值与所有权

当我们调用print_string函数并传递一个String类型的变量时,这个变量的所有权被移动到了函数内部。因此,一旦函数调用完成,原始变量s就不再持有这个字符串的所有权,也就无法再次访问它。这是因为String类型的数据存储在堆上,Rust通过所有权机制来管理堆内存,确保内存安全。

栈上分配的基本类型与所有权

相比之下,基本类型如i32存储在栈上,当它们被传递给函数时,Rust会进行数据的拷贝而不是移动所有权。这意味着即使在调用print_int函数后,原始变量i仍然可以被访问,因为它的值在函数调用时被复制了。

函数返回值与所有权的转移

为了解决因所有权转移而导致的变量不可用的问题,我们可以通过函数返回值来重新获得所有权。在修改后的print_string例子中,函数接收一个String类型的参数,并将这个参数作为返回值返回。这样做的结果是,函数内部的所有权操作完成后,将所有权返回给调用者。

fn main() {
    let s = String::from("Brian");
    let s = print_string(s); // 将s的所有权传给函数,然后通过返回值重新获得所有权
    println!("{}", s); // 这里可以正常使用s,因为所有权已经通过函数返回值返回
}

fn print_string(s_in: String) -> String {
    println!("{}", s_in);
    s_in // 返回s_in,这将所有权从函数内部转移回调用者
}

阴影(shadowing)与冻结变量

Rust还允许"阴影"变量,即在相同的作用域内用新的值重新声明同名变量:

fn main() {
    let shadowed_var = 12; 
    {
        println!("before being shadowed: {}", shadowed_var);

        let shadowed_var = "abc"; 

        println!("shadowed in inner block: {}", shadowed_var);
    }
    println!("outside inner block: {}", shadowed_var);

    let shadowed_var = 22; 
    println!("shadowed in outer block: {}", shadowed_var);
}

最后,变量还可以被冻结,即在某个作用域内,之前可变的变量变为不可变:

fn main() {
    let mut mutable_var = 7i32;

    {
        let mutable_var = mutable_var;
        println!("{}", mutable_var);
        // mutable_var = 50; // 错误!在这个作用域内`mutable_var`是不可变的

    }

    mutable_var = 3;
    println!("{}", mutable_var);
}

通过这些例子,我们可以看到,Rust通过所有权、作用域、变量阴影和冻结等机制,提供了一种既高效又安全的方式来管理内存。这些概念初看起来可能有些复杂,但一旦掌握,你将能够编写出更加安全和高效的Rust代码。

借用(Borrowing)和引用(References)

在Rust中,借用(Borrowing)和引用(References)是管理和访问数据的关键机制,而不需要获取数据的所有权。这使得在不改变原始数据所有权的情况下,安全地共享和操作数据成为可能。

不可变借用

通过不可变借用,你可以创建对变量的引用,这样就可以读取或使用数据,而无需修改它。看看下面的例子:

fn main() {
    let data = String::from("Brian");
    let reference_a = &data;
    let reference_b = &data;
    println!("Original data: {}", data); // 因为我们采用了引用,所以data被借用了,并且仍然可以访问
    println!("Reference a: {}", reference_a);
    println!("Reference b: {}", reference_b);
}

如果你尝试移除第三行中reference_b声明的&符号,改为let reference_b = data;,编译器将会报错。这是因为reference_a已经“借用”了data的值,而此时你又尝试将data的所有权移动到reference_b,这违反了Rust的内存安全规则。

借用检查器

编译器中负责检查这些规则的部分叫做借用检查器(borrow checker)。当它发现代码可能违反Rust的内存规则时,它会引发错误,阻止代码编译。这种在编译时期就发现内存问题的能力,对于保持运行时性能和安全性来说是一个巨大的优势。

函数中的不可变引用

在函数中使用不可变引用是非常常见的,这允许你传递数据给函数而不转移所有权:

fn combined_length(s1: &String, s2: &String) -> usize {
    s1.len() + s2.len()
}

fn main() {
    let first = String::from("Brian");
    let second = String::from("Enochson");
    let total_length = combined_length(&first, &second);
    println!("The combined length of my two string is: {}", total_length);
    // 因为我们只传递了引用,所以变量在这里仍然可以使用
    println!("Second string: {}", second);
}

可变借用

Rust同样支持可变借用,但需要明确声明。这符合Rust的设计哲学,旨在避免给开发者带来意外的行为:

fn main() {
    let mut first = String::from("Brian");
    let mut_second = &mut first;
    mut_second.push_str(" Enochson");
    println!("Modified data via reference: {}", mut_second);
    // 注意:此时尝试直接访问first可能会引起编译错误,因为已经存在对first的可变引用
}

解引用

使用*符号对引用的变量进行解引用,这不是类型转换,而是指示编译器“跟随”引用到底层类型:

fn swap(a: &mut i32, b: &mut i32) {
    let temp_v = *a;
    *a = *b;
    *b = temp_v;
}

fn main() {
    let mut a = 5;
    let mut b = 10;
    swap(&mut a, &mut b);
    println!("After swap a: {}, b: {}", a, b); 
}

原始指针

虽然在Rust中直接操作原始指针不常见,但在某些场景,尤其是库开发中可能会用到。这通常需要使用unsafe代码块,因为它允许绕过Rust的安全保证:

fn main() {
    let x = 5;
    let raw = &x as *const i32; // 将x的引用转换为原始指针

    let points_at = unsafe { *raw };

    println!("raw pointers value is {}", points_at);
}

这里使用unsafe关键字是因为解引用原始指针可能会导致未定义行为,Rust要求开发者在这种情况下明确表明自己的意图。通过这些机制,Rust在提供强大功能的同时,确保了代码的安全性和高效性。

结束

在这第二期中,我们深入探讨了Rust的内存管理概念,并通过代码示例来凸显每个要点。我们研究了基于所有权的Rust独特的内存模型。同时,也覆盖了阴影(Shadowing)、借用(Borrowing)、引用(References)以及指针(Pointers)等主题。有了这些基础知识,我们将在下一篇《Rust学习笔记》文章中探讨流程控制,并更深入地研究函数。

Rust的内存安全特性和所有权系统提供了一种高效且安全的方式来管理内存,避免了传统编程语言中常见的内存泄漏和数据竞争问题。通过不可变和可变借用,Rust能够在编译时检查数据竞争,从而在不牺牲性能的情况下,确保并发安全。此外,Rust通过引用和指针提供了灵活的数据访问方式,同时保持了代码的安全性。

理解和掌握这些概念对于编写高效、安全的Rust代码至关重要。随着我们对Rust更深入的探索,你将能够利用这些强大的特性来构建可靠和高性能的应用程序。

期待在接下来的文章中,我们将继续探索Rust的更多高级特性,包括流程控制和函数等。希望你能在学习Rust的过程中发现其独特的魅力,并将这些知识应用到实际的项目中去。

相关内容

Rust学习笔记:基础概念介绍(一)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

线程的同步互斥机制3月4日

题目&#xff1a; 代码&#xff1a; #include <stdio.h> #include <pthread.h> #include <string.h> #include <semaphore.h> #include <unistd.h>sem_t sem1,sem2;void* callback1(void*arg) {while(1){if(sem_wait(&sem1)<0) //等待…

python二级常见题目

一.常见语法 jieba—第三方中文分词函数库 jieba—第三方中文分词函数库_jieba库函数-CSDN博客 Python基础——format格式化 Python基础——format格式化_python format-CSDN博客 format()方法的使用超全_format方法-CSDN博客 Python中random函数用法整理 Python中random…

判断回文字符串

判断回文字符串 题目描述&#xff1a;解法思路&#xff1a;解法代码&#xff1a;运行结果&#xff1a; 题目描述&#xff1a; 输入⼀个字符串&#xff0c;判断这个字符串是否是回文字符串&#xff08;字符串的长度小于等于30&#xff0c;字符串不包含空格&#xff09;&#xf…

聊一聊日常开发中如何优雅的避免那无处不在的空指针异常

在Java编程语言中&#xff0c;NullPointerException&#xff08;简称NPE&#xff09;是一种常见的运行时异常&#xff0c;当程序试图访问或操作一个还未初始化&#xff08;即值为null&#xff09;的对象引用时&#xff0c;Java虚拟机就会抛出NullPointerException。如果我们在日…

DataWorks(ODPS)性能优化技巧指南

使用阿里云DataWorks进行数据处理的时候&#xff0c;有时候会遇到一个sql或pyodps&#xff08;本质上还是转化为sql&#xff09;执行很长的情况&#xff0c;这个时候有必要对代码进行性能优化。 一、打开ODPS运行评估报告 一个sql脚本执行完毕后&#xff0c;在运维中心的周期…

java中Timer和Timertask的关系

一 time和timertask的关系 1.1 timer和timertask关系 1.Timer来讲就是一个调度器,而TimerTask呢只是一个实现了run方法的一个类&#xff1b; 2.Timer和TimerTask成对出现&#xff0c;Timer是定时器&#xff0c;TimerTask是定时任务。换句话说&#xff0c;定时任务TimerTask是…

程序员必备的linux常用的26条命令

04 穿越功耗墙&#xff0c;我们该从哪些方面提升“性能”&#xff1f; 上一讲&#xff0c;在讲 CPU 的性能时&#xff0c;我们提到了这样一个公式&#xff1a; 程序的 CPU 执行时间 指令数CPIClock Cycle Time 这么来看&#xff0c;如果要提升计算机的性能&#xff0c;我们可以…

鸿蒙实战开发:【SIM卡管理】

概述 本示例展示了电话服务中SIM卡相关功能&#xff0c;包含SIM卡的服务提供商、ISO国家码、归属PLMN号信息&#xff0c;以及默认语音卡功能。 样例展示 基础信息 介绍 本示例使用sim相关接口&#xff0c;展示了电话服务中SIM卡相关功能&#xff0c;包含SIM卡的服务提供商、…

Chat GPT:AI聊天机器人的革命性突破!

一、引言 近年来&#xff0c;人工智能&#xff08;AI&#xff09;技术的发展日新月异&#xff0c;其中最具代表性的成果之一便是Chat GPT。这款基于自然语言处理&#xff08;NLP&#xff09;技术的聊天机器人&#xff0c;以其高度智能、灵活多变的特点&#xff0c;迅速吸引了全…

C/C++工程师面试题(STL篇)

STL 中有哪些常见的容器 STL 中容器分为顺序容器、关联式容器、容器适配器三种类型&#xff0c;三种类型容器特性分别如下&#xff1a; 1. 顺序容器 容器并非排序的&#xff0c;元素的插入位置同元素的值无关&#xff0c;包含 vector、deque、list vector&#xff1a;动态数组…

LeetCode-第162题-寻找峰值

1.题目描述 峰值元素是指其值严格大于左右相邻值的元素。 给你一个整数数组 nums&#xff0c;找到峰值元素并返回其索引。数组可能包含多个峰值&#xff0c;在这种情况下&#xff0c;返回 任何一个峰值 所在位置即可。 你可以假设 nums[-1] nums[n] -∞ 。 你必须实现时间…

Kali Linux 2024.1

Kali Linux 2024.1刚刚发布&#xff0c;标志着这个备受欢迎的安全重点Linux发行版在今年的首次重大更新。以其先进的渗透测试和安全审计功能而闻名&#xff0c;它是安全专业人员和爱好者的首选工具。 Kali 2024.1 亮点 本次发布由 Linux 内核 6.6 提供支持&#xff0c;突出了…

四年一段旅途,一个起点,一个机会

不得不感慨一下&#xff0c;现在的年轻人、大学生实在是太厉害了 最近加入了一个社群&#xff0c;是一名大三学生创建的&#xff0c;他短短一年间&#xff0c;就创建了一个数千人的社群&#xff0c;还运营的几十个副业社群&#xff0c;一年的时间变现100W&#xff0c;这些成绩…

动态前缀和数组:树状数组

前缀和的不足 前缀和是一种常见的算法思想&#xff0c;能够实现在常数时间复杂度下得到某个子区间内所有元素和。以一维数组 nums 为例&#xff0c;定义前缀和数组 preSum&#xff0c;preSum[i] 表示 nums 前 i 个元素的和&#xff0c;利用动态规划的思想&#xff0c;易得 pre…

力扣128. 最长连续序列(哈希表)

Problem: 128. 最长连续序列 文章目录 题目描述思路复杂度Code 题目描述 思路 1.先将数组中的元素存入到一个set集合中&#xff08;去除重复的元素&#xff09; 2.欲找出最长连续序列&#xff08;先定义两个int变量longestSequence和currentSequence用于记录最长连续序列和当前…

HTML5:七天学会基础动画网页7

CSS3高级特效 2D转换方法 移动:translate() 旋转:rotate() 缩放:scale() 倾斜:skew() 属性:transform 作用:对元素进行移动,旋转,缩放,倾斜。 2D移动 设定元素从当前位置移动到给定位置(x,y) 方法 说明 translate(x,y) 2D转换 沿X轴和Y轴移…

【Python】OpenCV-使用ResNet50进行图像分类

使用ResNet50进行图像分类 如何使用ResNet50模型对图像进行分类。 import os import cv2 import numpy as np from tensorflow.keras.applications.resnet50 import ResNet50, preprocess_input, decode_predictions from tensorflow.keras.preprocessing import image# 设置…

【计算机网络】IO多路转接之poll

文章目录 一、poll函数接口二、socket就绪条件三、poll的优点四、poll的缺点五、poll使用案例--只读取数据的server服务器1.err.hpp2.log.hpp3.sock.hpp4.pollServer.hpp5.main.cc 一、poll函数接口 #include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int t…

EmoLLM(心理健康大模型)——探索心灵的深海,用智能的语言照亮情感的迷雾。

文章目录 介绍&#xff1a;应用地址&#xff1a;模型地址&#xff1a;Github地址&#xff1a;视频介绍&#xff1a;效果图&#xff1a; 介绍&#xff1a; EmoLLM是一个基于 InternLM 等模型微调的心理健康大模型&#xff0c;它涵盖了认知、情感、行为、社会环境、生理健康、心…

Python绘制不同形状词云图

目录 1.基本词云图1.1 导入所需库1.2 准备词汇1.3 配置参数并生成词云图1.4 在Python窗口中显示图片1.5 效果展示1.6 完整代码 2. 不同形状词云图2.1 找到自己所需形状图片2.2 利用PS将图片设置为黑白色2.3 在代码中设置背景2.4 效果展示 1.基本词云图 1.1 导入所需库 import…