【Rust】引用和借用,字符串切片 (slice) 类型 (&str)——Rust语言基础12

1. 前言

首先让我们认真回顾一下上篇文章介绍的所有权以及作用域,在拥有之前的知识基础上看看下面的代码会有什么问题。

fn main() {
    let s1 = String::from("ImagineMiracle");

    let len = str_length(s1);

    println!("The length of '{}' is {}.", s1, len);

}

fn str_length(str: String) -> usize {
    str.len()
}

如果之前的知识你已经完全掌握的话,那么你一定看的出来,这段代码会报什么错误。是的,这段代码完全不会被编译通过,由于 s1 的所有权发生了移动,转移到 str_length 函数的参数 str 中,当 str 离开作用域后 Rust 将会自动调用 drop 函数将其对应的内存空间释放,因此,这段代码尝试访问了一个已经发生移动 (Move) 的变量,那么编译器将不允许这样的操作,而报错。

尝试编译:

imaginemiracle:ownership$ cargo build
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
error[E0382]: borrow of moved value: `s1`
  --> src/main.rs:16:43
   |
12 |     let s1 = String::from("ImagineMiracle");
   |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
13 |
14 |     let len = str_length(s1);
   |                          -- value moved here
15 |
16 |     println!("The length of '{}' is {}.", s1, len);
   |                                           ^^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

没错,正如我们分析的那样,而针对这样问题的解决方案,在上一篇文章中也提出了一种。即利用函数的返回值将变量的所有权转移给调用函数中的一个变量,则可以正常访问该段内存空间。显然这样的方法麻烦的有点不合理,因此本文介绍另一种变量的使用方法——引用 (Reference)。

2. 引用(Reference)

引用 (Reference) 这个概念在大多数编程语言中也都存在,相信各位对这个概念也不陌生了。引用 (Reference) 与指针较为相似,引用同样是一个地址(被引用变量的地址),我们可以通过引用访问来访问变量的数据。 似乎指针也是这样的功能,但一定不要搞混,两者绝非一物。

1:引用本身并不占据内存空间(至少你所学的知识或书本是这样告诉你的),而指针本身则需要为其分配内存空间用于存储指针变量指向的地址值(指针的大小与系统结构有关,32 位系统下指针为 4 Byte64 位系统下指针大小为 8 Byte);

2:指针可以指向 NULL,引用不可以;

3: 指针在初始化之后可以再次修改指向内存地址,引用必须在定义时初始化好,且之后不能再更改。

为了避免有种喧宾夺主的感觉,引用和指针的区别和分析放在了文章最后,让我们继续来看 Rust 中的引用。

引用的符号为 &,而在 Rust 中,对变量使用引用可以让我们正常的访问该变量中的数据,而不获取其所有权。这么方便?我们将上面的代码修改为引用的方式试试看:

fn main() {
    let s1 = String::from("ImagineMiracle");

    let len = str_length(&s1);

    println!("The length of '{}' is {}.", s1, len);

}

fn str_length(str: &String) -> usize {
    str.len()
}

执行此代码:

imaginemiracle:ownership$ cargo run
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
     Running `target/debug/ownership`
The length of 'ImagineMiracle' is 14.

原本会报错的代码,再使用引用后变得正常使用了,让我们分析一下其原因吧。

    let s1 = String::from("ImagineMiracle");

    let len = str_length(&s1);

这段代码中我们向 str_length 函数传入的是 &s1 即这里程序在跳入 str_length 函数执行时,会创建 s1 的引用,引用会获取变量指向的内存空间的地址,可以被使用者访问,但引用并不会拥有变量的所有权。

fn str_length(str: &String) -> usize {
    str.len()
}

因此当 str_length 函数结束后,其参数即这里的引用离开自己的作用域后,Rust 不会对其指向的内存做任何操作(因为不曾拥有过,何谈释放呢),仅无效化 str 这个参数而已。我们在之后的程序中还可以正常的使用 s1 变量。

Rust 将这样创建一个变量的引用的这种行为称为 借用 (Borrowing)。Rust 允许一个引用借用一个变量的值来使用。

那么我们能不能修改引用借来的值呢??来尝试一下:

fn main() {
    let str = String::from("Imagine");

    change(&str);
}

fn change(str: &String) {
    str.push_str("Miracle");
}

尝试编译此代码:

imaginemiracle:ownership$ cargo build
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
error[E0596]: cannot borrow `*str` as mutable, as it is behind a `&` reference
  --> src/main.rs:33:5
   |
32 | fn change(str: &String) {
   |                ------- help: consider changing this to be a mutable reference: `&mut String`
33 |     str.push_str("Miracle");
   |     ^^^^^^^^^^^^^^^^^^^^^^^ `str` is a `&` reference, so the data it refers to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

我们将会看到这样的错误,编译器很清楚的告诉我们,说由于这里是引用的 str,由于其变量本身是不可变的,那么引用一样,默认情况是不允许修改引用的值。

3. 可变引用

如果可以通过引用也可以修改原变量的值,这样应该会很酷吧!那么为了达到这个目的,实际上也很简单,来看代码:

fn main() {
    let mut str = String::from("Imagine");

    change(&mut str);
}

fn change(str: &mut String) {
    str.push_str("Miracle");
}

我们只需要在 str 的定义处为其添加 mut 关键字,表示改变量是可变的,同时在函数的定义处以及调用出同时加上 mut,即清楚的表明了,该函数的确是要修改这个引用的变量。

即便我们现在可以修改引用的值,但仍存在一个问题,一起来看看这段代码:

fn main() {

    let mut str = String::from("Imagine");

    let s1 = &mut str;
    let s2 = &mut str;

    println!("{} {}", s1, s2);
}

这段代码创建了两个引用,两个都希望能够引用可变的变量,那么问题来了。我们是否可以通过这两个引用随意的修改变量的值呢?

答案是否定的,我们来编译看看结果:

imaginemiracle:ownership$ cargo build
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
error[E0499]: cannot borrow `str` as mutable more than once at a time
  --> src/main.rs:43:14
   |
42 |     let s1 = &mut str;
   |              -------- first mutable borrow occurs here
43 |     let s2 = &mut str;
   |              ^^^^^^^^ second mutable borrow occurs here
44 |
45 |     println!("{} {}", s1, s2);
   |                       -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

我们得到了这样的错误,编译器告诉我们说,“你不能同时借用多次可变变量 str”,即不能同时创建多个可变变量的引用。

Rust 这样做的目的是为了保证数据的安全性,防止发生数据竞争。想象一下,当你正在某处使用这个引用时,但在另一处程序使用引用将其的数据修改了,这样的莫名修改将会导致不可预知的后果。因此 Rust 将不允许这样操作。从而来保证数据的同步。

3.1. 多个可变引用的定义条件

当然这样的限制,是有条件的,不可以同时创建多个可变引用,是不是不在同一时刻(作用域)即可。

尝试一下,看这段代码:

fn main() {

    let mut str = String::from("Imagine");

    {
        let s1 = &mut str;		// 第一次创建可变引用

        s1.push_str("Miracle");

        println!("s1 = {}", s1);
    }
    let s2 = &mut str;		// 第二次创建可变引用

    s2.push_str(" Say Hello!");

    println!("s2 = {}", s2);
}

这段代码创建了两次可变引用,但是与之前不同的是,这次其中一个被限制了作用域,等创建第二个可变引用之前将会因离开其作用域而无效。这样就保证了同时只拥有一个可变引用的效果,看看会不会正确的执行吧。

imaginemiracle:ownership$ cargo run
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
    Finished dev [unoptimized + debuginfo] target(s) in 0.22s
     Running `target/debug/ownership`
s1 = ImagineMiracle
s2 = ImagineMiracle Say Hello!

OK! 没问题,它的确正确的在工作了,看到了吗,它将字符串修改了两次。

3.2. 一个可变引用和多个不可变引用

那么看到这里,你是否会产生这样的疑问。如果创建多个引用,其中只有一个是可变的引用,而其他的都是不可变的引用,这样是否可以编译通过呢?这样看起来应该是合乎 Rust 的规则,同时只存在一个可变引用。(你是不是想到了一个 Bug 😏)

我们来尝试这样做:

fn main() {

    let mut str = String::from("Imagine");

    let s1 = &str;
    let s2 = &str;

    let s3 = &mut str;

    s3.push_str("Miracle");

    println!("{} {} {}", s1, s2, s3);
}

如果正确执行的话,这段代码应该最后会输出三次 ImagineMiracle,编译来看看吧:

imaginemiracle:ownership$ cargo build

   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
error[E0502]: cannot borrow `str` as mutable because it is also borrowed as immutable
  --> src/main.rs:65:14
   |
62 |     let s1 = &str;
   |              ---- immutable borrow occurs here
...
65 |     let s3 = &mut str;
   |              ^^^^^^^^ mutable borrow occurs here
...
69 |     println!("s1 = {}, s2 = {}, s3 = {}", s1, s2, s3);
   |                                           -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

原来 Rust 也不允许同时存在一个或多个可变引用和不可变引用。因为其它不可变引用不希望正在使用的时候,所引用的值发生了改变,这与它本身的性质是冲突的。理解这一点,就明白 Rust 为何做这样的限制了。

那么是怎么样都不能同时存在存在可变引用和不可变引用吗?答案同样是否定的。这一点和之前多个可变引用的存在场景较为相似。
示例:

fn main() {

    let mut str = String::from("Imagine");

    let s1 = &str;
    let s2 = &str;

    println!("{} {}", s1, s2);
    // 从此之后再也不用 s1 s2

    let s3 = &mut str;

    s3.push_str("Miracle");

    println!("{}", s3);
}

这段代码与上面不同的是在定义可变引用之前使用过了不可变引用,而在创建可变引用之后,再未使用过之前的不可变引用。这样是否可以正确执行呢?

imaginemiracle:ownership$ cargo run
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/ownership`
Imagine Imagine
ImagineMiracle

哟~,它正确的工作了。我们来分析一下原因,原来 Rust 是这样想的,只要你们可变引用与不可变引用二者(两类引用)河水不犯井水,那么你们就可以共存。也就是说编译器会判断引用的生命周期,若两类引用的生命周期没有发生重叠,那么编译器将允许其共存。

3.3. 悬垂引用(Dangling Reference)

介绍到这里,引用的大部分功能和特性已经差不多说完了,还有一个小的问题 “悬垂引用”。

悬垂引用的定义是这样子的,当一个引用所借用变量的内存已经被释放,这时再使用这个引用将会导致悬垂引用这样的错误。

理解起来也很简单,所引用变量的内存已经没有了,那么引用本身将没有任何意义,也无法去访问任何数据,所以还想再用这个引用访问内存一定是不合理的。来看看什么时候会发生这种情况吧。

fn main() {

    let str = create_str();

    println!("{}", str);
}

fn create_str() -> &String {
    let str = String::from("ImagineMiracle");

    &str
}

这段代码的目的是想通过 create_str 这个函数来提供一个字符串创建的功能。
编译此代码:

imaginemiracle:ownership$ cargo run
   Compiling ownership v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/ownership)
error[E0106]: missing lifetime specifier
  --> src/main.rs:98:20
   |
98 | fn create_str() -> &String {
   |                    ^ expected named lifetime parameter
   |
   = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
   |
98 | fn create_str() -> &'static String {
   |                    ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error

虽然这里的编译器告诉我们这是有关声明周期的错误,建议使用 static 关键字修饰变量来扩展其生命周期。但在本文的目的不是这个,而是让读者理解这里返回的引用为什么无效。

原因是引用是表示的其借用变量的内存空间,而在此处借用的变量是由 create_str 函数内部定义的,当函数调用结束后,该变量会因为离开作用域而由 Rust 自动调用 drop 函数将不再使用的内存空间释放,而避免内存空间的浪费。但正是由与此操作,导致该引用所借用的内存空间不存在,因此,再使用引用来赋值自然是不允许的了。编译器将会阻止这样的动作。

我们可以使用上一篇文章学过的内容解决该问题,使用返回值将变量的所有权转移即可:

fn main() {

    let str = create_str();

    println!("{}", str);
}

fn create_str() -> String {
    let str = String::from("ImagineMiracle");

    str
}

这样便是OK!

4. 字符串切片(slice)类型——str

Rust 中除了 String 类型,还存在一种字符串类型 strstr 被称为字符串切片 (slice) 类型。

所谓的字符串切片 (slice),实际上是一个字符串的部分或全部引用由于其是 String 的引用因此类型前也需要加上 & 表示引用。非常好理解,我们来看下面这段代码。

fn main() {

    let s : String = String::from("ImagineMiracle");

    let s1 : &str = &s[0..7];
    let s2 : &str = &s[7..s.len()];


    println!("s = {}\ns1 = {}\ns2 = {}", s, s1,  s2);
}

编译此代码并执行:

imaginemiracle:slice$ cargo run
   Compiling slice v0.1.0 (/home/imaginemiracle/Miracle/Code/rust_projects/slice)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/slice`
s = ImagineMiracle
s1 = Imagine
s2 = Miracle

s1s2 均为 s 字符串的切片,是 s 的部分引用,即借用了 s 的部分内存空间。以图示表达将会是下面这样。

在这里插入图片描述
从这里也可以看得出切片的范围是由一组方括号 [] 中的内容来限制。下面将具体介绍范围限制的方法。
现字符串 let s = String::from("ImagineMiracle");,正如上图,该字符串的长度为 14

  • &s[0…7]、&s[7…14] 分别表示,截取从 0 号到 6 号索引的内容,截取从 7 号到 13 号索引的内容;
  • &s[…7] 表示截取从开始到 6 号索引的内容,与 &s[0..7] 相同;
  • &s[7…] 表示截取从 7 号索引到结尾的所有内容,与 &s[7..14] 相同;
  • &s[0…14]、&s[0…s.len()]、&s[…s.len()]、&s[…] 均表示截取整个字符串。

4.1. 字符串常量的真实面目

首先明确一点,什么是字符串常量。如下面这样的定义:

	let str_s = "ImagineMiracle";

这个 str_s 是无法被修改的,而字符串变量是由 Rust 花了一番功夫提供给给我们使用的一种类型 String

	let str = String::from("ImagineMiracle");

Rust 在这个类型中为我们提供了很多函数接口来对字符串操作。

那么回归到字符串常量,为什么它不能被修改呢。我们都知道的是,由于 ImagineMiracle 这段字符串会被编译器放进编译后的二进制文件的只读数据段 (.rodata),因此不能被修改,这个分析也是没问题的。

那么在 Rust 中是如何理解这个值不能被修改的呢?

在这里插入图片描述

正如你所理解的那样 ImagineMiracle 在程序中提供给 str_s 是一个确切的地址,这个地址存放着 ImagineMiracle 这样一各字符串常量。熟悉这个过程吗???

这不就是引用的定义嘛!!原来是这样,一个字符串常量原来是对一个常规字符串值的引用。默认的引用是不允许修改其值的,这也就回答了 Rust 是如何判断字符串常量不允许被修改的原因。因为它就是一个不可变的引用!!!

因此,当声明的函数中的参数是一个字符串引用的话,那么可以直接为其传入一个这样的字符串常量。
就像这样

fn main() {

    let str_s = "ImagineMiracle-01 str";

    let str = String::from("ImagineMiracle-02 String");

    print_str(str_s);
    print_str(&str_s[space_item(str_s)..str_s.len()]);
    print_str(&str);
    print_str(&str[space_item(&str)..str.len()]);

    let str_s = put_str(str_s);
    print_str(str_s);

    let str = put_str(&str);
    print_str(&str);
}

fn print_str(str: &str) {

    println!("{}", str);
}

fn put_str(s: &str) -> &str {

    &s[7..14]
}

fn space_item(str: &str) -> usize {
    let bytes = str.as_bytes();

    for i in 0..str.len() {

        if bytes[i] == b' ' {
            return i + 1;
        }
    }

    str.len()
}

这段代码分别展示了使用字符串常量、字符串 (String) 的全部引用(即就是切片类型 &str)以及字符串 (String) 的部分引用(也是字符串切片类型 &str)来作为函数参数的使用方法。
此代码的执行结果如下:

imaginemiracle:slice$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/slice`
ImagineMiracle-01 str
str
ImagineMiracle-02 String
String
Miracle
Miracle

也请各位读者们自己参考本文多加练习,只有自己动手才能真正掌握其中的精髓!

5. 引用(Reference)和指针(Point)

正因本小节按常理来讲是与本文关系不大的,但相信大多数读者不是第一次学习编程语言,因此一定是认识和了解引用和指针这两个概念的,但其本质区别以及为什么我们看到的课本是这样对其定义,以及编程语言为什么要 “多此一举” 的搞出这两个看似功能一样的东西呢。

为此笔者将会尝试使用 C++ 和汇编语言来为大家解答这一疑惑,同时让各位读者更加深切的认识到这两者的区别。
[注]:请注意,下面内容可能会引起少数人产生不适,做好准备~

5.1. 使用引用访问值

首先来看一个引用的简单代码:(所使用语言为 C++

#include <iostream>

int main()
{
    int val = 28;

    int& ref_val = val;

    std::cout << "ref_val = " << ref_val << std::endl;
    return 0;
}

编译并执行这段代码:

imaginemiracle:reference_and_point$ g++ ref.cpp -o ref
imaginemiracle:reference_and_point$ ./ref
ref_val = 28

这里打印出所引用变量的值,毫无疑问的发挥了我们认识的引用的作用。

再看一段使用指针代码:(所使用语言为 C++

#include <iostream>

int main()
{
    int val = 28;

    int* ptr_val = &val;

    std::cout << "ptr_val = " << *ptr_val << std::endl;

    return 0;
}

执行此代码

imaginemiracle:reference_and_point$ g++ ptr.cpp -o ptr
imaginemiracle:reference_and_point$ ./ptr
ptr_val = 28

同样毫无疑问的输出了 28

5.2. 分析引用的汇编代码

接下来我们对这两段代码编译为汇编语言。

imaginemiracle:reference_and_point$ g++ -S ref.cpp -o ref.S
imaginemiracle:reference_and_point$ g++ -S ptr.cpp -o ptr.S

首先查看 ref.S:(此处为汇编语言)
为了避免混乱,这里笔者只展示出与代码对应的片段

		; int val = 28;
        movl    $28, -20(%rbp)	
        
        ; int& ref_val = val;
        leaq    -20(%rbp), %rax
        movq    %rax, -16(%rbp)

这里笔者简单解释一下其中出现的相关指令和寄存器,这些指令均为 X86 架构的指令集:(汇编指令基本格式: 指令 源, 目的)

  • movl : mov 指令的扩展,将 32 位的值传给目标;
  • leaq : mov 指令的变种,将源操作数的地址加载到目的寄存器中(q 表示 64 位,即 8 字节);
  • movq :64 位的值传给目标。
    相关寄存器:
  • rbp : 保存程序栈底地址的寄存器;
  • rax : 累加器(这里可直接将其视为 64 位寄存器即可,此处无作他用)

OK,了解了这些指令和寄存器之后,我们看看三行汇编干了什么事情。(事实上笔者已经将对应的 C++ 语句注释在上方了)

        movl    $28, -20(%rbp)	

该行代码实际上是根据栈底寄存器间接寻址,将 28 这个数值保存进这个栈中 rbp - 20 这个位置。

        leaq    -20(%rbp), %rax
        movq    %rax, -16(%rbp)

这里第一行,首先获取 rbp - 20 这个位置数据的地址,并将该地址值保存到 rax 寄存器中。第二行指令则将 rax 寄存器中的值保存到 rbp - 16 的位置。(细心的朋友应该发现了,x86 架构采用的是满递减栈,而一个 int 型的变量占 4 字节,20 - 16 = 4,这块栈空间就是用来存储一个 int 型变量值为 28 的位置。这里提示一下 rax 寄存器大小为 64 位,即 8 字节)
在这里插入图片描述
在图示的进程的内存空间分布图中,这里的栈便是满递减栈x86 便采用的是这种结构。了解到这里,那么笔者将上述的汇编代码也以图片的方式展示出来。

在这里插入图片描述
此出的地址仅为了便于理解标识,并非实际地址。这样就可以看出来这三行汇编的工作,的确是将一个 28 写入栈中的 rbp - 20 的位置,并将其地址也写入一个 64 位的空间保存在栈中的 rbp - 16 的位置。那么也就是说引用也是占了空间的???别急继续往下看。
[注]:此处并非入栈出栈等操作,而是直接修改对应区域的值。入栈出栈需要固定的指令 push、pop。

5.3. 使用指针访问值

让我们再查看 ptr.S 文件:(此处为汇编语言)
为了避免混乱,这里笔者只展示出与代码对应的片段

		; int val = 28;
        movl    $28, -20(%rbp)	
        
        ; int& ref_val = val;
        leaq    -20(%rbp), %rax
        movq    %rax, -16(%rbp)

这里可不是笔者写错了啊,实际上这两个文件的汇编代码在这部分是完全一样的,还记得吗,指针的大小应该是对应体系结构的位数。这里应该是 64 位,正好就是 rax 寄存器的大小。

没错,是这样的,在汇编中和二进制文件中,我们看到的指针与引用完全会是一回事,事实上引用在计算机底层是使用指针实现的,似乎与大家在书本上看到的概念不同是吗?

5.4. 引用可以被修改吗?

也不是的,我们再往下看,这段代码:(如下代码使用 C++ 语言)

#include <iostream>

int main()
{
    int a = 66;
    int val = 28;

    int& ref_val = val;

    std::cout << "ref_val = " << ref_val << std::endl;
    std::cout << "addr of a = " << &a << std::endl;
    std::cout << "addr of val = " << &val << std::endl;
    std::cout << "addr of ref_val = " << &ref_val << std::endl;

    ref_val = a;

    std::cout << "ref_val = " << ref_val << std::endl;
    std::cout << "addr of a = " << &a << std::endl;
    std::cout << "addr of val = " << &val << std::endl;
    std::cout << "addr of val = " << &ref_val << std::endl;

    return 0;
}

可以看到,代码中首先定义了两个 int 型的变量,再定义了一个引用 ref_val 初始化为 val,即 ref_val 将指向 val 变量的内存空间。接下来依次打印 ref_val 的地址、a 变量的地址、val 变量的地址以及 ref_val 引用的值。

接下来将 ref_val 引用修改指向 a ???是这样吗?(我们学过,引用是不可被修改的),然后再次打印以上顺序。
这段代码的执行结果如下:

ref_val = 28
addr of a = 0x7fff5ccacec8
addr of val = 0x7fff5ccacecc
addr of ref_val = 0x7fff5ccacecc
ref_val = 66
addr of a = 0x7fff5ccacec8
addr of val = 0x7fff5ccacecc
addr of val = 0x7fff5ccacecc

你会发现,ref_val 的值改变为 66 ,而 ref_val 的地址并没有改变,还是与 val 相同,所以这里只是做了一次简单的赋值(如果不相信的你,可以去在后面打印 val 的值,你会发现也变成了 66)。

奥,明白了。原来是这样,我们所学的引用是不可修改的,这一点完全没错。

5.5. 指针可以被修改吗?

再来看指针是什么样的:(下面代码使用 C++

#include <iostream>

int main()
{

    int a = 66;
    int val = 28;

    int* ptr_val = &val;


    std::cout << "ptr_val = " << *ptr_val << std::endl;
    std::cout << "addr of a = " << &a << std::endl;
    std::cout << "addr of val = " << &val << std::endl;
    std::cout << "addr of ptr_val = " << &ptr_val << std::endl;
    std::cout << "addr of *ptr_val = " << ptr_val << std::endl;

    ptr_val = &a;


    std::cout << "ptr_val = " << *ptr_val << std::endl;
    std::cout << "addr of a = " << &a << std::endl;
    std::cout << "addr of val = " << &val << std::endl;
    std::cout << "addr of ptr_val = " << &ptr_val << std::endl;
    std::cout << "addr of *ptr_val = " << ptr_val << std::endl;

    return 0;
}

可以看到,代码中首先定义了两个 int 型的变量,再定义了一个指针 ptr_val 初始化为 &val,即 ptr_val 将指向 val 变量的内存空间。接下来依次打印 ptr_val 指针指向的值、a 变量的地址、val 变量的地址以及 ptr_val 指针本身的地址和 ptr_val 指针本身保存的值。

接下来,修改指针指向 a,再次重复打印。
来看看结果吧:

ptr_val = 28
addr of a = 0x7ffe0087c6a8
addr of val = 0x7ffe0087c6ac
addr of ptr_val = 0x7ffe0087c6b0
addr of *ptr_val = 0x7ffe0087c6ac
ptr_val = 66
addr of a = 0x7ffe0087c6a8
addr of val = 0x7ffe0087c6ac
addr of ptr_val = 0x7ffe0087c6b0
addr of *ptr_val = 0x7ffe0087c6a8

首先你会发现 ptr_val 指向的变量值的确是变为 66 了,并且,ptr_val 中保存的地址也变成了 a 变量的地址,那么这一点再次说明了,指针是可以被修改的,而引用不可以。(这里的修改说的是指向问题,即地址)

5.6. 但是,引用真的不可以被修改吗?????

看到了这里,是否觉得笔者再想陈述的问题自相矛盾,一会想说引用是不可以被修改的,一会又想证明引用可以被修改。没关系,我们再继续看下去。

看这段使用引用的代码:(如下代码使用 C++ 语言)

#include <iostream>

int main()
{
    int a = 66;
    int val = 28;

    int& ref_val = val;

    std::cout << "ref_val = " << ref_val << std::endl;
    std::cout << "addr of a = " << &a << std::endl;
    std::cout << "addr of val = " << &val << std::endl;
    std::cout << "addr of ref_val = " << &ref_val << std::endl;
    
    return 0;
}

这段代码实际上我们不用执行,也知道 ref_val 打印的值将会是 28。以免你不相信笔者执行一遍:

imaginemiracle:reference_and_point$ ./a.out
ref_val = 28
addr of a = 0x7ffe7218ed38
addr of val = 0x7ffe7218ed3c
addr of ref_val = 0x7ffe7218ed3c

OK,没错是吧。执行 g++ -S reandpoint.cpp -o reandpoint.S 的到汇编文件,那我们来看看对应的汇编代码:

        movl    $66, -24(%rbp)
        movl    $28, -20(%rbp)
        leaq    -20(%rbp), %rax
        movq    %rax, -16(%rbp)

好的,那么我们尝试在这里面修改引用的指向。在对应的汇编代码中添加一行指令 leaq -24(%rbp), %rax

        movl    $66, -24(%rbp)
        movl    $28, -20(%rbp)
        leaq    -20(%rbp), %rax
        leaq    -24(%rbp), %rax        
        movq    %rax, -16(%rbp)

读者们可以根据这里的位置,去添加即可,让我们编译这个修改后的汇编文件,并执行看看结果如何:

imaginemiracle:reference_and_point$ g++ reandpoint.S
imaginemiracle:reference_and_point$ ./a.out
ref_val = 66
addr of a = 0x7fff934d5118
addr of val = 0x7fff934d511c
addr of ref_val = 0x7fff934d5118

欸?!?什么情况??原本不可被修改的引用,现在值不仅变为 66,而且指向的地址也变为了 a 变量的地址。不是不可以修改吗??又可以修改了??

5.7. 小结(引用和指针)

实际上引用和指针的规则,是编程语言提供的机制,编程语言对其的限制只能在其对应的编译器中体现出来,即使用编译器编译使用引用和指针时,他们必须按照我们所学过的那样来编程,否则编译器则会报错。同理,如 C++ 中的 const 常量,以及我们正在学习的 Rust 中的不可变变量,都是可以通过修改其对应的汇编代码或者反汇编代码,甚至修改其二进制可执行程序文件等等内存操作手段,都是可以将原本编程语言中规定了不可被修改的值,修改为另一个值。因为内存操作本来就不会有什么限制。

那么是为什么要引出引用和指针呢,为的是更加方便的使用变量,同时利用编译器使用引用可以更加安全的操作内存空间,避免了内存重复释放以及忘记释放等等系列内存问题,为开发者提供了良好的使用工具。

#本文完

那么到这里本文也介绍了不少内容,但愿本文这大量的文字不会打消你学习的兴趣,正所谓当你感受到困难时,恰是正在进步的关键,绝不能放过任何一个令自己疑惑的问题!!!


Boys and Girls!!!
准备好了吗?下一节我们要还有一个小练习要做哦!

不!我还没准备好,让我先回顾一下之前的。
上一篇《【Rust】所有权——Rust语言基础11》

我准备好了,掛かって来い(放马过来)!
下一篇《Rust语言基础13》


觉得这篇文章对你有帮助的话,就留下一个赞吧v*
请尊重作者,转载还请注明出处!感谢配合~
[作者]: Imagine Miracle
[版权]: 本作品采用知识共享署名-非商业性-相同方式共享 4.0 国际许可协议进行许可。
[本文链接]: https://blog.csdn.net/qq_36393978/article/details/125958230

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Imagine Miracle

爱你哟 =^ v ^=

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值