Skip to content

基础概念

这个章节用于内联解释基础概念,比如 这是 零成本抽象, 点击后会跳转到本章对应的解释。

你不需要刻意阅读本章,当你在后续章节中遇到这些概念时,可以通过内联链接快速查看相关解释。

空指针

空指针(Null Pointer)是指一个指针变量没有指向任何有效的内存地址,通常被赋值为 nullnullptr。在许多编程语言中,空指针被用来表示一个无效的或未初始化的指针。访问空指针会导致运行时错误,如程序崩溃或未定义行为。

悬垂指针

悬垂指针(Dangling Pointer)也叫野指针(Wild Pointer),是指指向已经被释放或无效内存地址的指针。当程序试图访问或修改悬垂指针所指向的内存时,可能会导致未定义行为,如程序崩溃、数据损坏或安全漏洞。

悬垂引用

在支持指针操作的语言中,容易因释放内存后仍保有指向该内存的指针,产生悬垂指针(Dangling Pointer)。悬垂指针指向已被释放的内存,访问时会导致未定义行为。

Rust 编译器从根本上杜绝了悬垂引用: 引用必须总是有效的,即引用的生命周期不能超过其所指向数据的生命周期。

下面的代码无法通过编译:

rust
fn main() {
    let r = dangle();
}

fn dangle() -> &String {           
    let s = String::from("hello"); 
    &s   // 返回 s 的引用
}        // s 在此离开作用域,堆上字符串被释放,&s 变成悬垂引用

错误原因: 函数返回值 &s 是一个指向堆上字符串数据的引用,但函数结束后 s 离开作用域,其绑定的字符串数据被释放,返回的引用就指向了已释放的内存,成为悬垂引用。

修复方式: 直接返回 String 本体,转移所有权,而不是返回引用:

rust
fn main() {
    let s = no_dangle();
    println!("{}", s);
}

fn no_dangle() -> String {
    let s = String::from("hello");
    s  // ✅ 转移所有权给调用者,s 不会被销毁
}

悬垂引用与生命周期密切相关,Rust 编译器会通过生命周期检查在编译阶段阻止一切悬垂引用的出现。详细内容参见生命周期章节。

二次释放

二次释放(Double Free)是指程序试图释放已经被释放的内存资源。这种错误可能会导致内存泄漏、数据损坏或安全漏洞。当程序第一次释放内存时,相关的内存被标记为可用,系统可能会将其分配给其他程序或线程使用,但如果程序再次尝试释放同一块内存,就会引发二次释放错误。

抽象

抽象是指在编程中隐藏复杂的实现细节,只暴露必要的接口和功能。通过抽象,开发者可以更专注于问题的核心,而不必担心底层的实现细节。这有助于提高代码的可读性、可维护性和复用性。

简单理解: 抽象就是把复杂的东西简单化,提取他们的共性,隐藏他们的个性。苹果和橘子可以抽象为一个水果类,虽然它们有不同的颜色、味道和形状,但它们都有一个共同的特征: 都是水果。通过抽象,我们可以把苹果和橘子看作是同一类事物,从而更方便地进行操作和理解。

零成本抽象

零成本抽象(Zero-cost Abstraction)是指在编程语言中,某些高级抽象机制在编译后不会引入额外的运行时开销。换句话说,使用这些抽象机制的代码在性能上与手写的底层代码相当。这使得开发者可以使用更高层次的抽象来提高代码的可读性和可维护性,而不必担心性能损失。

简单理解: 你写了简单的代码,但是却和你手写更底层更复杂的代码一样快,这就是零成本抽象。一种可以简单化但却不需要付出任何代价的抽象机制。

垃圾回收器

垃圾回收器(Garbage Collector,GC)是一种自动内存管理机制,负责在程序运行时自动回收不再使用的内存资源.GC 的主要作用是帮助开发者避免内存泄漏和悬垂指针等内存错误,提高程序的安全性和稳定性。

vscode快捷输入

  • 操作方法: 输入变量名后打 .,在建议列表中选择片段
  • 常用后缀: x.if (生成 if 块)、x.match (生成 match)、x.dbg (生成 dbg!(x))

自举

自举(Bootstrapping)是指一个编程语言的编译器使用该语言本身来实现和编译自己。这意味着,Rust 的编译器(rustc)是用 Rust 编写的,并且可以用 rustc 来编译 rustc 的源代码。

算法复杂度

算法复杂度(Big O Notation)是衡量代码运行效率的"标尺",主要关注随着数据规模 n 的增长,执行时间或内存占用是如何变化的。

1. 时间复杂度阶梯 (由快到慢)

复杂度名称形象描述典型例子
O(1)常数阶无论 n 多大,速度始终如一访问数组下标、哈希表查找
O(log n)对数阶数据翻倍,耗时只加一单位二分查找
O(n)线性阶工作量与数据量成正比遍历数组、简单搜索
O(n log n)线性对数阶高效排序的门槛快速排序、归并排序
O(n^2)平方阶嵌套循环,效率随规模剧增冒泡排序、选择排序
O(2^n)指数阶数据增加,计算量翻倍增长暴力递归、穷举

算法复杂度 = 时间复杂度 + 空间复杂度

2. 三种复杂度评估维度

同一个算法在不同输入下表现不同,通常我们关注:

  • 最好情况: 运气最好的时候(如: 在数组第一位就找到了目标)。
  • 最坏情况(最常用): 保证程序运行的"底线",通常作为算法性能的标准答案。
  • 平均情况: 所有可能输入的期望表现,最符合实际使用中的感知。

3. 核心原则

  • 只看最高阶: 如果复杂度是 O(n^2 + n + 1),简写为 O(n^2)
  • 忽略常数: O(2n)O(100n) 在大 O 表示法中都记作 O(n)

重载

重载(Overloading)是指在编程语言中,允许你定义多个名字完全一样的函数,但它们的参数(数量或类型)必须不同。编译器会根据你传给它的数据,自动决定调用哪一个。

如果没有重载,你必须为每个微小的功能差别起一堆名字:

  • print_int(10)
  • print_string("你好")
  • print_double(3.14)

有了重载,你只需要记住一个统一的名字:

  • print(10) --> 自动知道要打印整数
  • print("你好") --> 自动知道要打印字符串
  • print(3.14) --> 自动知道要打印小数

注意

Rust 语言本身是不支持上面这种传统意义上的“函数重载”的(不能在同一个作用域定义两个同名函数)。 Rust 是通过 Trait(特征/接口) 和 泛型 来实现类似重载的功能(例如 Into 特征或运算符重载)。

析构函数

析构函数(Destructor)是一种特殊的函数,在对象生命周期结束时自动调用,用于释放资源或执行清理操作。在 Rust 中,析构函数由 Drop trait 的 drop 方法实现。当一个值离开作用域时,Rust 会自动调用其 drop 方法来执行必要的清理工作,如释放内存、关闭文件等。

FFI 外部函数接口

FFI(Foreign Function Interface)是指编程语言提供的一种机制,允许程序调用用其他语言编写的函数或库。通过 FFI,开发者可以利用其他语言的功能或性能优势,同时保持 Rust 代码的安全性和效率。

命名规范

蛇形命名法(Snake Case)

使用小写字母和下划线分隔单词的命名风格,不能以数字开头,不能包含空格和特殊字符。例如: my_variable_namecalculate_sum 等。这种命名方式在 Rust 中被广泛使用,尤其是用于函数、变量和模块的命名。

大写蛇形命名法(Upper Snake Case)

使用大写字母和下划线分隔单词的命名风格,通常用于常量和静态变量的命名。例如: MY_VARIABLE_NAMECALCULATE_SUM 等。

驼峰命名法(Camel Case)

一般指小驼峰命名法(Lower Camel Case),是指在命名时第一个单词的首字母小写,后续单词的首字母大写,并且没有下划线分隔的命名风格。例如: myVariableNamecalculateSum 等。在 Rust 中非常罕见,不推荐使用。

帕斯卡命名法(Pascal Case)

也叫大驼峰命名法(Upper Camel Case),是指在命名时每个单词的首字母都大写,并且没有下划线分隔的命名风格。例如: MyVariableNameCalculateSum 等。这种命名方式在 Rust 中通常用于类型和枚举的命名。

Self 和 self 的区别

1. 大写的 Self —— 它是【类型】的替身

  • Self 是一个类型别名。它指代当前正在实现 Trait 或方法的那个具体的类型。

2. 小写的 self —— 它是【实例】的替身

  • self 是一个具体的对象(或者是对象的引用)。它代表调用这个方法的那个变量本身。

基于 MIT 协议发布