← Back to list

Rust Borrowing vs. C Pointers

Discover how Rust’s borrowing system guarantees memory safety, while C’s pointers put the responsibility on the programmer to avoid bugs.

Josip Vojak · 2024-10-18 08:17 · 2 claps · 5.3 min read
#rust #c-programming #memory-management #race-prevention #dangling-pointer
Open on Medium ↗
Wiki topics: SAF · Safety & Alignment BIZ · Business Strategy 💻 · Programming

Rust Borrowing vs. C Pointers

When I first started working with Rust, one of the biggest shifts in mindset was understanding its ownership and borrowing system. Coming from a background in C, where pointers are the norm for managing memory, I thought I had a pretty good grasp of how references worked. But Rust completely changed the game by introducing strict rules around borrowing that ensure memory safety and eliminate entire classes of bugs like dangling pointers and data races — all at compile time.

In this post, I’ll dive into the key differences between borrowing in Rust and how C handles memory with pointers. If you’ve worked with C pointers before, you’ll find some familiar concepts, but also a lot of new ideas that make Rust stand out as one of the safest languages for systems programming. Let’s explore how Rust’s borrow checker guarantees safety, while C leaves much of that responsibility in the programmer’s hands.

What is borrowing?

Borrowing in Rust is a core concept tied to Rust’s ownership system, which ensures memory safety without needing a garbage collector. Borrowing allows one or more parts of code to temporarily access a value without taking ownership of it.

Key Points of Borrowing:

  1. Ownership remains with the original variable:
  • When you borrow a value, you don’t take ownership of it. The original owner retains full control, and the borrower can only use the value temporarily.

2. Two types of borrowing:

  • Immutable Borrowing (&T): You can borrow a value immutably, meaning you can read the value but cannot modify it. Multiple immutable borrows are allowed at the same time.
  • Mutable Borrowing (&mut T): You can borrow a value mutably, meaning you can read and modify it. However, only one mutable borrow is allowed at any time, and no other borrows (immutable or mutable) can coexist.

3. Borrowing rules:

  • You can have multiple immutable references (&T) to a value.
  • You can have only one mutable reference (&mut T) at a time.
  • You cannot have both mutable and immutable references to the same value simultaneously. This prevents data races and ensures safe concurrency.

Example: Immutable and Mutable Borrowing

fn main() {
    let s = String::from("Hello, Rust!");
// Immutable borrow
    let len = calculate_length(&s);  // We borrow `s` immutably to calculate its length
    println!("The length of '{}' is {}.", s, len);  // We can still use `s`
    // Mutable borrow
    let mut s_mut = String::from("Hello");
    add_exclamation(&mut s_mut);  // We borrow `s_mut` mutably to modify it
    println!("{}", s_mut);  // The value has been modified
}
fn calculate_length(s: &String) -> usize {
    s.len()  // Immutable borrow - read-only access
}
fn add_exclamation(s: &mut String) {
    s.push_str(", world!");  // Mutable borrow - we modify the value
}

In this example:

  • calculate_length(&s) borrows s immutably, allowing the function to read s without taking ownership.
  • add_exclamation(&mut s_mut) borrows s_mut mutably, allowing it to modify the value.

Why Borrowing Matters:

  • Memory Safety: Borrowing ensures that Rust avoids dangling pointers and data races at compile time.
  • Ownership management: It allows you to pass values to functions without transferring ownership, so you can still use the original value after the function call.
  • Concurrency safety: By enforcing borrowing rules, Rust prevents data races in multi-threaded applications.

Rust Borrowing vs. C Pointers

In Rust, borrowing is a key feature that ensures memory safety and prevents data races without needing manual memory management, whereas in C, there are no formal concepts of ownership and borrowing, so similar tasks are managed with raw pointers and manual memory management. Let’s compare the two in terms of how they handle temporary access to values:

1. Ownership vs. Pointers:

  • Rust: Ownership is a strict system enforced by the compiler. A value has a single owner, and borrowing allows temporary access without transferring ownership. Rust ensures memory safety by tracking ownership and enforcing borrowing rules at compile time.
  • C: C doesn’t have an ownership model like Rust. Instead, it uses raw pointers (e.g., int *ptr). A pointer in C can reference any memory location, but there are no guarantees or checks for safety. The programmer must manually ensure that the pointer points to valid memory, and there’s no concept of borrowing or ownership enforced by the language.

2. Borrowing vs. Passing Pointers:

Rust Borrowing: In Rust, you can pass references (&T for immutable or &mut T for mutable) to a function. The Rust compiler enforces strict borrowing rules to ensure that:

— You can have multiple immutable borrows or one mutable borrow, but never both at the same time.

— When a value is borrowed, the compiler guarantees it is valid and prevents other unsafe operations (e.g., modifying the value when it is borrowed immutably).

fn print_length(s: &String) {
    println!("{}", s.len());
}

fn main() {
    let s = String::from("hello");
    print_length(&s);  // Borrow `s` without transferring ownership
    println!("{}", s); // Still valid because ownership is not transferred
}

C Pointers: In C, you can pass pointers (*T) to functions to achieve a similar effect of temporary access. However, C does not enforce any rules on how the memory pointed to by the pointer is used. The programmer must ensure that:

— The pointer points to valid memory.

— No memory corruption occurs (e.g., if you pass a pointer to a function that modifies the value but forget to ensure exclusive access, data races or memory corruption can occur).

#include <stdio.h>

void print_length(const char *s) { // Use `const` to prevent modification
    printf("%zu\n", strlen(s));    // Read the string's length
}
int main() {
    char s[] = "hello";
    print_length(s);  // Pass pointer to `s` (no ownership model)
    printf("%s\n", s); // Still valid because we manually ensure it is
    return 0;
}

Key Differences:

  • Safety: Rust enforces safety at compile-time (no data races, no dangling pointers). C relies entirely on the programmer to ensure that pointer usage is safe.
  • Compiler Guarantees: Rust guarantees that when a value is borrowed, no invalid access (e.g., concurrent mutable and immutable accesses) can occur. C has no such guarantees, leading to potential undefined behavior.
  • Mutable and Immutable References: Rust distinguishes between immutable (&T) and mutable (&mut T) references. You can only have one mutable reference at a time, preventing data races. In C, both const and non-const pointers can exist at the same time without restrictions, which can lead to unsafe operations.

3. Dangling Pointers vs. Borrowing Lifetimes:

  • Rust: Borrowing in Rust is tied to lifetimes, which means the Rust compiler checks that any reference (borrowed value) does not outlive the value it points to. This prevents dangling references, ensuring that you never reference memory that has already been freed or is out of scope.
fn main() {
    let r;
    {
        let x = 5;
        r = &x; // ERROR: `x` does not live long enough
    }
    println!("{}", r); // `r` would be a dangling reference
}
  • C: In C, there is no such lifetime checking. You can create dangling pointers by returning or using pointers to local variables that go out of scope. If you try to dereference such pointers, it leads to undefined behavior.
int* dangling_pointer() {
    int x = 5;
    return &x;  // Returns a pointer to a local variable (dangling pointer)
}

int main() {
    int *p = dangling_pointer(); // Pointer to invalid memory
    printf("%d\n", *p);          // Undefined behavior
    return 0;
}

4. Concurrency and Data Races:

  • Rust: The ownership and borrowing rules prevent data races in Rust. By allowing only one mutable reference at a time and preventing concurrent mutable and immutable borrows, Rust ensures that data cannot be mutated while it’s being read.
  • C: C provides no built-in protection against data races. Multiple threads or code paths can access and modify the same data at the same time, leading to undefined behavior or race conditions unless the programmer manually ensures synchronization.

While borrowing in Rust and pointers in C look similar in syntax (both involve passing references or addresses), Rust’s borrowing model adds significant safety guarantees that eliminate common bugs like dangling pointers and data races that are prevalent in C. Rust’s compiler checks these rules at compile-time, whereas C leaves all the safety management to the programmer.


메타데이터
post_id
ff0044d9a42f
slug
rust-borrowing-vs-c-pointers-ff0044d9a42f
url
https://medium.com/@josipvojak/rust-borrowing-vs-c-pointers-ff0044d9a42f
canonical_url
https://medium.com/@josipvojak/rust-borrowing-vs-c-pointers-ff0044d9a42f
author_url
https://medium.com/@josipvojak
status
ok
fetched_at
2026-07-07 00:45:30