Skip to content

Day 7: 运算符与表达式

欢迎来到第七天!今天我们将全面探索Zig中的运算符。运算符是构成表达式的基本工具,用于执行数据计算、比较和逻辑判断。Zig的运算符设计清晰,行为可预测,并且与C语言保持了很大程度的一致性。一个关键区别是,Zig不允许运算符重载,所有函数调用都必须是显式的,这符合其“无隐藏控制流”的哲学。

Zig支持标准的算术运算符:

  • + (加), - (减), * (乘)
  • / (除): 对于整数,除法会向零截断。例如 5 / 2 结果是 2,-5 / 2 结果是 -2。
  • % (取模): 结果的符号与被除数相同。例如 5 % 2 是 1,-5 % 2 是 -1。
const a = 10;
const b = 3;
const sum = a + b; // 13
const div = a / b; // 3
const rem = a % b; // 1

溢出行为: 在Debug和ReleaseSafe模式下,这些标准算术运算符 +, -, * 会在发生溢出时触发panic。在ReleaseFast模式下则会截断。

对于需要明确溢出行为的场景,Zig提供了特殊的运算符:

  • +% (wrapping add), -% (wrapping sub), *% (wrapping mul): 溢出时截断。
  • +? (catching add), -? (catching sub), *? (catching mul): 溢出时返回一个可选类型 ?T,值为 null。
  • 比较运算符: == (等于), != (不等于), < (小于), <= (小于等于), > (大于), >= (大于等于)。它们返回一个 bool 值。

  • 逻辑运算符:

    • and (&&): 逻辑与。如果左侧为 false,则不会评估右侧(短路行为)。
    • or (||): 逻辑或。如果左侧为 true,则不会评估右侧(短路行为)。
    • ! (not): 逻辑非。
const is_sunny = true;
const is_warm = false;
if (is_sunny and is_warm) { // false
// ...
}
if (is_sunny or is_warm) { // true
// ...
}

注意: Zig 0.12.0版本后,&& 和 || 被 and 和 or 关键字替代,以提高可读性。

对于底层编程,位运算必不可少。

  • & (按位与), | (按位或), ^ (按位异或)
  • ~ (按位取反)
  • << (左移), >> (右移)
const flags: u8 = 0b0000_1010;
const mask: u8 = 0b0000_1100;
const combined = flags | mask; // 0b0000_1110
const common = flags & mask; // 0b0000_1000
const toggled = flags ^ mask; // 0b0000_0110
const shifted = flags << 2; // 0b0010_1000

Zig的运算符优先级与C语言非常相似。当不确定时,最佳实践是使用括号 () 来明确指定运算顺序。

一个简化的优先级顺序(从高到低):

  1. () (函数调用), . (成员访问), [] (数组访问)
  2. ! (逻辑非), ~ (按位取反), - (负号)
  3. *, /, %
  4. +, -
  5. <<, >>
  6. &, ^, |
  7. ==, !=, <, <=, >, >=
  8. and
  9. or

除了wrapping运算符,Zig还提供了内建函数来更安全地处理可能溢出的运算:

  • @addWithOverflow(T: type, a: T, b: T) -> struct { T, bool }: 返回一个包含结果和是否溢出的布尔值的结构体。
  • @mulAdd(T: type, a: T, b: T, c: T) -> T: 计算 (a * b) + c,并在中间乘法步骤检查溢出。
const std = @import("std");
pub fn main() !void {
var a: u8 = 200;
var b: u8 = 100;
const result = @addWithOverflow(u8, a, b);
std.debug.print("Result: {d}, Overflowed: {any}\n", .{ result[0], result[1] });
// 输出: Result: 44, Overflowed: true
// 200 + 100 = 300, 超出u8范围(0-255), 300 % 256 = 44
}

我们可以使用位运算和循环来高效地计算一个数的整数次幂。

const std = @import("std");
fn power(base: i32, exp: u32) i32 {
var res: i32 = 1;
var b = base;
var e = exp;
while (e > 0) {
if (e & 1 == 1) {
res *= b;
}
b *= b;
e >>= 1;
}
return res;
}
pub fn main() !void {
const p = power(2, 10);
std.debug.print("2^10 = {d}\n", .{p}); // 输出: 1024
}

这是一个挑战性练习,旨在综合运用今天的知识。

尝试编写一个函数,它接受一个非常简单的字符串表达式,如 "5+3" 或 "9-2",然后计算并返回结果。

  1. 函数签名可以是 fn eval(expr: []const u8) !i32。
  2. 你不需要处理复杂的优先级或括号,只处理单个运算符和两个个位数字。
  3. 你需要遍历字符串,找到运算符,然后解析两边的数字。
  4. 使用 switch 语句来处理不同的运算符 (+, -, *, /)。

这个练习将让你思考如何将字符转换为数字,并处理简单的逻辑分支。

问:浮点数运算的精度问题如何处理?

答:与所有语言一样,Zig的浮点数运算也遵循IEEE 754标准,存在精度限制。对于需要高精度的计算(如金融领域),应该避免使用原生浮点数。可以考虑使用标准库提供的任意精度算术库,或者使用定点数算术(将整数用于表示小数)。

问:既然 and 和 or 会短路,那么按位与 & 和按位或 | 会吗?

答:不会。& 和 | 总是会评估两边的表达式,因为它们是按位操作,需要两边的所有位信息来计算结果。

今天,我们学习了Zig中丰富的运算符,从基础的算术运算到强大的位运算。我们理解了Zig对运算符溢出的安全处理机制,以及如何通过 and/or 实现逻辑短路。最重要的是,我们再次看到了Zig的设计哲学:明确、可预测且安全。

明天,我们将学习控制程序流程的两个基本结构:if 和 switch。