COMP3007 Modern Programming Languages-Week 4
COMP3007 Modern Programming Languages-Week 4
Fall 2024-2025
Occurs when a program tries to free memory that has already been
freed
Can cause memory corruption, crashes, or security vulnerabilities
1 fn main () {
2 let s1 = String :: from(" hello ");
3 let s2 = s1; // s1 is moved to s2
4
5 println !("{}, world !", s1); // RUST COMPILER ERROR
6 // Rust prevents use of s1 after move
7 }
Ownership Rules:
Each value has a single owner
When the owner goes out of scope, the value is dropped
Borrowing Rules:
Multiple immutable references OR one mutable reference
References must always be valid
Lifetime Annotations:
Ensure references are valid for the duration they’re used
Move Semantics:
Prevent use after move, eliminating double-free errors
Heap
Stack
Flexible memory allocation
Fast memory allocation
Dynamic size, unknown at
LIFO (Last In, First Out)
compile time
Fixed size, known at compile
Slower allocation and access
time
Manual memory management
Automatic memory management
(in many languages)
Heap
x: 5 String data
y: 10
Vec<i32> data
Stack
z: 15
ptr: 0x..
Free space
...
Stack: Fixed-size data, fast access. Heap: Dynamic-size data, flexible but slower.
Stack
e
Heap
1
currently in use) len: 5 2 l
Capacity (total amount of 3 l
capacity: 5 4 o
memory received from
allocator)
1 {
2 let s = String :: from(" hello "); // s is valid from
this point forward
3
4 // do stuff with s
5 } // this scope is now
over , and s is no
6 // longer valid
Stack
Heap
index value 1 e
len: 5 0 h capacity: 5 2 l
Stack
Heap
1 e 3 l
4 o
capacity: 5 2
3
l
l s2
4 o
s2 ptr index value
0 h
Heap
ptr 1
2
e
l
3 l
4 o
Stack
Copy
Assignment creates a copy, not a move y
Original variable remains valid after
assignment 5
No need for explicit cloning
More efficient for small, fixed-size data
1 let x = 5;
2 let y = x; // x is copied , not moved
3 println !("x is still accessible: {}", x); // This is valid
Unlike String and other heapallocated types, assigning a basic type doesn’t transfer ownership or invalidate the original variable.
1 fn main () {
2 let s = String :: from("hello"); // s comes into scope
3 takes_ownership(s); // s's value moves into the function ...
4 // ... and so is no longer valid here
5 let x = 5; // x comes into scope
6 makes_copy(x); // x would move into the function ,
7 // but i32 is Copy , so it 's okay to still
8 // use x afterward
9 } // Here , x goes out of scope , then s. But because s's value was moved , nothing
10 // special happens.
11
12 fn takes_ownership(some_string: String) { // some_string comes into scope
13 println !("{}", some_string);
14 } // Here , some_string goes out of scope and `drop ` is called. The backing
15 // memory is freed.
16
17 fn makes_copy(some_integer: i32) { // some_integer comes into scope
18 println !("{}", some_integer);
19 } // Here , some_integer goes out of scope. Nothing special happens.
1 fn main () {
2 let s1 = gives_ownership (); // gives_ownership moves its return
3 // value into s1
4 let s2 = String :: from("hello"); // s2 comes into scope
5 let s3 = takes_and_gives_back(s2); // s2 is moved into
6 // takes_and_gives_back , which also
7 // moves its return value into s3
8 } // Here , s3 goes out of scope and is dropped. s2 was moved , so nothing
9 // happens. s1 goes out of scope and is dropped.
10
11 fn gives_ownership () -> String {
12 let some_string = String :: from("yours"); // some_string comes into scope
13 some_string // some_string is returned and
14 // moves out to the calling function
15 }
16
17 fn takes_and_gives_back(a_string: String) -> String { // a_string comes into scope
18 a_string // a_string is returned and moves out to the calling function
19 }
1 fn main () {
2 let s1 = String :: from(" hello ");
3 let len = calculate_length (&s1);
4 println !("The length of '{}' is {}.", s1 , len);
5 }
6
7 fn calculate_length (s: & String ) -> usize {
8 s.len ()
9 }
(*) 3
4
l
o
1 fn main () {
2 let s1 = String :: from("hello");
3 let len = calculate_length (&s1);
4 println !("The length of '{s1}' is {len}.");
5 }
6
7 fn calculate_length(s: &String) -> usize {
8 s.len()
9 }
Key Points:
References are immutable
by default
1
2
fn main () {
let s = String :: from("hello"); Attempting to modify a
3
4 change (&s);
borrowed value results in a
5 } compiler error
6
7
8
fn change(some_string: &String) {
some_string.push_str(", world"); This prevents data races
9 } and ensures memory safety
To modify a borrowed
value, we need to use a
mutable reference (&mut)
Rust’s borrowing rules prevent unexpected modifications to data, enhancing
program safety and predictability.
1 fn main () {
2 let mut s = String :: from(" hello ");
3 change (& mut s);
4 }
5
At any given time, you can have either (but not both):
One mutable reference
Any number of immutable references
References must always be valid (no dangling references)
Examples:
Not allowed (two mutable refs):
Corrected code using mutable reference:
1 let mut s = String :: from("hello");
1 fn main () { 2 let r1 = &mut s;
2 let mut s = String :: from("hello"); 3 let r2 = &mut s; // Error!
3 change (& mut s);
4 }
5 Not allowed (mutable and immutable):
6 fn change(some_string: &mut String) {
7 some_string.push_str(", world"); 1 let mut s = String :: from("hello");
8 } 2 let r1 = &s;
3 let r2 = &mut s; // Error!
For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error
s
A string slice is a reference to part of a name value index value
String
ptr 0 h
len 11 1 e
Syntax: capacity 11 2 l
[starting_index..ending_index] 3 l
Using &str allows the function to accept both &String and &str
More flexible and doesn’t take ownership