Rust的常数、作用域与所有权

2024-09-07 10:20

本文主要是介绍Rust的常数、作用域与所有权,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

【图书介绍】《Rust编程与项目实战》-CSDN博客

《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

Rust到底值不值得学,之一  -CSDN博客

Rust到底值不值得学,之二-CSDN博客

Rust的数据类型-CSDN博客

3.7  常数的数据类型

在Rust语言中,变量有类型,常量也有类型。我们知道,在定义const常量的时候,就要确定数据类型,这个问题不大,而直接常量如何确定类型呢?首先想个问题,为什么要把常量分为不同的类型呢?这是因为在程序中出现的常量需要存放在计算机内存的存储单元中。如果确定了数据的类型,也就能确定应该分配给它多少字节,按什么方式存储。例如,程序中有整数100,默认情况下,Rust编译器会分配给它4字节,按补码方式存储。那么怎样确定常量的类型呢?

从常量的表示形式即可判定其类型。对于字符常量很简单,只要看到由单撇号引起来的单个字符或转义字符就是字符常量。下面我们来讲一下数值常量。

(1)整数。整数在Rust中有一种特殊的表达方式,比如let a =33u16,因为u8、i8、u16、i32等都可以表示33,所以不指定类型的话,只有33,Rust就不知道它的精度是多少,于是let a=33会自动将a推断成i32。总之,对于没有明确指定整数类型的整数,且其值在i32范围内,Rust默认就认为它是i32类型。如果超出i32范围,且在i64范围内,则默认它是i64类型。

如果在整数后面加上u16类型,那么Rust就知道这是个u16类型的整数。对于let a=33u16,可以知道a是一个u16类型的变量,这和let a:u16=33的作用相同。当然,let a:u16=33u16也可以,只不过有点多此一举。但是let a:u16=33u32这种方式不可行,因为前后矛盾了。同样,如果在整数后面加上i8,且该整数没超出i8范围,就认为它是i8类型。

以上整数都是用十进制表示的,我们也可以使用二进制、八进制或十六进制创建整数,比如:

fn main() {let a = 33u16;let b: i32 = 0b11_01_10_11;                     // 二进制let c = 0o567i64;                                // 八进制let d = 0xFFFFu32;                               // 十六进制println!("{} {} {} {}", a, b, c, d);           // 输出:33 219 375 65535}

(2)浮点数。凡以小数形式或指数形式出现的实数,都是浮点型常量,在内存中都以指数形式存储。例如,10是整型常量,10.0是浮点型常量。那么对浮点型常量是按f32处理还是按f64处理呢?Rust编译器把浮点型常量都按f64处理,分配8字节。

如果要明确指定浮点数类型,可以在后面加上类型,比如3.14159f32,这样就按f32处理了。

3.8  作  用  域

Rust的所有权系统和作用域息息相关,因此有必要先理解Rust的作用域规则。在Rust中,任何一个可用来包含代码的大括号都是一个单独的作用域。类似于Struct{}这样用来定义数据类型的大括号,不在讨论范围之内,本章后面所说的大括号也都不考虑这种大括号。以下几种结构中的大括号都有自己的作用域:

(1)if、while等流程控制语句中的大括号。

(2)match模式匹配的大括号。

(3)单独的大括号。

(4)函数定义的大括号。

(5)mod定义模块的大括号。

例如,可以单独使用一个大括号来开启一个作用域:

{                                         // s在这一行无效,因为它尚未声明let s = "hello";                     // 从这行起,s是有效的println!("{}", s);                   // 使用sprintln!("hello,world");            // 这行没有用到s,但s依然是有效的}                                         // 到了这行,此作用域已结束,s不再有效

上面的代码中,变量s绑定了字符串字面值,在跳出作用域后,变量s失效,变量s所绑定的值会自动被销毁。

实际上,变量跳出作用域失效时,会自动调用Drop trait的drop函数来销毁该变量绑定在内存中的数据,这里特指销毁堆和栈上的数据,而字符串字面量是存放在全局内存中的,它会在程序启动到程序终止期间一直存在,不会被销毁。可通过如下代码验证:

fn main(){{let s = "hello";println!("{:p}", s);  // 0x7ff6ce0cd3f8}let s = "hello";println!("{:p}", s);  // 0x7ff6ce0cd3f8}

因此,上面的示例中只是让变量s失效了,仅此而已,并没有销毁s所绑定的字符串字面量。但一般情况下不考虑这些细节,而是照常描述为跳出作用域时,会自动销毁变量所绑定的值。

任意大括号之间都可以嵌套。例如,可以在函数定义的内部再定义函数,在函数内部使用单独的大括号,在函数内部使用mod定义模块,等等。示例如下:

fn main(){fn ff(){println!("hello world");}ff();let mut a = 33;{a += 1;}println!("{}", a);  // 结果输出:34}

虽然任何一种大括号都有自己的作用域,但函数作用域比较特别。在函数作用域内无法访问函数外部的变量,而其他大括号的作用域可以访问大括号外部的变量。比如:

fn main() {let x = 32;fn f(){// 编译错误,不能访问函数外面的变量x和y// println!("{}, {}", x, y); }let y = 33;f();let mut a = 33;{// 可以访问大括号外面的变量aa += 1;}println!("{}", a);  //结果输出:34}

在Rust中,能否访问外部变量称为捕获环境。比如,函数是不能捕获环境的,而大括号可以捕获环境。对于可捕获环境的大括号作用域,要注意Rust的变量遮盖行为。分析下面的代码:

fn main(){let mut a = 33;{a += 1;           // 访问并修改外部变量a的值// 又声明变量a,这会发生变量遮盖现象// 从此开始,大括号内访问的变量a都是该变量let mut a = 44;a += 2;println!("{}", a);             // 输出46}                                 // 大括号内声明的变量a失效println!("{}", a);               // 输出34}

这种行为和其他语言不太一样,因此这种行为需要引起注意。

3.9  所  有  权

3.9.1  让我们回忆栈和堆

在学习C/C++时,老师经常出某变量被分配在栈上还是堆上的题目,几乎每次都有很大一批同学在这种题目上失手。栈和堆都是代码在运行时可供使用的内存,但是它们的结构不同。栈是一种后进先出(Last In First Out,LIFO)的数据结构,栈中的所有数据都必须占用已知且固定大小的内存。堆就好理解了,它是一个没有组织的结构,想怎么使用就怎么使用,只要堆够大,就可以申请一段内存空间,然后把这段内存标记为已使用,并得到指向这段内存开头的指针;当不再使用时,再将这段内存标记为未使用。当声明一个指针但并没有分配空间时,这个指针是空指针;当内存已经标记为未使用,而指针依然指向这段空间时,这个指针就是野指针。

C++中的堆和栈定义如下。

  • 堆:由程序员手动分配和释放,完全不同于数据结构中的堆,分配方式类似于链表。由malloc或者new来分配,由free和delete来释放。若程序员不释放,则程序结束时由系统释放。
  • 栈:由编译器自动分配和释放,存放函数的参数值、局部变量的值等。栈里面变量的内存必须是已知且固定大小的。函数调用时,参数、本地变量、指向堆的指针都压入一个栈,函数完成时退出。操作方式类似于数据结构中的栈(C和Python中也有,只要基于C的都有这个概念)。

其实分辨起来很容易,动态分配的变量就是在堆上,其他的都在栈上。

栈是一个成熟的结构,基本不会引发内存的问题,而没有组织的堆却很容易引发内存问题。垃圾回收和所有权都是为了解决堆的内存管理问题。

这就是C++相比于垃圾回收机制语言的优势,灵活高效,但是也会带来内存安全问题。

3.9.2  什么是所有权

计算机程序必须在运行时管理它们所使用的内存资源。大多数编程语言都有管理内存的功能:C/C++这样的语言主要通过手动方式管理内存,开发者需要手动申请和释放内存资源。但为了提高开发效率,只要不影响程序功能的实现,许多开发者没有及时释放内存的习惯。所以手动管理内存的方式常常造成资源浪费。

Java语言编写的程序在Java虚拟机(Java Virtual Machine,JVM)中运行,JVM具备自动回收内存资源的功能。但这种方式常常会降低运行时效率,所以JVM会尽可能少地回收资源,这样也会使程序占用较大的内存资源。

所有权对大多数开发者而言是一个新颖的概念,它是Rust语言为高效使用内存而设计的语法机制。所有权概念是为了让Rust在编译阶段更有效地分析内存资源的有用性以实现内存管理而诞生的概念。

Rust的所有权是一个跨时代的理念,是内存管理的第二次革命。较低级的语言依赖程序员分配和释放内存,一不小心就会出现空指针、野指针破坏内存;较高级的语言使用垃圾回收机制管理内存,在程序运行时不断地寻找不再使用的内存,虽然安全,却加重了程序的负担。Rust的所有权理念横空出世,通过所有权系统管理内存,编译器在编译时会根据一系列的规则进行检查,在运行时,所有权系统的任何功能都不会减慢程序,把安全的内存管理推向了零开销的新时代。

所有权概念是Rust语言的一个重要特性,因为通过它才使得Rust的“安全”“高并发”得以发挥出优势。因为它让Rust无须垃圾回收,即可保障内存安全。对于C/C++程序员来说,可能一直在跟内存安全打交道,如内存泄露、智能指针等。对于别的语言来说,会有垃圾回收机制。例如Python的垃圾回收机制,有“标记清除”“分代回收”等方式。这两种方式各有优缺点。Rust则是通过所有权和借用来保证内存安全的。很多人不理解为什么Rust是内存安全的,其实就是在默认情况下,是写不出内存不安全的代码的。

Rust的所有权并不难理解,它有且只有如下三条规则:

(1)Rust中的每个值都有一个被称为其所有者的变量(即值的所有者是某个变量)。

(2)值在任一时刻有且只有一个所有者。

(3)当所有者(变量)离开作用域时,这个值将被销毁。

这里对第三点做一些补充性的解释,所有者离开作用域会导致值被销毁,这个过程实际上是调用一个名为drop的函数来销毁数据释放内存的。在前面解释作用域规则时曾提到过,销毁的数据特指堆栈中的数据,如果变量绑定的值是全局内存区内的数据,则数据不会被销毁。例如:

fn main(){{let mut s = String::from("hello");} // 跳出作用域,栈中的变量s将被销毁,其指向的堆中的数据也被销毁// 但全局内存区的字符串字面量仍被保留}

Rust中的每个值都有一个所有者,但这个说法比较容易产生误会。例如:

#![allow(unused)]fn main() {let s = String::from("hello");}

很多人可能会误以为变量s是堆中字符串数据hello的所有者,但实际上不是。String字符串的实际数据在堆中,但是String大小不确定,所以在栈中使用一个胖指针结构来表示这个String类型的数据,这个胖指针中的指针指向堆中的String的实际数据。也就是说,变量s的值是那个胖指针,而不是堆中的实际数据。因此,变量s是那个胖指针的所有者,而不是堆中实际数据的所有者。但是,由于胖指针是指向堆中数据的,很多时候为了简化理解,简化描述方式,经常会说s是哪个堆中实际数据的所有者。但无论如何描述,都需要理解所有者和值之间的真相。

这篇关于Rust的常数、作用域与所有权的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1144795

相关文章

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

第二十四章 rust中的运算符重载

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust中的运算符重载 目录 注意一、前言二、基本使用三、常用运算符四、通用约束 一、前言 C/C++中有运算符重载这一概念,它的目的是让即使含不相干的内容也能通过我们自定义的方法进行运算符操作运算。 比如字符串本身是不能相加的,但由于C++中的String重载了运算符+,所以我们就可以将两个字符串进行相加、但实际

【C++】作用域指针、智能指针、共享指针、弱指针

十、智能指针、共享指针 从上篇文章 【C++】如何用C++创建对象,理解作用域、堆栈、内存分配-CSDN博客 中我们知道,你的对象是创建在栈上还是在堆上,最大的区别就是对象的作用域不一样。所以在C++中,一旦程序进入另外一个作用域,那其他作用域的对象就自动销毁了。这种机制有好有坏。我们可以利用这个机制,比如可以自动化我们的代码,像智能指针、作用域锁(scoped_lock)等都是利用了这种机制。

js私有作用域(function(){})(); 模仿块级作用域

摘自:http://outofmemory.cn/wr/?u=http%3A%2F%2Fwww.phpvar.com%2Farchives%2F3033.html js没有块级作用域,简单的例子: for(var i=0;i<10;i++){alert(i);}alert(i); for循环后的i,在其它语言像c、java中,会在for结束后被销毁,但js在后续的操作中仍然能访

C语言作用域

作用域 (scope) 是描述程序可以访问标识符的区域。         一个标识符可以有块作用域、函数作用域、函数原型作用域、文件作用域和全局作用域。 1. 块作用域 (block scope)         块是一对花括号 {} 括起来的区域 或 函数体内任意复合语句定义范围内的区域。         定义在块中的变量具有块作用域。块作用域的变量的可见范围从定义处开始

【Rust光年纪】Rust 机器人学库全景:功能、安装与API概览

机器人学+Rust语言=无限可能:六款库带你开启创新之旅! 前言 随着机器人技术的快速发展,对于机器人学领域的高效、可靠的编程语言和库的需求也日益增加。本文将探讨一些用于 Rust 语言的机器人学库,以及它们的核心功能、使用场景、安装配置和 API 概览,旨在为机器人学爱好者和开发人员提供参考和指导。 欢迎订阅专栏:Rust光年纪 文章目录 机器人学+Rust语言=无限可能:

大话C++:第6篇 命名空间namespace作用域

1 命名空间概述 在一个大型的软件项目中,可能会有许多不同的代码文件,这些文件可能由不同的开发者编写,或者来自不同的库和模块。如果这些代码文件中存在同名的变量、函数、类或其他标识符,那么在编译或运行时就可能发生命名冲突,导致程序无法正确执行。 通过使用命名空间(namespace),开发者可以将相关的代码、变量、函数等组织在一起,形成一个独立的命名空间。这样,即使不同的代码片段中使用了相同的标

第二十二章 rust数据库使用:sea-orm详解

注意 本系列文章已升级、转移至我的自建站点中,本章原文为:rust数据库使用:sea-orm详解 目录 注意一、前言二、项目管理三、迁移文件四、实体文件五、业务使用 一、前言 只要开发稍微大型一点的项目,数据库都是离不开的。 rust目前并没有特别成熟的数据库框架,sea-orm这个框架是我目前所看到的成熟度最高的一个,并且仍在积极开发中。 所以本文将以sea-orm框

Rust使用之【宏】

一、简单使用clap clap = { version = "4.5.17", features = ["derive"] } 其中,什么是features = ["derive"]:表示你希望在添加 clap 依赖时启用 derive 特性。这通常意味着你希望使用 clap 的派生(derive)宏功能,这些功能可以简化创建命令行接口的代码。例如,derive 特性可以让你使用 #[der