Rust学习笔记(2025-11-24)
1.浮点数判断相等
fn main() { assert!(0.1+0.2==0.3); }
-
你可能在测试的时候,是相等的,原因是在某些模式下保持f32精度,也就是1e-3.
-
如果在精度更高的f64,肯定不通过,因为 浮点数二进制表示无法精确表达 0.1 和 0.2,因此
0.1 + 0.2的结果 不等于 0.3,而是一个非常接近但不精确的值。
所以不建议使用==来判断浮点数
正确写法
fn main() {
assert!((0.1f32 + 0.2f32 - 0.3f32).abs() < 1e-3);
assert!((0.1f64 + 0.2f64 - 0.3f64).abs() < 1e-6);
}
2.char类型占用4个字节
在 Rust 中,char 类型固定占用 4 个字节(32 bit Unicode 标量值)。
而在C/C++的char则占用1个ASCII
use std::mem::size_of_val;
fn main() {
let c1 = 'a';
assert_eq!(size_of_val(&c1),4);
let c2 = '中';
assert_eq!(size_of_val(&c2),4);
println!("Success!")
}
3.元组,结构体,内存对齐
3.1.什么是内存对齐
类型的“对齐值”(alignment) = 这个类型的值在内存中起始地址必须是某个整数倍(通常是 1、2、4、8、16)。
举例:
u8对齐 = 1
可以放在任意地址:0、1、2、3、4……u16对齐 = 2
必须放在偶数地址:0、2、4、6……u32对齐 = 4u64/f64对齐 = 8- 指针
*const T/ 引用&T通常对齐 =usize的对齐(64 位平台一般 = 8)
3.2.为什么要内存对齐
- 硬件和总线访问要求
CPU 一般按“字长”(word size)访问内存,比如 4 字节或 8 字节对齐位置上同时读取/写入一整个 word。
如果一个 8 字节的值(如 f64)跨越了 8 字节边界,例如:
- 起始地址是 0x1003(不是 8 的倍数)
- 这个值占 8 字节:0x1003 ~ 0x100A
- 它跨越了两个对齐块
在一些架构上可能:
- 访问这个值会变慢(要两次总线访问)
- 甚至出现硬件异常(未对齐访问 fault)
所以编译器通过“对齐”保证:
- 类型的值总是在适合 CPU 的边界上,保证速度和安全。
- 性能
即使某些架构允许未对齐访问,也几乎总是“对齐访问”性能更好,所以语言 runtime 和编译器都会尽量保证对齐。
3.3.获取类型的对齐值
使用 std::mem::align_of::<T>()获取类型的对齐值
- 基础类型
println!("align_of::<u8>() = {}", align_of::<u8>());
println!("align_of::<u16>() = {}", align_of::<u16>());
println!("align_of::<u32>() = {}", align_of::<u32>());
println!("align_of::<f64>() = {}", align_of::<f64>());
println!("align_of::<&str>() = {}", align_of::<&str>());
align_of::<u8>() = 1
align_of::<u16>() = 2
align_of::<u32>() = 4
align_of::<f64>() = 8
align_of::<&str>() = 8
-
结构体
struct MyStruct { a: u8, b: u32, c: u16, } println!("size_of::= {}", std::mem::align_of:: ()); size_of:: = 4 struct MyStruct { a: u8, b: u32, c: u16, d: f64, e: i128 } println!("size_of::= {}", std::mem::align_of:: ()); size_of:: = 16 结构体对齐值按照字段类型的最大值,例如第一个结构体,最大的类型对齐值是 u32,那么结果就是4,第二个最大对齐值的类型是i128,那么结构就是16
-
元组和结构体是一致的
3.4.获取“变量 / 值”的对齐值
使用 std::mem::align_of_val
let my_struct = MyStruct {
a: 1,
b: 2,
c: 3,
d: 4.0,
e: 5,
};
println!("align_of_val(&my_struct) = {}", std::mem::align_of_val(&my_struct));
align_of_val(&my_struct) = 16
3.5.结构体or元组在内存中的表现形式
我们先计算结构体在内存中占多少字节.1+(padding + 3 ) + 4 +2 +(padding + 6 ) + 8 +(padding + 8) +16 = 48
struct MyStruct {
a: u8,
b: u32,
c: u16,
d: f64,
e: i128
}
println!("sizeof ptr {}", std::mem::size_of_val(&my_struct));
sizeof ptr 48
你在测试的时候,可能会输出32.因为编译器默认是#[repr(Rust)]布局不保证跨编译器版本稳定,仅保证在当前编译下自洽、合理对齐
#[repr(Rust)](默认)
- 编译器可以根据需要插入 padding,保证 alignment。
- 对字段顺序目前是遵循源码顺序布局,但标准不保证未来完全不变
- 内存布局只在本 crate、当前编译环境下有效。
你平时看到的 struct 和 tuple 都是这种。
#[repr(C)]
用于和 C 交互,或者你需要固定布局时:
#[repr(C)]
struct Foo {
a: u8,
b: u32,
c: u16,
}
特点:
- 字段顺序严格按写的顺序。
- 对齐和 padding 规则尽量模拟 C 的 ABI。
size_of和align_of都变得“跨语言稳定”
#[repr(align(N))]
手动提高对齐要求:
#[repr(align(16))]
struct Align16(u8);
这样的 Align16 对齐值至少是 16:
println!("{}", std::mem::align_of::<Align16>()); // 16 或以上(一般就是 16)
所以我在结构体前加上了#[repr(C),告诉编译器要严格按照字段的顺序编写
#[repr(C)]
struct MyStruct {
a: u8,
b: u32,
c: u16,
d: f64,
e: i128
}
如果不加#[repr(C),字段是怎样的顺序呢?可以用memoffset这个库来查看
use memoffset::offset_of;
println!("offset a: {}", offset_of!(MyStruct, a));
println!("offset b: {}", offset_of!(MyStruct, b));
println!("offset c: {}", offset_of!(MyStruct, c));
println!("offset d: {}", offset_of!(MyStruct, d));
println!("offset e: {}", offset_of!(MyStruct, e));
// 加上#[repr(C)
offset a: 0
offset b: 4
offset c: 8
offset d: 16
offset e: 32
// 未加上#[repr(C)
offset a: 30
offset b: 24
offset c: 28
offset d: 16
offset e: 0
可以看到,是完全不一样.加上#[repr(C)]是严格按照字段的顺序的,我们在内存中查看一下

-
0x45e58ff320是启示地址 +0 = 因为是 u8 ,大小是1 = 所以是1 后面三个字节是填充的,不管它,
-
0x45e58ff320+4=2 ....后续与赋值的变量是一样的.
-
let my_struct = MyStruct { a: 1, b: 2, c: 3, d: 4.0, e: 5, };
那么,再来看优化后的内存结构

可以反推回去
- 0x59a493f550 +0 = i128 = 5
- 0x59a493f550 +i128 = f64 = 4
- 0x59a493f550 +i128 +f64 + u32 = 2 ....
最后结果,计算它占用的内存大小是32,比优化前占用少了16
struct MyStruct {
e: i128,
d: f64,
b: u32,
c: u16,
a: u8,
}
3.6.如何计算结构体,元组在内存中的大小
例子1:
#[repr(C)]
struct MyTuple {
a: i32,
b: &'static str ,
c: i32,
d: u8,
e: f64,
}
let v3 =MyTuple {
a: 1,
b: "hello",
c: 3,
d: 4,
e: 5.0,
};
println!("size_of_val v3: {}", std::mem::size_of_val(&v3));
-
先找到结构体最大占用类型:是 &str 和 f64 .对齐值是8
-
a 占用4字节,然后检查下一个 b对齐值是8,那么地址要+4 对齐8的倍数 所以 (a +(填充4字节) = 8的倍数)
-
接下来是检查下一个c,对齐值是4,(4+(4)+16) = 24 ,是8的倍数,那就不用管.(4+(4)+16 +4)
-
继续检查下一个d,对齐值是1.直接加上就可以.(4+(4)+16 +4 + 1).
-
最后检查e,对齐值是8,那么先计算前面的大小(4+(4)+16 +4 + 1) = 29.不是8的倍数.取8的倍数 = 32
-
最后就是 32+ 8 = 40,检查是不是8的倍数,是,最终结构就是40
offset: 0 4 8 24 28 29 32 40 |-----------|---|---------------------------|------|--|--|--------| | a |pad| b | c |d |pad| e | | i32 |4b | 16 bytes | i32 |u8|3 | f64 | |-----------|---|---------------------------|------|--|--|--------|
对齐值与类型在内存中占用的大小是不一样,比如&str ,他的对齐值是8,但是它内存占用大小是16.直接看内存结构就明白了

&str offset 0 -8 = ptr字符串指针 ,8-16是字符串长度,
3.7.内存对齐小结
- 了解结构体字段内存对齐,对Rust语言更加“得心应手”,对逆向知识更加巩固.
- 变量或结构体或元组.内存对齐值与在内存中占用的大小概念是不一样的.
- 结构体添加
#[repr(C)],可以让编译器严格执行字段的顺序编译。 - 元组和结构体是一样的,但是元组不能通过添加
#[repr(C)]来禁止编译器优化.
4.发散类型
发散类型(Never type,写作 !)表示:该表达式永远不会返回到调用者。
也就是说,使用 ! 的函数或表达式:
- 永不结束(如无限循环);
- 或者结束但终止当前流程(如
panic!()、进程退出、线程退出); - 因为永不会返回,它在类型系统上 可自动强制转换(coerce)到任何类型。
fn my_fail() -> ! {
panic!("fatal");
}
可以下情况下使用 !:
- 函数明确不会返回:panic、abort、exit、死循环。
- match 分支中用发散表达式保持类型一致。
- 表示永不会结束的主循环或任务系统。
- 在 trait 的方法中表示“调用后不可能继续执行”。
- 证明某些分支永远不会发生(优化、不可能的情况)。
5.字符转义
为什么需要转义?
- 有些字符在源码中无法直接表示,比如不可见字符、控制字符,或者会引起歧义的字符(如引号、反斜杠)。
- Unicode字符
- 无法直接写出的字节
5.1.Unicode转义
- 单字符
\u{NNNN}
NNNN 是十六进制的 Unicode code point,范围是 \u{0} 到 \u{10FFFF}。
例如:
let c = '\u{4E2D}'; // '中'
let s = "\u{1F600}"; // 😀
- 字节转义
\xNN
\xNN 用于转义单字节(0x00 ~ 0xFF)。
用于:
- 字节字符串
b"..."中(最常见) - 普通字符串也可以(但会自动 UTF-8 编码)
例:
let s = "\x41"; // "A"
let b = b"\x41"; // [65]
5.2.原始字符串
当你不想每个 \ 都写成 \\ 时,可以使用 原始字符串:
let s = r"C:\windows\system32\drivers";
- 原始字符串以
r#" ... "#的形式包裹 - 内部 不处理转义符,所有内容原样保留
- 如果内容包含
"#,可以用更多#:
let s = r#"hello "world" \n no escape"#;
需要包含#时
let s = r##"text "with # inside""##;
5.3.主要使用场景
-
使用
r"...":
内容仅包含\,没有"
Windows 路径
简单正则 -
使用
r#"..."#需要在字符串中包含
"
需要写 JSON、HTML、SQL 模板
需要嵌套语言 -
使用
br"..."
你想获得 字节数组 而不是 UTF-8 字符串
内容是 ASCII,且不需要 Unicode
处理二进制协议、网络数据、正则(regex 的字节 API)等
Comments NOTHING