Rust学习笔记-2025-11-24

Amber 发布于 8 天前 70 次阅读


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 对齐 = 4
  • u64 / f64 对齐 = 8
  • 指针 *const T / 引用 &T 通常对齐 = usize 的对齐(64 位平台一般 = 8)

3.2.为什么要内存对齐

  1. 硬件和总线访问要求

CPU 一般按“字长”(word size)访问内存,比如 4 字节或 8 字节对齐位置上同时读取/写入一整个 word。

如果一个 8 字节的值(如 f64)跨越了 8 字节边界,例如:

  • 起始地址是 0x1003(不是 8 的倍数)
  • 这个值占 8 字节:0x1003 ~ 0x100A
  • 它跨越了两个对齐块

在一些架构上可能:

  • 访问这个值会变慢(要两次总线访问)
  • 甚至出现硬件异常(未对齐访问 fault)

所以编译器通过“对齐”保证:

  • 类型的值总是在适合 CPU 的边界上,保证速度和安全。
  1. 性能

即使某些架构允许未对齐访问,也几乎总是“对齐访问”性能更好,所以语言 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)]布局不保证跨编译器版本稳定,仅保证在当前编译下自洽、合理对齐

  1. #[repr(Rust)](默认)
  • 编译器可以根据需要插入 padding,保证 alignment。
  • 对字段顺序目前是遵循源码顺序布局,但标准不保证未来完全不变
  • 内存布局只在本 crate、当前编译环境下有效。

你平时看到的 structtuple 都是这种。

  1. #[repr(C)]

用于和 C 交互,或者你需要固定布局时:

#[repr(C)]
struct Foo {
    a: u8,
    b: u32,
    c: u16,
}

特点:

  • 字段顺序严格按写的顺序。
  • 对齐和 padding 规则尽量模拟 C 的 ABI。
  • size_ofalign_of 都变得“跨语言稳定”
  1. #[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)]是严格按照字段的顺序的,我们在内存中查看一下

image-20251124135616494

  • 0x45e58ff320是启示地址 +0 = 因为是 u8 ,大小是1 = 所以是1 后面三个字节是填充的,不管它,

  • 0x45e58ff320+4=2 ....后续与赋值的变量是一样的.

  • let my_struct = MyStruct {
      a: 1,
      b: 2,
      c: 3,
      d: 4.0,
      e: 5,
    };

那么,再来看优化后的内存结构
image-20251124140323646
可以反推回去

  • 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));
  1. 先找到结构体最大占用类型:是 &str 和 f64 .对齐值是8

  2. a 占用4字节,然后检查下一个 b对齐值是8,那么地址要+4 对齐8的倍数 所以 (a +(填充4字节) = 8的倍数)

  3. 接下来是检查下一个c,对齐值是4,(4+(4)+16) = 24 ,是8的倍数,那就不用管.(4+(4)+16 +4)

  4. 继续检查下一个d,对齐值是1.直接加上就可以.(4+(4)+16 +4 + 1).

  5. 最后检查e,对齐值是8,那么先计算前面的大小(4+(4)+16 +4 + 1) = 29.不是8的倍数.取8的倍数 = 32

  6. 最后就是 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.直接看内存结构就明白了

image-20251124144207654

&str offset 0 -8 = ptr字符串指针 ,8-16是字符串长度,

3.7.内存对齐小结

  1. 了解结构体字段内存对齐,对Rust语言更加“得心应手”,对逆向知识更加巩固.
  2. 变量或结构体或元组.内存对齐值与在内存中占用的大小概念是不一样的.
  3. 结构体添加#[repr(C)],可以让编译器严格执行字段的顺序编译。
  4. 元组和结构体是一样的,但是元组不能通过添加#[repr(C)]来禁止编译器优化.

4.发散类型

发散类型(Never type,写作 !)表示:该表达式永远不会返回到调用者。

也就是说,使用 ! 的函数或表达式:

  • 永不结束(如无限循环);
  • 或者结束但终止当前流程(如 panic!()、进程退出、线程退出);
  • 因为永不会返回,它在类型系统上 可自动强制转换(coerce)到任何类型
fn my_fail() -> ! {
    panic!("fatal");
}

可以下情况下使用 !

  1. 函数明确不会返回:panic、abort、exit、死循环。
  2. match 分支中用发散表达式保持类型一致
  3. 表示永不会结束的主循环或任务系统
  4. 在 trait 的方法中表示“调用后不可能继续执行”
  5. 证明某些分支永远不会发生(优化、不可能的情况)

5.字符转义

为什么需要转义?

  • 有些字符在源码中无法直接表示,比如不可见字符、控制字符,或者会引起歧义的字符(如引号、反斜杠)。
  • Unicode字符
  • 无法直接写出的字节

5.1.Unicode转义

  1. 单字符 \u{NNNN}

NNNN 是十六进制的 Unicode code point,范围是 \u{0}\u{10FFFF}

例如:

let c = '\u{4E2D}'; // '中'
let s = "\u{1F600}"; // 😀
  1. 字节转义\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)等