Posted on :: Tags: , ,

Rust VS C++

我们都知道Rust语言最大的特点就是内存安全,而恰巧内存安全问题是C++最大的弊端。

随意本文我们就从内存安全讨论一下两种语言的区别。

Rust

Rust的内存安全,并不是靠垃圾回收器,而是依靠强大的类型检查系统。

在编译期间,rust编译器会强制检查所有在C++看起来可以通过的地方不通过,例如,C++引用。

const int* r;
{
    int x = 5;
    r = &x; // 合法
}
// 这里r悬挂,访问r是未定义行为
std::cout << *r << std::endl;

上述代码在C++编译期可以通过,但是执行时会崩溃,因为x作为局部变量,离开作用域之后生命周期已经结束,内存空间被释放。r再次访问就会报错。

let r;
{
    let x = 5;
    r = &x; // ❌ 编译报错:x已经要被释放
}
// println!("{}", r);

而rust依靠强检查的编译器,在编译期会禁止编译通过。

这其实就是rust的所有权系统:

Rust 中,每一块数据(内存)都有一个独一无二的所有者(Owner)。同一时刻,某块内存只能被一个变量拥有。当拥有者离开作用域,这块内存自动释放。

fn main() {
    let s = String::from("hello"); // s 拥有 String
    let t = s; // 把所有权转移给 t,s 失效
    println!("{}", s); // ❌ 编译报错,s已经失效
}

可变借用

可变借用是rust的概念,意思是允许你创建一个可以修改数据的引用。其实C++中也可以有类似的行为:

#include <iostream>
using namespace std;

int main() {
    int x = 5;
    
    // 可变借用(通过引用)
    int& r = x;
    r += 1; // 修改 x 的值
    
    cout << "x: " << x << endl; // 输出:x: 6
}

c++的“可变借用”是通过引用实现的,int& r = x; 创建了 x 的可变引用,r 是 x 的别名,通过 r 可以修改 x 的值。

当然也可以通过指针进行可变借用

#include <iostream>
using namespace std;

int main() {
    int x = 5;
    
    // 可变借用(通过指针)
    int* p = &x;
    *p += 1; // 修改 x 的值
    
    cout << "x: " << x << endl; // 输出:x: 6
}

但是C++中修改引用的行为可能会导致运行崩溃的问题:一个变量拥有多个可变引用,如果在多线程中同时修改这个变量,会引发数据竞争,此时C++需要加锁来确保线程安全。

而在 Rust 中,编译器会通过严格的借用检查,避免多个线程同时修改同一数据。编译器强制让你在同一时间只能有一个可变借用,从而避免了并发问题。

fn main() {
    let mut x = 5;
    
    // 可变借用
    let y = &mut x;
    *y += 1; // 修改 x 的值

    println!("x: {}", x); // 输出:x: 6
}

多线程数据竞争

在多线程中,rust不允许跨线程直接调用数据,和C++一样,rust一般都是通过Arc(原子引用计数)和 Mutex(互斥锁)来跨线程访问统一资源,但不同的是,C++需要显式的加锁操作来防止数据竞争:

#include <iostream>
#include <thread>
#include <mutex>

int shared_var = 0;
std::mutex mtx;

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // 自动加锁
        shared_var++;  // 同步访问
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);

    t1.join();
    t2.join();

    std::cout << "Shared variable: " << shared_var << std::endl;  // 正确输出
    return 0;
}

除非使用原子操作std::atomic,可以不用显式加锁

void increment() {
    for (int i = 0; i < 1000000; ++i) {
        shared_var++;  // 原子操作,无需显式锁
    }
}

而在rust中,默认不需要手动加锁即可实现同一时间只有一个线程访问同一变量,这依靠的其实就是rust的借用规则。

借用规则强制数据只能有一个可变借用(&mut T)或者多个不可变借用(&T),但不能同时存在可变和不可变借用。这些规则由编译器通过借用检查器在编译期强制执行,从而避免了运行时数据竞争。

例如,当某个线程持有可变借用时,其他线程无法访问该数据,避免了数据竞争。

use std::sync::{Arc, Mutex};
use std::thread;

fn increment(shared_data: Arc<Mutex<i32>>) {
    for _ in 0..1_000_000 {
        let mut data = shared_data.lock().unwrap();
        *data += 1; //不需要显式加锁,因为lili
    }
}

fn main() {
    let shared_data = Arc::new(Mutex::new(0));
    
    let data1 = Arc::clone(&shared_data);
    let handle1 = thread::spawn(move || {
        increment(data1);
    });
    
    let data2 = Arc::clone(&shared_data);
    let handle2 = thread::spawn(move || {
        increment(data2);
    });

    handle1.join().unwrap();
    handle2.join().unwrap();
    
    println!("{}", *shared_data.lock().unwrap());
}

Copyright 2025 wakamda 冀ICP备2025102883号-1