Table of Contents
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());
}