1.类型安全
| 维度 | C | C++ | Rust 的设计保障 |
|---|---|---|---|
| 内存管理 | malloc/free 手动管理,易泄漏、悬垂指针、重复释放 |
new/delete 也存在悬垂、资源泄漏(RAII 缓解但依赖程序员习惯) |
所有权 (Ownership) + 借用检查 (Borrow checking),编译期强制保证:资源只能有一个所有者,作用域结束自动释放 |
| 指针安全 | 原指针可随意解引用,可能产生野指针、悬垂指针 | 同 C,增加了智能指针 | 不允许随意解引用原指针,unsafe 代码块被强制标记;默认使用 &T / &mut T 的安全引用 |
| 类型转换 | void* 可强制转换为任意类型,导致UB |
隐式转换规则复杂 | 没有 void*,强制类型安全;转换必须显式 (as),且很多情况需要 unsafe |
| 数组与缓冲区 | 无边界检查,strcpy, gets 易导致缓冲区溢出 |
同 C,STL 容器安全性较高,但 T[] 依旧危险 |
数组和 Vec 访问必须在边界内 (index 会 panic,get 返回 Option) |
| 空指针 | NULL 引用随处可能产生崩溃 |
nullptr 改善了语义,但依然可能被解引用 |
没有空指针,使用 Option<T> 明确表示“可能为空” |
| 未初始化变量 | 可使用未初始化内存,导致 UB | 局部变量不自动初始化,未定义行为 | 所有变量在使用前必须初始化,编译器检查 |
| 异常/错误处理 | 返回码 | try/catch |
无异常机制,采用 Result<T, E> 明确错误类型,编译期强制处理或传播 |
| 泛型/多态 | void* 实现泛型,非常不安全 |
多态依赖虚函数指针,可能悬垂 | 单一机制:零成本抽象(泛型 + Trait bound),无虚函数开销;编译期保证类型一致 |
类型安全问题
- C/C++ 典型的类型安全问题主要来自
- 指针不安全(悬空、野指针、
void*、函数指针乱用) - 内存访问不安全(越界、溢出、释放后访问)
- 类型系统过于宽松(隐式转换、强制转换、
union、对象切片) - 未定义行为(UB)泛滥
- 指针不安全(悬空、野指针、
- 举例:野指针
int *p; { int x = 10; p = &x; } // x 已经销毁 printf("%d\n", *p); // UBRust是静态类型语言,在编译时会进行类型检查。同时支持类型推断,因此不需要显示标注每个变量的类型。但是,不允许隐式的类型转换,必须显式的进行。
- 强类型
- 编译时严格检查不同类型之间的兼容性,禁止隐式转换
- 静态类型
- 每个变量在
编译时有且只有一个类型,运行时不可改变类型
- 每个变量在
- 类型大小
- 编译器必须知道变量在栈上的大小(编译期固定)。
- 大部分类型是
Sized(大小在编译期确定,如 i32, bool, struct)。 - 有些类型是
!Sized(大小未知,如str,[T],dyn Trait)。- str 不定长,
let s: str;:x:。必须通过指针来间接使用,如&str、Box<dyn Trait>。 - Rust 内置
Sizedtrait,默认泛型参数要求T: Sized。
- str 不定长,
- 大部分类型是
- 👉 这个机制保证了内存布局可知,防止C中存在的野指针或数组越界问题。
- 编译器必须知道变量在栈上的大小(编译期固定)。
- 类型推导
- 编译器尽量从右边的值推导出左边变量的类型。
- 如果推导不出,必须显式标注。
枚举类型
Rust中,若预计某变量值在某时刻可能为空值,则尽量用Option/Result定义它。为此设定了enum Option和enum Result两个枚举类型。
Optionstd::option 表示一个可选值
// 每个option要么是一个some中包含一个值
// 要么是一个None
pub enum Option<T>{
None,
Some(T),
}
// 由此,一旦某变量不是Option类型的
// 我们则认为它的值永远不会是空
Resultstd::result::Result Result很常用,直接被包含到
prelude中 因此不需要手动引入这个包
enum Result<T, E>{// 泛型T表示正确执行后返回值的类型,E表示异常时存放的错误值
Ok(T), // 返回值的存放方式为Ok(T)
Err(E),// 返回值的存放方式为Err(E)
}
2.内存安全
| 方面 | SaferCPlusPlus | Rust |
|---|---|---|
| 越界访问 | 容器、视图类型自带边界检查 | 编译时保证 + 部分运行时检查 |
| 悬垂指针 | 用更安全的智能指针 / view 类型替代原生指针 | 编译时借用检查 |
| 未初始化变量 | safer int/bool 类型避免使用未初始化值 | Rust 默认禁止未初始化,必须通过 MaybeUninit 显式声明 |
| Use-after-free | 通过 safer 指针/引用降低风险 | Rust 编译器禁止,生命周期系统强制约束 |
所有权系统ownership
谁能管理资源
- 每个值都有一个所有者(owner)。
- 在同一时刻,值只能有一个所有者。
- 当所有者离开作用域时,值会被自动销毁(drop)。
借用检查borrow checking
谁能临时使用资源
- 不可变引用
&- 只读访问数据,不能修改引用指向的数据
- 可变引用
&mut- 允许修改数据
- 同一时间只能有一个可变引用,且不能与不可变引用同时存在
智能指针&RAII
智能指针
相对于“裸指针”:把生命周期管理、引用计数、并发锁机制都包装在类型里。
- Box
:在堆上分配数据,所有权在 Box 上。 - Rc
:**单线程**场景下实现引用计数。 - Arc
:**多线程**场景下实现引用计数。 - RefCell
/ Cell :在运行时执行借用检查(内部可变性)。 - Mutex
/ RwLock :并发场景下的智能指针,保证线程安全。 RAII
资源获取与生命周期绑定 在rust中,变量不仅指代相应的内存,也拥有资源(如示例中的文件资源)。
// 示例代码
let wordlist_file = File::open(&args[1])?;
❓ 文件操作只有open,没有close
- 当超出作用域时,对象会自动调用析构函数,释放内存
- 这里的文件句柄
wordlist_file的作用域为main函数,当main函数返回时,文件会自动关闭
- 这里的文件句柄
生命周期lifetime
编译器需要知道引用的数据在内存中存活的时间,以保证引用在有效范围内是安全的
如果编译器无法推断出引用的生命周期,则需要显式提供生命周期注释
- 用法:
'+标识符,如'a,通常放在函数签名或结构体的定义中
生命周期注释降低了可读性;在复杂共享环境下,可结合智能指针
std::rc::Rc(reference counting)
Rc–单线程场景下实现引用计数Arc–多线程场景下实现引用计数
注意:引用计数天然的存在循环引用问题。
3.并发安全
| 方面 | SaferCPlusPlus | Rust |
|---|---|---|
| 并发模型 | 提供 safer 共享类型,避免常见 data race | Send/Sync trait + 所有权,编译期保证无数据竞争 |
| 安全等级 | 较高(但依赖库封装) | 最高级别:编译时阻止错误发生 |
Send&SynctraitSend:是否可以在线程间传递所有权Sync:能否在多个线程中安全的共享
- 基于共享状态的并发
- 并发原语
- 【读共享】
Arc<T>:多线程共享不可变数据(只读)。 - 【写共享】
Arc<Mutex<T>>:允许多线程共享可变数据(加锁保证安全)。 - 【写共享】
Arc<RwLock<T>>:允许读多写一的共享访问。 - 【写共享】
Atomic<T>:对一些基础类型提供无锁共享修改.
- 【读共享】
- 并发原语
- 基于消息传递的并发
Channelstd::sync::mpsc标准库(multi-producer, single-consumer)- “共享内存的替代方案”:不要用共享内存来通信,而用通信来共享内存。
- “通信/消息传递”:基于所有权转移的线程通信。
- 所有权
go中的channel:语言特性、语言内置的管道机制Rust中的channel:类型安全+所有权转移的通信管道
0.编译器静态检查
系统编程语言发展趋势
系统编程语言的选择质量维度:安全、性能、开发效率
0. 内存安全风险
- 关于安全类型库SaferCplusplus
- 提供某些标准库、标准容器、数据类型的安全替代方案;
- 提供多线程环境的安全共享类型,减少并发错误、数据竞争;
- 提供指针和引用的安全替代(智能指针);
- 内置逻辑检查;可选的静态检查器;
- 关于静态分析器scptools
1. safe CPP
关于safecpp proposal:2024-09-11,Sean Baxter、Christian Mazakas提出的“C++安全扩展提案”
不是传统意义上的库
也不是对现有标准库的简单替代
而是语言层面的提案,旨在通过定义一个“安全子集(safe context)”确保内存安全、线程安全、生命周期安全等特性,同时尽量保持C++的性能和兼容性。 需要编译器层面的支持。
- 实现C++超集,这个超集具有一个安全子集(排除
undefined behavior)。 - 安全上下文与不安全上下文划清界限。
- 安全子集必须是有用的。需要提供安全的替代方案来取代不安全技术,比如用类型选择和借用取代unions和pointers
- 兼容现行代码。
2. fil-C
invisiCAP pointer不可见权限指针(64bits)==普通指针(64bits)+(访问边界+[读/写/执行]权限)(不会显式存储在内存中)FUGC(Fil’s unbelievable garbage collection)==并行mark-and-sweapGCfil-c ==编译器+运行时检查系统- 实质:编译现有的C代码,并在运行时检查无效的内存访问
- 问题:性能
3. C-CPP-Rust混合编程(三分天下)
Rust 是一种可以提供严格的内存安全的主流系统级、非垃圾回收语言。但 C++ 和 Rust 具有不同的设计,互操作能力有限,这使得从 C++ 到 Rust 的增量迁移成为一个艰苦的过程:Rust 缺乏函数重载、模板、继承和异常。C++ 缺乏traits、重定位relocation和借用检查borrow-checking。因此,通过在 Rust 中重写关键部分来强化 C++ 应用程序很困难。
| 特性 | C++ | SaferCPlusPlus | Rust |
|---|---|---|---|
| 内存安全 | ❌ 无内建保障,易出现悬垂指针、缓冲区溢出 | ⚠️ 部分保障,通过 safer 类型(如 nii_array, safe_iterator)减少错误 |
✅ 编译期借用检查 + 所有权模型,系统级内存安全 |
| 线程安全 | ❌ 共享内存和数据竞争常见 | ⚠️ 提供 safer 容器和同步工具,避免部分数据竞争 | ✅ 编译器强制检查,数据竞争在编译期报错 |
| 运行时开销 | ✅ 无额外开销 | ⚠️ 较低(主要是额外的检查和 safer 类型包装) | ⚠️ 一般与 C++ 持平 |
| 兼容性 | ✅ 完全兼容现有 C++ | ✅ 基于 C++11/14 标准库扩展,现有项目能渐进迁移 | ❌ 语法和编译模型完全不同,需要重写 |
| 迁移成本 | ✅ 无成本 | ⚠️ 中等:逐步替换不安全类型 | ❌ 高:需要大规模重构或重写 |
| 目标定位 | 系统级性能和灵活性 | 在 C++ 中引入更安全的默认实践 从语言层面彻底解决安全问题 |