The Stack and the Heap
Both the stack and the heap are parts of memory that are available to your code to use at runtime, but they are structured in different ways. The stack stores values in the order it gets them and removes the values in the opposite order. Adding data is called pushing onto the stack, and removing data is called popping off the stack.All data stored on the stack must have a known, fixed size. Data with an unknown size at compile time or a size that might change must be stored on the heap instead. The heap is less organized: when you put data on the heap, you request a certain amount of space. The operating system finds an empty spot in the heap that is big enough, marks it as being in use, and returns a pointer, which is the address of that location. This process is called allocating on the heap. Because the pointer is a known, fixed size, you can store the pointer on the stack, but when you want the actual data, you must follow the pointer.
Accessing data in the heap is slower than accessing data on the stack because you have to follow a pointer to get there.
When your code calls a function, the values passed into the function (including, potentially, pointers to data on the heap) and the function’s local variables get pushed onto the stack. When the function is over, those values get popped off the stack.
Ownership Rules
- Each value in Rust has a variable that’s called its owner.
- There can be only one owner at a time.
- When the owner goes out of scope, the value will be dropped.
let s = "hello";
With the
String
type, in order to support a mutable, growable piece of text, we need to allocate an amount of memory on the heap, unknown at compile time, to hold the contents. This means:- The memory must be requested from the operating system at runtime. This is done when we call
String::from
. - We need a way of returning this memory to the operating system when we’re done with our String. In Rust, the memory is automatically returned once the variable that owns it goes out of scope. A scope is the range within a program for which an item is valid. The variable is valid from the point at which it’s declared until the end of the current scope.
{ let mut s = String::from("hello"); s.push_str(", world!"); println!("{}", s); };
When a variable goes out of scope, Rust calls a special function for us. This function is calleddrop
, and it’s where the author of String can put the code to return the memory. Rust callsdrop
automatically at the closing curly bracket.
Multiple variables interacting with the same data
Let’s look at this example:let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // Error
A String is made up of three parts, shown as below: a pointer to the memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. On the right is the memory on the heap that holds the contents.
When we assign
s1
to s2
, the String data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to. In other words, the data representation in memory looks like below:The above figure shows both data pointers pointing to the same location. This is a problem: when
s2
and s1
go out of scope, they will both try to free the same memory. Freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities. To ensure memory safety, instead of trying to copy the allocated memory, Rust considers s1
to no longer be valid and, therefore, Rust doesn’t need to free anything when s1
goes out of scope. This is known as a move. In this example, we would say that s1
ptr was moved into s2
.Let’s look at this valid code, which seems to contradict what we just learned.
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. Rust has a special annotation called the
Copy
trait that we can place on types like integers that are stored on the stack. If a type has the Copy
trait, an older variable is still usable after assignment. Rust won’t let us annotate a type with the Copy
trait if the type, or any of its parts, has implemented the Drop
trait. Any group of simple scalar values can be Copy
, and nothing that requires allocation or is some form of resource is Copy.References and Borrowing
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
In the above example, we are passing &s1 into calculate_length and, in its definition, we take &String rather than String. These ampersands are references, and they allow you to refer to some value (s1) without taking ownership of it. Because it does not own it, the value it points to will not be dropped when the reference goes out of scope. Likewise, the signature of the function uses & to indicate that the type of the parameter s is a reference. When functions have references as parameters instead of the actual values, we don’t need to return the values in order to give back ownership, because we never had ownership.
We call having references as function parameters borrowing. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back.
0 comments:
Post a Comment