变量
变量可能是编程语言中最广泛适用的概念。 实际上,我见过的几乎所有编程语言都使用了变量的概念(除了领域特定语言)。
那么,什么是变量? 简单来说,"变量" 是 "存在的东西"。 在编程语言中, 变量组成程序的状态。 变量可以是任何东西,可以是一个简单的整数或字符串, 也可以是磁盘上的文件或网络套接字。 从ISA的角度来看,变量通常是对内存片段的抽象。
代码实验室:变量基础知识
让我们在Python和C++中创建一些基本变量!
在Python脚本中输入以下内容:
运行该脚本并观察输出结果。 如果你还没有安装Python或者不知道如何运行Python脚本,请询问一个AI。 (提示:考虑使用诸如Anaconda之类的环境管理器,以避免破坏操作系统。)
在上面的代码中,a
、b
和c
都是变量,
而最后的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;" 来声明一个名为 a
的 int
(整数类型)变量;
在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 int 、int32_t 、long 、long long 、size_t (其他变种) |
usize 、i32 、u32 、i64 、u64 |
十进制数 | float |
float 、double |
f32 、f64 |
布尔 | bool |
bool |
bool |
字符 | 不适用 | char |
char |
指针 | 不适用 | <type>* ,例如 int* 、float* 、void* |
&<type> ,例如 &i32 、&f32 、&bool |
问题
通过观察上表中的内容,可以提出一些有趣的问题:
- 在上面Rust类型标识符中,
u
和i
代表什么意思? - 在上面的类型标识符中的数字
32
、64
等代表什么意思? - 使用
int
还是int32_t
更好? - 为什么十进制数被称为
float
而不是decimal
? - "boolean" 的语义是什么?
- "指针" 的语义是什么?为什么Python没有指针?
- 为什么C++和Rust在整数/小数方面有这么多不同的类型?
你能回答这些问题吗? (如果找不出答案,可以问一个AI,因为有些问题对初学者来说有点困难。)
代码实验室:类型转换
下面的C++代码将一个小数赋给一个整数变量,并显示整数变量的值:
编译和运行这段代码。 你观察到了什么?为什么会出现这种情况?
现在,让我们尝试在Rust中做同样的事情:
尝试编译代码。 你观察到了什么?
根据C++和Rust编译器的行为, 你认为C++的设计比Rust好,还是反过来? 为什么?
(提示:搜索 "silent bugs"。)
代码实验室:类型和变量名存在吗?
这是一段简单的C++代码:
将此代码编译为汇编语言(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
的字符串字段:
注意,字段也可以是组合类型。
你可以通过以下方式在C++中定义相应的 Student
类型:
在Python中:
组合类型的常见标识符
编程语言在定义组合类型的语法上几乎完全相同;
一个组合类型通常被称为 struct
或 class
,具体取决于具体的语言。
所以,如果你刚开始学一门语言并想知道定义组合类型的语法,
尝试搜索 struct
或 class
。
例如,你可以询问一个AI "如何在Go中定义一个struct?"
代码实验室:组合类型
在Rust中尝试定义自己的组合类型。 请使用一个AI帮助你创建一个该类型的变量,并正确地将其显示在控制台上。
枚举类型
几乎所有的编程语言都支持定义枚举类型。 枚举(enum)是一种可以取一组有限值之一的类型; 其中的每个值被称为枚举变体。
例如,以下C++代码定义了一个名为 Color
的 enum
类型,可以是 RED
、GREEN
或 BLUE
:
然后,你可以声明一个 Color
变量并将其赋值为 RED
:
扩展的枚举
一些编程语言的枚举功能比仅定义一组有限的枚举变体要强大得多。
例如,Rust允许你为不同的枚举变体关联不同的字段,例如:
变量相关的共同概念和设计选择
静态与动态类型
与变量相关的一个常见设计选择是使用静态类型还是动态类型。
静态类型意味着变量的类型在其生命周期中是不可变的; 动态类型则允许你在运行时更改变量的类型。
通常(但不总是),编译型语言使用静态类型, 而解释型语言使用动态类型。
问题:静态 vs. 动态类型
编译型语言通常使用静态类型, 而解释型语言通常使用动态类型。 为什么?
提示:考虑程序运行的过程。 这个问题对初学者来说可能有点困难; 可以问一个AI来获取帮助。
栈和堆变量
对低级语言(尤其是没有垃圾回收器的语言,如C++和Rust)而言, 区分栈和堆变量非常重要。
顾名思义, 栈变量的内存分配在栈上进行, 而堆变量的内存分配在堆上进行 (但堆变量可能在栈上具有指针或元数据)。
通常,栈变量较小,其生命周期较短; 而堆变量较大,其生命周期较长。
问题:栈 vs. 堆变量
结合AI和互联网的帮助, 尝试回答以下问题:
- 为什么栈变量的生命周期较短?
- 访问栈变量比访问堆变量更快吗?为什么?
- 什么时候应该使用栈变量?堆变量呢?
- 你在Python中区分栈和堆变量吗?为什么?
变量可变性与默认可变性
可变性是与变量相关的另一个重要概念。 通常,变量可以是可变的或不可变的。
如果一个变量是可变的,那么一旦分配了初始值,就可以将其赋给另一个值。 如果它是不可变的,在分配了初始值后就不能再赋予其他值了。
许多编程语言都有可变性的概念,并允许您将变量标记为可变或不可变。
然而,对于变量应该默认为可变还是不可变这个问题, 还没有达成一致的意见。 例如,C/C++将变量默认为可变,而Rust将其默认为不可变。
问题:默认可变性
将变量默认为可变还是不可变是更好的设计选择? 为什么C/C++将变量默认为可变,而Rust将其默认为不可变?
提示:回答这些问题需要一些编码经验, 所以如果你是初学者,可以向一个AI咨询。
生命周期
变量的生命周期是变量有效和存在的范围。
生命周期的概念适用于几乎所有的编程语言, 但很少有语言有特殊的语法来明确注释生命周期。 Rust是其中之一。
问题:生命周期
知道变量的生命周期有什么好处吗?
代码实验室:生命周期实践
查看以下Rust代码:
a
的生命周期是多久?b
呢?
可以请一个AI帮助你回答这个问题。
所有权
所有权是资源管理的重要概念和模型。 基本上,所有权系统为一个资源指定一个或多个所有者变量; 当所有所有者都消失时,该资源将被释放。
对于本初学者部分的目的来说,所有权可能有点超出范围,
所以这里不会详细解释。
如果你感兴趣,可以搜索 "Rust ownership"、"RAII" 以及C++中的特殊 "智能指针"(如 std::unique_ptr
和 std::shared_ptr
)。
结论
哇,这是很多的材料! 现在你已经学完了所有的材料, 问题和代码实验, 你应该对变量有了基本的了解, 以及如何在代码中使用它们。 更重要的是, 你已经学会如何咨询AI和互联网来找到你需要的东西!
我们在本节中涵盖了很多内容:
- 变量表示 "事物"。
- 变量具有 类型。
- 与变量相关的常见语法: 变量的 赋值、声明 变量, 基本和组合类型, 枚举 类型。
- 与变量相关的常见概念和设计选择: 静态和动态类型, 栈和堆变量, 变量 可变性、生命周期、所有权。