跳转至

变量

变量可能是编程语言中最广泛适用的概念。 实际上,我见过的几乎所有编程语言都使用了变量的概念(除了领域特定语言)。

那么,什么是变量? 简单来说,"变量" 是 "存在的东西"。 在编程语言中, 变量组成程序的状态。 变量可以是任何东西,可以是一个简单的整数或字符串, 也可以是磁盘上的文件或网络套接字。 从ISA的角度来看,变量通常是对内存片段的抽象。

代码实验室:变量基础知识

让我们在Python和C++中创建一些基本变量!

在Python脚本中输入以下内容:

a = 5
b = "考拉太可爱了!"
c = {
    "a": 1,
    "b": [
        1, {3, 5, 7}
    ]
}

print(a, b, c, sep='\n')

运行该脚本并观察输出结果。 如果你还没有安装Python或者不知道如何运行Python脚本,请询问一个AI。 (提示:考虑使用诸如Anaconda之类的环境管理器,以避免破坏操作系统。)

在上面的代码中,abc都是变量, 而最后的print语句将它们的内容打印到控制台上。

现在,我们来看看C++中的变量是如何工作的。

在C++源文件中输入以下内容:

#include <iostream>
#include <string>

int main() {
    int a = 5;
    std::string b = "考拉太可爱了!";
    std::cout << a << "\n" << b << std::endl;
    return 0;
}

编译该文件并运行可执行文件。 如果不知道如何操作,可以询问一个AI。

比较一下Python代码和C/C++代码。 你能看到Python/C++在处理变量创建方面的差别吗? 为什么会有这样的差别? (提示:Python是一种解释型语言,而C++是一种编译型语言。 如果你刚开始学编程的话,这个问题有点难。 如果你找不出答案,请询问一个AI并看看它是如何回答的。)

变量类型

与变量相关的一个常见概念是它的类型。 变量的类型回答了以下问题:

  • 这个变量是什么东西?
  • 我能用它做什么?

从本质上讲,类型赋予变量意义。 没有已知类型的变量只是一个毫无意义的内存片段; 你盯着它却不知道可以用它做什么。 你也可以将类型视为变量的抽象。 尽管变量本质上只是一些内存片段, 但很少有开发者会以这种方式思考: 显然,用整数、字符串和文件等来思考比用二进制值0xffee37来思考要容易得多。

与此同时,重要的是要注意类型(以及变量)只是抽象,而不是物理对象。 当你使用C++编写应用程序时, 在编译后的应用程序中没有类型和变量, 只有内存操作和由ISA定义的二进制机器码。

基本的变量语法

不同编程语言中定义变量的确切语法可能有所不同, 但也有许多共同之处, 正如本小节所解释的那样。

变量赋值

几乎所有的语言都使用单个等号 "=" 来赋值给变量。

将一个新值赋给变量会将其内容更改为该值; 从ISA的角度来看,给变量赋值相当于写入与变量相对应的内存片段。

下表显示了如何在几种语言中将值 5 赋给整数类型变量 a

Python C/C++ Rust Java C#
a = 5 a = 5; a = 5; a = 5; a = 5;

变量声明

编译型语言通常要求在为变量赋值或读取变量之前声明变量。 变量的声明基本上是一个 "声明此变量存在" 的占位符。

例如,在C/C++中,你可以使用 "int a;" 来声明一个名为 aint(整数类型)变量; 在Rust中,你可以使用 let a = 5; 来做同样的事情。

由于先声明再赋值会显得太冗长,大多数编译型语言允许你将声明和赋予初始值的操作结合起来。

下表显示了如何在几种语言中声明一个 int 变量并将其赋值为 5

C/C++ Rust Java C#
int a = 5; let a = 5; int a = 5; int a = 5;

类型推断和类型注解

注意,Rust不要求你明确注解你声明的是一个整数变量, 尽管它是一种编译型的静态类型语言。 这是因为Rust编译器可以根据你赋予变量的初始值(在本例中是 5 )来推断变量的类型。

编译器推断变量类型的能力称为 类型推断。 类型推断在许多编程语言中都得到支持, 当类型名称较长或者不确定变量的确切类型时,类型推断非常有用。

类型推断表明 "无需注解类型等同于动态类型" 是不正确的。

相反,注释类型也不等同于静态类型。 例如,Python允许但不要求你注解变量的类型, 在与用户交互的代码中注解变量类型通常是一种好习惯(如库函数签名)。

问题:变量声明

编译型语言为什么需要声明,而解释型语言则不需要?

提示:思考一下静态类型和动态类型,以及编译型语言和解释型语言的运行方式。 如果你还是弄不清楚,可以问一个AI。

基本类型

几乎每种编程语言都有一组预定义的基本类型。 这些类型通常是原子的、小型的(就这些类型的变量的内存使用而言), 并且无法分解成更小的部分。

下表列出了几乎所有编程语言支持的一些基本类型 (变量类型的"标识符" 是该类型在特定语言中的名称)。

类型 标识符(Python) 标识符(C++) 标识符(Rust)
整数 int int(基本)、unsigned intint32_tlonglong longsize_t(其他变种) usizei32u32i64u64
十进制数 float floatdouble f32f64
布尔 bool bool bool
字符 不适用 char char
指针 不适用 <type>*,例如 int*float*void* &<type>,例如 &i32&f32&bool

问题

通过观察上表中的内容,可以提出一些有趣的问题:

  • 在上面Rust类型标识符中,ui 代表什么意思?
  • 在上面的类型标识符中的数字 3264 等代表什么意思?
  • 使用 int 还是 int32_t 更好?
  • 为什么十进制数被称为 float 而不是 decimal
  • "boolean" 的语义是什么?
  • "指针" 的语义是什么?为什么Python没有指针?
  • 为什么C++和Rust在整数/小数方面有这么多不同的类型?

你能回答这些问题吗? (如果找不出答案,可以问一个AI,因为有些问题对初学者来说有点困难。)

代码实验室:类型转换

下面的C++代码将一个小数赋给一个整数变量,并显示整数变量的值:

#include <iostream>

int main() {
    int a = 5.6;
    std::cout << a << std::endl;
    return 0;
}

编译和运行这段代码。 你观察到了什么?为什么会出现这种情况?

现在,让我们尝试在Rust中做同样的事情:

fn main() {
    let a: i32 = 5.6; // 显式注解为i32,以避免编译器推断为f64
    println!("{}", a);
}

尝试编译代码。 你观察到了什么?

根据C++和Rust编译器的行为, 你认为C++的设计比Rust好,还是反过来? 为什么?

(提示:搜索 "silent bugs"。)

代码实验室:类型和变量名存在吗?

这是一段简单的C++代码:

int main() {
    int koalas_are_so_cute = 5;
    return 0;
}

将此代码编译为汇编语言(ASM), 确保关闭优化选项 (如果不知道如何做,请向AI询问)。

你可能会得到类似于以下的结果:

    .file   "test.cpp"
    .text 
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    endbr64
    pushq   %rbp
    .cfi_def_cfa_offset 16
    .cfi_offset 6, -16
    movq    %rsp, %rbp
    .cfi_def_cfa_register 6
    movl    $5, -4(%rbp)
    movl    $0, %eax
    popq    %rbp
    .cfi_def_cfa 7, 8
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0"
    .section    .note.GNU-stack,"",@progbits
    .section    .note.gnu.property,"a"
    .align 8
    .long   1f - 0f
    .long   4f - 1f
    .long   5
0:
    .string "GNU"
1:
    .align 8
    .long   0xc0000002
    .long   3f - 2f
2:
    .long   0x3
3:
    .align 8
4:

你在上面的编译后ASM中看到了变量名或者 int 吗? 你能否在上述ASM中确定变量 koalas_are_so_cute 的位置? 基于这个观察, 变量名或类型是否存在于编译后的可执行文件中? 为什么会这样?

提示:对初学者来说,这些问题非常困难。 你应该向一个AI提供C++源代码和ASM,并要求AI解释ASM的每一行的含义。

代码实验室:大数

尝试编写一些代码,找出C++中整数能够表示的最大可能数。 对于Rust和Python,请尝试做同样的事情。 (在C++中使用 int,在Rust中使用 i32,在Python中使用 int。)

观察程序的输出并在互联网上搜索一下。 你能解释Python如何以与C++或Rust不同的方式表示整数吗?

组合类型

大多数编程语言允许您定义自己的组合类型

组合类型是由其他类型的字段组成的类型。 例如,以下Rust代码定义了一个名为 Student 的组合类型 其中包含一个名为 score 的十进制数字段和一个名为 name 的字符串字段:

struct Student {
    score: f32,
    name: String
}

注意,字段也可以是组合类型。

你可以通过以下方式在C++中定义相应的 Student 类型:

struct Student {
    float score;
    std::string name;
};

在Python中:

@dataclass
class Student:
    score: float
    name: str

组合类型的常见标识符

编程语言在定义组合类型的语法上几乎完全相同; 一个组合类型通常被称为 structclass,具体取决于具体的语言。 所以,如果你刚开始学一门语言并想知道定义组合类型的语法, 尝试搜索 structclass。 例如,你可以询问一个AI "如何在Go中定义一个struct?"

代码实验室:组合类型

在Rust中尝试定义自己的组合类型。 请使用一个AI帮助你创建一个该类型的变量,并正确地将其显示在控制台上。

枚举类型

几乎所有的编程语言都支持定义枚举类型。 枚举(enum)是一种可以取一组有限值之一的类型; 其中的每个值被称为枚举变体

例如,以下C++代码定义了一个名为 Colorenum 类型,可以是 REDGREENBLUE

enum Color {
    RED,
    GREEN,
    BLUE
};

然后,你可以声明一个 Color 变量并将其赋值为 RED

Color a = RED;

扩展的枚举

一些编程语言的枚举功能比仅定义一组有限的枚举变体要强大得多。

例如,Rust允许你为不同的枚举变体关联不同的字段,例如:

enum Person {
    Student(f32), // `f32` 字段表示分数
    Teacher(String), // `String` 字段表示姓名
    Soldier // 没有字段
}

变量相关的共同概念和设计选择

静态与动态类型

与变量相关的一个常见设计选择是使用静态类型还是动态类型。

静态类型意味着变量的类型在其生命周期中是不可变的; 动态类型则允许你在运行时更改变量的类型。

通常(但不总是),编译型语言使用静态类型, 而解释型语言使用动态类型。

问题:静态 vs. 动态类型

编译型语言通常使用静态类型, 而解释型语言通常使用动态类型。 为什么?

提示:考虑程序运行的过程。 这个问题对初学者来说可能有点困难; 可以问一个AI来获取帮助。

栈和堆变量

对低级语言(尤其是没有垃圾回收器的语言,如C++和Rust)而言, 区分栈和堆变量非常重要。

顾名思义, 栈变量的内存分配在栈上进行, 而堆变量的内存分配在堆上进行 (但堆变量可能在栈上具有指针或元数据)。

通常,栈变量较小,其生命周期较短; 而堆变量较大,其生命周期较长。

问题:栈 vs. 堆变量

结合AI和互联网的帮助, 尝试回答以下问题:

  • 为什么栈变量的生命周期较短?
  • 访问栈变量比访问堆变量更快吗?为什么?
  • 什么时候应该使用栈变量?堆变量呢?
  • 你在Python中区分栈和堆变量吗?为什么?

变量可变性与默认可变性

可变性是与变量相关的另一个重要概念。 通常,变量可以是可变的或不可变的。

如果一个变量是可变的,那么一旦分配了初始值,就可以将其赋给另一个值。 如果它是不可变的,在分配了初始值后就不能再赋予其他值了。

许多编程语言都有可变性的概念,并允许您将变量标记为可变或不可变。

然而,对于变量应该默认为可变还是不可变这个问题, 还没有达成一致的意见。 例如,C/C++将变量默认为可变,而Rust将其默认为不可变。

问题:默认可变性

将变量默认为可变还是不可变是更好的设计选择? 为什么C/C++将变量默认为可变,而Rust将其默认为不可变?

提示:回答这些问题需要一些编码经验, 所以如果你是初学者,可以向一个AI咨询。

生命周期

变量的生命周期是变量有效和存在的范围。

生命周期的概念适用于几乎所有的编程语言, 但很少有语言有特殊的语法来明确注释生命周期。 Rust是其中之一。

问题:生命周期

知道变量的生命周期有什么好处吗?

代码实验室:生命周期实践

查看以下Rust代码:

fn main() {
    let a = 5;

    {
        let b = 1;
    }

    println!("{}, {}", a, b);
}

a 的生命周期是多久?b 呢?

可以请一个AI帮助你回答这个问题。

所有权

所有权是资源管理的重要概念和模型。 基本上,所有权系统为一个资源指定一个或多个所有者变量; 当所有所有者都消失时,该资源将被释放。

对于本初学者部分的目的来说,所有权可能有点超出范围, 所以这里不会详细解释。 如果你感兴趣,可以搜索 "Rust ownership"、"RAII" 以及C++中的特殊 "智能指针"(如 std::unique_ptrstd::shared_ptr)。

结论

哇,这是很多的材料! 现在你已经学完了所有的材料, 问题和代码实验, 你应该对变量有了基本的了解, 以及如何在代码中使用它们。 更重要的是, 你已经学会如何咨询AI和互联网来找到你需要的东西!

我们在本节中涵盖了很多内容:

  • 变量表示 "事物"。
  • 变量具有 类型
  • 与变量相关的常见语法: 变量的 赋值声明 变量, 基本和组合类型, 枚举 类型。
  • 与变量相关的常见概念和设计选择: 静态动态类型, 变量, 变量 可变性、生命周期、所有权。