从C到Rust:告别 C 的"指针 + 长度"手动模式

切片与胖指针 ------ 告别 C 的"指针 + 长度"手动模式

一、C 的数组:退化为指针,丢失长度

1.1 数组参数退化为指针

C 中数组作为函数参数时自动退化为指针,长度信息丢失:

c 复制代码
// 这三个声明完全等价

void process(int* arr);

void process(int arr[]); // 这里的 [] 只是个语法糖

void process(int arr[10]); // ← 10 被编译器忽略了!

  


// 等价于:

void process(int* arr); // 全部退化为 int*

这意味着函数无法从参数中知道数组有多大

c 复制代码
void process(int arr[]) {

// arr 的类型是 int*,没有长度信息

// 我不知道 arr 指向 1 个 int 还是 100 个 int

}

  


void caller() {

int arr[10];

process(arr); // 传的是 int*,长度 10 留在了 caller 的栈帧里

}

1.2 补救方案:手动传递长度

C 的补救是额外传一个长度参数------这是约定,不是类型系统的强制:

c 复制代码
// 方案 A:传递指针 + 长度(最常见的 C 模式)

void process(int* arr, size_t len); // len 是调用者的保证

  


// 方案 B:在数组中用哨兵值标记结尾

int process_until_sentinel(int* arr, int sentinel);

  


// 方案 C:返回的数组自带长度(结构体包装)

struct Array {

int* data;

size_t len;

};

void process_arr(struct Array a);

方案 A 的问题arrlen 是两个独立的参数,可能不一致:

c 复制代码
void caller() {

int arr[5];

process(arr, 10); // ❌ 编译器不报错!arr 只有 5 个元素

// 但 process 以为有 10 个------越界写入

}

  


void process(int* arr, size_t len) {

for (size_t i = 0; i <= len; i++) { // off-by-one

arr[i] = 0; // ❌ 越界

}

}

1.3 越界访问没有保护

c 复制代码
int arr[5] = {1, 2, 3, 4, 5};

arr[10] = 42; // ❌ 编译通过,运行时越界写入

// 可能覆盖其他变量、可能 segfault、可能一切正常------未定义行为

C 的 arr[i] 没有边界检查。 每一次数组访问都是潜在的缓冲区溢出漏洞。

1.4 C 字符串的问题

c 复制代码
// C 字符串依赖 NULL 结尾------需要遍历到 \0 才能知道长度

const char* s = "hello";

size_t len = strlen(s); // O(n)------必须遍历找到 \0

  


// 如果忘记 NULL 结尾------strlen 会继续读直到遇到随机内存中的 \0

char buf[5] = {'h', 'e', 'l', 'l', 'o'};

// buf 没有 NULL 结尾!

size_t len = strlen(buf); // ❌ 缓冲区溢出读------未定义行为

  


// 字符串操作中的常见 bug:

void copy_name(char* dest, const char* src, size_t dest_size) {

strcpy(dest, src); // ❌ 如果 src 比 dest_size 长------溢出

// strncpy(dest, src, dest_size); // 稍微安全,但不 NULL 结尾如果超长

// snprintf(dest, dest_size, "%s", src); // 推荐的做法

}

C 字符串是 C 中最多安全漏洞的来源之一。 NULL 结尾、手动管理长度、缓冲区溢出------所有问题都源于"长度不在类型中"。

1.5 C 中"手动胖指针"模式

有些 C 程序员会自己包装"指针 + 长度"的结构体:

c 复制代码
// C 中的"手动胖指针"

struct ByteSlice {

uint8_t* data;

size_t len;

};

  


void print_slice(struct ByteSlice s) {

for (size_t i = 0; i < s.len; i++) {

printf("%02x ", s.data[i]);

}

}

  


// 但这只是结构体------不是语言内置的概念

// 每个库各自定义自己的 Slice 结构体

// 不能和原生数组互操作

C 的手动胖指针问题是:

  • 不是语言内置------每个库自己定义

  • 不能和原生数组无缝互操作

  • 没有编译器检查------可以构造 ByteSlice { data: NULL, len: 100 }

  • 没有边界检查------s.data[i] 仍然可能越界


二、Rust 的切片:带长度的胖指针

2.1 切片是什么

切片(slice)是一个"胖指针"------包含两个东西:指向数据的指针 + 长度。

rust 复制代码
// 切片类型:&[T]

// 在 64 位系统上是 16 字节(8 字节指针 + 8 字节长度)

let arr = [1, 2, 3, 4, 5];

let slice: &[i32] = &arr; // 对整个数组的切片

  


// 也可以切一部分

let slice: &[i32] = &arr[1..4]; // [2, 3, 4]

  


// 切片的长度是已知的

println!("长度: {}", slice.len()); // 3

  


// 访问元素

println!("第一个: {}", slice[0]); // 2

// println!("{}", slice[100]); // ❌ panic: index out of bounds
yaml 复制代码
切片的内存布局(64 位系统):

  


栈上(16 字节):

┌──────────────────────┐

│ ptr: 0x7ffe12345600 │ ← 指向数据的指针(8 字节)

├──────────────────────┤

│ len: 3 │ ← 元素个数(8 字节)

└──────────────────────┘

│

▼

堆/栈上的数据:

┌────┬────┬────┬────┬────┐

│ 1 │ 2 │ 3 │ 4 │ 5 │

└────┴────┴────┴────┴────┘

^^^^ 只取这 3 个

2.2 切片可以从多种来源创建

rust 复制代码
// 从数组引用

let arr: [i32; 5] = [1, 2, 3, 4, 5];

let s: &[i32] = &arr; // 全数组切片

let s: &[i32] = &arr[1..4]; // 部分切片

  


// 从 Vec

let v: Vec<i32> = vec![1, 2, 3, 4, 5];

let s: &[i32] = &v; // Vec 自动 Deref 为 &[i32]

let s: &[i32] = &v[2..]; // 从索引 2 到末尾

  


// 从字符串

let s: &str = "hello"; // 字符串切片(&str 也是一种切片)

let s: &str = &"hello world"[0..5]; // "hello"

切片是所有连续数据类型的统一"视图":

scss 复制代码
Vec<i32> ──── deref ──→ &[i32]

[i32; 5] ──── & ──────→ &[i32]

[i32; 100] ── & ──────→ &[i32]

│

统一的读写接口:

.len() .is_empty() .iter() [i]

2.3 切片是胖指针,不拥有数据

rust 复制代码
// 切片只是"视图"------不拥有数据

fn get_slice() -> &[i32] {

let arr = [1, 2, 3, 4, 5];

&arr[1..3] // ❌ 编译错误!arr 是局部变量,离开函数被销毁

} // 返回的切片会成为悬垂指针

  


// 正确的做法:把拥有权交给调用者,或者从参数借

fn get_slice<'a>(data: &'a [i32]) -> &'a [i32] {

&data[1..3] // ✅ 返回的切片和输入的生命周期相同

}

2.4 切片的安全保证

rust 复制代码
// 1. 边界检查------越界 panic 而不是内存破坏

let s = &arr[1..4];

// s[100]; // ❌ panic: index out of bounds

  


// 2. 永远不可能是 NULL

// &[T] 是一个有效的胖指针------不能是 NULL

// 如果需要"可能没有数据",用 Option<&[T]>

  


// 3. 长度是类型的一部分------不可能"忘记传递长度"

fn process(s: &[i32]) {

for i in 0..s.len() { // ✅ s.len() 是确定的

println!("{}", s[i]);

}

}

// 调用者不需要额外传长度------长度在切片里

三、&str ------ 字符串切片

3.1 不是 C 字符串

rust 复制代码
// &str 是一个胖指针:指向 UTF-8 字节的指针 + 字节长度

let s: &str = "hello";

  


println!("长度: {}", s.len()); // 5(字节数,不是字符数)

println!("是否为空: {}", s.is_empty());

  


// &str 不是 NULL 结尾的!

// "hello" 在内存中是 5 个字节 [0x68, 0x65, 0x6C, 0x6C, 0x6F]

// 没有 \0 结尾

  


// 不需要 strlen()------长度在切片里

// s.len() 是 O(1)------不是 O(n)

3.2 UTF-8 保证

rust 复制代码
// &str 保证是有效的 UTF-8

// 无法构造一个非法 UTF-8 的 &str

  


let s = "hello"; // ASCII(兼容 UTF-8)

let s = "你好"; // ✅ 中文 UTF-8(6 字节,2 个字符)

let s = "😀"; // ✅ emoji UTF-8(4 字节,1 个字符)

  


// 按字符(Unicode 标量值)迭代

for c in "你好".chars() {

println!("{}", c); // '你' '好'

}

  


// 按字节迭代

for b in "你好".bytes() {

println!("{}", b); // 228 189 160 229 165 189

}

3.3 C 字符串 vs &str

维度 C 字符串(char* Rust &str
终止符 \0(NULL 结尾) 无(长度在胖指针中)
获取长度 strlen(s) --- O(n) s.len() --- O(1)
编码 通常 ASCII/扩展 ASCII 总是有效 UTF-8
越界访问 无保护(缓冲区溢出漏洞) 有保护(panic)
空指针 可以为 NULL 不能为 NULL
子串操作 需要手动管理偏移 &s[0..5] --- 类型安全
c 复制代码
// C 字符串的 O(n) strlen

const char* s = "hello world";

size_t len = strlen(s); // 必须遍历到 \0 才知道长度

// 如果 s 没有 \0 结尾------strlen 继续读直到遇到 \0 或 crash
rust 复制代码
// Rust &str 的 O(1) len

let s: &str = "hello world";

let len = s.len(); // 直接从胖指针读取------O(1)

四、胖指针与普通指针

4.1 什么是胖指针

胖指针(fat pointer)是"带额外元数据的指针"。Rust 中有两种胖指针:

rust 复制代码
// 1. 切片引用(ptr + len)

let s: &[i32] = &[1, 2, 3]; // 16 字节(64 位系统)

let s: &str = "hello"; // 16 字节

  


// 2. Trait 对象(ptr + vtable)

let t: &dyn std::fmt::Debug = &42; // 16 字节(ptr + vtable_ptr)

  


// 普通引用

let r: &i32 = &42; // 8 字节(只是一个指针)
rust 复制代码
内存中的大小(64 位系统):

&i32 = 8 字节(普通指针)

&[i32] = 16 字节(指针 + 长度)

&dyn Debug = 16 字节(指针 + vtable 指针)

Box<dyn Debug> = 16 字节(堆上的指针 + vtable)

4.2 切片胖指针 vs 普通指针

rust 复制代码
use std::mem::size_of;

  


// 普通引用------一个机器字

assert_eq!(size_of::<&i32>(), 8); // 普通指针

  


// 切片引用------两个机器字(胖指针)

assert_eq!(size_of::<&[i32]>(), 16); // 指针 + 长度

assert_eq!(size_of::<&str>(), 16); // 指针 + 长度

  


// Option 优化

assert_eq!(size_of::<Option<&[i32]>>(), 16);

// Option<&[i32]> 也是 16 字节------和 &[i32] 一样大!

// None 用合法的非空胖指针中不可能的值来表示

  


// 普通指针的 Option 也有优化

assert_eq!(size_of::<Option<&i32>>(), 8); // = &i32

// None = NULL 指针

4.3 胖指针在 Rust 中的无处不在

rust 复制代码
// 函数参数------不需要额外传长度

fn process(data: &[u8]) {

// data 携带了指针和长度

}

  


// 返回值------同样携带长度

fn read_file(path: &str) -> Vec<u8> {

// ...

}

// 如果需要返回一个"视图",返回 &[u8]

  


// 子切片------自动调整指针和长度

fn first_half(data: &[u8]) -> &[u8] {

&data[..data.len() / 2]

// 自动调整 ptr 和 len

}

五、切片操作详解

5.1 切片的基本方法

rust 复制代码
let s = &[1, 2, 3, 4, 5];

  


s.len(); // 5------元素个数

s.is_empty(); // false------是否为空

s.first(); // Some(&1)------第一个元素

s.last(); // Some(&5)------最后一个元素

s.get(2); // Some(&3)------安全的索引

s.get(100); // None------越界不会 panic

s[2]; // 3------直接索引(越界 panic)

  


// 子切片

&s[1..4]; // [2, 3, 4]

&s[..3]; // [1, 2, 3]

&s[2..]; // [3, 4, 5]

&s[..]; // [1, 2, 3, 4, 5]------整个切片

  


// 分割

let (left, right) = s.split_at(2); // ([1, 2], [3, 4, 5])

5.2 可变切片 &mut [T]

rust 复制代码
let mut arr = [1, 2, 3, 4, 5];

let s: &mut [i32] = &mut arr;

  


s[0] = 100; // ✅ 通过可变切片修改

s.reverse(); // ✅ 反转:[5, 4, 3, 2, 1]

s.sort(); // ✅ 排序:[1, 2, 3, 4, 5]

s.copy_within(0..2, 3); // ✅ 内存内复制

5.3 切片的迭代

rust 复制代码
let s = &[1, 2, 3, 4, 5];

  


// 按元素迭代

for x in s {

println!("{}", x); // x: &i32

}

  


// 按元素迭代(可变)

for x in &mut s.clone() {

*x *= 2;

}

  


// 迭代器适配器

let sum: i32 = s.iter().sum(); // 15

let max = s.iter().max(); // Some(&5)

let contains = s.contains(&3); // true

let windows = s.windows(3); // [1,2,3], [2,3,4], [3,4,5]

六、"胖指针"的 C 对应

6.1 C 中的"手动胖指针"

c 复制代码
// C 程序员手动模拟切片的常见方式

struct Slice {

int* data;

size_t len;

};

  


void print_slice(struct Slice s) {

for (size_t i = 0; i < s.len; i++) {

printf("%d ", s.data[i]);

}

}

问题在于:

c 复制代码
// 不是语言内置------每个库各自定义

// 我的 Slice、你的 ArrayView、他的 ByteRange

  


// 没有边界保证

struct Slice bad = { NULL, 100 }; // ✅ C 允许!dangling 访问

  


// 不能和原生数组无缝互操作

int arr[10];

struct Slice s = { arr, 10 }; // ✅ 需要手动构造

// 但 &arr[..5] 这种语法不存在

  


// 运行时没有检查

s.data[100] = 42; // ❌ 不会 panic,直接覆盖内存

6.2 C 中的"字符串切片"

c 复制代码
// C 中"字符串切片"需要手动管理

const char* text = "hello world";

  


// 想要一个"指向"hello"的切片?------需要自己跟踪起点和长度

struct StrSlice {

const char* start;

size_t len;

};

  


struct StrSlice s = { text, 5 }; // "hello"

// 但 s 不保证是完整的 UTF-8 序列

// 不保证点到的是合法字符串

// 不保证数据没有被释放

6.3 C vs Rust 切片对比

维度 C(手写 Slice) Rust(&[T]
语言内置 否(各库自定义) 是(&[T] 是原生类型)
长度获取 从结构体字段读 .len() --- O(1)
边界检查 无(需手动检查) 有(越界 panic)
空指针 可以构造 {NULL, 100} 不能为 NULL
子切片 需要手动调整指针+长度 &s[1..4]
与数组互操作 手动构造 &arr 自动成为 &[T]
与 Vec 互操作 不能 &vec 自动成为 &[T]
作为函数参数 void f(struct Slice s) fn f(s: &[T])
传递长度 手动 + 易错 自动 + 安全

6.4 同一个功能:C vs Rust

c 复制代码
// C ------ 计算数组前 n 个元素的和

// 需要手动传递指针和长度

int sum_array(const int* data, size_t len) {

int total = 0;

for (size_t i = 0; i < len; i++) {

total += data[i]; // 如果 i >= len------越界

}

return total;

}

  


int caller() {

int arr[] = {1, 2, 3, 4, 5};

int s1 = sum_array(arr, 5); // ✅ 正确

int s2 = sum_array(arr, 100); // ❌ 编译通过!越界

return s1 + s2;

}
rust 复制代码
// Rust ------ 计算切片中元素的和

fn sum_array(data: &[i32]) -> i32 {

let mut total = 0;

for i in 0..data.len() { // ✅ data.len() 在切片中

total += data[i]; // 如果 i 越界------panic,不是内存破坏

}

total

// 或者更简单:data.iter().sum()

}

  


fn caller() {

let arr = [1, 2, 3, 4, 5];

let s1 = sum_array(&arr); // ✅

// let s2 = sum_array(&arr[..100]); // ❌ 编译时不会报错

// 但运行时 panic(slice 索引越界)

// 不会静默越界

}

七、与 C 程序员的对话

"传个数组还要传 length,C 不也这么干?"

C 程序员:"C 中传数组时传指针+长度是标准做法,有什么问题吗?"
Rust :"问题是 '指针 + 长度' 在 C 中是两个独立的东西------类型系统不认为它们有关联。你可以传长度为 100 但指针只指向 5 个元素------编译器不报错。Rust 的切片把指针和长度捆绑为一个类型------它们永远不会分家。而且切片的 len() 是直接读胖指针里的值------O(1),不像 C 的 strlen 需要 O(n)。"

c 复制代码
// C ------ 指针和长度是分离的

void process(int* data, size_t len); // 各传各的

process(arr, 100); // 传错了编译器不报错
rust 复制代码
// Rust ------ 指针和长度在一起

fn process(data: &[i32]); // 一个参数包含两者

process(&arr); // ✅ 长度自动从 arr 推导

// process(&arr[..100]); // ❌ 如果超过实际长度,运行时 panic

"strlen 不是一直用得好好的吗?"

C 程序员 :"C 的字符串用 \0 结尾,strlen 遍历到 \0 就知道长度了------有什么问题?"
Rust :"两个问题。第一:strlen 是 O(n)------字符串越长,获取长度越慢。对长度为 1GB 的字符串调用 strlen 就是灾难。第二:如果没有 \0 结尾------strlen 会继续读直到在随机内存中碰到一个 \0------要么读到错误数据,要么 segfault。Rust 的 &str 长度是胖指针中的字段------O(1),而且不依赖任何哨兵值。"

c 复制代码
// C ------ 依赖哨兵值

const char* s = get_string();

size_t len = strlen(s); // O(n)------必须遍历到 \0

// 如果 s 没有 \0 结尾------未定义行为
rust 复制代码
// Rust ------ 长度在胖指针中

let s: &str = get_string();

let len = s.len(); // O(1)------直接从胖指针读

// 不需要遍历,不需要哨兵值

// 总是准确的

"那我用 struct { int* data; size_t len; } 不是一样吗?"

C 程序员:"C 中我用 struct 把指针和长度包在一起,不就是 Rust 的切片吗?"
Rust :"语法上类似,但 Rust 的切片是语言原生的 。你的 struct 没有边界检查------s.data[i] 越界了不会告诉你。你的 struct 不能和 Vec 或原生数组直接用 & 互操作。你的 struct 不能直接用 &s[1..4] 取子切片。这些不是语法糖------是编译器检查和类型系统的集成。你的 struct 是结构体,切片是语言特性。"


八、小结

8.1 C 的问题

c 复制代码
数组退化为指针 → 丢失长度信息

→ 必须手动传递 size_t

→ 指针和长度可能不一致

→ 越界访问无保护

  


字符串 NULL 结尾

→ strlen 是 O(n)

→ 缺少 \0 导致缓冲区溢出读

→ 长度不在类型中

  


"手动胖指针"模式

→ 每个库自己定义

→ 没有边界检查

→ 没有语言集成

8.2 Rust 的解决

C 的问题 Rust 的解决
数组退化为指针,丢失长度 &[T] 是胖指针(ptr + len)
指针和长度分离 指针和长度在同一个类型中
越界无保护 越界 panic(不是 UB)
strlen 是 O(n) s.len() 是 O(1)
NULL 结尾 &str 自带长度,不需要哨兵
手动取子串 &s[1..4] --- 子切片
各库各自定义 Slice &[T] 是语言原生类型

8.3 胖指针是 Rust 的关键创新

ini 复制代码
普通指针:&i32 = ptr(8 字节)------只知道地址

胖指针: &[i32] = ptr + len(16 字节)------知道地址和长度

&str = ptr + len(16 字节)------知道地址、长度、UTF-8 保证

&dyn T = ptr + vtable(16 字节)------知道地址和类型行为

  


C 中:指针就是地址------只有 8 字节(64 位)

Rust 中:指针可以携带元数据------最多 16 字节

一句话总结 :C 的数组退化为指针时,"长度"被遗弃在了调用者的栈上。你必须手动传递它,而且很容易传错。Rust 的切片是胖指针------指针和长度永远在一起 ,一起被传递、一起被检查、一起被销毁。越界时 panic 而不是内存破坏,获取长度是 O(1) 而不是 O(n)------这些都是胖指针带来的根本改进。对于 C 字符串,&str 把 NULL 结尾换成了显式长度 + UTF-8 保证------不再有哨兵依赖,不再有缓冲区溢出读。

相关推荐
掘金安东尼2 小时前
中小厂前端候选人简历面试拆解:从 HR 面、技术面到主管面的双赢提问法
前端·面试
doiito3 小时前
【Agent Harness】 给 ComfyUI 装上一个 Rust 大脑:media_agent 架构深度揭秘
ai·rust·架构设计·系统设计·ai agent
天平11 小时前
油猴脚本创建webworker踩坑记录
前端·javascript·typescript
原则猫12 小时前
前端基础大厦
前端
陈随易13 小时前
编程语言级别的Skill市场,AI Agent 的未来形态
前端·后端·程序员
SoaringHeart14 小时前
Flutter进阶:基于 EasyRefresh 的下拉刷新封装 n_easy_refresh_mixin.dart
前端·flutter
IT_陈寒16 小时前
Vite的热更新突然不香了,排查三小时差点砸键盘
前端·人工智能·后端
子兮曰16 小时前
Agency-Agents 深度解析:400+ AI 专家的"梦之队"如何重塑开发工作流
前端·后端·vibecoding