rust references和Box在内存中的表示方式有什么区别< T>?

4nkexdtk  于 2023-02-04  发布在  其他
关注(0)|答案(3)|浏览(150)

我试图理解references和Box<T>是如何工作的,让我们考虑一个代码示例:

fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

在我的想象中,Rust在内存中保存的值为:

考虑Box<T>的第二个代码片段:

fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

x将如何存储到Box中?内存是什么样子的?
上面的例子来自Treating Smart Pointers Like Regular References with the Deref Trait。对于第二个例子,书中解释为:
清单15-7和清单15-6的唯一区别是,这里我们将y设置为指向x中的值的box的示例,而不是指向x的值的引用。
这是否意味着框中的y直接指向值5

wz8daaqr

wz8daaqr1#

简单情况下的图很好,尽管可能不太清楚,因为值和地址都使用了5,我在图中移动了y以避免混淆。

Box<T>的内存是什么样的?

Box的等效图看起来类似,但添加了堆:

Stack

     ADDR                    VALUE
    +------------------------------+
x = |0x0001|                     5 |
y = |0x0002|                0xFF01 |
    |0x0003|                       |
    |0x0004|                       |
    |0x0005|                       |
    +------------------------------+

    Heap

     ADDR                    VALUE
    +------------------------------+
    |0xFF01|                     5 |
    |0xFF02|                       |
    |0xFF03|                       |
    |0xFF04|                       |
    |0xFF05|                       |
    +------------------------------+

(See下面关于此图的学究式注解)
Box已经在堆中为我们分配了足够的空间,这里是地址0xFF01。然后将值从堆栈移到堆中。
这是否意味着框中的y直接指向
y持有指向Box分配的数据的指针。它 * 必须 * 这样做,以便能够在Box超出作用域时释放分配的内存。
您正在阅读的这一章的要点是Rust将为您透明地取消引用Box,因此您通常不需要关心这个事实。
另见:

内存有什么不同?

这可能会让你的大脑有点弯曲!
看看这两个例子的栈,这两种情况并没有真正的区别--reference和Box都作为指针存储在栈中,唯一的区别是 * 在代码中 *,它知道根据它是reference还是Box来区别对待栈中的值。
事实上,Rust!中的所有内容都是如此,对于计算机来说,所有内容都只是位,二进制程序中编码的结构是唯一能区分字节blob的东西。

为什么x在移到Box后仍在堆栈上?

细心的读者会注意到,我在堆栈中保留了x的值5,这有两个相关的原因:
1.这实际上是内存中发生的事情。程序通常不会"重置"它们处理完的值,因为这将是不必要的开销。Rust通过将变量标记为已移动并禁止访问已移动的变量来避免问题。
1.在这个例子中,i32实现了Copy,这意味着在它被移动之后可以访问它的值,编译器实际上允许我们继续访问x,如果x是一个没有实现Copy的类型,比如StringBox,这就不成立了。
另见:

学究式图表注解

  • 这张图不是按比例绘制的i32占用4个字节,指针/引用占用的字节数取决于平台,但假设所有内容的大小都相同会更简单。
  • 堆栈通常从高地址开始并向下增长,而堆从低地址开始并向上增长。
yzckvree

yzckvree2#

虽然一般规则与What are the differences between Rust's String and str?的答案完全相同,但我也在这里回答。
Rust引用(几乎)完全符合您的描述:一个指向内存中某个值的指针。(并不总是这样。例如,切片也包含一个长度,指向traits的指针也包含一个v表。这些被称为胖指针。)在开始时,Box<T>是一个值,就像Rust中的任何其他值一样,因此,区别是显而易见的-一个是对内存中某个位置的引用,而第二个是内存中某个位置的值。令人困惑的是,Box<T>内部包含了一个对内存的引用,但该引用是在堆而不是堆栈上分配的,两者之间的区别在于堆栈是函数本地的,并且非常小(在我的macOS上,最大值为8192 KiB)。
例如,您不能执行类似这样的操作,原因如下:

fn foo() -> &u32 {
    let a = 5;

    &a
}

最重要的原因是foo()返回后a将不在那里。内存将被清除(虽然不总是如此),并且可能很快会被更改为另一个值。这在C和C++中是未定义的行为,在Rust中是一个错误,它不允许任何未定义的行为(在不使用unsafe的代码中)。
另一方面,如果您这样做:

fn foo() -> Box<u32> {
    let a = Box::new(5);

    a
}

一些与我们相关的事情将会发生:

  • 内存将在堆栈上分配。此内存完全独立于当前函数作用域,这意味着当不需要它时需要释放它
  • 我们将移动,因此不涉及生存期
  • a的所有权将转移到调用方

为方便起见,Box<T>在很多情况下会像引用一样,因为这两个函数可以互换使用。例如,请看下面的C程序,其中我们提供了与第二个示例类似的功能:

int* foo(void) {
  int* a = malloc(sizeof(int));
  *a = 5;

  return a;
}

正如你所看到的,指针被用来存储内存的地址,并被进一步传递。

pdkcd3nj

pdkcd3nj3#

“5”被复制到堆中,“y”智能指针指向它。由于指针的大小是固定的,它们被存储在堆栈中。看起来像这样:

如果你需要得到对智能指针“y”的引用

println!("{}",5==*y)

你正在得到指针的引用,你正在得到存储在堆中的值的引用。这可能是因为智能指针实现Deref。这是Deref如何为智能指针实现的

impl Deref for YouSmartPointer{
    type Target=i32;
    fn deref(&self)->&i32{
        &self.value
    }
}

返回值为&self.value。如果它没有返回引用,则当该值被使用一次时,智能指针将失去所有权。通过返回引用,rust确保指针始终是所有者

相关问题